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

Refactor change namespace service #31447

Merged
merged 7 commits into from
Dec 13, 2018
Merged

Conversation

genlu
Copy link
Member

@genlu genlu commented Nov 29, 2018

This completed the separation of ChangeNamespaceService from SyncNamespaceCodeRefactoringProvider.

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.

@genlu genlu added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Nov 29, 2018
@genlu genlu requested a review from a team as a code owner November 29, 2018 20:56
@genlu genlu force-pushed the refactoringService branch from 40d43ea to 62058af Compare November 29, 2018 21:02
@CyrusNajmabadi
Copy link
Member

Can you ping me when you'd lke a review? Thanks!

@genlu genlu removed the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Nov 30, 2018
@genlu
Copy link
Member Author

genlu commented Nov 30, 2018

@dotnet/roslyn-ide @CyrusNajmabadi Please take a look. Thanks!

@jinujoseph jinujoseph modified the milestones: 16.0, 16.0.P2 Dec 3, 2018
@genlu
Copy link
Member Author

genlu commented Dec 3, 2018

Ping @CyrusNajmabadi

}
else
{
return default;
Copy link
Member

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?

Copy link
Member Author

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?)

Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Dec 8, 2018

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

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]?

Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member Author

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

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.

Copy link
Member Author

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

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.

Copy link
Member Author

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

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.

Copy link
Member Author

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

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.

Copy link
Member Author

@genlu genlu Dec 4, 2018

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.

Copy link
Member Author

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

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.

Copy link
Member Author

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

Choose a reason for hiding this comment

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

cast

Copy link
Member Author

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

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.

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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.

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

Choose a reason for hiding this comment

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

  1. Should be plural.
  2. 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);
Copy link
Member

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.

Copy link
Member Author

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

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.

Copy link
Member Author

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

Choose a reason for hiding this comment

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

docuent what 'container' is

Copy link
Member Author

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

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

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

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.

Copy link
Member Author

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

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?

Copy link
Member Author

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

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?

Copy link
Member Author

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

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?

Copy link
Member Author

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

Choose a reason for hiding this comment

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

newline above this.

Copy link
Member Author

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

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?

Copy link
Member Author

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.

Copy link
Contributor

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);
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 also checking that hte namespace name doesn't match the file name? or is that done at a different level?

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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

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.

Copy link
Member Author

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);
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 called from multiple locations? if not, just inline.

Copy link
Member Author

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

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.

Copy link
Member Author

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

Copy link
Member Author

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@genlu
Copy link
Member Author

genlu commented Dec 4, 2018

@CyrusNajmabadi Please take another look. Thanks!
@ryzngard could you please review this too? You are going to the the first one to use this service ;)

@genlu
Copy link
Member Author

genlu commented Dec 5, 2018

@jinujoseph @vatsalyaagrawal for approval

@genlu
Copy link
Member Author

genlu commented Dec 6, 2018

Ping... @CyrusNajmabadi @dotnet/roslyn-ide

@CyrusNajmabadi
Copy link
Member

I'll be able to get to this tonight. thanks!

@genlu
Copy link
Member Author

genlu commented Dec 10, 2018

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

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?

Copy link
Member Author

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

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

@genlu genlu Dec 12, 2018

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

Choose a reason for hiding this comment

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

nit: "contains an"

Copy link
Contributor

@ryzngard ryzngard left a 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

@genlu
Copy link
Member Author

genlu commented Dec 12, 2018

@jinujoseph for approval

@genlu genlu merged commit fb53ad2 into dotnet:master Dec 13, 2018
@genlu genlu deleted the refactoringService branch December 13, 2018 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants