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

Remove unnecessary imports #749

Merged
merged 12 commits into from Aug 11, 2020
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
1 change: 0 additions & 1 deletion src/Analyzers/AnalyzerRunner.cs
@@ -1,7 +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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
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(includeCompilerDiagnostics: true), new SolutionCodeFixApplier()),
new AnalyzerFormatter(Resources.Analyzer_Reference, new AnalyzerReferenceInformationProvider(), new AnalyzerRunner(includeCompilerDiagnostics: true), 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