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

Ambiguous type codefix provider (using alias) #24022

Merged
merged 27 commits into from
Mar 13, 2018

Conversation

MaStr11
Copy link
Contributor

@MaStr11 MaStr11 commented Jan 3, 2018

Customer scenario

Codefix for CS0104 'reference' is an ambiguous reference between 'identifier' and 'identifier' (BC30561 in VB).

Before:

using N1;
using N2;

namespace N1
{
    public class AmbiguousClass { }
}
namespace N2
{
    public class AmbiguousClass { }
}
namespace NTest
{
    public class Test: AmbiguousClass //CS0104
    {
    }
}

After

using N1;
using N2;
using AmbiguousClass = N1.AmbiguousClass; // Fix

namespace N1
{
    public class AmbiguousClass { }
}
namespace N2
{
    public class AmbiguousClass { }
}
namespace NTest
{
    public class Test: AmbiguousClass
    {
    }
}

2018-01-03 20_31_22-

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 use class 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 use CodeActionWithNestedActions or register a code fix per ambiguous type. The FeaturesResources.Alias_ambiguous_type_0 text template was created to work with CodeActionWithNestedActions and therefore the text template is inadequate in the context it is used now.

@MaStr11 MaStr11 requested a review from a team as a code owner January 3, 2018 20:01
}
}";
await TestInRegularAndScriptAsync(initialMarkup, expectedMarkupTemplate.Replace("#", "using Ambiguous = N1.Ambiguous;"), 0);
await TestInRegularAndScriptAsync(initialMarkup, expectedMarkupTemplate.Replace("#", "using Ambiguous = N2.Ambiguous;"), 1);
Copy link
Member

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.

Copy link
Member

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
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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
Copy link
Member

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
Copy link
Member

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);
Copy link
Member

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.

Copy link
Contributor Author

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);
Copy link
Member

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.
Copy link
Member

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?

Copy link
Contributor Author

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";
Copy link
Member

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);
Copy link
Member

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)
Copy link
Member

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...

@CyrusNajmabadi
Copy link
Member

Very nice!

I tried to use class 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 use CodeActionWithNestedActions or register a code fix per ambiguous type.

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.

@CyrusNajmabadi
Copy link
Member

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);
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Contributor Author

@MaStr11 MaStr11 Jan 4, 2018

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()));
Copy link
Member

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.

Copy link
Member

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.

@MaStr11
Copy link
Contributor Author

MaStr11 commented Jan 8, 2018

Sure. It looks like that approach would work. However, IMO, it's just as simple to just build the string yourself.

I gave this a try in b752f84 and did a simple $"using { alias } = { typeSymbol.ToNameDisplayString() };";. Doing so allows to also delay the call to syntaxGenerator.AliasImportDeclaration to the CodeAction phase.

@MaStr11
Copy link
Contributor Author

MaStr11 commented Jan 8, 2018

@CyrusNajmabadi I'm currently done with the changes on this PR.
Regarding some open points raised in the conversation:

In my VS the grouping didn't worked even for more than 10 entries.

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.

Do you know of a way how to get an extern alias applied.

There is currently no way (afaik). You'd have to go update the SymbolDisplay code.

and

There are many open general problems with just adding a "using X = ....X" approach.

and

My recommendation would be to not go down this path and leave it as an open known issue

I will leave the extern alias bug as is. How should I document the issue?

It would be useful to test, whether the fixed code is without compiler errors. If not would you agree that it is useful to have such an option and could I implement the feature?

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.

@MaStr11
Copy link
Contributor Author

MaStr11 commented Jan 15, 2018

@CyrusNajmabadi could I get a second review please.

{
internal abstract class AbstractAliasAmbiguousTypeCodeFixProvider : CodeFixProvider
{

Copy link
Member

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));
Copy link
Member

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
Copy link
Member

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

Copy link
Member

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

Copy link
Member

Choose a reason for hiding this comment

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

extra newline.

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi left a 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.

Copy link
Member

@sharwell sharwell left a 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);
Copy link
Member

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.

@sharwell sharwell added this to Community Priority in IDE: sharwell Feb 14, 2018
@sharwell sharwell added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Feb 15, 2018
…ntaxNode for the name to create an alias for.
@sharwell sharwell moved this from Community Priority to Needs Response from Me in IDE: sharwell Mar 1, 2018
@sharwell sharwell moved this from Needs Response from Me to Waiting to Merge in IDE: sharwell Mar 1, 2018
@MaStr11
Copy link
Contributor Author

MaStr11 commented Mar 2, 2018

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:

Microsoft.CodeAnalysis.Editing.SyntaxGenerator.AliasImportDeclaration(string aliasIdentifierName, Microsoft.CodeAnalysis.INamespaceOrTypeSymbol symbol) -> Microsoft.CodeAnalysis.SyntaxNode
abstract Microsoft.CodeAnalysis.Editing.SyntaxGenerator.AliasImportDeclaration(string aliasIdentifierName, Microsoft.CodeAnalysis.SyntaxNode name) -> Microsoft.CodeAnalysis.SyntaxNode
abstract Microsoft.CodeAnalysis.Editing.SyntaxGenerator.NameExpression(Microsoft.CodeAnalysis.INamespaceOrTypeSymbol namespaceOrTypeSymbol) -> Microsoft.CodeAnalysis.SyntaxNode

Can anyone confirm this?

@sharwell sharwell force-pushed the AmbiguousTypeCodefixProvider branch from d4f7d8a to 9287ce4 Compare March 8, 2018 17:15
@sharwell
Copy link
Member

sharwell commented Mar 8, 2018

@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.

@sharwell sharwell force-pushed the AmbiguousTypeCodefixProvider branch from 9287ce4 to 0d7c640 Compare March 8, 2018 18:48
@sharwell sharwell merged commit 0dd9b97 into dotnet:master Mar 13, 2018
@sharwell sharwell removed this from Waiting to Merge in IDE: sharwell Mar 13, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Community The pull request was submitted by a contributor who is not a Microsoft employee.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants