Pull Members Up adds Usings#54455
Conversation
Add symbol annotations so that members pulled up will also include correct usings/imports. Add 12 new C# tests regarding pulling members up and import adding.
| using System.Collections.Generic; | ||
| using Microsoft.CodeAnalysis.Test.Utilities.PullMemberUp; | ||
| using Roslyn.Test.Utilities; | ||
| using System.Xml.Linq; |
There was a problem hiding this comment.
I really wish if we wanted usings sorted it would at least fail the correctness test
</rant>
| { | ||
| type = symbol.GetMemberType(); | ||
| } | ||
| return SymbolAnnotation.Create(type ?? compilation.ObjectType); |
There was a problem hiding this comment.
Emm, in which case could 'type' be null here?
There was a problem hiding this comment.
i also don't really understand what is going on here. you're finding all the declared symbols... and then doing either COnvertToType or GetMemberType... because? I don't really get the logic of what's going on here.
| // add import symbol annotations for all the code we may be moving | ||
| newDestination = newDestination.WithAdditionalAnnotations( | ||
| semanticModel.GetAllDeclaredSymbols(syntax, cancellationToken) | ||
| .Select((symbol) => |
There was a problem hiding this comment.
| .Select((symbol) => | |
| .Select(symbol => |
There was a problem hiding this comment.
consider indenting this too.
| var type = symbol.ConvertToType(compilation); | ||
| if (type.Equals(compilation.ObjectType)) | ||
| { | ||
| type = symbol.GetMemberType(); |
There was a problem hiding this comment.
It is a comment for all the tests.
Could you write tests that imports some other namespaces? (Not 'System', but like 'System.Linq, or System.Threading')
This is because System is so widely used, at this line, if the symbol is a method, then it would return the 'System.Func',
which cause 'System' is imported later.
However, because all your tests now are importing 'System', if the member is
void Bar() { var c = Uri.TryCreate() ... }
'System' could be imported by 'Func', not 'Uri'
There was a problem hiding this comment.
Also, semanticModel.GetAllDeclaredSymbols will only returns the 'declared' symbol,
If I have a method like
void bar() { Uri.TryCreate(...) }
I really worry 'Uri' won't be passed in this method
…and non-declared type
| // add import symbol annotations for all the code we may be moving | ||
| newDestination = newDestination.WithAdditionalAnnotations( | ||
| syntax.DescendantNodesAndSelf() | ||
| .Select((node) => semanticModel.GetTypeInfo(node, cancellationToken).Type) | ||
| .Where((type) => type != null && | ||
| !type.IsErrorType()) | ||
| .Distinct() | ||
| .Select((type) => SymbolAnnotation.Create(type))); |
There was a problem hiding this comment.
This other way to do it gets all descendant syntax nodes, and adds any that have a valid type. One issue is that this is possibly slow/inefficient, in which case I think we can filter on only Identifier nodes, which would require converting this to a language service.
There was a problem hiding this comment.
I think it's worse than this. First, we want to check any start of expression (which could be identifiers, generic names, etc.). Second, things like extension methods are brought in by using statements, so we need to bind methods.
AN alternate approach that sidesteps this (though introduces its own problems is), is to avoid this kind of binding entirely.
Instead:
- pull all the usings up to the destination location that are not already in the destination.
- mark all existing usings in teh destination location somehow (or, alternatively, mark only the usings that were pulled up.
- remove all unused usings as the final step but filter that only to the set that was added.
Effectively, this will leave us with only the set that was needed to bind the code that was moved. THe downside is though that some of the added usings may cause conflicts in the destination due to ambiguous references. However, that was already possible with the approach we're talking here, so maybe it's not too bad.
There was a problem hiding this comment.
: ) Just one point added, It is ok if you finally need to convert to non-static language service. I remember when this class is originally written, we have a discussion around whether it should be a static helper or abstract class -> C# impl/VB impl.
There was a problem hiding this comment.
Also, a document is passed in. So you can always acquire language services from that.
There was a problem hiding this comment.
One other option to consider would be to use the simplifier to expand (and later collapse) the moved nodes into using their fully qualified names. This might get around the destination conflicts problem as well, although that may need more testing.
| } | ||
| protected override bool FilterType(SyntaxNode node) |
There was a problem hiding this comment.
| } | |
| protected override bool FilterType(SyntaxNode node) | |
| } | |
| protected override bool FilterType(SyntaxNode node) |
| => node switch | ||
| { | ||
| IdentifierNameSyntax => true, | ||
| ObjectCreationExpressionSyntax => true, | ||
| _ => false | ||
| }; |
There was a problem hiding this comment.
| => node switch | |
| { | |
| IdentifierNameSyntax => true, | |
| ObjectCreationExpressionSyntax => true, | |
| _ => false | |
| }; | |
| => node is IdentifierNameSyntax or ObjectCreationExpressionSyntax; |
There was a problem hiding this comment.
I haven't followed the logic here, but is implicit object creation dropped intentionally?
There was a problem hiding this comment.
this won't work for things like List<int>
| Where((node) => FilterType(node)). | ||
| Select((node) => semanticModel.GetTypeInfo(node, cancellationToken).Type). | ||
| Where((type) => type != null). | ||
| Distinct(). | ||
| Select((type) => SymbolAnnotation.Create(type))); |
There was a problem hiding this comment.
| Where((node) => FilterType(node)). | |
| Select((node) => semanticModel.GetTypeInfo(node, cancellationToken).Type). | |
| Where((type) => type != null). | |
| Distinct(). | |
| Select((type) => SymbolAnnotation.Create(type))); | |
| .Where(node => FilterType(node)) | |
| .Select(node => semanticModel.GetTypeInfo(node, cancellationToken).Type) | |
| .Where(type => type != null) | |
| .Distinct() | |
| .Select(type => SymbolAnnotation.Create(type))); |
There was a problem hiding this comment.
yes. please don't parenthesize single parameters.
There was a problem hiding this comment.
this will not work for extension methods.
There was a problem hiding this comment.
this may be an enormous number of annotations. i'm not comfortable with this.
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| #nullable disable |
There was a problem hiding this comment.
this is a requirement now. no new code can be nullable disabled.
src/Features/Core/Portable/PullMemberUp/IMembersPullerService.cs
Outdated
Show resolved
Hide resolved
| return solutionEditor.GetChangedSolution(); | ||
| } | ||
|
|
||
| protected abstract bool FilterType(SyntaxNode node); |
There was a problem hiding this comment.
place abstract after constructors plz :)
|
|
||
| Protected Overrides Function FilterType(node As SyntaxNode) As Boolean | ||
| Return TypeOf node Is IdentifierNameSyntax Or TypeOf node Is ObjectCreationExpressionSyntax | ||
| End Function |
There was a problem hiding this comment.
we don't need a special service for thsi. these services can be removed and ISyntaxFactsService or ISyntaxKindsService can be used instead from the original code to make this determination for both C# and VB.
|
|
||
| Namespace Microsoft.CodeAnalysis.CodeRefactorings.PullMemberUp | ||
| <ExportLanguageService(GetType(IMembersPullerService), LanguageNames.VisualBasic), [Shared]> | ||
| Friend Class ViualBasicMembersPullerService |
There was a problem hiding this comment.
i didn't see vb tests in this PR.
…orts except already unused destination imports
… in destination file
…ld just keep all leading trivia now.
|
@CyrusNajmabadi @Cosifne Could you take another look at this? I think I had some success with merging the import statements, then removing non-destination unnecessary ones. Thanks! |
| { | ||
| var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); | ||
| return root.DescendantNodesAndSelf() | ||
| .Where((node) => node is CompilationUnitSyntax || node is NamespaceDeclarationSyntax) |
There was a problem hiding this comment.
The parenthesis is not needed : )
There was a problem hiding this comment.
The parenthesis is not needed : )
Also could be simplified to
| .Where((node) => node is CompilationUnitSyntax || node is NamespaceDeclarationSyntax) | |
| .Where(node => node is CompilationUnitSyntax or NamespaceDeclarationSyntax) |
There was a problem hiding this comment.
Also I remembered DescendantNodesAndSelf() has an overload takes a predicate, so you could merge this where clause.
There was a problem hiding this comment.
@Cosifne I think most of the time, Where isn't an equivalent for the overload taking a descendIntoChildren, which IIRC just stops going deep in the tree once the condition is false. But since the node we're looking for is either the root (compilation unit), or a child of the root (namespace declaration), using this overload may work.
There was a problem hiding this comment.
@Youssef1313 thanks for your further explanation. ( I just omit to explain the recursively part of that function) : )
|
I will try to check tonight :) |
… more tests too for namespaces, aliases, and extensions.
|
I added the change to synthesize a namespace import when we encounter a declaration as we traverse up a tree. I believe I also have all the tests listed, plus ones for that namespace import change. @CyrusNajmabadi, if you could take another look that would be great 😄 |
|
|
||
| protected override UsingDirectiveSyntax GenerateNamespaceImportDeclaration(NamespaceDeclarationSyntax node, SyntaxGenerator generator) | ||
| { | ||
| return (UsingDirectiveSyntax)generator.NamespaceImportDeclaration(node.Name); |
There was a problem hiding this comment.
this is not quite right. consider something like:
namespace Outer {
namespace Inner {
class StartingPoint { ... }
}
}
This will likely create using Inner instead of using Outer.Inner. to resolve this, don't use syntax this way. Instead, get the containing namespace symbol for the type you're in. ANd then just generate the name for that and the corresponding namespace import. this can all be done in teh base class.
src/Features/Core/Portable/PullMemberUp/AbstractMembersPullerService.cs
Outdated
Show resolved
Hide resolved
| @@ -265,6 +292,13 @@ private static async Task<Solution> PullMembersIntoClassAsync( | |||
| options: await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false)); | |||
| var newDestination = codeGenerationService.AddMembers(destinationSyntaxNode, pullUpMembersSymbols, options: options, cancellationToken: cancellationToken); | |||
There was a problem hiding this comment.
is there some code somewhere that validates teh destination type's language is the same as the source type's language?
There was a problem hiding this comment.
It looks like the quick action is not offered when the base type is in another language, as I can't seem to find it when the base class is written in VB and derived in C#. However, I can't find any line of code that validates this (here is the closest thing), or any tests that confirm this behavior, but it's possible that there is a check.
I think the move to base class shouldn't work at all when the languages are different, as the generator takes an option which tells it to reuse the existing member syntax, which should already be in its language-specific form. Moving to an interface should probably still work, since the generator rewrites the member signature from scratch. Would you suggest I add some sort of check here, or would that be another issue entirely?
| sourceImports.AddRange(GetImports(syntax, destinationEditor.Generator) | ||
| .Select(import => import | ||
| .WithAppendedTrailingTrivia(syntaxFacts.ElasticCarriageReturnLineFeed) | ||
| .WithPrependedLeadingTrivia(syntaxFacts.ElasticCarriageReturnLineFeed) |
There was a problem hiding this comment.
interesting. are both crlfs needed?
src/Features/Core/Portable/PullMemberUp/AbstractMembersPullerService.cs
Outdated
Show resolved
Hide resolved
CyrusNajmabadi
left a comment
There was a problem hiding this comment.
feels almost there :)
…d namespace import test.
|
Hey @CyrusNajmabadi, I replaced the CRLFs with formatter annotations. I believe this would correctly add lines according to preferences. I'm worried it might over-format other existing imports, even when all the source imports turn out to be unnecessary, resulting in just whitespace changes in the imports, which may be confusing/annoying for people. Is there perhaps a better way to correctly format the whitespace around the imports while minimally changing existing whitespace? Ideally I'd want to retain whitespace trivia above and below the pre-existing import list. |
I don't think this will actually a problem. Here's why: You are adding hte elastic trivia to the new imports you are adding. However, if all the imports turn out to be unnecessary, they'll be removed entirely from the file. So, the file will not contain those imports (or hteir elastic trivia). So the formatter should not end up doing anything to the file. The formatter would only need to format the things that were actually added and has the elastic trivia. If they aren't there, there's nothing for it to do. |
…leted a line that should probably stay there.
…ed an error with duplicating first member trivia. Fixed tests that expected duplicate first member trivia.
…ewlines when they weren't necessary.
|
@CyrusNajmabadi, could you give this another look? I think I got the whitespace formatting correct, and I just removed all the leading trivia. Normalizing whitespace wasn't really working because the import was still attached to the other source imports, in the original member tree. I figured that moving comments along with the imports isn't really desired behavior, since we only want to get our member code working, and importing other trivia like comments might mess up formatting as well. |
src/Features/CSharp/Portable/PullMemberUp/CSharpMembersPullerService.cs
Outdated
Show resolved
Hide resolved
|
|
||
| protected override SyntaxList<UsingDirectiveSyntax> GetCompilationImports(CompilationUnitSyntax node) => node.Usings; | ||
|
|
||
| protected override SyntaxList<UsingDirectiveSyntax> GetNamespaceImports(NamespaceDeclarationSyntax node) => node.Usings; |
There was a problem hiding this comment.
so we already have:
interface ISyntaxFacts
{
SyntaxList<SyntaxNode> GetMembersOfNamespaceDeclaration(SyntaxNode namespaceDeclaration);
SyntaxList<SyntaxNode> GetMembersOfCompilationUnit(SyntaxNode compilationUnit);
}We shoudl add equivalent GetImportsOfNamespaceDeclaration (and ocmpilation unit).
Then we can just use ISyntaxFacts, and remove the IMemberPullService entirely.
src/Features/Core/Portable/PullMemberUp/AbstractMembersPullerService.cs
Outdated
Show resolved
Hide resolved
src/Features/Core/Portable/PullMemberUp/AbstractMembersPullerService.cs
Outdated
Show resolved
Hide resolved
CyrusNajmabadi
left a comment
There was a problem hiding this comment.
LGTM with the change to use ISyntaxFacts
… to before + refactor service usages
| .SelectMany(node => | ||
| { | ||
| if (node is ICompilationUnitSyntax) | ||
| { | ||
| return syntaxFacts.GetImportsOfCompilationUnit(node); | ||
| } | ||
| else | ||
| { | ||
| // node is namespace declaration | ||
| return syntaxFacts.GetImportsOfNamespaceDeclaration(node); | ||
| } | ||
| }) |
There was a problem hiding this comment.
| .SelectMany(node => | |
| { | |
| if (node is ICompilationUnitSyntax) | |
| { | |
| return syntaxFacts.GetImportsOfCompilationUnit(node); | |
| } | |
| else | |
| { | |
| // node is namespace declaration | |
| return syntaxFacts.GetImportsOfNamespaceDeclaration(node); | |
| } | |
| }) | |
| .SelectMany(node => node is ICompilationUnitSyntax | |
| ? syntaxFacts.GetImportsOfCompilationUnit(node) | |
| : syntaxFacts.GetImportsOfNamespaceDeclaration(node)) |
| => ((CompilationUnitSyntax)compilationUnit).Members; | ||
|
|
||
| public SyntaxList<SyntaxNode> GetImportsOfNamespaceDeclaration(SyntaxNode namespaceDeclaration) | ||
| => ((NamespaceDeclarationSyntax)namespaceDeclaration).Usings; |
There was a problem hiding this comment.
| => ((NamespaceDeclarationSyntax)namespaceDeclaration).Usings; | |
| => ((BaseNamespaceDeclarationSyntax)namespaceDeclaration).Usings; |
also add test for filescopednamespace case.
There was a problem hiding this comment.
(note: you'll need to fixup IsNamespaceDeclaration. i'm doing that in one of my PRs. but it's ok if you do that as well.
…ake sure we find those namespaces.
Closes #46010 and #39744
Pull Members up should now add the correct using directives when needed for properties, methods, events, and fields
Add symbol annotations so that members pulled up to base classes from derived classes in different files will also include correct usings/imports.
Add 12 new C# tests regarding pulling members up and import adding, some of which test behavior pulling up to interfaces. Pulling up to interfaces wasn't broken in the first place, but there are now tests to ensure it works.