Exemple #1
0
        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()
                                         ));
        }
Exemple #2
0
        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));
        }
Exemple #3
0
        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;
            }
        }
Exemple #4
0
        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()
                                             ));
            }
        }
Exemple #5
0
        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;
            }
        }
Exemple #6
0
        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);
            }
        }