File: DefaultableTypeShouldHaveDefaultableFieldsAnalyzer.cs
Web Access
Project: src\src\RoslynAnalyzers\Roslyn.Diagnostics.Analyzers\Core\Roslyn.Diagnostics.Analyzers.csproj (Roslyn.Diagnostics.Analyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable warnings
 
using System.Collections.Concurrent;
using System.Collections.Immutable;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
 
namespace Roslyn.Diagnostics.Analyzers
{
    using static RoslynDiagnosticsAnalyzersResources;
 
    /// <summary>
    /// RS0040: <inheritdoc cref="DefaultableTypeShouldHaveDefaultableFieldsTitle"/>
    /// </summary>
#pragma warning disable RS1004 // Recommend adding language support to diagnostic analyzer
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
#pragma warning restore RS1004 // Recommend adding language support to diagnostic analyzer
    public class DefaultableTypeShouldHaveDefaultableFieldsAnalyzer : DiagnosticAnalyzer
    {
        internal static readonly DiagnosticDescriptor Rule = new(
            RoslynDiagnosticIds.DefaultableTypeShouldHaveDefaultableFieldsRuleId,
            CreateLocalizableResourceString(nameof(DefaultableTypeShouldHaveDefaultableFieldsTitle)),
            CreateLocalizableResourceString(nameof(DefaultableTypeShouldHaveDefaultableFieldsMessage)),
            DiagnosticCategory.RoslynDiagnosticsReliability,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            description: CreateLocalizableResourceString(nameof(DefaultableTypeShouldHaveDefaultableFieldsDescription)),
            helpLinkUri: null,
            customTags: WellKnownDiagnosticTagsExtensions.Telemetry);
 
        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 nonDefaultableAttribute = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.RoslynUtilitiesNonDefaultableAttribute);
                if (nonDefaultableAttribute is null)
                    return;
 
                var knownNonDefaultableTypes = new ConcurrentDictionary<ITypeSymbol, bool>();
                context.RegisterSymbolAction(context => AnalyzeField(context, nonDefaultableAttribute, knownNonDefaultableTypes), SymbolKind.Field);
                context.RegisterSymbolAction(context => AnalyzeNamedType(context, nonDefaultableAttribute, knownNonDefaultableTypes), SymbolKind.NamedType);
            });
        }
 
        private static void AnalyzeField(SymbolAnalysisContext context, INamedTypeSymbol nonDefaultableAttribute, ConcurrentDictionary<ITypeSymbol, bool> knownNonDefaultableTypes)
        {
            AnalyzeField(context, (IFieldSymbol)context.Symbol, nonDefaultableAttribute, knownNonDefaultableTypes);
        }
 
        private static void AnalyzeNamedType(SymbolAnalysisContext context, INamedTypeSymbol nonDefaultableAttribute, ConcurrentDictionary<ITypeSymbol, bool> knownNonDefaultableTypes)
        {
            var namedType = (INamedTypeSymbol)context.Symbol;
            foreach (var member in namedType.GetMembers())
            {
                if (member.Kind != SymbolKind.Field)
                    continue;
 
                if (!member.IsImplicitlyDeclared)
                    continue;
 
                AnalyzeField(context, (IFieldSymbol)member, nonDefaultableAttribute, knownNonDefaultableTypes);
            }
        }
 
        private static void AnalyzeField(SymbolAnalysisContext originalContext, IFieldSymbol field, INamedTypeSymbol nonDefaultableAttribute, ConcurrentDictionary<ITypeSymbol, bool> knownNonDefaultableTypes)
        {
            if (field.IsStatic)
                return;
 
            var containingType = field.ContainingType;
            if (containingType.TypeKind != TypeKind.Struct)
                return;
 
            if (!IsDefaultable(containingType, nonDefaultableAttribute, knownNonDefaultableTypes))
            {
                // A non-defaultable type is allowed to have fields of any type
                return;
            }
 
            if (IsDefaultable(field.Type, nonDefaultableAttribute, knownNonDefaultableTypes))
            {
                // Any type is allowed to have defaultable fields
                return;
            }
 
#pragma warning disable RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer
            var semanticModel = originalContext.Compilation.GetSemanticModel(field.Locations[0].SourceTree);
#pragma warning restore RS1030 // Do not invoke Compilation.GetSemanticModel() method within a diagnostic analyzer
            if (!semanticModel.GetNullableContext(field.Locations[0].SourceSpan.Start).WarningsEnabled())
            {
                // Warnings are not enabled for this field
                return;
            }
 
            var sourceSymbol = (field.IsImplicitlyDeclared ? field.AssociatedSymbol : null) ?? field;
            originalContext.ReportDiagnostic(field.CreateDiagnostic(Rule, field.ContainingType, sourceSymbol.Name));
        }
 
        private static bool IsDefaultable(ITypeSymbol type, INamedTypeSymbol nonDefaultableAttribute, ConcurrentDictionary<ITypeSymbol, bool> knownNonDefaultableTypes)
        {
            switch (type.TypeKind)
            {
                case TypeKind.Class:
                case TypeKind.Interface:
                case TypeKind.Delegate:
                    return type.NullableAnnotation != NullableAnnotation.NotAnnotated;
 
                case TypeKind.Enum:
                    return true;
 
                case TypeKind.Struct:
                    if (type is not INamedTypeSymbol namedType)
                        return true;
 
                    if (knownNonDefaultableTypes.TryGetValue(namedType, out var isNonDefaultable))
                        return !isNonDefaultable;
 
                    isNonDefaultable = namedType.HasAnyAttribute(nonDefaultableAttribute);
                    return !knownNonDefaultableTypes.GetOrAdd(namedType, isNonDefaultable);
 
                case TypeKind.TypeParameter:
                    if (knownNonDefaultableTypes.TryGetValue(type, out isNonDefaultable))
                        return !isNonDefaultable;
 
                    isNonDefaultable = type.HasAnyAttribute(nonDefaultableAttribute);
                    return !knownNonDefaultableTypes.GetOrAdd(type, isNonDefaultable);
 
                case TypeKind.Unknown:
                case TypeKind.Array:
                case TypeKind.Dynamic:
                case TypeKind.Error:
                case TypeKind.Module:
                case TypeKind.Pointer:
                case TypeKind.Submission:
                default:
                    return true;
            }
        }
    }
}