Skip to content

Added CodeStyle option for 'using' placement#30760

Closed
JoeRobich wants to merge 25 commits intodotnet:masterfrom
JoeRobich:add-usings-codestyle
Closed

Added CodeStyle option for 'using' placement#30760
JoeRobich wants to merge 25 commits intodotnet:masterfrom
JoeRobich:add-usings-codestyle

Conversation

@JoeRobich
Copy link
Member

@JoeRobich JoeRobich commented Oct 25, 2018

User is able to specify Preserve, Inside Namespaces, or Outside Namespaces. Default is Preserve.

Move Inside Namespace enabled:
move usings inside namespace

Move Outside Namespace enabled:
move usings outside namespace

Move Usings Options:
move usings code style options

resolves #27179

/CC: @sharwell

@JoeRobich JoeRobich requested review from a team as code owners October 25, 2018 18:46
@CyrusNajmabadi
Copy link
Contributor

User is able to specify No Preference, Inside Namespaces, or Outside Namespaces. Default is No Preference.

We don't really ever have a 'no preference' option. There's always a preference, there is just a configured severity that determines if the user is pushed to fix things up or not. Should we just have "Inside/Output" along with a severity of 'None'?

<ItemGroup>
<Compile Include="..\..\..\Features\Core\Portable\CodeFixes\PredefinedCodeFixProviderNames.cs" Link="PredefinedCodeFixProviderNames.cs" />
<Compile Include="..\..\..\Workspaces\Core\Portable\Shared\Utilities\IProgressTracker.cs" Link="IProgressTracker.cs" />
<Compile Include="..\..\..\Workspaces\Core\Portable\CodeFixes\FixAllOccurrences\DocumentBasedFixAllProvider.cs" Link="DocumentBasedFixAllProvider.cs" />
Copy link
Contributor

Choose a reason for hiding this comment

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

why?

Copy link
Contributor

Choose a reason for hiding this comment

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

➡️ The original implementation was based on this approach. We already had the type in the code base so it was linked it to support the new code fix.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not actually seeing FixAll tests, so i'm not sure how to evaluate this. It's also unclear to me why this is in src/CodeStyle/Core since the feature doesn't appear to be there.

Copy link
Member Author

Choose a reason for hiding this comment

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

DocumentBasedFixAllProvider was already in CodeStyle.Fixes. I moved it to Workspaces to make it accessible to the fixAll and other future fixAlls. I could have alternately refernced CodeStyle.Fixes from the CSharp.EditorFeatures project.

Copy link
Contributor

@sharwell sharwell Oct 26, 2018

Choose a reason for hiding this comment

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

I could have alternately refernced CodeStyle.Fixes from the CSharp.EditorFeatures project.

This is not currently possible due to #30472

@CyrusNajmabadi
Copy link
Contributor

Pausing my review. can you give a medium level overview of the implementation of this feature? There was a lot of unexpected complexity, and it would be good to have an understanding of what it is for, why existing facilities could not be used, and how ti all works. Thanks!

///<summary>
/// Prefer usings placed outside namespaces.
///</summary>
OutsideNamespace
Copy link
Contributor

Choose a reason for hiding this comment

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

better names might be 'CompilationLevel' and 'NamespaceLevel'. You may not have a namespace in your code, so saying 'OutputNamespace' doesn't really make a ton of sense to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

With only two values, this could potentially also be a bool. though i' mnot opposed to an enum.

Copy link
Contributor

@sharwell sharwell Oct 25, 2018

Choose a reason for hiding this comment

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

I don't see the suggested alternatives as a clear improvement over the current names. The current names align with the documentation described by both StyleCop Classic and StyleCop Analyzers, so it seems consistent with what users would likely expect today.

I'm a weak advocate of the current names and would consider other alternatives if they were presented.

<value>Wrapping preferences</value>
</data>
<data name="using_preferences" xml:space="preserve">
<value>using preferences</value>
Copy link
Contributor

Choose a reason for hiding this comment

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

no comment that 'using' should be locked?

Copy link
Contributor

Choose a reason for hiding this comment

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

Should using be in single quotes?

Copy link
Member Author

Choose a reason for hiding this comment

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

@Neme12 The var in var_preferences isn't in quotes, so I think it is OK.

Copy link
Contributor

Choose a reason for hiding this comment

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

one niggle there is htat 'var preferences' isn't really an english phrase. whereas 'using preferences' is a bit ambiguous. Is it hte preferences around 'using directives'? Or is it 'using your preferences'? That said, this is minor enough that i think it's fine either way.

Copy link
Contributor

Choose a reason for hiding this comment

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

looking at this, i would put 'using' in quotes.

nameof(CSharpEditorResources.Using_directives_must_be_placed_inside_a_namespace_declaration), CSharpEditorResources.ResourceManager, typeof(CSharpEditorResources));

private static readonly LocalizableResourceString s_localizableOutsideMessage = new LocalizableResourceString(
nameof(CSharpEditorResources.Using_directives_must_be_placed_outside_a_namespace_declaration), CSharpEditorResources.ResourceManager, typeof(CSharpEditorResources));
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO, this would all be cleaner with two separate analyzers. That way you can also just use the normal pattern of passing title/message to the base type and whatnot.

if (CountNamespaces(compilationUnit.Members) > 1)
{
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this done here, instead of during analysis?

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 using placement goes against the code style preference, but the code fix doesn't currently do the work of determining which namespaces to move each using into.

Copy link
Contributor

Choose a reason for hiding this comment

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

enhhhhhhhhh. i would think it would just be the outermost namespace. do we have people with another coding pattern somewhere?

Copy link
Contributor

Choose a reason for hiding this comment

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

enhhhhhhhhh

It's safe to assume that everything you hit like this was implemented as a direct result of a bug report filed to StyleCop Analyzers. This feature took more than a year to stabilize with some rather horrifying real-world edge cases.

@sharwell
Copy link
Contributor

sharwell commented Oct 25, 2018

Pausing my review. can you give a medium level overview of the implementation of this feature? There was a lot of unexpected complexity, and it would be good to have an understanding of what it is for, why existing facilities could not be used, and how ti all works. Thanks!

The implementation is derived from the work by @vweijsters in StyleCop Analyzers. The tests cover nearly all branches of the code fix; it turned out to be surprisingly complex for what seemed like a reasonably straightforward feature going in.

@CyrusNajmabadi
Copy link
Contributor

The implementation is derived from the work by @vweijsters in StyleCop Analyzers. The tests cover nearly all branches of the code fix; it turned out to be surprisingly complex for what seemed like a reasonably straightforward feature going in.

That doesn't really help me understand anything better. For example, i want to know about what each part is needed for, because it's very possible the IDE already has logic somewhere to handle things, and we may just end up duplicating stuff unnecessarily.

/// <summary>
/// Unit tests for the <see cref="MisplacedUsingsDiagnosticAnalyzer"/> and <see cref="MisplacedUsingsCodeFixProvider"/>.
/// </summary>
public class MisplacedUsingsCodeFixProviderTests
Copy link
Contributor

Choose a reason for hiding this comment

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

i'm confused why this isn't just using our normal code fix base class.

Copy link
Contributor

@sharwell sharwell Oct 25, 2018

Choose a reason for hiding this comment

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

Most of the deviations on this front are the result of avoiding unnecessary deviations from the original implementation in the initial submission, which allows @vweijsters to explain the implementation details and merge any bug fixes back to StyleCop Analyzers. There was no "normal code fix base class" in the original.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm ok with that as a first draft. But it does'nt make sense to me in terms of merging in. Will we just be producing more and more duplication of code that may already exist just because it's in StyleCop analyzers? I def do not like that. This just means more code to maintain and fixup when we encounter bugs and whatnot.

Copy link
Contributor

@sharwell sharwell Apr 6, 2019

Choose a reason for hiding this comment

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

dotnet/roslyn is one of the remaining outliers that does not use Microsoft.CodeAnalysis.Testing, the new common library for analyzer and code fix testing. The default configuration for that library includes a large number of much stronger assertions regarding "well-behaved" implementations, so the long term goal is migrating all current testing to be built on that library.

Diagnostic(MisplacedUsingsDiagnosticAnalyzer._insideDescriptor).WithLocation(6, 1),
Diagnostic(MisplacedUsingsDiagnosticAnalyzer._insideDescriptor).WithLocation(7, 1),
Diagnostic(MisplacedUsingsDiagnosticAnalyzer._insideDescriptor).WithLocation(9, 1),
Diagnostic(MisplacedUsingsDiagnosticAnalyzer._insideDescriptor).WithLocation(10, 1),
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. why are we reporting so many misplaced diagnostics? Users should really only see one diagnostic (either for the span of the first item, or perhaps the span of the entire set).
  2. I don't like this test because it's no longer declarative. i have no idea what 10,1 means, or how it relates to the code.

Copy link
Contributor

Choose a reason for hiding this comment

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

why are we reporting so many misplaced diagnostics? Users should really only see one diagnostic (either for the span of the first item, or perhaps the span of the entire set).

This is consistent with how we report other diagnostics. We have the option of tying the code fix to the helper diagnostic used by IDE0005.

@JoeRobich
Copy link
Member Author

@CyrusNajmabadi & @sharwell This PR is ready for another review.

Notable changes since October:

  • Replaced the StyleCop fixer (Sort, Group, and Move) to a fixer that just moves using directives
  • Split the Analyzer into one for the CompilationUnit and one for Namespaces
  • Updated the AddImportService to respect preferred placement

@JoeRobich JoeRobich removed the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Apr 12, 2019
<RoslynDiagnosticsNugetPackageVersion>2.6.2-beta2</RoslynDiagnosticsNugetPackageVersion>
<CodeStyleLayerCodeAnalysisVersion>2.8.2</CodeStyleLayerCodeAnalysisVersion>
<MicrosoftCodeAnalysisTestingVersion>1.0.0-beta1-63310-01</MicrosoftCodeAnalysisTestingVersion>
<MicrosoftCodeAnalysisTestingVersion>1.0.0-beta1-63812-02</MicrosoftCodeAnalysisTestingVersion>
Copy link
Contributor

Choose a reason for hiding this comment

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

why?

Copy link
Contributor

Choose a reason for hiding this comment

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

Newer versions of this library often come with usability enhancements for testing analyzers and code fixes.

}
}

private class MoveMisplacedUsingsCodeAction : CodeAction
Copy link
Contributor

Choose a reason for hiding this comment

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

we already have a CodeAction.DocumentChangeAction that we derive from for these cases.

// Provide a rooted path when filePath is just the file's name.
var fullPath = string.IsNullOrEmpty(Path.GetDirectoryName(filePath))
? Path.Combine(Environment.CurrentDirectory, filePath)
: filePath;
Copy link
Contributor

Choose a reason for hiding this comment

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

seeing Environment in a test is scary to me.


public override async Task<AddImportFixData> TryGetFixDataAsync(
Document document, SyntaxNode node, bool placeSystemNamespaceFirst, CancellationToken cancellationToken)
Document document, SyntaxNode node, bool placeSystemNamespaceFirst, AddImportPlacement placement, 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.

consider an AddImportOptions struct that can pass along the placement and the PlaceSystemNamespaceFirst value.

new CodeStylePreference(CSharpVSResources.Preserve, isChecked: false),
new CodeStylePreference(CSharpVSResources.Inside_namespace, isChecked: false),
new CodeStylePreference(CSharpVSResources.Outside_namespace, isChecked: false),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

having a 'preserve' option seems odd to me. we don't generlaly have that. our position up till now has been that we always have a preference, but we can have no-severity, meaning we won't tell you there's an issue, or otherwise try to get you to change it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sam brought up that when we update Organize Imports it would use the Preserve option to mean don't move imports. There is also the Add Import code fix, if we didn't have the Preserve option then the current location of using directives would get ignored in favor of the code style setting we defaulted to.

Copy link
Contributor

Choose a reason for hiding this comment

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

here is also the Add Import code fix, if we didn't have the Preserve option then the current location of using directives would get ignored in favor of the code style setting we defaulted to.

Hrmm... for add-import, i think we should respect the file no matter what the option is. Thoughts?

defaultValue: PreservePlacementWithSilentEnforcement,
storageLocations: new OptionStorageLocation[]{
new EditorConfigStorageLocation<CodeStyleOption<AddImportPlacement>>(
"csharp_using_directive_placement",
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 this name is good. but i would use 'using directive' more consistently in the rest of the UI. This is especially important as C# also has a 'using statement'

public static readonly CodeStyleOption<AddImportPlacement> PreservePlacementWithSilentEnforcement =
new CodeStyleOption<AddImportPlacement>(AddImportPlacement.Preserve, NotificationOption.Silent);

public static readonly Option<CodeStyleOption<AddImportPlacement>> PreferredUsingDirectivesPlacement = CreateOption(
Copy link
Contributor

Choose a reason for hiding this comment

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

i don't like that you use Directives here, but Directive below. can you keep pluralization consistent?


return false;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

remove?

IsNullableTypeInPointerExpression(name, reducedName) ||
name.IsNotNullableReplaceable(reducedName) ||
IsQualifiedNameInUsingDirective(semanticModel, name, reducedName))
(IsQualifiedNameInUsingDirective(semanticModel, name, reducedName) && !IsGlobalAliasQualifiedName(name)))
Copy link
Contributor

Choose a reason for hiding this comment

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

can you extract a named method for this? to keep all the cases just simple || checks with clear names?

Copy link
Member Author

Choose a reason for hiding this comment

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

Moved these changes into #34964

return name is AliasQualifiedNameSyntax aliasName
? aliasName.Alias.Identifier.IsKind(SyntaxKind.GlobalKeyword)
: false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. shoudl be commented.
  2. can just be => name is AliasQual aliasName && ...;

public override Task<CodeAction> GetFixAsync(FixAllContext fixAllContext)
{
// Disable RS0005 as this is test code and we don't need telemetry for created code action.
#pragma warning disable RS0005 // Do not use generic CodeAction.Create to create CodeAction
Copy link
Contributor

Choose a reason for hiding this comment

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

can you explain how this is test 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.

I'll remove

End Function

Protected Overrides Function GetImportPlacement(options As OptionSet) As AddImportPlacement
Return AddImportPlacement.OutsideNamespace
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe a simple omment that VB doesn't support imports inside namespaces.

@CyrusNajmabadi
Copy link
Contributor

i haven't looked through this all, but this PR is large enough that i think it would benefit from a few initial PRs sent in that break things out to make the final PR much easier to read. For example, moving to a simple model of having AddImportOptions would make it easy to then add the Placement option and update only the couple of places that need it.

Similarly, moving the FixAll stuff around looks ike it can be done independently.

This PR is currently at the point that it triggerred my "too big to fit in my head' point, especially with keeping the big picutre, and the individual logic around at the same time.

@JoeRobich
Copy link
Member Author

@CyrusNajmabadi Thanks for the review. I will look at extracting a some of these changes as you suggested.

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.

First pass done

}
}

private static int CountNamespaces(SyntaxList<MemberDeclarationSyntax> members)
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks similar to functions for AbstractMoveToNamespaceService. Worth pulling out into an extension method?

var newCompilationUnit = await ExpandUsingDirectivesAsync(document, compilationUnit, cancellationToken).ConfigureAwait(false);
ImmutableArray<SyntaxTrivia> fileHeader;

(newCompilationUnit, fileHeader) = RemoveFileHeader(newCompilationUnit);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you make new variables for each stage as file header is removed and added back so it's easier to follow?

private static async Task<CompilationUnitSyntax> ExpandUsingDirectivesAsync(Document document, CompilationUnitSyntax compilationUnit, CancellationToken cancellationToken)
{
// Expand the usings declarations in the compilation unit.
var expandCompilationUnitUsings = compilationUnit.Usings.Select(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: expandCompilationUnitUsingsTasks

// Expand the using declarations in the namespace declaration.
var namespaceDeclaration = newCompilationUnit.Members.OfType<NamespaceDeclarationSyntax>().First();

var expandNamespaceDeclarationUsings = namespaceDeclaration.Usings.Select(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: expandNamespaceDeclarationUsingsTasks

}

var newTrivia = splitIntoLines(firstMemberTrivia)
.SkipWhile(trivia => trivia.All(t => t.IsWhitespaceOrEndOfLine()) && trivia.Last().IsKind(SyntaxKind.EndOfLineTrivia))
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you need the && trivia.Last().IsKind(SyntaxKind.EndOfLineTrivia) in the SkipWhile? They should already be split into the last character being newline

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 don't need to remove leading whitespace trivia if it is on the same line.


// Only move using declarations inside the namespace when
// - There are no global attributes
// - There is only a single namespace declared at the top level
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you also add There are type definitions outside of the single top level namespace, which I presume is what the compilationUnit.Members.Count > 1 check is for

return result;
}

private static (CompilationUnitSyntax, ImmutableArray<SyntaxTrivia>) RemoveFileHeader(CompilationUnitSyntax syntaxRoot)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: You can name the return tuple members to hint at what they mean. (CompilationUnitSyntax compilationWithoutHeader, ImmutableArray<SyntaxTrivia> header)

break;

case SyntaxKind.EndOfLineTrivia:
hasHeader = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this right? It looks like if there's a blank line at the beginning of a file it's considered a header.

[Fact, Trait(Traits.Feature, Traits.Features.CodeActionsAddImport)]
public async Task TestAddsImportsInsideNamespaceIfPreferred()
{
await TestInRegularAndScriptAsync(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: It would probably be easier to read if you made a function like

TestInRegularAndScriptAsync(string original, string expected, OptionKey usingPlacementStyleOption, object codeStyleOption)

That way you don't have the new Dictionary<OptionKey, object>() all over the place and people can parse the information quicker

@JoeRobich
Copy link
Member Author

Closing and will take these changes through #35009

@JoeRobich JoeRobich closed this Apr 22, 2019
@JoeRobich JoeRobich deleted the add-usings-codestyle branch April 25, 2019 23:50
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.

[Code Style] Require or prevent usings inside a namespace

7 participants