private void LookForPropertyIdentifierReassignment(SyntaxNodeAnalysisContext context) { var assignment = context.Node as AssignmentExpressionSyntax; if (assignment == null) { return; } var targetName = assignment.Left as IdentifierNameSyntax; if (targetName == null) { return; } var assignmentTargetAsParameter = context.SemanticModel.GetSymbolInfo(assignment.Left).Symbol as IParameterSymbol; if ((assignmentTargetAsParameter == null) || !CommonAnalyser.HasPropertyIdentifierAttribute(assignmentTargetAsParameter)) { return; } context.ReportDiagnostic(Diagnostic.Create( NoReassignmentRule, assignment.Left.GetLocation() )); }
private static bool InvocationIsAllowableValidateCall(InvocationExpressionSyntax invocation, SyntaxNodeAnalysisContext context) { if (invocation == null) { throw new ArgumentNullException(nameof(invocation)); } var lastExpressionToken = invocation.Expression.GetLastToken(); if ((lastExpressionToken == null) || (lastExpressionToken.Text != "Validate")) { return(false); } var validateMethod = context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol as IMethodSymbol; return ((validateMethod != null) && !validateMethod.Parameters.Any() && (validateMethod.Arity == 0) && validateMethod.ReturnsVoid && !CommonAnalyser.HasDisallowedAttribute(validateMethod)); }
private void LookForIllegalCtorSetCall(SyntaxNodeAnalysisContext context) { var invocation = context.Node as InvocationExpressionSyntax; if (invocation == null) { return; } if ((invocation.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text != "CtorSet") { return; } var ctorSetMethod = context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol as IMethodSymbol; if ((ctorSetMethod == null) || (ctorSetMethod.ContainingAssembly == null) || (ctorSetMethod.ContainingAssembly.Name != CommonAnalyser.AnalyserAssemblyName)) { return; } // A SimpleMemberAccessExpression is a VERY simple "dot access" such as "this.CtorSet(..)" // - Anything more complicated is not what is recommended // - Anything that IS this simple but that does not target "this" is not what is recommended if ((invocation.Expression.Kind() != SyntaxKind.SimpleMemberAccessExpression) || (invocation.Expression.GetFirstToken().Kind() != SyntaxKind.ThisKeyword)) { context.ReportDiagnostic(Diagnostic.Create( SimpleMemberAccessRule, context.Node.GetLocation() )); return; } // Ensure that the CtorSet call is within a constructor (that's the only place that properties should be set on immutable types) var isInsideConstructor = false; var ancestor = invocation.Parent; while (ancestor != null) { if (ancestor.Kind() == SyntaxKind.ConstructorDeclaration) { isInsideConstructor = true; break; } ancestor = ancestor.Parent; } if (!isInsideConstructor) { context.ReportDiagnostic(Diagnostic.Create( ConstructorRule, context.Node.GetLocation() )); return; } var propertyRetrieverArgument = invocation.ArgumentList.Arguments.FirstOrDefault(); if (propertyRetrieverArgument == null) { // If there are no arguments then there should be a compile error and we shouldn't have got here - but better to pretend that // all is well until we DO get valid content, rather than cause an NRE below return; } // If the CtorSet method signature called is one with a TPropertyValue generic type argument then get that type. We need to pass // this to the GetPropertyRetrieverArgumentStatus method so that it can ensure that we are not casting the property down to a // less specific type, which would allow an instance of that less specific type to be set as a property value. For example, if // within a constructor of an IAmImmutable class that has a "Name" property of type string then the following should not be // allowed: // // this.CtorSet(_ => _.Name, new object()); // // This will compile (TPropertyValue willl be inferred as "Object") but we don't want to allow it since it will result in the // Name property being assigned a non-string reference. var typeArguments = ctorSetMethod.TypeParameters.Zip(ctorSetMethod.TypeArguments, (genericTypeParam, type) => new { genericTypeParam.Name, Type = type }); var propertyValueTypeIfKnown = typeArguments.FirstOrDefault(t => t.Name == "TPropertyValue")?.Type; IPropertySymbol propertyIfSuccessfullyRetrieved; switch (CommonAnalyser.GetPropertyRetrieverArgumentStatus(propertyRetrieverArgument, context, propertyValueTypeIfKnown, allowReadOnlyProperties: true, propertyIfSuccessfullyRetrieved: out propertyIfSuccessfullyRetrieved)) { case CommonAnalyser.PropertyValidationResult.Ok: case CommonAnalyser.PropertyValidationResult.UnableToConfirmOrDeny: return; case CommonAnalyser.PropertyValidationResult.IndirectTargetAccess: context.ReportDiagnostic(Diagnostic.Create( IndirectTargetAccessorAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.NotSimpleLambdaExpression: case CommonAnalyser.PropertyValidationResult.LambdaDoesNotTargetProperty: context.ReportDiagnostic(Diagnostic.Create( SimplePropertyAccessorArgumentAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.MissingGetter: context.ReportDiagnostic(Diagnostic.Create( SimplePropertyAccessorArgumentAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.GetterHasBridgeAttributes: case CommonAnalyser.PropertyValidationResult.SetterHasBridgeAttributes: context.ReportDiagnostic(Diagnostic.Create( BridgeAttributeAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.PropertyIsOfMoreSpecificTypeThanSpecificValueType: // propertyIfSuccessfullyRetrieved and propertyValueTypeIfKnown will both be non-null if PropertyIsOfMoreSpecificTypeThanSpecificValueType was returned // (since it would not be possible to ascertain that that response is appropriate without being able to compare the two values) context.ReportDiagnostic(Diagnostic.Create( PropertyMayNotBeSetToInstanceOfLessSpecificTypeRule, invocation.GetLocation(), propertyIfSuccessfullyRetrieved.GetMethod.ReturnType, // This will always have a value if we got PropertyIsOfMoreSpecificTypeThanSpecificValueType back propertyValueTypeIfKnown.Name )); return; case CommonAnalyser.PropertyValidationResult.MethodParameterWithoutPropertyIdentifierAttribute: context.ReportDiagnostic(Diagnostic.Create( MethodParameterWithoutPropertyIdentifierAttributeRule, propertyRetrieverArgument.GetLocation() )); return; } }
private void LookForIllegalPropertyAttributeIdentifierSpecification(SyntaxNodeAnalysisContext context) { var invocation = context.Node as InvocationExpressionSyntax; if (invocation == null) { return; } IEnumerable <IParameterSymbol> parameters; var delegateParameter = context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol as IParameterSymbol; if (delegateParameter != null) { var delegateType = delegateParameter.Type as INamedTypeSymbol; if ((delegateType != null) && (delegateType.TypeKind == TypeKind.Delegate) && (delegateType.DelegateInvokeMethod != null)) { parameters = delegateType.DelegateInvokeMethod.Parameters; } else { return; // We can't analyse this delegate call if we can't get the parameter data } } else { var method = context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol as IMethodSymbol; if (method == null) { return; } parameters = method.Parameters; } // Note: If the target method is an extension method then GetSymbolInfo does something clever based upon how it's called. If, for example, the extension method has two // arguments - the "this" argument and a second one - and the method is called as an extension method then the "method" instance here will have a single parameter // (because it only requires a single parameter to be provided since the first is provided by the reference that the extension method is being called on). However, if // the same extension method is called as a regular static method then the "method" instance here will list two parameters. So the number of argument values and the // number of expected method parameters will be consistent for the same extension method, even though it will appear to have one less parameter when called one way // rather than the other. One way that the argument values and the number of parameters MAY appear inconsistent, though, is if the method has parameters with default // values - in this case, there may be fewer argument values than there are parameters (meaning the last parameters are satisfied with their defaults). This means that // we need to be sure to only look at the provided argument values and to ignore any method parameters that are left to their defaults (default values have to be compile // time constants and so, for delegates, these will have to null - so it won't be possible for a method parameter to have an invalid default value other than null, so // we only need to worry about validating the actual argument values). var invocationArgumentDetails = parameters .Take(invocation.ArgumentList.Arguments.Count) // Only consider argument values that are specified (ignore any parameters that are taking default values) .Select((p, i) => new { Index = i, Parameter = p, HasPropertyIdentifierAttribute = CommonAnalyser.HasPropertyIdentifierAttribute(p) }); // Look for argument values passed to methods where the method argument is identified as [PropertyIdentifier] - we need to ensure that these meet the usual With / CtorSet / GetProperty criteria foreach (var propertyIdentifierArgumentDetails in invocationArgumentDetails.Where(a => a.HasPropertyIdentifierAttribute)) { var argumentValue = invocation.ArgumentList.Arguments[propertyIdentifierArgumentDetails.Index]; var parameterTypeNamedSymbol = propertyIdentifierArgumentDetails.Parameter.Type as INamedTypeSymbol; if ((parameterTypeNamedSymbol == null) || (parameterTypeNamedSymbol.DelegateInvokeMethod == null) || (parameterTypeNamedSymbol.DelegateInvokeMethod.ReturnsVoid)) { context.ReportDiagnostic(Diagnostic.Create( ArgumentMustBeTwoArgumentDelegateRule, argumentValue.GetLocation() )); continue; } IPropertySymbol propertyIfSuccessfullyRetrieved; switch (CommonAnalyser.GetPropertyRetrieverArgumentStatus(argumentValue, context, propertyValueTypeIfKnown: parameterTypeNamedSymbol.DelegateInvokeMethod.ReturnType, allowReadOnlyProperties: false, propertyIfSuccessfullyRetrieved: out propertyIfSuccessfullyRetrieved)) { case CommonAnalyser.PropertyValidationResult.Ok: case CommonAnalyser.PropertyValidationResult.UnableToConfirmOrDeny: continue; case CommonAnalyser.PropertyValidationResult.IndirectTargetAccess: context.ReportDiagnostic(Diagnostic.Create( IndirectTargetAccessorAccessRule, argumentValue.GetLocation() )); continue; case CommonAnalyser.PropertyValidationResult.NotSimpleLambdaExpression: case CommonAnalyser.PropertyValidationResult.LambdaDoesNotTargetProperty: context.ReportDiagnostic(Diagnostic.Create( SimplePropertyAccessorArgumentAccessRule, argumentValue.GetLocation() )); continue; case CommonAnalyser.PropertyValidationResult.MissingGetter: context.ReportDiagnostic(Diagnostic.Create( SimplePropertyAccessorArgumentAccessRule, argumentValue.GetLocation() )); continue; case CommonAnalyser.PropertyValidationResult.GetterHasBridgeAttributes: case CommonAnalyser.PropertyValidationResult.SetterHasBridgeAttributes: context.ReportDiagnostic(Diagnostic.Create( BridgeAttributeAccessRule, argumentValue.GetLocation() )); continue; case CommonAnalyser.PropertyValidationResult.IsReadOnly: context.ReportDiagnostic(Diagnostic.Create( ReadOnlyPropertyAccessRule, argumentValue.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.PropertyIsOfMoreSpecificTypeThanSpecificValueType: context.ReportDiagnostic(Diagnostic.Create( PropertyMayNotBeSetToInstanceOfLessSpecificTypeRule, invocation.GetLocation(), propertyIfSuccessfullyRetrieved.GetMethod.ReturnType, // This will always have a value if we got PropertyIsOfMoreSpecificTypeThanSpecificValueType back parameterTypeNamedSymbol.DelegateInvokeMethod.ReturnType.Name )); continue; case CommonAnalyser.PropertyValidationResult.MethodParameterWithoutPropertyIdentifierAttribute: context.ReportDiagnostic(Diagnostic.Create( MethodParameterWithoutPropertyIdentifierAttributeRule, argumentValue.GetLocation() )); continue; } } // While we're looking at method calls, ensure that we don't pass a [PropertyIdentifier] argument for the current method into another method as an out or ref argument because reassignment // of [PropertyIdentifier] arguments is not allowed (because it would be too difficult - impossible, actually, I think - to ensure that it doesn't come back in a form that would mess up // With calls in bad ways) foreach (var argumentDetails in invocationArgumentDetails) { var argumentValue = invocation.ArgumentList.Arguments[argumentDetails.Index]; if (argumentValue.RefOrOutKeyword.Kind() == SyntaxKind.None) { continue; } var argumentValueAsParameter = context.SemanticModel.GetSymbolInfo(argumentValue.Expression).Symbol as IParameterSymbol; if ((argumentValueAsParameter == null) || !CommonAnalyser.HasPropertyIdentifierAttribute(argumentValueAsParameter)) { continue; } context.ReportDiagnostic(Diagnostic.Create( NoReassignmentRule, argumentValue.GetLocation() )); } }
private void LookForIllegalGetPropertyCall(SyntaxNodeAnalysisContext context) { var invocation = context.Node as InvocationExpressionSyntax; if (invocation == null) { return; } if ((invocation.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text != "GetProperty") { return; } var getPropertyMethod = context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol as IMethodSymbol; if ((getPropertyMethod == null) || (getPropertyMethod.ContainingAssembly == null) || (getPropertyMethod.ContainingAssembly.Name != CommonAnalyser.AnalyserAssemblyName)) { return; } // The GetSymbolInfo call above does some magic so that when the GetProperty method is called as extension then it its parameters // list excludes the "this" parameter. See the WithCallAnalyzer for more details about this, the short version is that we need to // look at the getPropertyMethod's Parameters set to work out which argument in the current expression's argument list is the // property identifier / property retriever that we're interested in validating. var indexOfPropertyIdentifierArgument = getPropertyMethod.Parameters .Select((p, i) => new { Index = i, Parameter = p }) .Where(p => p.Parameter.Name == "propertyIdentifier") .Single() .Index; // See notes in WithCallAnalyzer and CtorSetCallAnalyzer about why it's important that we don't allow down casting of the property // type (if a "Name" property is of type string then don't allow the TPropertyValue type argument to be inferred as anything less // specific, such as object). var typeArguments = getPropertyMethod.TypeParameters.Zip(getPropertyMethod.TypeArguments, (genericTypeParam, type) => new { genericTypeParam.Name, Type = type }); var propertyValueTypeIfKnown = typeArguments.FirstOrDefault(t => t.Name == "TPropertyValue")?.Type; // Confirm that the propertyRetriever is a simple lambda (eg. "_ => _.Id") var propertyRetrieverArgument = invocation.ArgumentList.Arguments[indexOfPropertyIdentifierArgument]; IPropertySymbol propertyIfSuccessfullyRetrieved; switch (CommonAnalyser.GetPropertyRetrieverArgumentStatus(propertyRetrieverArgument, context, propertyValueTypeIfKnown, allowReadOnlyProperties: false, propertyIfSuccessfullyRetrieved: out propertyIfSuccessfullyRetrieved)) { case CommonAnalyser.PropertyValidationResult.Ok: case CommonAnalyser.PropertyValidationResult.UnableToConfirmOrDeny: return; case CommonAnalyser.PropertyValidationResult.IndirectTargetAccess: context.ReportDiagnostic(Diagnostic.Create( IndirectTargetAccessorAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.NotSimpleLambdaExpression: case CommonAnalyser.PropertyValidationResult.LambdaDoesNotTargetProperty: context.ReportDiagnostic(Diagnostic.Create( SimplePropertyAccessorArgumentAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.MissingGetter: context.ReportDiagnostic(Diagnostic.Create( SimplePropertyAccessorArgumentAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.GetterHasBridgeAttributes: case CommonAnalyser.PropertyValidationResult.SetterHasBridgeAttributes: context.ReportDiagnostic(Diagnostic.Create( BridgeAttributeAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.IsReadOnly: context.ReportDiagnostic(Diagnostic.Create( ReadOnlyPropertyAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.PropertyIsOfMoreSpecificTypeThanSpecificValueType: // propertyIfSuccessfullyRetrieved and propertyValueTypeIfKnown will both be non-null if PropertyIsOfMoreSpecificTypeThanSpecificValueType was returned // (since it would not be possible to ascertain that that response is appropriate without being able to compare the two values) context.ReportDiagnostic(Diagnostic.Create( PropertyMayNotBeSetToInstanceOfLessSpecificTypeRule, invocation.GetLocation(), propertyIfSuccessfullyRetrieved.GetMethod.ReturnType, // This will always have a value if we got PropertyIsOfMoreSpecificTypeThanSpecificValueType back propertyValueTypeIfKnown.Name )); return; case CommonAnalyser.PropertyValidationResult.MethodParameterWithoutPropertyIdentifierAttribute: context.ReportDiagnostic(Diagnostic.Create( MethodParameterWithoutPropertyIdentifierAttributeRule, propertyRetrieverArgument.GetLocation() )); return; } }
private void LookForIllegalIAmImmutableImplementations(SyntaxNodeAnalysisContext context) { var classDeclaration = context.Node as ClassDeclarationSyntax; if (classDeclaration == null) { return; } // Only bother looking this up (which is relatively expensive) if we know that we have to var classImplementIAmImmutable = new Lazy <bool>(() => CommonAnalyser.ImplementsIAmImmutable(context.SemanticModel.GetDeclaredSymbol(classDeclaration))); var publicMutableFields = classDeclaration.ChildNodes() .OfType <FieldDeclarationSyntax>() .Where(field => field.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.PublicKeyword))) .Where(field => !field.Modifiers.Any(modifier => modifier.IsKind(SyntaxKind.ReadOnlyKeyword))); foreach (var publicMutableField in publicMutableFields) { if (classImplementIAmImmutable.Value) { context.ReportDiagnostic(Diagnostic.Create( MayNotHavePublicNonReadOnlyFieldsRule, publicMutableField.GetLocation(), string.Join(", ", publicMutableField.Declaration.Variables.Select(variable => variable.Identifier.Text)) )); } } // When the "With" methods updates create new instances, the existing instance is cloned and the target property updated - the constructor is not called on the // new instance, which means that any validation in there is bypassed. I don't think that there's sufficient information available at runtime (in JavaScript) to // create a new instance by calling the constructor instead of using this approach so, instead, validation is not allowed in the constructor - only "CtorSet" // calls are acceptable with an optional "Validate" call that may appear at the end of the constructor. If this "Validate" method exists then it will be called // after each "With" call in order to allow validation to be performed after each property update. The "Validate" method must have no parameters but may have // any accessibility (private probably makes most sense). var constructorsThatShouldUseValidateMethodIfClassImplementsIAmImmutable = new List <ConstructorDeclarationSyntax>(); var instanceConstructors = classDeclaration.ChildNodes() .OfType <ConstructorDeclarationSyntax>() .Where(constructor => (constructor.Body != null)) // If the code is in an invalid state then the Body property might be null - safe to ignore .Where(constructor => !constructor.Modifiers.Any(modifier => modifier.Kind() == SyntaxKind.StaticKeyword)); foreach (var instanceConstructor in instanceConstructors) { var constructorShouldUseValidateMethodIfClassImplementsIAmImmutable = false; var constructorChildNodes = instanceConstructor.Body.ChildNodes().ToArray(); foreach (var node in constructorChildNodes.Select((childNode, i) => new { Node = childNode, IsLastNode = i == (constructorChildNodes.Length - 1) })) { var expressionStatement = node.Node as ExpressionStatementSyntax; if (expressionStatement == null) { constructorShouldUseValidateMethodIfClassImplementsIAmImmutable = true; break; } var invocation = expressionStatement.Expression as InvocationExpressionSyntax; if (invocation != null) { if (InvocationIsCtorSetCall(invocation, context) || (node.IsLastNode && InvocationIsAllowableValidateCall(invocation, context))) { continue; } } constructorShouldUseValidateMethodIfClassImplementsIAmImmutable = true; } if (constructorShouldUseValidateMethodIfClassImplementsIAmImmutable) { constructorsThatShouldUseValidateMethodIfClassImplementsIAmImmutable.Add(instanceConstructor); } } if (constructorsThatShouldUseValidateMethodIfClassImplementsIAmImmutable.Any()) { if (classImplementIAmImmutable.Value) { foreach (var constructorThatShouldUseValidateMethod in constructorsThatShouldUseValidateMethodIfClassImplementsIAmImmutable) { context.ReportDiagnostic(Diagnostic.Create( ConstructorWithLogicOtherThanCtorSetCallsShouldUseValidateMethod, constructorThatShouldUseValidateMethod.GetLocation() )); } } } // If there is a Validate method that should be called and this constructor isn't calling it then warn if (HasValidateMethodThatThisClassMustCall(classDeclaration)) { var constructorsThatNeedToWarnAreNotCallingValidate = instanceConstructors .Except(constructorsThatShouldUseValidateMethodIfClassImplementsIAmImmutable) // Don't warn about any constructors that are already being identified as needing attention .Where(instanceConstructor => // If this constructors calls another of the constructor overloads then don't warn (only warn about constructors that DON'T call another overload) (instanceConstructor.Initializer == null) || (instanceConstructor.Initializer.Kind() != SyntaxKind.ThisConstructorInitializer) ) .Where(instanceConstructor => !instanceConstructor.Body.ChildNodes() .OfType <ExpressionStatementSyntax>() .Select(expressionStatement => expressionStatement.Expression as InvocationExpressionSyntax) .Where(invocation => (invocation != null) && InvocationIsAllowableValidateCall(invocation, context)) .Any() ); if (constructorsThatNeedToWarnAreNotCallingValidate.Any() && classImplementIAmImmutable.Value) { foreach (var constructorThatShouldUseValidateMethod in constructorsThatNeedToWarnAreNotCallingValidate) { context.ReportDiagnostic(Diagnostic.Create( ConstructorDoesNotCallValidateMethod, constructorThatShouldUseValidateMethod.GetLocation(), classDeclaration.Identifier.Text )); } } } // This is likely to be the most expensive work (since it requires lookup of other symbols elsewhere in the solution, whereas the // logic below only look at code in the current file) so only perform it when required (leave it as null until we absolutely need // to know whether the current class implements IAmImmutable or not) foreach (var property in classDeclaration.ChildNodes().OfType <PropertyDeclarationSyntax>()) { if (property.ExplicitInterfaceSpecifier != null) { // Since CtorSet and With can not target properties that are not directly accessible through a reference to the // IAmImmutable-implementing type (because "_ => _.Name" is acceptable as a property retriever but not something // like "_ => ((IWhatever)_).Name") if a property is explicitly implemented for a base interface then the rules // below need not be applied to it. continue; } // If property.ExpressionBody is an ArrowExpressionClauseSyntax then it's C# 6 syntax for a read-only property that returns // a value (which is different to a readonly auto-property, which introduces a backing field behind the scenes, this syntax // doesn't introduce a new backing field, it returns an expression). In this case, there won't be an AccessorList (it will // be null). Diagnostic errorIfAny; if (property.ExpressionBody is ArrowExpressionClauseSyntax) { errorIfAny = Diagnostic.Create( MustHaveSettersOnPropertiesWithGettersAccessRule, property.GetLocation(), property.Identifier.Text ); } else { var getterIfDefined = property.AccessorList.Accessors.FirstOrDefault(a => a.Kind() == SyntaxKind.GetAccessorDeclaration); var setterIfDefined = property.AccessorList.Accessors.FirstOrDefault(a => a.Kind() == SyntaxKind.SetAccessorDeclaration); if ((getterIfDefined != null) && (setterIfDefined == null)) { // If getterIfDefined is non-null but has a null Body then it's an auto-property getter, in which case not having // a setter is allowed since it means that it's a read-only auto-property (for which Bridge will create a property // setter for in the JavaScript) if (getterIfDefined.Body != null) { errorIfAny = Diagnostic.Create( MustHaveSettersOnPropertiesWithGettersAccessRule, property.GetLocation(), property.Identifier.Text ); } else { continue; } } else if ((getterIfDefined != null) && CommonAnalyser.HasDisallowedAttribute(Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetDeclaredSymbol(context.SemanticModel, getterIfDefined))) { errorIfAny = Diagnostic.Create( MayNotHaveBridgeAttributesOnPropertiesWithGettersAccessRule, getterIfDefined.GetLocation(), property.Identifier.Text ); } else if ((setterIfDefined != null) && CommonAnalyser.HasDisallowedAttribute(Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetDeclaredSymbol(context.SemanticModel, setterIfDefined))) { errorIfAny = Diagnostic.Create( MayNotHaveBridgeAttributesOnPropertiesWithGettersAccessRule, setterIfDefined.GetLocation(), property.Identifier.Text ); } else if ((setterIfDefined != null) && IsPublic(property) && !IsPrivateOrProtected(setterIfDefined)) { errorIfAny = Diagnostic.Create( MayNotHavePublicSettersRule, setterIfDefined.GetLocation(), property.Identifier.Text ); } else { continue; } } // Enountered a potential error if the current class implements IAmImmutable - so find out whether it does or not (if it // doesn't then no further work is required and we can exit the entire process early) if (!classImplementIAmImmutable.Value) { return; } context.ReportDiagnostic(errorIfAny); } }
private void LookForIllegalWithCall(SyntaxNodeAnalysisContext context) { var invocation = context.Node as InvocationExpressionSyntax; if (invocation == null) { return; } if ((invocation.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text != "With") { return; } var withMethod = context.SemanticModel.GetSymbolInfo(invocation.Expression).Symbol as IMethodSymbol; if ((withMethod == null) || (withMethod.ContainingAssembly == null) || (withMethod.ContainingAssembly.Name != CommonAnalyser.AnalyserAssemblyName)) { return; } // The GetSymbolInfo call above does some magic so that when the With method is called as extension then it its parameters list // excludes the "this" parameter but when it's NOT called as an extension method then it DOES have the "this" parameter in the // list. So the signature // // T With<T, TPropertyValue>(this T source, Func<T, TPropertyValue> propertyIdentifier, TPropertyValue value) // // may be identified as the "withMethod" reference above and be described as having three arguments if it's called as // // ImmutabilityHelpers.With(x, _ => _.Id, 123) // // but described as only have two arguments if it's called as // // x.With(_ => _.Id, 123) // // This means that we need to look at the withMethod's Parameters set to work out which argument in the current expression's // argument list is the property identifier / property retriever that we're interested in validating var indexOfPropertyIdentifierArgument = withMethod.Parameters .Select((p, i) => new { Index = i, Parameter = p }) .Where(p => p.Parameter.Name == "propertyIdentifier") .Single() .Index; // If the With method signature called is one with a TPropertyValue generic type argument then get that type. We need to pass // this to the GetPropertyRetrieverArgumentStatus method so that it can ensure that we are not casting the property down to a // less specific type, which would allow an instance of that less specific type to be set as a property value. For example, if // "x" is an instance of an IAmImmutable class and it has a "Name" property of type string then the following should not be // allowed: // // x = x.With(_ => _.Name, new object()); // // This will compile (TPropertyValue willl be inferred as "Object") but we don't want to allow it since it will result in the // Name property being assigned a non-string reference. var typeArguments = withMethod.TypeParameters.Zip(withMethod.TypeArguments, (genericTypeParam, type) => new { genericTypeParam.Name, Type = type }); var propertyValueTypeIfKnown = typeArguments.FirstOrDefault(t => t.Name == "TPropertyValue")?.Type; // Confirm that the propertyRetriever is a simple lambda (eg. "_ => _.Id") var propertyRetrieverArgument = invocation.ArgumentList.Arguments[indexOfPropertyIdentifierArgument]; IPropertySymbol propertyIfSuccessfullyRetrieved; switch (CommonAnalyser.GetPropertyRetrieverArgumentStatus(propertyRetrieverArgument, context, propertyValueTypeIfKnown, allowReadOnlyProperties: false, propertyIfSuccessfullyRetrieved: out propertyIfSuccessfullyRetrieved)) { case CommonAnalyser.PropertyValidationResult.Ok: case CommonAnalyser.PropertyValidationResult.UnableToConfirmOrDeny: return; case CommonAnalyser.PropertyValidationResult.IndirectTargetAccess: context.ReportDiagnostic(Diagnostic.Create( IndirectTargetAccessorAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.NotSimpleLambdaExpression: case CommonAnalyser.PropertyValidationResult.LambdaDoesNotTargetProperty: context.ReportDiagnostic(Diagnostic.Create( SimplePropertyAccessorArgumentAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.MissingGetter: context.ReportDiagnostic(Diagnostic.Create( SimplePropertyAccessorArgumentAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.GetterHasBridgeAttributes: case CommonAnalyser.PropertyValidationResult.SetterHasBridgeAttributes: context.ReportDiagnostic(Diagnostic.Create( BridgeAttributeAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.IsReadOnly: context.ReportDiagnostic(Diagnostic.Create( ReadOnlyPropertyAccessRule, propertyRetrieverArgument.GetLocation() )); return; case CommonAnalyser.PropertyValidationResult.PropertyIsOfMoreSpecificTypeThanSpecificValueType: // propertyIfSuccessfullyRetrieved and propertyValueTypeIfKnown will both be non-null if PropertyIsOfMoreSpecificTypeThanSpecificValueType was returned // (since it would not be possible to ascertain that that response is appropriate without being able to compare the two values) context.ReportDiagnostic(Diagnostic.Create( PropertyMayNotBeSetToInstanceOfLessSpecificTypeRule, invocation.GetLocation(), propertyIfSuccessfullyRetrieved.GetMethod.ReturnType, propertyValueTypeIfKnown.Name )); return; case CommonAnalyser.PropertyValidationResult.MethodParameterWithoutPropertyIdentifierAttribute: context.ReportDiagnostic(Diagnostic.Create( MethodParameterWithoutPropertyIdentifierAttributeRule, propertyRetrieverArgument.GetLocation() )); return; } }
private void LookForEmptyConstructorsThatHaveArgumentsOnIAmImmutableImplementations(SyntaxNodeAnalysisContext context) { var classDeclaration = context.Node as ClassDeclarationSyntax; if (classDeclaration == null) { return; } // If it cheaper to look at symbols in the current file than to have to look elsewhere. So, firstly, just check whether the constructor // looks like it may or may not be applicable - if there are no constructor arguments (that aren't passed to a base constructor) or if // the constructor is already populated then do nothing. If there ARE constructor arguments that are not accounted for and the constructor // body is empty, then we need to do more analysis. foreach (var constructor in classDeclaration.ChildNodes().OfType <ConstructorDeclarationSyntax>()) { if (constructor.Body == null) // This implies incomplete content - there's no point trying to analyse it until it compiles { continue; } var constructorArgumentsToCheckFor = GetConstructorArgumentsThatAreNotPassedToBaseConstructor(constructor); if (!constructorArgumentsToCheckFor.Any()) { continue; } // 2018-03-09 DWR: Previously, this analyser/codefix only looked for empty constructors (the idea being that you would write just an // empty constructor and its arguments would be used to populate the rest of the class) but now there is support for adding a new // argument to an existing class and having the codefix fill in whatever is missing - we need to detect the two different scenarios // and raise a rule that is appropriate to whichever has occured (if either) Diagnostic diagnosticToRaise; if (!constructor.Body.ChildNodes().Any()) { diagnosticToRaise = Diagnostic.Create(EmptyConstructorRule, constructor.GetLocation(), classDeclaration.Identifier.Text); } else { var unaccountedForConstructorArguments = GetConstructorArgumentNamesThatAreNotAccountedFor(constructor); if (unaccountedForConstructorArguments.Any()) { diagnosticToRaise = Diagnostic.Create( OutOfSyncConstructorRule, constructor.GetLocation(), classDeclaration.Identifier.Text, string.Join(", ", unaccountedForConstructorArguments.Select(p => p.Identifier.Text)) ); } else { continue; } } // If the class doesn't implement IAmImmutable then we don't need to consider this constructor or any other constructor on it. It may // require looking at other files (if this class derives from another class, which implements IAmImmutable), though, so it makes sense // to only do this check if the constructor otherwise looks promising. if (!CommonAnalyser.ImplementsIAmImmutable(context.SemanticModel.GetDeclaredSymbol(classDeclaration))) { return; } context.ReportDiagnostic(diagnosticToRaise); } }