提交 864e9372 编写于 作者: S Sam Harwell

Implement a formatting analyzer

上级 57094884
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Test.Utilities;
using Xunit;
namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Formatting
{
public class FormattingAnalyzerTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest
{
internal override (DiagnosticAnalyzer, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace)
=> (new FormattingDiagnosticAnalyzer(), new FormattingCodeFixProvider());
[Fact, Trait(Traits.Feature, Traits.Features.Formatting)]
public async Task TrailingWhitespace()
{
var testCode =
"class X[| |]" + Environment.NewLine +
"{" + Environment.NewLine +
"}" + Environment.NewLine;
var expected =
"class X" + Environment.NewLine +
"{" + Environment.NewLine +
"}" + Environment.NewLine;
await TestInRegularAndScriptAsync(testCode, expected);
}
[Fact, Trait(Traits.Feature, Traits.Features.Formatting)]
public async Task TestMissingSpace()
{
var testCode = @"
class TypeName
{
void Method()
{
if$$(true)return;
}
}
";
var expected = @"
class TypeName
{
void Method()
{
if (true)return;
}
}
";
await TestInRegularAndScriptAsync(testCode, expected);
}
}
}
......@@ -21,6 +21,7 @@ internal static class PredefinedCodeFixProviderNames
public const string AddMissingReference = nameof(AddMissingReference);
public const string AddImport = nameof(AddImport);
public const string FullyQualify = nameof(FullyQualify);
public const string FixFormatting = nameof(FixFormatting);
public const string FixIncorrectFunctionReturnType = nameof(FixIncorrectFunctionReturnType);
public const string FixIncorrectExitContinue = nameof(FixIncorrectExitContinue);
public const string GenerateConstructor = nameof(GenerateConstructor);
......
......@@ -84,6 +84,8 @@ internal static class IDEDiagnosticIds
public const string UseExpressionBodyForLambdaExpressionsDiagnosticId = "IDE0053";
public const string FormattingDiagnosticId = "IDE0054";
// Analyzer error Ids
public const string AnalyzerChangedId = "IDE1001";
public const string AnalyzerDependencyConflictId = "IDE1002";
......
......@@ -1369,6 +1369,33 @@ internal class FeaturesResources {
}
}
/// <summary>
/// Looks up a localized string similar to Fix formatting.
/// </summary>
internal static string Formatting_analyzer_code_fix {
get {
return ResourceManager.GetString("Formatting_analyzer_code_fix", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Fix formatting.
/// </summary>
internal static string Formatting_analyzer_message {
get {
return ResourceManager.GetString("Formatting_analyzer_message", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Fix formatting.
/// </summary>
internal static string Formatting_analyzer_title {
get {
return ResourceManager.GetString("Formatting_analyzer_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Formatting document.
/// </summary>
......
......@@ -1430,4 +1430,13 @@ This version used in: {2}</value>
<data name="Type_0_has_a_private_member_1_that_can_be_removed_as_the_value_assigned_to_it_is_never_read" xml:space="preserve">
<value>Type '{0}' has a private member '{1}' that can be removed as the value assigned to it is never read.</value>
</data>
<data name="Formatting_analyzer_code_fix" xml:space="preserve">
<value>Fix formatting</value>
</data>
<data name="Formatting_analyzer_message" xml:space="preserve">
<value>Fix formatting</value>
</data>
<data name="Formatting_analyzer_title" xml:space="preserve">
<value>Fix formatting</value>
</data>
</root>
\ No newline at end of file
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Formatting
{
[ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic, Name = PredefinedCodeFixProviderNames.FixFormatting)]
[Shared]
internal class FormattingCodeFixProvider : SyntaxEditorBasedCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(IDEDiagnosticIds.FormattingDiagnosticId);
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
context.RegisterCodeFix(
new MyCodeAction(c => FixOneAsync(context.Document, context.Diagnostics, c)),
context.Diagnostics);
return Task.CompletedTask;
}
protected async Task<Document> FixOneAsync(Document document, ImmutableArray<Diagnostic> diagnostics, CancellationToken cancellationToken)
{
var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
var changes = new List<TextChange>();
foreach (var diagnostic in diagnostics)
{
if (!tree.Equals(diagnostic.Location.SourceTree)
|| !diagnostic.Properties.TryGetValue(FormattingDiagnosticAnalyzer.ReplaceTextKey, out var replacement))
{
continue;
}
changes.Add(new TextChange(diagnostic.Location.SourceSpan, replacement));
}
changes.Sort((left, right) => left.Span.Start.CompareTo(right.Span.Start));
return document.WithText(text.WithChanges(changes));
}
protected override async Task FixAllAsync(Document document, ImmutableArray<Diagnostic> diagnostics, SyntaxEditor editor, CancellationToken cancellationToken)
{
var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
var updatedDocument = await Formatter.FormatAsync(document, options, cancellationToken).ConfigureAwait(false);
editor.ReplaceNode(editor.OriginalRoot, await updatedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false));
}
private sealed class MyCodeAction : CodeAction.DocumentChangeAction
{
public MyCodeAction(Func<CancellationToken, Task<Document>> createChangedDocument)
: base(FeaturesResources.Formatting_analyzer_code_fix, createChangedDocument, FeaturesResources.Formatting_analyzer_code_fix)
{
}
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Formatting
{
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
internal class FormattingDiagnosticAnalyzer
: AbstractCodeStyleDiagnosticAnalyzer
{
public static readonly string ReplaceTextKey = nameof(ReplaceTextKey);
public static readonly ImmutableDictionary<string, string> RemoveTextProperties =
ImmutableDictionary.Create<string, string>().Add(ReplaceTextKey, "");
public FormattingDiagnosticAnalyzer()
: base(
IDEDiagnosticIds.FormattingDiagnosticId,
new LocalizableResourceString(nameof(FeaturesResources.Formatting_analyzer_title), FeaturesResources.ResourceManager, typeof(FeaturesResources)),
new LocalizableResourceString(nameof(FeaturesResources.Formatting_analyzer_message), FeaturesResources.ResourceManager, typeof(FeaturesResources)))
{
}
public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
=> DiagnosticAnalyzerCategory.SyntaxAnalysis;
public override bool OpenFileOnly(Workspace workspace)
=> false;
protected override void InitializeWorker(AnalysisContext context)
=> context.RegisterSyntaxTreeAction(AnalyzeSyntaxTree);
private void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context)
{
if (!(context.Options is WorkspaceAnalyzerOptions workspaceAnalyzerOptions))
{
return;
}
var options = context.Options.GetDocumentOptionSetAsync(context.Tree, context.CancellationToken).GetAwaiter().GetResult();
if (options == null)
{
return;
}
var workspace = workspaceAnalyzerOptions.Services.Workspace;
var formattingChanges = Formatter.GetFormattedTextChanges(context.Tree.GetRoot(context.CancellationToken), workspace, options, context.CancellationToken);
foreach (var formattingChange in formattingChanges)
{
var change = formattingChange;
if (change.NewText.Length > 0 && !change.Span.IsEmpty)
{
var oldText = context.Tree.GetText(context.CancellationToken);
// Handle cases where the change is a substring removal from the beginning
var offset = change.Span.Length - change.NewText.Length;
if (offset >= 0 && oldText.GetSubText(new TextSpan(change.Span.Start + offset, change.NewText.Length)).ContentEquals(SourceText.From(change.NewText)))
{
change = new TextChange(new TextSpan(change.Span.Start, offset), "");
}
else
{
// Handle cases where the change is a substring removal from the end
if (change.NewText.Length < change.Span.Length
&& oldText.GetSubText(new TextSpan(change.Span.Start, change.NewText.Length)).ContentEquals(SourceText.From(change.NewText)))
{
change = new TextChange(new TextSpan(change.Span.Start + change.NewText.Length, change.Span.Length - change.NewText.Length), "");
}
}
}
if (change.NewText.Length == 0 && change.Span.IsEmpty)
{
// No actual change
continue;
}
ImmutableDictionary<string, string> properties;
if (change.NewText.Length == 0)
{
properties = RemoveTextProperties;
}
else
{
properties = ImmutableDictionary.Create<string, string>().Add(ReplaceTextKey, change.NewText);
}
var location = Location.Create(context.Tree, change.Span);
context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
location,
ReportDiagnostic.Default,
additionalLocations: null,
properties));
}
}
}
}
......@@ -62,6 +62,21 @@
<target state="translated">Opravit překlep {0}</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">Formátuje se dokument.</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">Tippfehler "{0}" korrigieren</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">Dokument wird formatiert</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">Corregir error de escritura "{0}"</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">Aplicando formato al documento</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">Corriger la faute de frappe '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">Mise en forme du document</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">Correggere l'errore di ortografia '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">Formattazione del documento</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">'{0}' の入力ミスを修正します</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">ドキュメントの書式設定</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">오타 '{0}' 수정</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">문서 서식 지정</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">Popraw błąd pisowni „{0}”</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">Formatowanie dokumentu</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">Corrigir erro de digitação '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">Formatando documento</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">Исправьте опечатку "{0}"</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">Форматирование документа</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">'{0}' yazım hatasını düzeltin</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">Belge biçimlendiriliyor</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">修正笔误“{0}”</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">设置文档格式</target>
......
......@@ -62,6 +62,21 @@
<target state="translated">修正錯字 '{0}'</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_code_fix">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_message">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_analyzer_title">
<source>Fix formatting</source>
<target state="new">Fix formatting</target>
<note />
</trans-unit>
<trans-unit id="Formatting_document">
<source>Formatting document</source>
<target state="translated">正在將文件格式化</target>
......
......@@ -92,9 +92,13 @@ internal abstract partial class AbstractFormatEngine
_language = token1.Language;
}
// set synchronous task executor if it is debug mode or if there is not many things to format
this.TaskExecutor = optionSet.GetOption(FormattingOptions.DebugMode, _language) ? TaskExecutor.Synchronous :
(SpanToFormat.Length < ConcurrentThreshold) ? TaskExecutor.Synchronous : executor;
// set synchronous task executor if it is enabled (explicitly or as part of debug mode) or if there is not
// many things to format
var synchronousExecutorAllowed =
!optionSet.GetOption(FormattingOptions.AllowConcurrent)
|| optionSet.GetOption(FormattingOptions.DebugMode, _language);
var useSynchronousExecutor = synchronousExecutorAllowed || SpanToFormat.Length < ConcurrentThreshold;
TaskExecutor = useSynchronousExecutor ? TaskExecutor.Synchronous : executor;
}
protected abstract AbstractTriviaDataFactory CreateTriviaFactory();
......
......@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
......@@ -281,7 +282,13 @@ internal static Task<IFormattingResult> GetFormattingResult(SyntaxNode node, IEn
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The changes necessary to format the tree.</returns>
public static IList<TextChange> GetFormattedTextChanges(SyntaxNode node, Workspace workspace, OptionSet options = null, CancellationToken cancellationToken = default)
=> GetFormattedTextChangesAsync(node, workspace, options, cancellationToken).WaitAndGetResult_CanCallOnBackground(cancellationToken);
{
options = options ?? workspace.Options;
options = options.WithChangedOption(FormattingOptions.AllowConcurrent, false);
var resultTask = GetFormattedTextChangesAsync(node, workspace, options, cancellationToken);
Debug.Assert(resultTask.IsCompleted);
return resultTask.WaitAndGetResult_CanCallOnBackground(cancellationToken);
}
internal static Task<IList<TextChange>> GetFormattedTextChangesAsync(SyntaxNode node, Workspace workspace, OptionSet options = null, CancellationToken cancellationToken = default)
=> GetFormattedTextChangesAsync(node, SpecializedCollections.SingletonEnumerable(node.FullSpan), workspace, options, rules: null, cancellationToken: cancellationToken);
......
......@@ -45,6 +45,7 @@ private static Optional<string> ParseEditorConfigEndOfLine(string endOfLineValue
}
internal static PerLanguageOption<bool> DebugMode { get; } = new PerLanguageOption<bool>(nameof(FormattingOptions), nameof(DebugMode), defaultValue: false);
internal static Option<bool> AllowConcurrent { get; } = new Option<bool>(nameof(FormattingOptions), nameof(AllowConcurrent), defaultValue: true);
internal static Option<bool> AllowDisjointSpanMerging { get; } = new Option<bool>(nameof(FormattingOptions), nameof(AllowDisjointSpanMerging), defaultValue: false);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册