-
Notifications
You must be signed in to change notification settings - Fork 470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ConstructorParamShouldMatchPropNames analyzer #4877
base: main
Are you sure you want to change the base?
Changes from 55 commits
3dedbb2
6dc3ef9
6b71577
52360bb
d9308a4
700c4ac
d3769d6
5c82803
43ad9b1
41d44b2
e3db946
e9bc939
481f5ed
93c0429
3efee13
2b49119
c2051bf
e9aa041
9579d93
5a62547
33dccdd
57437ba
727423b
6c83c6e
2482c4c
d4bec8b
9089e0a
5aff394
5fba883
cd373f6
1fb8c27
da07af5
0c3e2a9
2309cb0
7285658
52dce8e
c603b66
afb760a
4d30c8f
641721d
65ea1ad
defb3c2
d4345d5
2008be6
c0687c6
313bdfd
fa0fd41
4d4a1ec
e059159
a95f803
1e62744
1e1d382
9a727e4
8dacfcb
ab492ca
b1deb98
2008bfe
194c3a2
a487621
f38390d
051d067
f86bf43
35b2346
aafe0de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
; Please do not edit this file manually, it should only be updated through code fix application. | ||
### New Rules | ||
Rule ID | Category | Severity | Notes | ||
--------|----------|----------|------- | ||
CA1071 | Design | Warning | ConstructorParametersShouldMatchPropertyNamesAnalyzer, [Documentation](https://docs.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1071) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Immutable; | ||
using System.Composition; | ||
using System.Globalization; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CodeActions; | ||
using Microsoft.CodeAnalysis.CodeFixes; | ||
using Microsoft.CodeAnalysis.Rename; | ||
using Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines; | ||
|
||
namespace Microsoft.NetCore.Analyzers.Runtime | ||
{ | ||
/// <summary> | ||
/// CA1071: Constructor parameters should match property and field names. | ||
/// Based on <see cref="ParameterNamesShouldMatchBaseDeclarationFixer"/>. | ||
/// </summary> | ||
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] | ||
public sealed class ConstructorParametersShouldMatchPropertyAndFieldNamesFixer : CodeFixProvider | ||
{ | ||
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(ConstructorParametersShouldMatchPropertyAndFieldNamesAnalyzer.RuleId); | ||
|
||
public sealed override FixAllProvider GetFixAllProvider() | ||
{ | ||
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers | ||
return WellKnownFixAllProviders.BatchFixer; | ||
} | ||
|
||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) | ||
{ | ||
SyntaxNode syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); | ||
SemanticModel semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); | ||
|
||
foreach (Diagnostic diagnostic in context.Diagnostics) | ||
{ | ||
SyntaxNode node = syntaxRoot.FindNode(context.Span); | ||
ISymbol declaredSymbol = semanticModel.GetDeclaredSymbol(node, context.CancellationToken); | ||
|
||
if (declaredSymbol.Kind != SymbolKind.Parameter) | ||
{ | ||
continue; | ||
} | ||
|
||
// This approach is very naive. Most likely we want to support NamingStyleOptions, available in Roslyn. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To Do: remove |
||
string newName = LowerFirstLetter(diagnostic.Properties[ConstructorParametersShouldMatchPropertyAndFieldNamesAnalyzer.ReferencedFieldOrPropertyNameKey]); | ||
|
||
context.RegisterCodeFix( | ||
CodeAction.Create( | ||
string.Format(CultureInfo.CurrentCulture, MicrosoftNetCoreAnalyzersResources.ConstructorParametersShouldMatchPropertyOrFieldNamesTitle, newName), | ||
cancellationToken => GetUpdatedDocumentForParameterRenameAsync(context.Document, declaredSymbol, newName, cancellationToken), | ||
nameof(ConstructorParametersShouldMatchPropertyAndFieldNamesFixer)), | ||
diagnostic); | ||
} | ||
} | ||
|
||
private static string LowerFirstLetter(string targetName) | ||
{ | ||
return $"{targetName[0].ToString().ToLower(CultureInfo.CurrentCulture)}{targetName[1..]}"; | ||
} | ||
|
||
private static async Task<Document> GetUpdatedDocumentForParameterRenameAsync(Document document, ISymbol parameter, string newName, CancellationToken cancellationToken) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it duplicates a method of |
||
{ | ||
Solution newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, parameter, newName, null, cancellationToken).ConfigureAwait(false); | ||
return newSolution.GetDocument(document.Id)!; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,213 @@ | ||||||||||
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||||||||||
|
||||||||||
using System; | ||||||||||
using System.Collections.Immutable; | ||||||||||
using System.Diagnostics.CodeAnalysis; | ||||||||||
using System.Linq; | ||||||||||
using Analyzer.Utilities; | ||||||||||
using Analyzer.Utilities.Extensions; | ||||||||||
using Microsoft.CodeAnalysis; | ||||||||||
using Microsoft.CodeAnalysis.Diagnostics; | ||||||||||
using Microsoft.CodeAnalysis.Operations; | ||||||||||
|
||||||||||
namespace Microsoft.NetCore.Analyzers.Runtime | ||||||||||
{ | ||||||||||
/// <summary> | ||||||||||
/// CA1071: Constructor parameters should match property and field names | ||||||||||
/// </summary> | ||||||||||
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] | ||||||||||
public sealed class ConstructorParametersShouldMatchPropertyAndFieldNamesAnalyzer : DiagnosticAnalyzer | ||||||||||
{ | ||||||||||
internal const string RuleId = "CA1071"; | ||||||||||
|
||||||||||
internal const string ReferencedFieldOrPropertyNameKey = "ReferencedPropertyOrFieldName"; | ||||||||||
internal const string DiagnosticReasonKey = "DiagnosticReason"; | ||||||||||
|
||||||||||
private static readonly LocalizableString s_localizableTitle = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.ConstructorParametersShouldMatchPropertyOrFieldNamesTitle), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources)); | ||||||||||
|
||||||||||
private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.ConstructorParameterShouldMatchPropertyOrFieldName), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources)); | ||||||||||
private static readonly LocalizableString s_localizableDescription = new LocalizableResourceString(nameof(MicrosoftNetCoreAnalyzersResources.ConstructorParametersShouldMatchPropertyOrFieldNamesDescription), MicrosoftNetCoreAnalyzersResources.ResourceManager, typeof(MicrosoftNetCoreAnalyzersResources)); | ||||||||||
|
||||||||||
internal static DiagnosticDescriptor PropertyOrFieldNameRule = DiagnosticDescriptorHelper.Create(RuleId, | ||||||||||
s_localizableTitle, | ||||||||||
s_localizableMessage, | ||||||||||
DiagnosticCategory.Design, | ||||||||||
RuleLevel.BuildWarning, | ||||||||||
description: s_localizableDescription, | ||||||||||
isPortedFxCopRule: false, | ||||||||||
isDataflowRule: false); | ||||||||||
|
||||||||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(PropertyOrFieldNameRule); | ||||||||||
|
||||||||||
public override void Initialize(AnalysisContext context) | ||||||||||
{ | ||||||||||
context.EnableConcurrentExecution(); | ||||||||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||||||||||
|
||||||||||
context.RegisterCompilationStartAction((context) => | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: parentheses are not needed.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||||||
{ | ||||||||||
INamedTypeSymbol? jsonConstructorAttributeNamedSymbol = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTextJsonSerializationJsonConstructorAttribute); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||||||
if (jsonConstructorAttributeNamedSymbol == null) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||||||
{ | ||||||||||
return; | ||||||||||
} | ||||||||||
|
||||||||||
var paramAnalyzer = new ParameterAnalyzer(jsonConstructorAttributeNamedSymbol); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the initialization is necessary? seems all references/methods of the this type can be static There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not necessary, but it may improve the readability. Because without it we have to pass json constructor attribute to two methods: public static bool ShouldAnalyzeMethod(IMethodSymbol method, INamedTypeSymbol jsonConstructorAttribute)
{
// We only care about constructors with parameters.
if (method.Parameters.IsEmpty)
{
return false;
}
// We only care about constructors that are marked with JsonConstructor attribute.
return ParameterAnalyzer.IsJsonConstructor(method, jsonConstructorAttribute);
} It means developers should figure out where to get Anyway, if you still insist, I've prepared a commit with those changes and can apply it at any time. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||
|
||||||||||
context.RegisterSymbolStartAction((context) => | ||||||||||
{ | ||||||||||
var constructors = ((INamedTypeSymbol)context.Symbol).InstanceConstructors; | ||||||||||
|
||||||||||
foreach (var ctor in constructors) | ||||||||||
{ | ||||||||||
if (paramAnalyzer.ShouldAnalyzeMethod(ctor)) | ||||||||||
{ | ||||||||||
context.RegisterOperationAction( | ||||||||||
context => ParameterAnalyzer.AnalyzeOperationAndReport(context), | ||||||||||
OperationKind.ParameterReference); | ||||||||||
} | ||||||||||
} | ||||||||||
}, SymbolKind.NamedType); | ||||||||||
}); | ||||||||||
} | ||||||||||
|
||||||||||
internal enum ParameterDiagnosticReason | ||||||||||
{ | ||||||||||
NameMismatch, | ||||||||||
PropertyInappropriateVisibility, | ||||||||||
FieldInappropriateVisibility, | ||||||||||
UnreferencedParameter, | ||||||||||
} | ||||||||||
|
||||||||||
private sealed class ParameterAnalyzer | ||||||||||
{ | ||||||||||
private readonly INamedTypeSymbol _jsonConstructorAttributeInfoType; | ||||||||||
|
||||||||||
public ParameterAnalyzer(INamedTypeSymbol jsonConstructorAttributeInfoType) | ||||||||||
{ | ||||||||||
_jsonConstructorAttributeInfoType = jsonConstructorAttributeInfoType; | ||||||||||
} | ||||||||||
|
||||||||||
public static void AnalyzeOperationAndReport(OperationAnalysisContext context) | ||||||||||
{ | ||||||||||
var operation = (IParameterReferenceOperation)context.Operation; | ||||||||||
|
||||||||||
IMemberReferenceOperation? memberReferenceOperation = TryGetMemberReferenceOperation(operation); | ||||||||||
ISymbol? referencedSymbol = memberReferenceOperation?.GetReferencedMemberOrLocalOrParameter(); | ||||||||||
|
||||||||||
// TODO: convert "IsStatic" to a separate diagnostic | ||||||||||
if (referencedSymbol == null || referencedSymbol.IsStatic) | ||||||||||
{ | ||||||||||
return; | ||||||||||
} | ||||||||||
|
||||||||||
IParameterSymbol param = operation.Parameter; | ||||||||||
|
||||||||||
if (referencedSymbol is IFieldSymbol field) | ||||||||||
{ | ||||||||||
if (!IsParamMatchesReferencedMemberName(param, field)) | ||||||||||
{ | ||||||||||
ReportFieldDiagnostic(context, PropertyOrFieldNameRule, ParameterDiagnosticReason.NameMismatch, param, field); | ||||||||||
} | ||||||||||
|
||||||||||
if (!field.IsPublic()) | ||||||||||
{ | ||||||||||
ReportFieldDiagnostic(context, PropertyOrFieldNameRule, ParameterDiagnosticReason.FieldInappropriateVisibility, param, field); | ||||||||||
} | ||||||||||
} | ||||||||||
else if (referencedSymbol is IPropertySymbol prop) | ||||||||||
{ | ||||||||||
if (!IsParamMatchesReferencedMemberName(param, prop)) | ||||||||||
{ | ||||||||||
ReportPropertyDiagnostic(context, PropertyOrFieldNameRule, ParameterDiagnosticReason.NameMismatch, param, prop); | ||||||||||
} | ||||||||||
|
||||||||||
if (!prop.IsPublic()) | ||||||||||
{ | ||||||||||
ReportPropertyDiagnostic(context, PropertyOrFieldNameRule, ParameterDiagnosticReason.PropertyInappropriateVisibility, param, prop); | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
public bool ShouldAnalyzeMethod(IMethodSymbol method) | ||||||||||
{ | ||||||||||
// We only care about constructors with parameters. | ||||||||||
if (method.Parameters.IsEmpty) | ||||||||||
{ | ||||||||||
return false; | ||||||||||
} | ||||||||||
|
||||||||||
// We only care about constructors that are marked with JsonConstructor attribute. | ||||||||||
return this.IsJsonConstructor(method); | ||||||||||
} | ||||||||||
|
||||||||||
private static bool IsParamMatchesReferencedMemberName(IParameterSymbol param, ISymbol referencedMember) | ||||||||||
{ | ||||||||||
if (param.Name.Length != referencedMember.Name.Length) | ||||||||||
{ | ||||||||||
return false; | ||||||||||
} | ||||||||||
|
||||||||||
var paramWords = WordParser.Parse(param.Name, WordParserOptions.SplitCompoundWords); | ||||||||||
var memberWords = WordParser.Parse(referencedMember.Name, WordParserOptions.SplitCompoundWords); | ||||||||||
|
||||||||||
return paramWords.SequenceEqual(memberWords, StringComparer.OrdinalIgnoreCase); | ||||||||||
} | ||||||||||
|
||||||||||
private bool IsJsonConstructor([NotNullWhen(returnValue: true)] IMethodSymbol? method) | ||||||||||
=> method.IsConstructor() && | ||||||||||
method.HasAttribute(this._jsonConstructorAttributeInfoType); | ||||||||||
|
||||||||||
private static IMemberReferenceOperation? TryGetMemberReferenceOperation(IParameterReferenceOperation paramOperation) | ||||||||||
{ | ||||||||||
if (paramOperation.Parent is IAssignmentOperation assignmentOperation | ||||||||||
&& assignmentOperation.Target is IMemberReferenceOperation assignmentTarget) | ||||||||||
{ | ||||||||||
return assignmentTarget; | ||||||||||
} | ||||||||||
|
||||||||||
if (paramOperation.Parent is ITupleOperation sourceTuple | ||||||||||
&& sourceTuple.Parent is IConversionOperation conversion | ||||||||||
&& conversion.Parent is IDeconstructionAssignmentOperation deconstruction | ||||||||||
&& deconstruction.Target is ITupleOperation targetTuple) | ||||||||||
{ | ||||||||||
var paramIndexInTuple = sourceTuple.Elements.IndexOf(paramOperation); | ||||||||||
|
||||||||||
return targetTuple.Elements[paramIndexInTuple] as IMemberReferenceOperation; | ||||||||||
} | ||||||||||
|
||||||||||
return null; | ||||||||||
} | ||||||||||
|
||||||||||
private static void ReportFieldDiagnostic(OperationAnalysisContext context, DiagnosticDescriptor diagnosticDescriptor, ParameterDiagnosticReason reason, IParameterSymbol param, IFieldSymbol field) | ||||||||||
{ | ||||||||||
var properties = ImmutableDictionary<string, string?>.Empty | ||||||||||
.SetItem(ReferencedFieldOrPropertyNameKey, field.Name) | ||||||||||
.SetItem(DiagnosticReasonKey, reason.ToString()); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||||||
|
||||||||||
context.ReportDiagnostic( | ||||||||||
param.CreateDiagnostic( | ||||||||||
diagnosticDescriptor, | ||||||||||
properties, | ||||||||||
param.ContainingType.ToDisplayString(SymbolDisplayFormats.ShortSymbolDisplayFormat), | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||||||
param.Name, | ||||||||||
field.Name)); | ||||||||||
} | ||||||||||
|
||||||||||
private static void ReportPropertyDiagnostic(OperationAnalysisContext context, DiagnosticDescriptor diagnosticDescriptor, ParameterDiagnosticReason reason, IParameterSymbol param, IPropertySymbol prop) | ||||||||||
{ | ||||||||||
var properties = ImmutableDictionary<string, string?>.Empty | ||||||||||
.SetItem(ReferencedFieldOrPropertyNameKey, prop.Name) | ||||||||||
.SetItem(DiagnosticReasonKey, reason.ToString()); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we know the two keys being added are unique.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using constant strings instead of an enum, which avoids doing Enum.Parse in codefix, and also matches what's done in other analyzers. |
||||||||||
|
||||||||||
context.ReportDiagnostic( | ||||||||||
param.CreateDiagnostic( | ||||||||||
diagnosticDescriptor, | ||||||||||
properties, | ||||||||||
param.ContainingType.ToDisplayString(SymbolDisplayFormats.ShortSymbolDisplayFormat), | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||||||
param.Name, | ||||||||||
prop.Name)); | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To Do: remove