Skip to content
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 analyzer for SemanticModel.GetDeclaredSymbol #6779

Merged
merged 17 commits into from
Oct 30, 2023

Conversation

CollinAlpert
Copy link
Contributor

Adds an analyzer to detect usages of SemanticModel.GetDeclaredSymbol which always returns null.

Fixes #336.

@CollinAlpert CollinAlpert requested a review from a team as a code owner July 17, 2023 20:06
@mavasani
Copy link
Member

@dotnet/roslyn-compiler for help in reviewing this analyzer. Couple of questions:

  1. Can we come up with an exhaustive list of syntax node types instead of just the three listed in the issue + implemented in the PR?
  2. We should only include the syntax node types where the compiler team has good confidence that the GetDeclaredSymbol API semantics are unlikely to change in future to return a non-null value.

{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(static compilationContext =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename compilationContext to context (intentionally shadowing the outer context)


public class Test {{
public void M(SemanticModel semanticModel, {type} syntax) {{
var x = [|semanticModel.GetDeclaredSymbol(syntax)|];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test with semanticModel.GetDeclaredSymbol() (which will produce a compilation error)

@codecov
Copy link

codecov bot commented Jul 25, 2023

Codecov Report

Merging #6779 (da0b5d4) into main (b4ed6a3) will decrease coverage by 0.01%.
The diff coverage is 95.45%.

@@            Coverage Diff             @@
##             main    #6779      +/-   ##
==========================================
- Coverage   96.43%   96.42%   -0.01%     
==========================================
  Files        1410     1412       +2     
  Lines      336268   336372     +104     
  Branches    11107    11113       +6     
==========================================
+ Hits       324268   324360      +92     
- Misses       8896     8905       +9     
- Partials     3104     3107       +3     

if (SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, getDeclaredSymbolMethod))
{
var syntaxNodeType = invocation.Arguments[1].Value.WalkDownConversion().Type;
if (syntaxNodeType is not null && syntaxNodeType.GetBaseTypesAndThis().ToSet().Overlaps(new[] { globalStatementSymbol, incompleteMemberSymbol, baseFieldDeclarationSymbol }))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allocates a new array for every invocation of GetDeclaredSymbol. I think this is not necessary.

Also note there is already a DerivesFrom extension method.

@@ -571,4 +571,13 @@
<data name="DoNotRegisterCompilerTypesWithBadAssemblyReferenceRuleTitle" xml:space="preserve">
<value>Compiler extensions should be implemented in assemblies with compiler-provided references</value>
</data>
<data name="SemanticModelGetDeclaredSymbolAlwaysReturnsNullDescription" xml:space="preserve">
<value>Calling 'SemanticModel.GetDeclaredSymbol' with an argument of type 'GlobalStatementSyntax', 'IncompleteMemberSyntax' or a type inheriting from 'BaseFieldDeclarationSyntax' will always return 'null'.</value>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like fields should get a specific error message that instructs the user to extract the variable declarators.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this description needs adjustment as fields now have a separate descriptor/description. Additionally, not sure if we should limit the description to just 'GlobalStatementSyntax' and 'IncompleteMemberSyntax' as the code handles all sub-types of SyntaxNode that do not have an overload. It's fine to mentioned these two as examples, but not as the only node types being handled.

|| (getDeclaredSymbolMethod = modelExtensions.GetMembers(nameof(ModelExtensions.GetDeclaredSymbol)).FirstOrDefault() as IMethodSymbol) is null
|| !typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftCodeAnalysisCSharpSyntaxGlobalStatementSyntax, out var globalStatementSymbol)
|| !typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftCodeAnalysisCSharpSyntaxIncompleteMemberSyntax, out var incompleteMemberSymbol)
|| !typeProvider.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftCodeAnalysisCSharpSyntaxBaseFieldDeclarationSyntax, out var baseFieldDeclarationSymbol))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could instead look for all the extension GetDeclaredSymbol methods, and use that a list of types that will return positive results? With maybe a special case for the field decl scenario, since that's somewhat misleading?

If we don't want to try doing that, I think we should at least include VariableDeclarationSyntax, as that is also a common error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion, I have implemented it.

Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. @mavasani, can you take a look?

@333fred 333fred requested a review from mavasani October 23, 2023 23:59
var invocation = (IInvocationOperation)context.Operation;
if (SymbolEqualityComparer.Default.Equals(invocation.TargetMethod, getDeclaredSymbolMethod))
{
var syntaxNodeDerivingType = invocation.Arguments[1].Value.WalkDownConversion().Type;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do we ensure that getDeclaredSymbolMethod has at least 2 parameters?

Copy link
Member

@mavasani mavasani Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would also not work as expected if named arguments are used and arguments are out of order. You probably want to use one of the below helper methods:

public static bool TryGetArgumentForParameterAtIndex(

public static IArgumentOperation GetArgumentForParameterAtIndex(

Comment on lines 78 to 91
Diagnostic? diagnostic = null;
if (syntaxNodeDerivingType.DerivesFrom(baseFieldDeclarationType))
{
diagnostic = invocation.CreateDiagnostic(FieldDiagnosticDescriptor, syntaxNodeDerivingType.Name);
}
else if (allowedTypes.All(type => !syntaxNodeDerivingType.DerivesFrom(type, baseTypesOnly: true, checkTypeParameterConstraints: false)))
{
diagnostic = invocation.CreateDiagnostic(DiagnosticDescriptor, syntaxNodeDerivingType.Name);
}

if (diagnostic is not null)
{
context.ReportDiagnostic(diagnostic);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
Diagnostic? diagnostic = null;
if (syntaxNodeDerivingType.DerivesFrom(baseFieldDeclarationType))
{
diagnostic = invocation.CreateDiagnostic(FieldDiagnosticDescriptor, syntaxNodeDerivingType.Name);
}
else if (allowedTypes.All(type => !syntaxNodeDerivingType.DerivesFrom(type, baseTypesOnly: true, checkTypeParameterConstraints: false)))
{
diagnostic = invocation.CreateDiagnostic(DiagnosticDescriptor, syntaxNodeDerivingType.Name);
}
if (diagnostic is not null)
{
context.ReportDiagnostic(diagnostic);
}
DiagnosticDescriptor descriptor;
if (syntaxNodeDerivingType.DerivesFrom(baseFieldDeclarationType))
{
descriptor = FieldDiagnosticDescriptor;
}
else if (allowedTypes.All(type => !syntaxNodeDerivingType.DerivesFrom(type, baseTypesOnly: true, checkTypeParameterConstraints: false)))
{
descriptor = DiagnosticDescriptor;
}
else
{
return;
}
context.ReportDiagnostic(invocation.CreateDiagnostic(descriptor, syntaxNodeDerivingType.Name));

}
}

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DiagnosticDescriptor, FieldDiagnosticDescriptor);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to the top of the file above Initialize method

Copy link
Member

@mavasani mavasani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM. Have a few minor suggestions, I can merge once these are addressed. Thanks for the PR @CollinAlpert!

@CollinAlpert
Copy link
Contributor Author

Done!

@mavasani mavasani merged commit b40db00 into dotnet:main Oct 30, 2023
11 checks passed
@CollinAlpert CollinAlpert deleted the issue_336 branch October 30, 2023 08:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Create analyzer to detect calls to SemanticModel.GetDeclaredSymbol() with fields and other invalid syntax
4 participants