-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Ambiguous type codefix provider (using alias) #24022
Ambiguous type codefix provider (using alias) #24022
Conversation
This reverts commit 3e6b04e.
} | ||
}"; | ||
await TestInRegularAndScriptAsync(initialMarkup, expectedMarkupTemplate.Replace("#", "using Ambiguous = N1.Ambiguous;"), 0); | ||
await TestInRegularAndScriptAsync(initialMarkup, expectedMarkupTemplate.Replace("#", "using Ambiguous = N2.Ambiguous;"), 1); |
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.
FWIW, i'm not a fan of this sort of approach. With code actions, we explicitly spell out the before/after code without the user having to read the test code to figure out how the strings are manipulated.
This has the benefit of making it very easy to see and grok what will happen without any intermediary steps to understand.
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.
nit: named param for 0 and 1.
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
|
||
namespace Microsoft.CodeAnalysis.CSharp.Ambiguity |
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.
nit: namespaces here should match the feature name. So this would be AliasAmbiguousType
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.
The idea was that all ambiguity code fixer would go into this namespace. Other candidates would be CS0121 for extension methods (could offer full qualification with transformation in a call to a static method) or CS0433 for same class in same namespace in different assemblies and maybe others.
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.
That's generally not how we organize the codebase though :-/
{ | ||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.AliasAmbiguousType), Shared] | ||
[ExtensionOrder(After = PredefinedCodeFixProviderNames.FullyQualify)] | ||
internal class CSharpAmbiguousTypeCodeFixProvider : AbstractAmbiguousTypeCodeFixProvider |
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.
type name should match provider name. if provider name is AliasAmbiguousType, type names shoudl be CSharpAliasAmbiguousTypeCodeFixProvider and so on
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
using static Microsoft.CodeAnalysis.CodeActions.CodeAction; | ||
|
||
namespace Microsoft.CodeAnalysis.AmbiguityCodeFixProvider |
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.
namespace doesn't match folder name. Folder name should be named something like AliasAmbiguousType
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
// Innermost: We are looking for an IdentifierName. IdentifierName is sometimes at the same span as its parent (e.g. SimpleBaseTypeSyntax). | ||
var diagnosticNode = root.FindNode(context.Span, getInnermostNodeForTie: true); |
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.
"getInnermostNodeForTie: true" is correct. But can you verify that this is working by writing a test case where your amibiguous type name is an argument to some method? That way we can demonstrate that FindNode is finding the name and not he ArgumentSyntax node.
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.
I have some tests where getInnermostNodeForTie is relevant including this one for method arguments.
document.Project.Solution.Workspace, | ||
optionSet, | ||
cancellationToken).ConfigureAwait(false); | ||
var newRoot = addImportService.AddImport(compilation, root, diagnosticNode, aliasDirective, placeSystemNamespaceFirst); |
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.
nit, move this code into the lambda below to make RegisterCodeFixAsync do as little work as possible.
private static string GetAliasFromDiagnosticNode(ISyntaxFactsService syntaxFacts, SyntaxNode diagnosticNode) | ||
{ | ||
// The content of the node is a good candidate for the alias | ||
// For attributes VB requires that the alias ends with 'Attribute' while C# is fine with or without the suffix. |
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.
if only VB needs this, then why is this code running for both C# and VB?
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.
I changed the logic to rely on symbol.Name instead of the text of the IdentifierNameSyntaxNode. This way it is easier and also covers the attribute requirements of VB.
{ | ||
if (!nodeText.EndsWith("Attribute")) | ||
{ | ||
nodeText += "Attribute"; |
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.
while it's likely minor, this code is not going to be correct in a lot of edge cases for attributes.
|
||
private static async Task<string> GetTextPreviewOfChangeAsync(SyntaxNode newNode, Workspace workspace, OptionSet optionSet, CancellationToken cancellationToken) | ||
{ | ||
var formattedNode = await Formatter.FormatAsync(newNode, workspace, optionSet, cancellationToken).ConfigureAwait(false); |
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.
easier and cheaper to just call NormalizeWhitespace on newNode.
'BC30561: '<name1>' is ambiguous, imported from the namespaces or types '<name2>' | ||
Private Const BC30561 As String = NameOf(BC30561) | ||
|
||
Public Overrides ReadOnly Property FixableDiagnosticIds As ImmutableArray(Of String) |
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.
can just say = ImmutableArray.Create...
Very nice!
I'd like to understand this a bit more. The way things work in VS is that VS will respect if an action is grouped if there are a lot of code actions to show. If there aren't that many, it will just flatten the children into the main list. This way if there are only a few actions, you can get to them directly. But if there are a lot, you don't have an OMG humongous list. Grouping would be appropriate, and we should try to have that here. |
Question: will this only add aliases for types? or it is possible this diagnostic will trigger for other ambiguous situations (like namespace, or extension methods, etc?)? |
// Aliasing as a closed constructed type is possible but would require to remove the type arguments from the diagnosed node. | ||
// It is unlikely that the user wants that and so generic types are not supported. | ||
symbolInfo.CandidateSymbols.All(symbol => symbol.IsKind(SymbolKind.NamedType) && | ||
symbol.GetArity() == 0); |
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.
ah, you check that it's a named type. good.
probably good to check that its containing symbol is a namespace as well.
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.
probably good to check that its containing symbol is a namespace as well.
Nested classes can also be ambigues. The corresponding test is here. The test case is copied from NameCollisionTests.cs
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.
Gotcha. Are there any issues if there are generics in the containing type (or an intermediary generic type)? might be good to have tests.
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.
I think the ambiguity for nested types is only possible with generic classes in combination with using static. So containing generic types are already tested. The test is called TestAmbiguousNestedClass in line 208 in AmbiguousTypeTests.cs.
|
||
protected override SyntaxNode GetAliasDirective(string typeName, ISymbol symbol) | ||
=> SyntaxFactory.UsingDirective(SyntaxFactory.NameEquals(typeName), | ||
SyntaxFactory.IdentifierName(symbol.ToNameDisplayString())); |
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.
better would be to use the SyntaxGenerator to create a TypeExpression instead of using IdentifierName.
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.
Note: SyntaxGenerator already has NamespaceImportDeclaration. Probably just want to add AliasImportDeclaration to that location. Then, you won't even need any VB/C# specific code here. The base type will just be able to call that new method on SyntaxGenerator.
I gave this a try in b752f84 and did a simple |
@CyrusNajmabadi I'm currently done with the changes on this PR.
That's resolved. The grouping is only active if there are more than three different kinds of suggestions present. So it is not related to the total number of entries.
and
and
I will leave the
I looked into it and it seemed more complicated than I thought first. And because you don't see any value here it doesn't seems worth the effort. |
@CyrusNajmabadi could I get a second review please. |
{ | ||
internal abstract class AbstractAliasAmbiguousTypeCodeFixProvider : CodeFixProvider | ||
{ | ||
|
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.
extra new line.
var newRoot = addImportService.AddImport(compilation, root, diagnosticNode, aliasDirective, placeSystemNamespaceFirst); | ||
return Task.FromResult(document.WithSyntaxRoot(newRoot)); | ||
}; | ||
codeActionsBuilder.Add(new MyCodeAction(codeActionPreviewText, CreateChangedDocument)); |
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.
nit: our pattern is to generally use a lambda here. but it's not a big deal. if you prefer the above form, then keep it.
} | ||
} | ||
|
||
private class GroupingCodeAction : CodeActionWithNestedActions |
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.
no need for this type. CodeActionWithNestedActions can just be instanted directly. (however, keep MyCodeAction, that specific subclass is used for telemetry purposes).
Imports Microsoft.CodeAnalysis.CodeFixes | ||
|
||
Namespace Microsoft.CodeAnalysis.VisualBasic.AliasAmbiguousType | ||
|
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.
extra newline.
Protected Overrides Function GetTextPreviewOfChange(aliasName As String, typeSymbol As ITypeSymbol) As String | ||
Return $"Imports { aliasName } = { typeSymbol.ToNameDisplayString() }" | ||
End Function | ||
|
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.
extra newline.
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.
Needs @dotnet/roslyn-ide review as well.
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.
My only issue with this is the new SyntaxGenerator relies on a symbol as input instead of a syntax node. This can be a barrier to future performance, so I'd like to see it updated to operate primarily on syntax nodes. The symbol argument could be provided via an overload for cases where it's already available.
/// </summary> | ||
/// <param name="aliasIdentifierName">The name of the alias.</param> | ||
/// <param name="symbol">The namespace or type to be aliased.</param> | ||
public abstract SyntaxNode AliasImportDeclaration(string aliasIdentifierName, INamespaceOrTypeSymbol symbol); |
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.
❗️ This should have the following signature:
public abstract SyntaxNode AliasImportDeclaration(string alias, SyntaxNode name);
If you want to keep a signature that operates on INamespaceOrTypeSymbol
, you can use a second overload:
public SyntaxNode AliasImportDeclaration(string alias, INamespaceOrTypeSymbol symbol) =>
AliasImportDeclaration(alias, { see below });
The missing section of the helper method should call a method similar to TypeExpression(ITypeSyntax)
, except it should forward the call to GenerateNameSyntax
instead of GenerateTypeSyntax
.
…ntaxNode for the name to create an alias for.
I think I did a mistake while I was solving the merge conflict. The file src/Workspaces/Core/Portable/PublicAPI.Unshipped.txt should only contain these lines:
Can anyone confirm this? |
d4f7d8a
to
9287ce4
Compare
@MaStr11 The previous merge was incorrect, but I amended the commit to be what you intended and force-pushed to the branch so it should appear correct now. |
9287ce4
to
0d7c640
Compare
Customer scenario
Codefix for
CS0104 'reference' is an ambiguous reference between 'identifier' and 'identifier'
(BC30561 in VB).Before:
After
Bugs this fixes
Feature request #23373.
Workarounds, if any
Fully qualify type.
Risk
Golden bar from failing code fix provider.
Performance impact
Low. Code fix is only triggered by one diagnostic (CS0104 or BC30561)
Is this a regression from a previous update?
New feature.
Root cause analysis
User request to get closer to parity with Resharper.
How was the bug found?
Customer reported #23373.
Test documentation updated?
No.
Remarks
Update 8/1/18: This is resolved:
I tried to useclass GroupingCodeAction : CodeAction.CodeActionWithNestedActions
to group the re-factorings like here but then all tests were failing and in VS it didn't made a difference whether I useCodeActionWithNestedActions
or register a code fix per ambiguous type. The FeaturesResources.Alias_ambiguous_type_0 text template was created to work withCodeActionWithNestedActions
and therefore the text template is inadequate in the context it is used now.