|
// 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.Threading;
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.Operations;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.MakeStructMemberReadOnly;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpMakeStructMemberReadOnlyDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
public CSharpMakeStructMemberReadOnlyDiagnosticAnalyzer()
: base(IDEDiagnosticIds.MakeStructMemberReadOnlyDiagnosticId,
EnforceOnBuildValues.MakeStructMemberReadOnly,
CSharpCodeStyleOptions.PreferReadOnlyStructMember,
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Make_member_readonly), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Member_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.CSharp8)
return;
context.RegisterSymbolStartAction(context =>
{
// Only run on non-readonly structs. If the struct is already readonly, no need to make the members readonly.
if (context.Symbol is not INamedTypeSymbol
{
TypeKind: TypeKind.Struct,
IsReadOnly: false,
DeclaringSyntaxReferences: [var reference, ..],
} structType)
{
return;
}
var cancellationToken = context.CancellationToken;
var declaration = reference.GetSyntax(cancellationToken);
var options = context.GetCSharpAnalyzerOptions(declaration.SyntaxTree);
var option = options.PreferReadOnlyStructMember;
if (!option.Value)
return;
context.RegisterOperationBlockAction(
context => AnalyzeBlock(context, option.Notification.Severity));
}, SymbolKind.NamedType);
});
private void AnalyzeBlock(
OperationBlockAnalysisContext context,
ReportDiagnostic severity)
{
var cancellationToken = context.CancellationToken;
// if it's not a method, or it's already readonly, nothing to do.
if (context.OwningSymbol is not IMethodSymbol
{
MethodKind: MethodKind.Ordinary or MethodKind.ExplicitInterfaceImplementation or MethodKind.PropertyGet or MethodKind.PropertySet,
IsReadOnly: false,
IsStatic: false,
IsImplicitlyDeclared: false,
DeclaringSyntaxReferences: [var methodReference, ..],
} owningMethod)
{
return;
}
foreach (var blockOperation in context.OperationBlocks)
{
// If we have a trivial method that is just `{ throw ... }` or `=> throw ...`, then do not bother
// analyzing/reporting that it could be made 'readonly'. These members are likely just being written (or
// have been generated) and spamming the user with notifications to go change these all is unhelpful.
if (blockOperation is IBlockOperation { Operations: [IThrowOperation or IExpressionStatementOperation { Operation: IThrowOperation }] })
return;
if (BlockOperationPotentiallyMutatesThis(owningMethod, blockOperation, cancellationToken))
return;
}
var declaration = methodReference.GetSyntax(cancellationToken);
var nameToken = declaration switch
{
MethodDeclarationSyntax methodDeclaration => methodDeclaration.Identifier,
AccessorDeclarationSyntax accessorDeclaration => accessorDeclaration.Keyword,
PropertyDeclarationSyntax propertyDeclaration => propertyDeclaration.Identifier,
IndexerDeclarationSyntax indexerDeclaration => indexerDeclaration.ThisKeyword,
ArrowExpressionClauseSyntax arrowExpression => arrowExpression.ArrowToken,
_ => (SyntaxToken?)null
};
if (declaration is ArrowExpressionClauseSyntax)
declaration = declaration.GetRequiredParent();
if (nameToken is null)
return;
context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
nameToken.Value.GetLocation(),
severity,
additionalLocations: ImmutableArray.Create(declaration.GetLocation()),
properties: null));
}
private static bool BlockOperationPotentiallyMutatesThis(
IMethodSymbol owningMethod,
IOperation blockOperation,
CancellationToken cancellationToken)
{
var semanticModel = blockOperation.SemanticModel;
Contract.ThrowIfNull(semanticModel);
foreach (var operation in blockOperation.DescendantsAndSelf())
{
// Do not suggest making containing method readonly until we have full understanding of it.
if (operation is IInvalidOperation)
return true;
if (operation is IInstanceReferenceOperation instanceOperation &&
InstanceReferencePotentiallyMutatesThis(semanticModel, owningMethod, instanceOperation, cancellationToken))
{
return true;
}
}
return false;
}
private static bool IsPotentiallyValueType(IOperation? instance)
{
// 1. A struct is a value type.
// 2. A type paramater that does not have the explicit 'class' constraint is potentially a value type.
return instance is { Type.TypeKind: TypeKind.Struct } ||
instance is { Type: ITypeParameterSymbol { HasReferenceTypeConstraint: false } };
}
private static bool InstanceReferencePotentiallyMutatesThis(
SemanticModel semanticModel,
IMethodSymbol owningMethod,
IInstanceReferenceOperation instanceOperation,
CancellationToken cancellationToken)
{
// if we have an explicit 'this' in code, and we're overwriting it directly (e.g. `ref this` or `this = ...`
// then can't make this `readonly`.
if (!instanceOperation.IsImplicit &&
CSharpSemanticFacts.Instance.IsWrittenTo(semanticModel, instanceOperation.Syntax, cancellationToken))
{
return true;
}
// Now walk up the instance-operation and see if any operation actually or potentially mutates this value.
for (var operation = instanceOperation.Parent; operation != null; operation = operation.Parent)
{
if (operation is IFieldReferenceOperation { Field.IsReadOnly: false } fieldReference &&
IsPotentiallyValueType(fieldReference.Instance))
{
// If we're writing to a field off of 'this'. Can't make this `readonly`.
if (CSharpSemanticFacts.Instance.IsWrittenTo(semanticModel, fieldReference.Syntax, cancellationToken))
return true;
// otherwise, keeping walking upwards to make sure subsequent accesses off this field are ok.
continue;
}
if (operation is IPropertyReferenceOperation propertyReference &&
IsPotentiallyValueType(propertyReference.Instance))
{
// If we're writing to a prop off of 'this'. Can't make this `readonly`.
if (CSharpSemanticFacts.Instance.IsWrittenTo(semanticModel, propertyReference.Syntax, cancellationToken))
return true;
// If we're reading, that's only ok if we know the get-accessor exists and it is itself readonly.
// Otherwise it could mutate the value.
if (propertyReference.Property.GetMethod is null ||
!propertyReference.Property.GetMethod.IsReadOnly)
{
return true;
}
// a safe property reference. Can stop looking upwards as this cannot return a value that could
// mutate this value.
return false;
}
if (operation is IEventReferenceOperation eventReference &&
IsPotentiallyValueType(eventReference.Instance))
{
// += or -= on an event off of a struct will cause a copy if we become readonly.
if (operation.Parent is IEventAssignmentOperation)
return true;
// a safe event reference. Can stop looking upwards as this cannot return a value that could
// mutate this value.
return false;
}
// See if we're accessing or invoking a method.
if (operation is IMethodReferenceOperation methodRefOperation)
{
// Either a mutating or not mutating method reference. Regardless, once we examine it, we're done
// looking up as the method itself cannot return anything that could mutate this.
return IsPotentiallyMutatingMethod(owningMethod, methodRefOperation.Instance, methodRefOperation.Method);
}
if (operation is IInvocationOperation invocationOperation)
{
// Either a mutating or not mutating method reference. Regardless, once we examine it, we're done
// looking up as the method itself cannot return anything that could mutate this.
return IsPotentiallyMutatingMethod(owningMethod, invocationOperation.Instance, invocationOperation.TargetMethod);
}
// Wasn't something that mutates this instance. Go onto the next instance expression.
break;
}
return false;
}
private static bool IsPotentiallyMutatingMethod(
IMethodSymbol owningMethod,
IOperation? instance,
IMethodSymbol methodReference)
{
if (!IsPotentiallyValueType(instance))
return false;
// Calling a readonly method off of a struct is fine since we know it can't mutate.
if (methodReference.IsReadOnly)
return false;
// If we're referencing the method we're in (Which isn't readonly yet) we don't want to mark us as
// not-readonly. a recursive call shouldn't impact the final result.
if (methodReference.Equals(owningMethod))
return false;
// Any methods from System.Object or System.ValueType called on this `this` don't stop this from being readonly.
if (methodReference.ContainingType.SpecialType is SpecialType.System_Object or SpecialType.System_ValueType)
return false;
// Any other non-readonly method usage on this means we can't be readonly.
return true;
}
}
|