Skip to content

Add the UseConreteType analyzer#6370

Merged
buyaa-n merged 2 commits into
dotnet:mainfrom
geeknoid:main
Jan 5, 2023
Merged

Add the UseConreteType analyzer#6370
buyaa-n merged 2 commits into
dotnet:mainfrom
geeknoid:main

Conversation

@geeknoid

Copy link
Copy Markdown
Member

As per dotnet/runtime#51193. This recommends using concrete types for fields, local, parameters, and method returns instead of interface/abstract types when possible and when it doesn't affect the API surface of a class. The idea is to help eliminate virtual/interface dispatch, while also making available potentially richer APIs exposed by implementations relative to their interface.

@geeknoid geeknoid requested a review from a team as a code owner December 19, 2022 23:59
@codecov

codecov Bot commented Dec 20, 2022

Copy link
Copy Markdown

Codecov Report

Merging #6370 (5fe2c65) into main (c4909ec) will increase coverage by 0.02%.
The diff coverage is 95.86%.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6370      +/-   ##
==========================================
+ Coverage   96.12%   96.14%   +0.02%     
==========================================
  Files        1361     1365       +4     
  Lines      316084   317413    +1329     
  Branches    10183    10263      +80     
==========================================
+ Hits       303841   305187    +1346     
+ Misses       9814     9790      -24     
- Partials     2429     2436       +7     

@buyaa-n

buyaa-n commented Jan 3, 2023

Copy link
Copy Markdown

Thank you @geeknoid, I run the analyzer in runtime repo and it is found 544 diagnostics for one build (over 3000 for all platforms build). With that many diagnostics it would be very useful to have a fixer. Do you plan to add a fixer in this PR or later?

I take a look some of the diagnostics (the ones found in System.Private.CoreLib) and suspect some of the are false positives:

Why might false positive Location Diagnostic reported
Warns on parameter of a public API runtime\src\libraries\System.Private.CoreLib\ src\System\MemoryExtensions.cs(2155,124) Change type of parameter 'comparer' from 'System.Collections.Generic.IEqualityComparer?' to 'System.Collections.Generic.EqualityComparer' for improved performance
Warns on parameter of a public API runtime\src\libraries\System.Private.CoreLib\ src\System\MemoryExtensions.cs(3197,121) Change type of parameter 'comparer' from 'System.Collections.Generic.IEqualityComparer?' to 'System.Collections.Generic.EqualityComparer' for improved performance
Method not always return the suggested type runtime\src\coreclr\System.Private.CoreLib\ src\System\Reflection\Emit\TypeBuilder.cs(1711,27) Change return type of method 'CreateTypeNoLock' from 'System.Reflection.TypeInfo?' to 'System.RuntimeType?' for improved performance
Method returns the interface method runtime\src\libraries\System.Private.CoreLib\ src\System\Resources\ResourceSet.cs(126,26) Change type of variable 'copyOfTableAsIDictionary' from 'System.Collections.IDictionary?' to 'System.Collections.Generic.Dictionary<object, object?>?' for improved performance
Private/internal method called in public API that returns the interface runtime\src\libraries\System.Private.CoreLib\ src\System\Environment.Win32.cs(64,36) Change return type of method 'GetEnvironmentVariablesFromRegistry' from 'System.Collections.IDictionary' to 'System.Collections.Hashtable' for improved performance
Private/internal method called in public API that returns the interface src\coreclr\System.Private.CoreLib\ src\System\Reflection\Assembly.CoreCLR.cs(86,34) Change return type of method 'GetEntryAssemblyInternal' from 'System.Reflection.Assembly?' to 'System.Reflection.RuntimeAssembly?' for improved performance
Private/internal method called in public API that returns the interface runtime\src\coreclr\System.Private.CoreLib\ src\System\Runtime\Loader\ AssemblyLoadContext.CoreCLR.cs(57,26) Change return type of method 'InternalLoadFromPath' from 'System.Reflection.Assembly' to 'System.Reflection.RuntimeAssembly' for improved performance
Private/internal method called in public API that returns the interface runtime\src\coreclr\System.Private.CoreLib\ src\System\Reflection\ RuntimeCustomAttributeData.cs(95,51) Change return type of method 'GetCombinedList' from 'System.Collections.Generic.IList<System.Reflection.CustomAttributeData>' to 'System.Collections.ObjectModel.ReadOnlyCollection<System.Reflection.CustomAttributeData>' for improved performance
Private/internal method called in public API that returns the interface runtime\src\coreclr\System.Private.CoreLib\ src\System\Reflection\Emit\TypeBuilder.cs(1711,27) Change return type of method 'CreateTypeNoLock' from 'System.Reflection.TypeInfo?' to 'System.RuntimeType?' for improved performance
  • for Private/internal method called in public API that returns the interface - not sure it is actually a false positive and changing the return type of private/internal method would make a difference

I have attached the findings for reference: CA1859-log.txt


public class Test
{
private void Method(IFoo {|#0:foo|})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It would be better to squiggle below the symbol that needs an action, i.e. it would make more sense to squiggle under IFoo not foo ( {|#0:IFoo|} foo )


public class Test
{
private IFoo? {|#0:Method1|}(FooProvider? provider)

@buyaa-n buyaa-n Jan 3, 2023

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same here and others places, private {|#0:IFoo|}? Method1(FooProvider? provider) not private IFoo? {|#0:Method1|}(FooProvider? provider)

As per dotnet/runtime#51193. This recommends using concrete types for
fields, local, parameters, and method returns instead of
interface/abstract types when possible and when it doesn't affect the
API surface of a class. The idea is to help eliminate
virtual/interface dispatch, while also making available potentially
richer APIs exposed by implementations relative to their interface.
@geeknoid

geeknoid commented Jan 4, 2023

Copy link
Copy Markdown
Member Author

@buyaa-n Thanks for the detailed test and report. I tried each case you highlighted. Here's my input in order:

// STATUS
//   1..2 : this is related to some confusion in handling of generics
//   3 ; pushed a fix, the logic generally wasn't recognizing uses of 'this'
//   4..8 : all as designed, upgrading these types are all part of the point of the analyzer
//   9 : same case as #3, so that's fixed.

I've updated the PR to fix case 3.

I could use some help for cases 1 and 2 however. I think I don't understand how Roslyn deals with generic types, and this is leading to both false positives and false negatives.

If you look my test cases called ShouldTrigger1 and ShouldTrigger2, these should lead to a diagnostic about upgrading the parameter of the Do method to be of type Foo or Foo. But it's not firing. I have a list of candidate parameters which could be upgraded, which includes the parameter "foo" of the Do method. Now I also have a list of the different types assigned to each parameter by all the call sites. The problem is that when I use SymbolComparer.Default.Equals to compare those two (identical!) parameters, I am told they are different.

If I compare the two parameter instances by hand, comparing each field, they seem to be identical. Yet Roslyn is claiming otherwise.

If I take the generics out of the picture, this kind of code triggers just fine.

I've just spent a couple hours on this and made no progress. I could use some help.

Thanks

@geeknoid

geeknoid commented Jan 4, 2023

Copy link
Copy Markdown
Member Author

@buyaa-n To answer your question, I'm not currently planning a fixer for this. I unfortunately don't have the bandwidth for that at this point.

@mavasani

mavasani commented Jan 4, 2023

Copy link
Copy Markdown

I could use some help for cases 1 and 2 however. I think I don't understand how Roslyn deals with generic types, and this is leading to both false positives and false negatives.

@geeknoid This normally indicates that you are trying to compare a symbol reference with a symbol definition. The solution in most cases should be to use ISymbol.OriginalDefinition property on the symbol reference to get the definition and then compare the definition symbols.

@geeknoid

geeknoid commented Jan 4, 2023

Copy link
Copy Markdown
Member Author

Thanks @mavasani , that did the trick!

@buyaa-n I've updated the PR which will hopefully address the false positives. That just leaves reporting the squiggles on the type name, rather than on the symbol name. I'm going to have to think about that, I think it might require a fair bit of change to how the code works now. But maybe I can find some clever way to do it.

Comment on lines +30 to +31
public INamedTypeSymbol? Void { get; private set; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
public INamedTypeSymbol? Void { get; private set; }

Comment on lines +48 to +49
Void = null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
Void = null;

Comment on lines +63 to +65
var c = _pool.Allocate();
c.Void = compilation.GetSpecialType(SpecialType.System_Void);
return c;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
var c = _pool.Allocate();
c.Void = compilation.GetSpecialType(SpecialType.System_Void);
return c;
return _pool.Allocate();

Comment on lines +112 to +130
context.RegisterSymbolStartAction(context =>
{
var coll = Collector.GetInstance(context.Compilation);

context.RegisterOperationAction(context => coll.HandleInvocation((IInvocationOperation)context.Operation), OperationKind.Invocation);
context.RegisterOperationAction(context => coll.HandleSimpleAssignment((ISimpleAssignmentOperation)context.Operation), OperationKind.SimpleAssignment);
context.RegisterOperationAction(context => coll.HandleCoalesceAssignment((ICoalesceAssignmentOperation)context.Operation), OperationKind.CoalesceAssignment);
context.RegisterOperationAction(context => coll.HandleDeconstructionAssignment((IDeconstructionAssignmentOperation)context.Operation), OperationKind.DeconstructionAssignment);
context.RegisterOperationAction(context => coll.HandleFieldInitializer((IFieldInitializerOperation)context.Operation), OperationKind.FieldInitializer);
context.RegisterOperationAction(context => coll.HandleVariableDeclarator((IVariableDeclaratorOperation)context.Operation), OperationKind.VariableDeclarator);
context.RegisterOperationAction(context => coll.HandleDeclarationExpression((IDeclarationExpressionOperation)context.Operation), OperationKind.DeclarationExpression);
context.RegisterOperationAction(context => coll.HandleReturn((IReturnOperation)context.Operation), OperationKind.Return);

context.RegisterSymbolEndAction(context =>
{
Report(context, coll);
Collector.ReturnInstance(coll, context.CancellationToken);
});
}, SymbolKind.NamedType);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
context.RegisterSymbolStartAction(context =>
{
var coll = Collector.GetInstance(context.Compilation);
context.RegisterOperationAction(context => coll.HandleInvocation((IInvocationOperation)context.Operation), OperationKind.Invocation);
context.RegisterOperationAction(context => coll.HandleSimpleAssignment((ISimpleAssignmentOperation)context.Operation), OperationKind.SimpleAssignment);
context.RegisterOperationAction(context => coll.HandleCoalesceAssignment((ICoalesceAssignmentOperation)context.Operation), OperationKind.CoalesceAssignment);
context.RegisterOperationAction(context => coll.HandleDeconstructionAssignment((IDeconstructionAssignmentOperation)context.Operation), OperationKind.DeconstructionAssignment);
context.RegisterOperationAction(context => coll.HandleFieldInitializer((IFieldInitializerOperation)context.Operation), OperationKind.FieldInitializer);
context.RegisterOperationAction(context => coll.HandleVariableDeclarator((IVariableDeclaratorOperation)context.Operation), OperationKind.VariableDeclarator);
context.RegisterOperationAction(context => coll.HandleDeclarationExpression((IDeclarationExpressionOperation)context.Operation), OperationKind.DeclarationExpression);
context.RegisterOperationAction(context => coll.HandleReturn((IReturnOperation)context.Operation), OperationKind.Return);
context.RegisterSymbolEndAction(context =>
{
Report(context, coll);
Collector.ReturnInstance(coll, context.CancellationToken);
});
}, SymbolKind.NamedType);
context.RegisterCompilationStartAction(context =>
{
var voidType = context.Compilation.GetSpecialType(SpecialType.System_Void);
context.RegisterSymbolStartAction(context =>
{
var coll = Collector.GetInstance(context.Compilation);
context.RegisterOperationAction(context => coll.HandleInvocation((IInvocationOperation)context.Operation), OperationKind.Invocation);
context.RegisterOperationAction(context => coll.HandleSimpleAssignment((ISimpleAssignmentOperation)context.Operation), OperationKind.SimpleAssignment);
context.RegisterOperationAction(context => coll.HandleCoalesceAssignment((ICoalesceAssignmentOperation)context.Operation), OperationKind.CoalesceAssignment);
context.RegisterOperationAction(context => coll.HandleDeconstructionAssignment((IDeconstructionAssignmentOperation)context.Operation), OperationKind.DeconstructionAssignment);
context.RegisterOperationAction(context => coll.HandleFieldInitializer((IFieldInitializerOperation)context.Operation), OperationKind.FieldInitializer);
context.RegisterOperationAction(context => coll.HandleVariableDeclarator((IVariableDeclaratorOperation)context.Operation), OperationKind.VariableDeclarator);
context.RegisterOperationAction(context => coll.HandleDeclarationExpression((IDeclarationExpressionOperation)context.Operation), OperationKind.DeclarationExpression);
context.RegisterOperationAction(context => coll.HandleReturn((IReturnOperation)context.Operation), OperationKind.Return);
context.RegisterSymbolEndAction(context =>
{
Report(context, coll, voidType);
Collector.ReturnInstance(coll, context.CancellationToken);
});
}, SymbolKind.NamedType);
}

/// <summary>
/// Given all the accumulated analysis state, generate the diagnostics.
/// </summary>
private static void Report(SymbolAnalysisContext context, Collector coll)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
private static void Report(SymbolAnalysisContext context, Collector coll)
private static void Report(SymbolAnalysisContext context, Collector coll, INamedTypeSymbol voidType)

{
using var types = PooledHashSet<ITypeSymbol>.GetInstance(assignments, SymbolEqualityComparer.Default);

var assignedNull = types.Remove(coll.Void!);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
var assignedNull = types.Remove(coll.Void!);
var assignedNull = types.Remove(voidType);

@buyaa-n

buyaa-n commented Jan 4, 2023

Copy link
Copy Markdown

To answer your question, I'm not currently planning a fixer for this. I unfortunately don't have the bandwidth for that at this point.

OK, thank you for letting us know. We can add that later, I will create an issue for fixer implementation and put it up for grabs.

That just leaves reporting the squiggles on the type name, rather than on the symbol name. I'm going to have to think about that, I think it might require a fair bit of change to how the code works now. But maybe I can find some clever way to do it.

Thank you, there is one analyzer that I know did that:

private SyntaxNode? GetPreviewSyntaxNodeFromSymbols(ISymbol symbol,
ISymbol previewType)
{
switch (symbol)
{
case IFieldSymbol:
case IEventSymbol:
return GetPreviewSyntaxNodeForFieldsOrEvents(symbol, previewType);
case IMethodSymbol methodSymbol:
return GetPreviewParameterSyntaxNodeForMethod(methodSymbol, previewType);

@geeknoid

geeknoid commented Jan 4, 2023

Copy link
Copy Markdown
Member Author

@Youssef1313 Unless I'm missing something, I think your suggestions of removing the Void property from the collector type doesn't work. Line 333 of the Collector.cs file needs this value, that's why it was present in the object in the first place.

@Youssef1313

Youssef1313 commented Jan 4, 2023

Copy link
Copy Markdown
Member

@Youssef1313 Unless I'm missing something, I think your suggestions of removing the Void property from the collector type doesn't work. Line 333 of the Collector.cs file needs this value, that's why it was present in the object in the first place.

I'm getting the void type once in compilation start and passing it down with these suggestions. Yes I missed this usage, but the idea is the same.

@buyaa-n buyaa-n left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Overall LGTM, thank you!

@buyaa-n

buyaa-n commented Jan 5, 2023

Copy link
Copy Markdown

Created an issue for fixer and diagnostic location. Merging the PR

Merging the PR, @Youssef1313 your suggestions makes sense to me, but as @geeknoid mentioned above applying your suggestions on the PR directly might fail the build, feel free to raise a PR with your suggestions after merge, thank you!

@buyaa-n buyaa-n merged commit 2f82379 into dotnet:main Jan 5, 2023
@github-actions github-actions Bot added this to the vNext milestone Jan 5, 2023
@geeknoid

geeknoid commented Jan 5, 2023

Copy link
Copy Markdown
Member Author

@buyaa-n Thanks!

@stephentoub

Copy link
Copy Markdown
Member

I'm trying this out on dotnet/runtime, and I'm getting these warnings:

D:\repos\runtime\src\libraries\System.Private.CoreLib\src\System\MemoryExtensions.cs(3192,121): warning CA1859: Change type of parameter 'comparer' from 'System.Collections.Generic.IEqualityComparer<T>?' to 'System.Collections.Generic.EqualityComparer<T>' for improved
performance [D:\repos\runtime\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj]
D:\repos\runtime\src\libraries\System.Private.CoreLib\src\System\MemoryExtensions.cs(2150,124): warning CA1859: Change type of parameter 'comparer' from 'System.Collections.Generic.IEqualityComparer<T>?' to 'System.Collections.Generic.EqualityComparer<T>' for improved
performance [D:\repos\runtime\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj]

That's for these public methods on a public type:
https://github.com/dotnet/runtime/blob/db28821852151f62fb81d83d0fd1c8de9e0e5640/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs#L3192
https://github.com/dotnet/runtime/blob/db28821852151f62fb81d83d0fd1c8de9e0e5640/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs#L2150

Is it a bug that these are being reported? I thought the intent was to only fire for internal/private? I tried adding:

dotnet_diagnostic.CA1859.api_surface = private, internal

and it didn't help.

@geeknoid

geeknoid commented Jan 5, 2023

Copy link
Copy Markdown
Member Author

These are bugs. Are these the only ones showing up for you? It's not supposed to fire on public signatures.

I specifically tried to repro these two and it's just not happening from within the test environment. I'm wondering if there's something funny going on because its corelib. This rule has been running for a year on hundreds of projects and this hasn't surfaced before, so there's something peculiar about those two methods.

@buyaa-n

buyaa-n commented Jan 5, 2023

Copy link
Copy Markdown

I'm trying this out on dotnet/runtime, and I'm getting these warnings:

Oops I thought those were fixed, @stephentoub would you suggest reverting this PR or can we fix this with different PR?

@stephentoub

stephentoub commented Jan 6, 2023

Copy link
Copy Markdown
Member

Are these the only ones showing up for you?

The only public ones for corelib. I've not yet tried beyond that.

would you suggest reverting this PR or can we fix this with different PR?

No need to revert. Separate PR once the issue is diagnosed is fine.

@geeknoid

geeknoid commented Jan 6, 2023

Copy link
Copy Markdown
Member Author

What magic do I need to do to run the analyzer against core lib in such a way that I can continuously run the analyzer while deleting code from MemoryExtensions.cs? I need to create a reproducible example. I've got a top-level condition to not produce diagnostics against public symbols, and somehow this is slipping through.

@geeknoid

geeknoid commented Jan 6, 2023

Copy link
Copy Markdown
Member Author

Aha, never mind. I have a repro. The problem emerges when functions assign values to their own incoming parameters. The analyzer is getting confused about what this means.

@geeknoid

geeknoid commented Jan 6, 2023

Copy link
Copy Markdown
Member Author

#6407

@geeknoid

Copy link
Copy Markdown
Member Author

As a follow-up to this, I realized I never created docs for this. So here's a PR: dotnet/docs#35182

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.

5 participants