File: Microsoft.CodeQuality.Analyzers\ApiDesignGuidelines\CollectionPropertiesShouldBeReadOnly.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
    using static MicrosoftCodeQualityAnalyzersResources;

    /// <summary>
    /// CA2227: <inheritdoc cref="CollectionPropertiesShouldBeReadOnlyTitle"/>
    ///
    /// Cause:
    /// An externally visible writable property is a type that implements System.Collections.ICollection.
    /// Arrays, indexers(properties with the name 'Item'), and permission sets are ignored by the rule.
    ///
    /// Description:
    /// A writable collection property allows a user to replace the collection with a completely different collection.
    /// A read-only property stops the collection from being replaced but still allows the individual members to be set.
    /// If replacing the collection is a goal, the preferred design pattern is to include a method to remove all the elements
    /// from the collection and a method to re-populate the collection.See the Clear and AddRange methods of the System.Collections.ArrayList class
    /// for an example of this pattern.
    ///
    /// Both binary and XML serialization support read-only properties that are collections.
    /// The System.Xml.Serialization.XmlSerializer class has specific requirements for types that implement ICollection and
    /// System.Collections.IEnumerable in order to be serializable.
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public class CollectionPropertiesShouldBeReadOnlyAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA2227";

        internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
            RuleId,
            CreateLocalizableResourceString(nameof(CollectionPropertiesShouldBeReadOnlyTitle)),
            CreateLocalizableResourceString(nameof(CollectionPropertiesShouldBeReadOnlyMessage)),
            DiagnosticCategory.Usage,
            RuleLevel.Disabled, // Guidance needs to be improved to be more clear
            description: CreateLocalizableResourceString(nameof(CollectionPropertiesShouldBeReadOnlyDescription)),
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterCompilationStartAction(
                (context) =>
                {
                    var wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(context.Compilation);
                    var knownTypes = new KnownTypes(wellKnownTypeProvider);

                    if (knownTypes.ICollectionType == null ||
                        knownTypes.GenericICollectionType == null ||
                        knownTypes.ArrayType == null)
                    {
                        return;
                    }

                    context.RegisterSymbolAction(c => AnalyzeSymbol(c, knownTypes), SymbolKind.Property);
                });
        }

        private static void AnalyzeSymbol(SymbolAnalysisContext context, KnownTypes knownTypes)
        {
            RoslynDebug.Assert(knownTypes.ICollectionType != null &&
                knownTypes.GenericICollectionType != null &&
                knownTypes.ArrayType != null);

            var property = (IPropertySymbol)context.Symbol;

            // check whether it has a public setter
            IMethodSymbol? setter = property.SetMethod;
            if (setter == null || !setter.IsExternallyVisible())
            {
                return;
            }

            // make sure this property is NOT an indexer
            if (property.IsIndexer)
            {
                return;
            }

            // make sure this property is NOT an init
            if (setter.IsInitOnly)
            {
                return;
            }

            // make sure return type is NOT array
            if (Inherits(property.Type, knownTypes.ArrayType))
            {
                return;
            }

            // make sure property type implements ICollection or ICollection<T>
            if (!Inherits(property.Type, knownTypes.ICollectionType) && !Inherits(property.Type, knownTypes.GenericICollectionType))
            {
                return;
            }

            // exclude Immutable collections
            // see https://github.com/dotnet/roslyn-analyzers/issues/1900 for details
            if (!knownTypes.ImmutableInterfaces.IsEmpty &&
                property.Type.AllInterfaces.Any(i => knownTypes.ImmutableInterfaces.Contains(i.OriginalDefinition)))
            {
                return;
            }

            // exclude readonly collections
            if (SymbolEqualityComparer.Default.Equals(property.Type.OriginalDefinition, knownTypes.ReadonlyCollection) ||
                SymbolEqualityComparer.Default.Equals(property.Type.OriginalDefinition, knownTypes.ReadonlyDictionary) ||
                SymbolEqualityComparer.Default.Equals(property.Type.OriginalDefinition, knownTypes.ReadonlyObservableCollection))
            {
                return;
            }

            // Special case: the DataContractSerializer requires that a public setter exists.
            if (property.HasAnyAttribute(knownTypes.DataMemberAttribute))
            {
                return;
            }

            if (property.IsImplementationOfAnyInterfaceMember())
            {
                return;
            }

            context.ReportDiagnostic(property.CreateDiagnostic(Rule, property.Name));
        }

        private static bool Inherits(ITypeSymbol symbol, ITypeSymbol baseType)
        {
            Debug.Assert(SymbolEqualityComparer.Default.Equals(baseType, baseType.OriginalDefinition));
            return symbol?.OriginalDefinition.Inherits(baseType) ?? false;
        }

        private sealed class KnownTypes
        {
            public KnownTypes(WellKnownTypeProvider wellKnownTypeProvider)
            {
                ICollectionType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsICollection);
                GenericICollectionType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsGenericICollection1);
                ArrayType = wellKnownTypeProvider.Compilation.GetSpecialType(SpecialType.System_Array);
                DataMemberAttribute = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeSerializationDataMemberAttribute);
                ImmutableInterfaces = GetIImmutableInterfaces(wellKnownTypeProvider);
                ReadonlyCollection = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsObjectModelReadOnlyCollection1);
                ReadonlyDictionary = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsObjectModelReadOnlyDictionary2);
                ReadonlyObservableCollection = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsObjectModelReadOnlyObservableCollection1);
            }

            public INamedTypeSymbol? ICollectionType { get; }
            public INamedTypeSymbol? GenericICollectionType { get; }
            public INamedTypeSymbol? ArrayType { get; }
            public INamedTypeSymbol? DataMemberAttribute { get; }
            public ImmutableHashSet<INamedTypeSymbol> ImmutableInterfaces { get; }
            public INamedTypeSymbol? ReadonlyCollection { get; }
            public INamedTypeSymbol? ReadonlyDictionary { get; }
            public INamedTypeSymbol? ReadonlyObservableCollection { get; }

            private static ImmutableHashSet<INamedTypeSymbol> GetIImmutableInterfaces(WellKnownTypeProvider wellKnownTypeProvider)
            {
                var builder = ImmutableHashSet.CreateBuilder<INamedTypeSymbol>();
                builder.AddIfNotNull(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsImmutableIImmutableDictionary2));
                builder.AddIfNotNull(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsImmutableIImmutableList1));
                builder.AddIfNotNull(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsImmutableIImmutableQueue1));
                builder.AddIfNotNull(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsImmutableIImmutableSet1));
                builder.AddIfNotNull(wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemCollectionsImmutableIImmutableStack1));
                return builder.ToImmutable();
            }
        }
    }
}