Skip to content

Pull Members Up adds Usings#54455

Merged
Soreloser2 merged 24 commits intodotnet:mainfrom
Soreloser2:PullMemberUpUsing
Jul 21, 2021
Merged

Pull Members Up adds Usings#54455
Soreloser2 merged 24 commits intodotnet:mainfrom
Soreloser2:PullMemberUpUsing

Conversation

@Soreloser2
Copy link
Contributor

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.

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.
@Soreloser2 Soreloser2 requested a review from JoeRobich June 28, 2021 23:34
@Soreloser2 Soreloser2 requested a review from a team as a code owner June 28, 2021 23:34
@dnfadmin
Copy link

dnfadmin commented Jun 28, 2021

CLA assistant check
All CLA requirements met.

using System.Collections.Generic;
using Microsoft.CodeAnalysis.Test.Utilities.PullMemberUp;
using Roslyn.Test.Utilities;
using System.Xml.Linq;
Copy link
Member

Choose a reason for hiding this comment

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

Is this using needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

sort.

Copy link
Contributor

Choose a reason for hiding this comment

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

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

@Cosifne Cosifne Jun 29, 2021

Choose a reason for hiding this comment

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

Emm, in which case could 'type' be null here?

Copy link
Contributor

Choose a reason for hiding this comment

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

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) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.Select((symbol) =>
.Select(symbol =>

Copy link
Contributor

Choose a reason for hiding this comment

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

consider indenting this too.

var type = symbol.ConvertToType(compilation);
if (type.Equals(compilation.ObjectType))
{
type = symbol.GetMemberType();
Copy link
Member

@Cosifne Cosifne Jun 29, 2021

Choose a reason for hiding this comment

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

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'

Copy link
Member

Choose a reason for hiding this comment

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

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

Comment on lines +280 to +287
// 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)));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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:

  1. pull all the usings up to the destination location that are not already in the destination.
  2. mark all existing usings in teh destination location somehow (or, alternatively, mark only the usings that were pulled up.
  3. 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.

Copy link
Member

Choose a reason for hiding this comment

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

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, a document is passed in. So you can always acquire language services from that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +28 to +29
}
protected override bool FilterType(SyntaxNode node)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
}
protected override bool FilterType(SyntaxNode node)
}
protected override bool FilterType(SyntaxNode node)

Comment on lines +30 to +35
=> node switch
{
IdentifierNameSyntax => true,
ObjectCreationExpressionSyntax => true,
_ => false
};
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
=> node switch
{
IdentifierNameSyntax => true,
ObjectCreationExpressionSyntax => true,
_ => false
};
=> node is IdentifierNameSyntax or ObjectCreationExpressionSyntax;

Copy link
Member

Choose a reason for hiding this comment

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

I haven't followed the logic here, but is implicit object creation dropped intentionally?

Copy link
Contributor

Choose a reason for hiding this comment

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

this won't work for things like List<int>

Comment on lines +278 to +282
Where((node) => FilterType(node)).
Select((node) => semanticModel.GetTypeInfo(node, cancellationToken).Type).
Where((type) => type != null).
Distinct().
Select((type) => SymbolAnnotation.Create(type)));
Copy link
Member

@Youssef1313 Youssef1313 Jul 2, 2021

Choose a reason for hiding this comment

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

Suggested change
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)));

Copy link
Contributor

Choose a reason for hiding this comment

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

yes. please don't parenthesize single parameters.

Copy link
Contributor

Choose a reason for hiding this comment

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

this will not work for extension methods.

Copy link
Contributor

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

Remove if possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

this is a requirement now. no new code can be nullable disabled.

return solutionEditor.GetChangedSolution();
}

protected abstract bool FilterType(SyntaxNode node);
Copy link
Contributor

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

i didn't see vb tests in this PR.

@Soreloser2
Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

The parenthesis is not needed : )

Copy link
Member

Choose a reason for hiding this comment

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

The parenthesis is not needed : )

Also could be simplified to

Suggested change
.Where((node) => node is CompilationUnitSyntax || node is NamespaceDeclarationSyntax)
.Where(node => node is CompilationUnitSyntax or NamespaceDeclarationSyntax)

Copy link
Member

Choose a reason for hiding this comment

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

Also I remembered DescendantNodesAndSelf() has an overload takes a predicate, so you could merge this where clause.

Copy link
Member

Choose a reason for hiding this comment

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

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

Copy link
Member

@Cosifne Cosifne Jul 7, 2021

Choose a reason for hiding this comment

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

@Youssef1313 thanks for your further explanation. ( I just omit to explain the recursively part of that function) : )

@CyrusNajmabadi
Copy link
Contributor

I will try to check tonight :)

… more tests too for namespaces, aliases, and extensions.
@Soreloser2
Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

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.

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

Choose a reason for hiding this comment

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

is there some code somewhere that validates teh destination type's language is the same as the source type's language?

Copy link
Contributor Author

@Soreloser2 Soreloser2 Jul 15, 2021

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

interesting. are both crlfs needed?

Copy link
Contributor

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

feels almost there :)

@Soreloser2
Copy link
Contributor Author

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.

@CyrusNajmabadi
Copy link
Contributor

CyrusNajmabadi commented Jul 16, 2021

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

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.
Soren Dahl added 2 commits July 19, 2021 17:49
…ed an error with duplicating first member trivia. Fixed tests that expected duplicate first member trivia.
@Soreloser2
Copy link
Contributor Author

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


protected override SyntaxList<UsingDirectiveSyntax> GetCompilationImports(CompilationUnitSyntax node) => node.Usings;

protected override SyntaxList<UsingDirectiveSyntax> GetNamespaceImports(NamespaceDeclarationSyntax node) => node.Usings;
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

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

LGTM with the change to use ISyntaxFacts

Comment on lines +401 to +412
.SelectMany(node =>
{
if (node is ICompilationUnitSyntax)
{
return syntaxFacts.GetImportsOfCompilationUnit(node);
}
else
{
// node is namespace declaration
return syntaxFacts.GetImportsOfNamespaceDeclaration(node);
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.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;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
=> ((NamespaceDeclarationSyntax)namespaceDeclaration).Usings;
=> ((BaseNamespaceDeclarationSyntax)namespaceDeclaration).Usings;

also add test for filescopednamespace case.

Copy link
Contributor

Choose a reason for hiding this comment

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

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

@Soreloser2 Soreloser2 merged commit e62bf4e into dotnet:main Jul 21, 2021
@ghost ghost added this to the Next milestone Jul 21, 2021
@allisonchou allisonchou modified the milestones: Next, 17.0.P3 Jul 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Pull member up" refactoring does not add necessary usings

7 participants