Skip to content

Commit

Permalink
Merge pull request #749 from JoeRobich/remove-unnecessary-imports
Browse files Browse the repository at this point in the history
Remove unnecessary imports
  • Loading branch information
JoeRobich committed Aug 11, 2020
2 parents f57fdec + 0961210 commit fc437f6
Show file tree
Hide file tree
Showing 31 changed files with 472 additions and 118 deletions.
18 changes: 18 additions & 0 deletions docs/Supported-.editorconfig-options.md
Expand Up @@ -13,3 +13,21 @@ The dotnet-format global tool supports the core set of [EditorConfig options](ht
[*] The options `trim_trailing_whitespace` and `max_line_length` are not supported. Currently insignificant whitespace is **always** removed by the formatter.

[**] [Formatting conventions](https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019) are enforced by default. Use the `--fix-style` option to enforce [Language conventions](https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019) and [Naming conventions](https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-naming-conventions?view=vs-2019).

## Removing unnecessary imports
In order to remove unnecessary imports the IDE0005 (unnecessary import) diagnostic id must be configured in your .editorconfig. When running dotnet-format pass the `--fix-style` option and specify a severity that includes the configured IDE0005 severity.

*Example:*

.editorconfig
```ini
root = true

[*.{cs,vb}]
dotnet_diagnostic.IDE0005.severity = warning
```

command
```console
dotnet-format ./format.sln --fix-style warn
```
94 changes: 86 additions & 8 deletions src/Analyzers/AnalyzerOptionExtensions.cs
Expand Up @@ -2,28 +2,33 @@

using System;
using System.Linq;
using Microsoft.CodeAnalysis.Tools.Analyzers;

namespace Microsoft.CodeAnalysis.Diagnostics
{
internal static class AnalyzerOptionsExtensions
{
private const string DotnetAnalyzerDiagnosticPrefix = "dotnet_analyzer_diagnostic";
private const string CategoryPrefix = "category";
private const string SeveritySuffix = "severity";
internal const string DotnetDiagnosticPrefix = "dotnet_diagnostic";
internal const string DotnetAnalyzerDiagnosticPrefix = "dotnet_analyzer_diagnostic";
internal const string CategoryPrefix = "category";
internal const string SeveritySuffix = "severity";

private const string DotnetAnalyzerDiagnosticSeverityKey = DotnetAnalyzerDiagnosticPrefix + "." + SeveritySuffix;
internal const string DotnetAnalyzerDiagnosticSeverityKey = DotnetAnalyzerDiagnosticPrefix + "." + SeveritySuffix;

private static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(string category)
internal static string GetDiagnosticIdBasedDotnetAnalyzerDiagnosticSeverityKey(string diagnosticId)
=> $"{DotnetDiagnosticPrefix}.{diagnosticId}.{SeveritySuffix}";
internal static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(string category)
=> $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{category}.{SeveritySuffix}";

/// <summary>
/// Tries to get configured severity for the given <paramref name="descriptor"/>
/// for the given <paramref name="tree"/> from bulk configuration analyzer config options, i.e.
/// 'dotnet_analyzer_diagnostic.category-%RuleCategory%.severity = %severity%'
/// for the given <paramref name="tree"/> from analyzer config options, i.e.
/// 'dotnet_diagnostic.%descriptor.Id%.severity = %severity%',
/// 'dotnet_analyzer_diagnostic.category-%RuleCategory%.severity = %severity%',
/// or
/// 'dotnet_analyzer_diagnostic.severity = %severity%'
/// </summary>
public static bool TryGetSeverityFromBulkConfiguration(
public static bool TryGetSeverityFromConfiguration(
this AnalyzerOptions? analyzerOptions,
SyntaxTree tree,
Compilation compilation,
Expand Down Expand Up @@ -78,6 +83,79 @@ private static string GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(string
return false;
}

/// <summary>
/// Determines whether a diagnostic is configured in the <paramref name="analyzerConfigOptions" />.
/// </summary>
public static bool IsDiagnosticSeverityConfigured(this AnalyzerConfigOptions analyzerConfigOptions, SyntaxTree tree, string diagnosticId, string? diagnosticCategory)
{
return tree.DiagnosticOptions.TryGetValue(diagnosticId, out _)
|| (diagnosticCategory != null && analyzerConfigOptions.TryGetValue(GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(diagnosticCategory), out _))
|| analyzerConfigOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out _);
}

/// <summary>
/// Get the configured severity for a diagnostic analyzer from the <paramref name="analyzerConfigOptions" />.
/// </summary>
public static DiagnosticSeverity GetDiagnosticSeverity(this AnalyzerConfigOptions analyzerConfigOptions, SyntaxTree tree, string diagnosticId, string? diagnosticCategory)
{
return analyzerConfigOptions.TryGetSeverityFromConfiguration(tree, diagnosticId, diagnosticCategory, out var reportSeverity)
? reportSeverity.ToSeverity()
: DiagnosticSeverity.Hidden;
}

/// <summary>
/// Tries to get configured severity for the given <paramref name="diagnosticId"/>
/// for the given <paramref name="tree"/> from analyzer config options, i.e.
/// 'dotnet_diagnostic.%descriptor.Id%.severity = %severity%',
/// 'dotnet_analyzer_diagnostic.category-%RuleCategory%.severity = %severity%',
/// or
/// 'dotnet_analyzer_diagnostic.severity = %severity%'
/// </summary>
public static bool TryGetSeverityFromConfiguration(
this AnalyzerConfigOptions? analyzerConfigOptions,
SyntaxTree tree,
string diagnosticId,
string? diagnosticCategory,
out ReportDiagnostic severity)
{
if (analyzerConfigOptions is null)
{
severity = default;
return false;
}

// If user has explicitly configured severity for this diagnostic ID, that should be respected.
// For example, 'dotnet_diagnostic.CA1000.severity = error'
if (tree.DiagnosticOptions.TryGetValue(diagnosticId, out severity))
{
return true;
}

string? value;
if (diagnosticCategory != null)
{
// If user has explicitly configured default severity for the diagnostic category, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.category-security.severity = error'
var categoryBasedKey = GetCategoryBasedDotnetAnalyzerDiagnosticSeverityKey(diagnosticCategory);
if (analyzerConfigOptions.TryGetValue(categoryBasedKey, out value) &&
TryParseSeverity(value, out severity))
{
return true;
}
}

// Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected.
// For example, 'dotnet_analyzer_diagnostic.severity = error'
if (analyzerConfigOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) &&
TryParseSeverity(value, out severity))
{
return true;
}

severity = default;
return false;
}

internal static bool TryParseSeverity(string value, out ReportDiagnostic severity)
{
var comparer = StringComparer.OrdinalIgnoreCase;
Expand Down
26 changes: 13 additions & 13 deletions src/Analyzers/Extensions.cs
Expand Up @@ -92,6 +92,17 @@ public static bool Any(this SolutionChanges solutionChanges)
return severity;
}

public static DiagnosticSeverity ToSeverity(this ReportDiagnostic reportDiagnostic)
{
return reportDiagnostic switch
{
ReportDiagnostic.Error => DiagnosticSeverity.Error,
ReportDiagnostic.Warn => DiagnosticSeverity.Warning,
ReportDiagnostic.Info => DiagnosticSeverity.Info,
_ => DiagnosticSeverity.Hidden
};
}

private static DiagnosticSeverity GetSeverity(
this DiagnosticAnalyzer analyzer,
Document document,
Expand All @@ -113,9 +124,9 @@ public static bool Any(this SolutionChanges solutionChanges)
break;
}

if (analyzerOptions.TryGetSeverityFromBulkConfiguration(tree, compilation, descriptor, out var reportDiagnostic))
if (analyzerOptions.TryGetSeverityFromConfiguration(tree, compilation, descriptor, out var reportDiagnostic))
{
var configuredSeverity = ToSeverity(reportDiagnostic);
var configuredSeverity = reportDiagnostic.ToSeverity();
if (configuredSeverity > severity)
{
severity = configuredSeverity;
Expand All @@ -140,17 +151,6 @@ public static bool Any(this SolutionChanges solutionChanges)

return severity;

static DiagnosticSeverity ToSeverity(ReportDiagnostic reportDiagnostic)
{
return reportDiagnostic switch
{
ReportDiagnostic.Error => DiagnosticSeverity.Error,
ReportDiagnostic.Warn => DiagnosticSeverity.Warning,
ReportDiagnostic.Info => DiagnosticSeverity.Info,
_ => DiagnosticSeverity.Hidden
};
}

static bool TryGetSeverityFromCodeStyleOption(
DiagnosticDescriptor descriptor,
Compilation compilation,
Expand Down
3 changes: 2 additions & 1 deletion src/CodeFormatter.cs
Expand Up @@ -28,7 +28,8 @@ internal static class CodeFormatter
new FinalNewlineFormatter(),
new EndOfLineFormatter(),
new CharsetFormatter(),
new ImportsFormatter(),
new OrganizeImportsFormatter(),
new UnnecessaryImportsFormatter(),
new AnalyzerFormatter(Resources.Code_Style, new CodeStyleInformationProvider(), new AnalyzerRunner(), new SolutionCodeFixApplier()),
new AnalyzerFormatter(Resources.Analyzer_Reference, new AnalyzerReferenceInformationProvider(), new AnalyzerRunner(), new SolutionCodeFixApplier()),
}.ToImmutableArray();
Expand Down
18 changes: 18 additions & 0 deletions src/Formatters/DocumentFormatter.cs
Expand Up @@ -181,5 +181,23 @@ private static void LogFormattingChanges(string filePath, bool changesAreErrors,
logger.LogWarning(formatMessage);
}
}

protected static async Task<bool> IsSameDocumentAndVersionAsync(Document a, Document b, CancellationToken cancellationToken)
{
if (a == b)
{
return true;
}

if (a.Id != b.Id)
{
return false;
}

var aVersion = await a.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);
var bVersion = await b.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);

return aVersion == bVersion;
}
}
}
Expand Up @@ -13,9 +13,9 @@
namespace Microsoft.CodeAnalysis.Tools.Formatters
{
/// <summary>
/// ImportsFormatter that uses the <see cref="Formatter"/> to format document import directives.
/// OrganizeImportsFormatter that uses the <see cref="Formatter"/> to format document import directives.
/// </summary>
internal sealed class ImportsFormatter : DocumentFormatter
internal sealed class OrganizeImportsFormatter : DocumentFormatter
{
protected override string FormatWarningDescription => Resources.Fix_imports_ordering;
private readonly DocumentFormatter _endOfLineFormatter = new EndOfLineFormatter();
Expand Down Expand Up @@ -59,24 +59,5 @@ internal sealed class ImportsFormatter : DocumentFormatter
return sourceText;
}
}

private static async Task<bool> IsSameDocumentAndVersionAsync(Document a, Document b, CancellationToken cancellationToken)
{
if (a == b)
{
return true;
}

if (a.Id != b.Id)
{
return false;
}

var aVersion = await a.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);
var bVersion = await b.GetTextVersionAsync(cancellationToken).ConfigureAwait(false);

return aVersion == bVersion;
}
}
}

63 changes: 63 additions & 0 deletions src/Formatters/UnnecessaryImportsFormatter.cs
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Tools.Reflection;
using Microsoft.Extensions.Logging;

namespace Microsoft.CodeAnalysis.Tools.Formatters
{
/// <summary>
/// UnnecessaryImportsFormatter that removes unsused imports when fixing code style errors.
/// </summary>
internal sealed class UnnecessaryImportsFormatter : DocumentFormatter
{
internal const string IDE0005 = nameof(IDE0005);
internal const string Style = nameof(Style);

protected override string FormatWarningDescription => Resources.Remove_unnecessary_import;

internal override async Task<SourceText> FormatFileAsync(
Document document,
SourceText sourceText,
OptionSet optionSet,
AnalyzerConfigOptions analyzerConfigOptions,
FormatOptions formatOptions,
ILogger logger,
CancellationToken cancellationToken)
{
// If we are fixing CodeStyle and the 'IDE0005' diagnostic is configured, then
// see if we can remove unused imports.
if (!formatOptions.FixCodeStyle)
{
return sourceText;
}

var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
if (tree is null)
{
return sourceText;
}

var severity = analyzerConfigOptions.GetDiagnosticSeverity(tree, IDE0005, Style);
if (severity < formatOptions.CodeStyleSeverity)
{
return sourceText;
}

var formattedDocument = await RemoveUnnecessaryImportsHelper.RemoveUnnecessaryImportsAsync(document, cancellationToken).ConfigureAwait(false);

var isSameVersion = await IsSameDocumentAndVersionAsync(document, formattedDocument, cancellationToken).ConfigureAwait(false);
if (isSameVersion)
{
return sourceText;
}

var formattedText = await formattedDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
return formattedText;
}
}
}
26 changes: 26 additions & 0 deletions src/Reflection/RemoveUnnecessaryImportsHelper.cs
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.CodeAnalysis.Tools.Reflection
{
internal static class RemoveUnnecessaryImportsHelper
{
private static readonly Assembly? s_microsoftCodeAnalysisFeaturesAssembly = Assembly.Load(new AssemblyName("Microsoft.CodeAnalysis.Features"));
private static readonly Type? s_abstractRemoveUnnecessaryImportsCodeFixProviderType = s_microsoftCodeAnalysisFeaturesAssembly?.GetType("Microsoft.CodeAnalysis.RemoveUnnecessaryImports.AbstractRemoveUnnecessaryImportsCodeFixProvider");
private static readonly MethodInfo? s_removeUnnecessaryImportsAsyncMethod = s_abstractRemoveUnnecessaryImportsCodeFixProviderType?.GetMethod("RemoveUnnecessaryImportsAsync", BindingFlags.Static | BindingFlags.NonPublic);

public static async Task<Document> RemoveUnnecessaryImportsAsync(Document document, CancellationToken cancellationToken)
{
if (s_removeUnnecessaryImportsAsyncMethod is null)
{
return document;
}

return await (Task<Document>)s_removeUnnecessaryImportsAsyncMethod.Invoke(obj: null, new object[] { document, cancellationToken });
}
}
}
3 changes: 3 additions & 0 deletions src/Resources.resx
Expand Up @@ -270,4 +270,7 @@
<data name="Unable_to_fix_0_No_associated_code_fix_found" xml:space="preserve">
<value>Unable to fix {0}. No associated code fix found.</value>
</data>
<data name="Remove_unnecessary_import" xml:space="preserve">
<value>Remove unnecessary import.</value>
</data>
</root>
2 changes: 0 additions & 2 deletions src/Workspaces/FolderWorkspace.cs
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
Expand Down
5 changes: 5 additions & 0 deletions src/xlf/Resources.cs.xlf
Expand Up @@ -152,6 +152,11 @@
<target state="translated">{0} obsahuje více souborů řešení MSBuild. Určete, který soubor chcete použít, pomocí argumentu &lt;workspace&gt;.</target>
<note />
</trans-unit>
<trans-unit id="Remove_unnecessary_import">
<source>Remove unnecessary import.</source>
<target state="new">Remove unnecessary import.</target>
<note />
</trans-unit>
<trans-unit id="Run_3rd_party_analyzers_and_apply_fixes">
<source>Run 3rd party analyzers and apply fixes.</source>
<target state="translated">Spustit analyzátory třetích stran a použít opravy</target>
Expand Down
5 changes: 5 additions & 0 deletions src/xlf/Resources.de.xlf
Expand Up @@ -152,6 +152,11 @@
<target state="translated">In "{0}" wurden mehrere MSBuild-Projektmappendateien gefunden. Geben Sie die zu verwendende Datei mit dem &lt;workspace&gt;-Argument an.</target>
<note />
</trans-unit>
<trans-unit id="Remove_unnecessary_import">
<source>Remove unnecessary import.</source>
<target state="new">Remove unnecessary import.</target>
<note />
</trans-unit>
<trans-unit id="Run_3rd_party_analyzers_and_apply_fixes">
<source>Run 3rd party analyzers and apply fixes.</source>
<target state="translated">Führen Sie Drittanbieter-Analysetools aus, und wenden Sie Korrekturen an.</target>
Expand Down

0 comments on commit fc437f6

Please sign in to comment.