-
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
Refactor change namespace service #31447
Conversation
40d43ea
to
62058af
Compare
Can you ping me when you'd lke a review? Thanks! |
@dotnet/roslyn-ide @CyrusNajmabadi Please take a look. Thanks! |
Ping @CyrusNajmabadi |
} | ||
else | ||
{ | ||
return default; |
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.
does this indicate a bug with teh caller?
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.
Possibly, but since this is a service, I believe we have to handle it. (or the pattern is to throw?)
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 would throw then and treat this as argument validation. caller should be passing in the right value (which should be documented).
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.
OK, will fix.
Are you done with the reviewing or you just started?
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'm done reviewing for now.. it looks good to me :) Was going to sign off once you made any further changes and i got a chance to look at them.
ImmutableArray<string> declaredNamespaceParts, | ||
ImmutableArray<string> targetNamespaceParts) | ||
{ | ||
Debug.Assert(!declaredNamespaceParts.IsDefault && !targetNamespaceParts.IsDefault); | ||
var container = root.GetAnnotatedNodes(ContainerAnnotation[0]).Single(); |
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 looks weird. What is ContainerANnotation[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.
Also... it's indexable... but sounds singular...
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 an single item array for ContainerAnnotation instead so we don't have create it repeatedly, because an array is required for both the overload to add annotation (using params) and the method to remove the annotation.
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.
hmm, for some reason I thought the method removes annotation only takes an array... fixed.
var targetNamespaceDecl = SyntaxFactory.NamespaceDeclaration( | ||
name: CreateNameSyntax(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1) | ||
.WithAdditionalAnnotations(WarningAnnotation), | ||
externs: default, | ||
usings: default, | ||
members: compilationUnit.Members); | ||
return compilationUnit.WithMembers(new SyntaxList<MemberDeclarationSyntax>(targetNamespaceDecl)); | ||
return compilationUnit.WithMembers(new SyntaxList<MemberDeclarationSyntax>(targetNamespaceDecl)) |
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.
consider extracting this if-body to a helper method.
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.
Fixed
namespaceDecl.WithName( | ||
CreateNameSyntax(targetNamespaceParts, aliasQualifier: null, targetNamespaceParts.Length - 1) | ||
.WithTriviaFrom(namespaceDecl.Name).WithAdditionalAnnotations(WarningAnnotation)) | ||
.WithoutAnnotations(ContainerAnnotation)); |
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.
consider extracing this if-body to a helper method. this method is getting quite large.
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.
Fixed
/// </summary> | ||
protected override async Task<SyntaxNode> TryGetApplicableContainerFromSpanAsync(Document document, TextSpan span, CancellationToken cancellationToken) | ||
{ | ||
var compilationUnit = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) as CompilationUnitSyntax; |
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.
just cast. 'as' indicates you think it might not be this type and want to check for null. so an 'as' should always have a null-check, otherwise, you shoudl be casting.
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.
Fixed
// first and last member, otherwise, simply attach all to the EOF token. | ||
if (members.Count > 0) | ||
var hasNamespaceDecl = compilationUnit.DescendantNodes(IsCompilationUnitOrNamespaceDeclaration) | ||
.OfType<NamespaceDeclarationSyntax>().Any(); |
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.
you're asking if any descendants of the CompilationUnit are themselve a CompilationUnit. Seems odd.
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, I'm asking if any descendants is NamespaceDeclaration
Ah, I see what you are saying.
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.
Fixed
var lastWithTrivia = last.WithAppendedTrailingTrivia(namespaceClosingTrivia); | ||
members = members.Replace(last, lastWithTrivia); | ||
var namespaceDecl = node.AncestorsAndSelf().OfType<NamespaceDeclarationSyntax>().SingleOrDefault(); | ||
if (namespaceDecl?.Name.GetDiagnostics().All(diag => diag.DefaultSeverity != DiagnosticSeverity.Error) != 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.
the ?.
and All
and != true
are jsut super confusing. First, if you don't have a namespace, handle htat. Then just check the diagnostics of the name.
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.
Fixed
|
||
private static SyntaxNode GetTriggeringNode(CompilationUnitSyntax compilationUnit, int position) | ||
{ | ||
var compilationUnit = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) as CompilationUnitSyntax; |
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.
cast
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.
Fixed
|
||
private static SyntaxNode GetTriggeringNode(CompilationUnitSyntax compilationUnit, int position) | ||
{ | ||
var compilationUnit = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) as CompilationUnitSyntax; | ||
var namespaceDecls = compilationUnit.DescendantNodes(n => n is CompilationUnitSyntax || n is NamespaceDeclarationSyntax) | ||
.OfType<NamespaceDeclarationSyntax>().ToImmutableArray(); |
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.
repeat of the CompilationUnit || NamespcaeDecl code. Repeat of hte "find me the namespace decls" code.
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.
Yes, but I don't want to promote this to SyntaxFacts until we have a need for this in VB service, and there's no good place to share it between the refactoring and service. I think it's Ok since it's just a one liner.
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.
and there's no good place to share it between the refactoring and service.
Can tehre not just be a sibling class with helpers that are shared? That's what we generally do elsewhere...
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.
Sure. I just thought it's a overkill for this.
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 fair. I don't care that much either :) Just something i noticed :)
/// <summary> | ||
/// The annotation used to track applicable container in each document to be fixed. | ||
/// </summary> | ||
protected static SyntaxAnnotation[] ContainerAnnotation { get; } = new[] { new SyntaxAnnotation() }; |
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.
- Should be plural.
- Don't have mutable top level arrays. Someone might change it out from underneat you.
|
||
protected abstract string GetDeclaredNamespace(SyntaxNode container); | ||
|
||
protected abstract Task<ImmutableArray<(DocumentId id, SyntaxNode container)>> CanChangeNamespaceWorkerAsync(Document document, SyntaxNode container, CancellationToken cancellationToken); |
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.
document the return value here.
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.
Fixed
public override async Task<bool> CanChangeNamespaceAsync(Document document, SyntaxNode container, CancellationToken cancellationToken) | ||
{ | ||
var applicableContainers = await CanChangeNamespaceWorkerAsync(document, container, cancellationToken).ConfigureAwait(false); | ||
return !applicableContainers.IsDefault; |
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 CanChangeNamespaceWorkerAsync seems to be returning something non-boolean indicates to me the name o hte method is wonky.
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.
Yes, I agree, I will try to think a better name
ImmutableArray<DocumentId> documentIds, | ||
string declaredNamespace, | ||
Document document, | ||
SyntaxNode container, | ||
string targetNamespace, | ||
CancellationToken cancellationToken) |
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.
docuent what 'container' is
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.
There's comment on the interface
return solution; | ||
} | ||
|
||
var containersFromAllDocuments = await CanChangeNamespaceWorkerAsync(document, container, 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.
this method name doesn't match the value it reutrns.
return solution; | ||
} | ||
|
||
var annotatedSolution = await AnnotateContainersAsync(solution, containersFromAllDocuments, 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.
document what you're doing here.
|
||
var solutionAfterNamespaceChange = solution; | ||
var ids = containersFromAllDocuments.SelectAsArray(pair => pair.id); |
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.
what sort of ids are these? if documentIds, call these documentIds.
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.
Fixed
@@ -123,7 +153,7 @@ await ChangeNamespaceToMatchFoldersAsync(solutionAfterNamespaceChange, id, decla | |||
|
|||
var solutionAfterImportsRemoved = await RemoveUnnecessaryImportsAsync( | |||
solutionAfterFirstMerge, | |||
documentIds, | |||
containersFromAllDocuments.SelectAsArray(pair => pair.id), |
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.
did you already do this above?
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.
Yup, Fixed
foreach (var document in documents) | ||
{ | ||
var container = await TryGetApplicableContainerFromSpanAsync(document, span, cancellationToken).ConfigureAwait(false); | ||
containers.Add((document.Id, container)); |
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.
you did a try-cal, but didn't check the eturn value. is that intentional?
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 if-else below is used as check, should have moved the containers.Add
after it.
} | ||
} | ||
|
||
return spanForContainers.Count == 1 ? containers.ToImmutable() : default; |
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.
wait... you only return the container if you had one thing in it... so why is this method returning a list? WHy is it not just returning the container if you found one, or null if you found 0/many?
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'm returning multiple containers if they all have the same span
return true; | ||
} | ||
} | ||
return 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.
newline above this.
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.
Fixed
@@ -36,17 +38,32 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.ChangeNamespace | |||
End If | |||
End Function | |||
|
|||
' This is only reachable when called from a VB refacoring provider, which is not implemented yet. | |||
' TODO: Implement the service for 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.
if we haven't done this feature for VB, why do we need this service at all? Why not just remove it entirely?
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.
Need to call TryGetReplacementReferenceSyntax
, so we can fix references in 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.
The implementation is coming soon, since we'll need VB implementation of this service for #30896
/// | ||
/// Returns <see langword="true"/> only when all the requirements above are met. | ||
/// </summary> | ||
Task<bool> CanChangeNamespaceAsync(Document document, SyntaxNode container, CancellationToken cancellationToken); |
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.
is this also checking that hte namespace name doesn't match the file name? or is that done at a different level?
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.
Why namespace would match file name?
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.
Basically, i'm trying to figure htis out. Say i have "namespace A.B.C { }" and i'm in a file called A\B\C\Foo.cs
would this return 'true' (because it does'nt care about if the namespace is already properly named based on the document), or would it return 'false' (because the namespace is already correct based on the doc.
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 see. No, this service doesn't care, it will return ``true`. That's the logic left behind in the SyncNamespace refactoring. All this requires to change a namespace name is that the declaration isn't nested.
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.
Thanks. That's probably worth calling out in the doc comment IMO.
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.
Not sure about this. This service has nothing to do with file name and there's no mention about it in the code. Adding a doc comment here seems odd, especially for people who don't know how this service is evolved from the sync namespace refactoring.
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, as someone reading this, it was the first question that came to mind. i.e. "here's a service that will change a namespace to match something... so... will it do anything if the namespace already matches the document i gave it?" I don't think it hurts to have remarks to help clarify things where there are multiple reasonable interpretations that can be made". Up to you.
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.
Sure, if you think that would help. (Maybe it's me who's biased because I have been working on this and know the difference?)
/// Change namespace for given <paramref name="container"/> to the name specified by <paramref name="targetNamespace"/>. | ||
/// Everything declared in the <paramref name="container"/> will be moved to the new namespace. | ||
/// Change will only be made if <see cref="CanChangeNamespaceAsync"/> returns <see langword="true"/> and <paramref name="targetNamespace"/> | ||
/// is a valid name for namespace (we use "" to specify global namespace). |
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.
end hte sentence after 'for namespace'. Next sentence: Use "" for <paramref targetNamespace> to specify the global namespace.
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.
Fixed
} | ||
return false; | ||
} | ||
|
||
private static string ChangeNamespaceActionTitle(State state) | ||
=> state.TargetNamespace.Length == 0 | ||
? FeaturesResources.Change_to_global_namespace | ||
: string.Format(FeaturesResources.Change_namespace_to_0, state.TargetNamespace); |
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.
is this called from multiple locations? if not, just inline.
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.
Fixed
var service = document.GetLanguageService<IChangeNamespaceService>(); | ||
var solutionChangeAction = new SolutionChangeAction(ChangeNamespaceActionTitle(state), token => service.ChangeNamespaceAsync(state.Solution, state.DocumentIds, state.DeclaredNamespace, state.TargetNamespace, token)); | ||
var solutionChangeAction = new SolutionChangeAction( |
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.
you need a subclass of this type for our telemetry to work.
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.
Hmm, so title alone isn't enough? @heejaechang
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.
Fixed
/// - If <paramref name="relativeTo"/> is "A.B" and <paramref name="namespace"/> is also "A.B", then | ||
/// the relative namespace is "". | ||
/// - If <paramref name="relativeTo"/> is "" then the relative namespace us <paramref name="namespace"/>. | ||
/// </summary> |
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.
Specifythat 'null' is returned if there is no relation here.
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.
Fixed
@CyrusNajmabadi Please take another look. Thanks! |
@jinujoseph @vatsalyaagrawal for approval |
Ping... @CyrusNajmabadi @dotnet/roslyn-ide |
I'll be able to get to this tonight. thanks! |
@CyrusNajmabadi Pushed the change that make it throw on invalid arguments. Please take a look. |
|
||
var documents = ids.SelectAsArray(id => solution.GetDocument(id)); | ||
var containers = ArrayBuilder<(DocumentId, SyntaxNode)>.GetInstance(ids.Length); | ||
var spanForContainers = PooledHashSet<TextSpan>.GetInstance(); |
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.
Since we only care if a single TextSpan exists, why not use a nullable type here and just check for that?
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 is implemented this way so we can catch the case where the container nodes found in multiple documents have slightly different spans due to different target frameworks (e.g. maybe caused by #if
, etc.), or even completely different kind of container node (although very unlikely)
/// the relative namespace is "". | ||
/// - If <paramref name="relativeTo"/> is "" then the relative namespace us <paramref name="namespace"/>. | ||
/// </summary> | ||
private static string GetRelativeNamespace(string relativeTo, string @namespace, ISyntaxFactsService syntaxFacts) |
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.
Should this just be an extension method on ISyntaxFactsService
? It seems easily testable, well contained, and reusable.
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.
It doesn't really morally feel like a snytax fact...
It's actually unclear why this would need ISyntaxFactsService in teh first place. If this is just about case-sensitivity, then that feels like it should just be a boolean that is passed in IMO.
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 feel this is doing something useful only in a very limited scope (i.e. this service), don't want to promote it until there's a need for it.
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 see. What's the benefit of using the ISyntaxFactsService.StringComparer
for cases of equality? It looks like it just uses StringComparer.Ordinal
. Does the comparison need to align with ISyntaxFactsService
?
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.
In general, when you can use ISyntaxFactsService
, you do that to avoid duplicate code. Additionally in this case, it is used because VB has different rules: when the refactoring was implemented, we still had VB in mind, even though we later decided not to provide this for VB, the class hierarchy remained the same, just in case we change our mind in the future.
/// | ||
/// An <see cref="System.ArgumentException"/> will be thrown if: | ||
/// 1. <paramref name="container"/> is not a namespace declaration or a compilation unit node. | ||
/// 2. <paramref name="targetNamespace"/> is null or an contain invalid character. |
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: "contains an"
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.
Still open question about string comparison, but everything else looks good
@jinujoseph for approval |
This completed the separation of
ChangeNamespaceService
fromSyncNamespaceCodeRefactoringProvider
.Most of the logic for checking the availability of the refactoring now moved to
ChangeNamespaceService
, with only the bits related to sync namespace with folder remained. The service now also supports changing the name of a namespace declaration when there's multiple namespace declared in the same document, as long as the selected one is not nested.