File: CSharpMakeStructReadOnlyDiagnosticAnalyzer.cs
Web Access
Project: ..\..\..\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle)
// 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.
 
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Options;
 
namespace Microsoft.CodeAnalysis.CSharp.MakeStructReadOnly;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpMakeStructReadOnlyDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
    public CSharpMakeStructReadOnlyDiagnosticAnalyzer()
        : base(IDEDiagnosticIds.MakeStructReadOnlyDiagnosticId,
               EnforceOnBuildValues.MakeStructReadOnly,
               CSharpCodeStyleOptions.PreferReadOnlyStruct,
               new LocalizableResourceString(nameof(CSharpAnalyzersResources.Make_struct_readonly), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
               new LocalizableResourceString(nameof(CSharpAnalyzersResources.Struct_can_be_made_readonly), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
    {
    }
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
    protected override void InitializeWorker(AnalysisContext context)
        => context.RegisterCompilationStartAction(context =>
        {
            var compilation = context.Compilation;
            if (compilation.LanguageVersion() < LanguageVersion.CSharp7_2)
                return;
 
            context.RegisterSymbolStartAction(context =>
            {
                // First, see if this at least strongly looks like a struct that could be converted.
                if (!IsCandidate(context, out var typeDeclaration, out var option))
                    return;
 
                // Looks good.  However, we have to make sure that the struct has no code which actually overwrites 'this'
 
                var writesToThis = false;
                context.RegisterSyntaxNodeAction(context =>
                {
                    var semanticModel = context.SemanticModel;
                    var thisExpression = (ThisExpressionSyntax)context.Node;
                    var cancellationToken = context.CancellationToken;
                    writesToThis = writesToThis || thisExpression.IsWrittenTo(semanticModel, cancellationToken);
                }, SyntaxKind.ThisExpression);
 
                context.RegisterSymbolEndAction(context =>
                {
                    // if we wrote to 'this', then we cannot convert this struct.
                    if (writesToThis)
                        return;
 
                    context.ReportDiagnostic(DiagnosticHelper.Create(
                        Descriptor,
                        typeDeclaration.Identifier.GetLocation(),
                        option.Notification.Severity,
                        additionalLocations: ImmutableArray.Create(typeDeclaration.GetLocation()),
                        properties: null));
                });
            }, SymbolKind.NamedType);
        });
 
    private static bool IsCandidate(
        SymbolStartAnalysisContext context,
        [NotNullWhen(true)] out TypeDeclarationSyntax? typeDeclaration,
        [NotNullWhen(true)] out CodeStyleOption2<bool>? option)
    {
        var typeSymbol = (INamedTypeSymbol)context.Symbol;
        var cancellationToken = context.CancellationToken;
 
        typeDeclaration = null;
        option = null;
        if (typeSymbol.TypeKind is not TypeKind.Struct)
            return false;
 
        if (typeSymbol.IsReadOnly)
            return false;
 
        if (typeSymbol.DeclaringSyntaxReferences.Length == 0)
            return false;
 
        typeDeclaration = typeSymbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken) as TypeDeclarationSyntax;
        if (typeDeclaration is null)
            return false;
 
        var options = context.GetCSharpAnalyzerOptions(typeDeclaration.SyntaxTree);
        option = options.PreferReadOnlyStruct;
        if (!option.Value)
            return false;
 
        // Now, ensure we have at least one field and that all fields are readonly.
        var hasField = false;
        foreach (var member in typeSymbol.GetMembers())
        {
            if (member is IFieldSymbol field)
            {
                hasField = true;
                if (!field.IsReadOnly)
                    return false;
            }
        }
 
        if (!hasField)
            return false;
 
        return true;
    }
}