/// <summary> /// For a field/property assignment, figure out what needs to be checked for it. /// </summary> /// <param name="assignment">The assignment syntax for the field/property (possibly null)</param> /// <param name="query">The query to do (if the return value is AssignmentQueryKind.ImmutabilityQuery.)</param> /// <param name="diagnostic">A diagnostic to report (if the return value is AssignmentQueryKind.Hopeless.)</param> /// <returns>The details about what to check for this assignment</returns> private AssignmentQueryKind GetQueryForAssignment( AssignmentInfo assignment, out ImmutabilityQuery query, out Diagnostic diagnostic ) { // When we have an assignment we use it to narrow our check, e.g. // // private readonly object m_lock = new object(); // // is safe even though object (the type of the field) in general is not. query = default; diagnostic = null; // Easy cases switch (assignment.Expression.Kind()) { case SyntaxKind.NullLiteralExpression: // Sometimes people explicitly (but needlessly) assign null in // an initializer... this is safe. return(AssignmentQueryKind.NothingToCheck); case SyntaxKind.SimpleLambdaExpression: case SyntaxKind.ParenthesizedLambdaExpression: if (assignment.IsInitializer) { // Lambda initializers for readonly members are safe // because they can only close over other members, // which will be checked independently, or static // members of another class, which are also analyzed. return(AssignmentQueryKind.NothingToCheck); } // But for assignments inside a constructor we're in more // trouble. They could capture arbitrary state from their // lexical scope. // static functions can't capture state. if (assignment.Expression.IsStaticFunction()) { return(AssignmentQueryKind.NothingToCheck); } // In general we must panic. diagnostic = Diagnostic.Create( Diagnostics.AnonymousFunctionsMayCaptureMutability, assignment.Expression.GetLocation() ); return(AssignmentQueryKind.Hopeless); case SyntaxKind.InvocationExpression: // Some methods are known to have return values that are // immutable (such as Enumerable.Empty()). // These should be considered immutable by the Analyzer. SemanticModel semanticModel = m_compilation.GetSemanticModel(assignment.Expression.SyntaxTree); if (semanticModel .GetSymbolInfo(assignment.Expression) .Symbol is not IMethodSymbol methodSymbol ) { break; } if (m_context.IsReturnValueKnownToBeImmutable(methodSymbol)) { return(AssignmentQueryKind.NothingToCheck); } break; } // If nothing above was caught, then fallback to querying. if (assignment.Expression is BaseObjectCreationExpressionSyntax _) { // When we have a new T() we don't need to worry about the value // being anything other than an instance of T. query = new ImmutabilityQuery( ImmutableTypeKind.Instance, type: assignment.AssignedType ); } else { // In general we need to handle subtypes. query = new ImmutabilityQuery( ImmutableTypeKind.Total, type: assignment.AssignedType ); } return(AssignmentQueryKind.ImmutabilityQuery); }
/// <summary> /// Determines if a type is known to be immutable. /// </summary> /// <param name="query">The type to check (and what kind of check to do.)</param> /// <param name="diag">If this method returns false, an explaination for why its not known to be immutable.</param> /// <returns>Is the type immutable?</returns> public bool IsImmutable( ImmutabilityQuery query, Func <Location> getLocation, out Diagnostic diagnostic ) { if (query.Kind == ImmutableTypeKind.None) { throw new ArgumentException( "ImmutabilityKind.None is not a valid question to ask this function", nameof(query.Kind) ); } diagnostic = null; // Things like int are totally OK if (m_totallyImmutableSpecialTypes.Contains(query.Type.SpecialType)) { return(true); } // "new object()" are always immutable (and that's the only // constructor for System.Object) but in general things of type // System.Object (any reference type) may be mutable. // // This is hard-coded (rather than in m_extraImmutableTypes) to // avoid the ITypeSymbol lookup. if (query.Kind == ImmutableTypeKind.Instance && query.Type.SpecialType == SpecialType.System_Object) { return(true); } if (query.Type is INamedTypeSymbol namedType) { ImmutableTypeInfo info = GetImmutableTypeInfo(namedType); if (info.Kind.HasFlag(query.Kind)) { return(info.IsImmutableDefinition( context: this, definition: namedType, getLocation: getLocation, out diagnostic )); } } switch (query.Type.TypeKind) { case TypeKind.Error: // Just say this is fine -- there is some other compiler // error in this case and we don't need to pile on. return(true); case TypeKind.Enum: // Enums are like ints -- always immutable return(true); case TypeKind.Array: diagnostic = Diagnostic.Create( Diagnostics.ArraysAreMutable, getLocation(), (query.Type as IArrayTypeSymbol).ElementType.Name ); return(false); case TypeKind.Delegate: diagnostic = Diagnostic.Create( Diagnostics.DelegateTypesPossiblyMutable, getLocation() ); return(false); case TypeKind.Dynamic: diagnostic = Diagnostic.Create( Diagnostics.DynamicObjectsAreMutable, getLocation() ); return(false); case TypeKind.TypeParameter: if (GetImmutabilityFromAttributes(query.Type).HasFlag(ImmutableTypeKind.Total)) { return(true); } if (query.Type is ITypeParameterSymbol tp && m_conditionalTypeParameters.Contains(tp)) { return(true); } diagnostic = Diagnostic.Create( Diagnostics.TypeParameterIsNotKnownToBeImmutable, getLocation(), query.Type.ToDisplayString() ); return(false); case TypeKind.Class: diagnostic = Diagnostic.Create( Diagnostics.NonImmutableTypeHeldByImmutable, getLocation(), query.Type.TypeKind.ToString().ToLower(), query.Type.ToDisplayString(), query.Kind == ImmutableTypeKind.Instance && !query.Type.IsSealed ? " (or [ImmutableBaseClass])" : "" ); return(false); case TypeKind.Interface: case TypeKind.Struct: diagnostic = Diagnostic.Create( Diagnostics.NonImmutableTypeHeldByImmutable, getLocation(), query.Type.TypeKind.ToString().ToLower(), query.Type.ToDisplayString(), "" ); return(false); case TypeKind.Unknown: default: diagnostic = Diagnostic.Create( Diagnostics.UnexpectedTypeKind, location: getLocation(), query.Type.Kind ); return(false); } }