/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Analyzers/Core/CodeFixes/FileHeaders/AbstractFileHeaderCodeFixProvider.cs
234 строки
10 KB
Cyrus Najmabadi
Reapply "Update methods to be `async`." (#81808)
31 дек 2025, 02:04
31 дек 2025, 02:04
ba729ad
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FileHeaders; internal abstract class AbstractFileHeaderCodeFixProvider : CodeFixProvider { protected abstract AbstractFileHeaderHelper FileHeaderHelper { get; } public override ImmutableArray<string> FixableDiagnosticIds { get; } = [IDEDiagnosticIds.FileHeaderMismatch]; public override async Task RegisterCodeFixesAsync(CodeFixContext context) { foreach (var diagnostic in context.Diagnostics) { context.RegisterCodeFix( CodeAction.Create( CodeFixesResources.Add_file_header, cancellationToken => GetTransformedDocumentAsync(context.Document, cancellationToken), nameof(AbstractFileHeaderCodeFixProvider)), diagnostic); } } private async Task<Document> GetTransformedDocumentAsync(Document document, CancellationToken cancellationToken) => document.WithSyntaxRoot(await GetTransformedSyntaxRootAsync(document, cancellationToken).ConfigureAwait(false)); private async Task<SyntaxNode> GetTransformedSyntaxRootAsync(Document document, CancellationToken cancellationToken) { var options = await document.GetLineFormattingOptionsAsync(cancellationToken).ConfigureAwait(false); var generator = document.GetRequiredLanguageService<SyntaxGeneratorInternal>(); var newLineTrivia = generator.EndOfLine(options.NewLine); return await GetTransformedSyntaxRootAsync(generator.SyntaxFacts, FileHeaderHelper, newLineTrivia, document, fileHeaderTemplate: null, cancellationToken).ConfigureAwait(false); } internal static async Task<SyntaxNode> GetTransformedSyntaxRootAsync(ISyntaxFacts syntaxFacts, AbstractFileHeaderHelper fileHeaderHelper, SyntaxTrivia newLineTrivia, Document document, string? fileHeaderTemplate, CancellationToken cancellationToken) { var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false); // If we weren't given a header lets get the one from editorconfig if (fileHeaderTemplate is null && !document.Project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree).TryGetEditorConfigOption(CodeStyleOptions2.FileHeaderTemplate, out fileHeaderTemplate)) { // No header supplied, no editorconfig setting, nothing to do return root; } if (RoslynString.IsNullOrEmpty(fileHeaderTemplate)) { // Header template is empty, nothing to do. This shouldn't be possible if this method is called in // reaction to a diagnostic, but this method is also used when creating new documents so lets be defensive. return root; } var expectedFileHeader = fileHeaderTemplate.Replace("{fileName}", Path.GetFileName(document.FilePath)); var fileHeader = fileHeaderHelper.ParseFileHeader(root); SyntaxNode newSyntaxRoot; if (fileHeader.IsMissing) { newSyntaxRoot = AddHeader(syntaxFacts, fileHeaderHelper, newLineTrivia, root, expectedFileHeader); } else { newSyntaxRoot = ReplaceHeader(syntaxFacts, fileHeaderHelper, newLineTrivia, root, expectedFileHeader); } return newSyntaxRoot; } private static SyntaxNode ReplaceHeader(ISyntaxFacts syntaxFacts, AbstractFileHeaderHelper fileHeaderHelper, SyntaxTrivia newLineTrivia, SyntaxNode root, string expectedFileHeader) { // Skip single line comments, whitespace, and end of line trivia until a blank line is encountered. var triviaList = root.GetLeadingTrivia(); // True if the current line is blank so far (empty or whitespace); otherwise, false. The first line is // assumed to not be blank, which allows the analysis to detect a file header which follows a blank line at // the top of the file. var onBlankLine = false; // The set of indexes to remove from 'triviaList'. After removing these indexes, the remaining trivia (if // any) will be preserved in the document along with the replacement header. var removalList = new List<int>(); // The number of spaces to indent the new header. This is expected to match the indentation of the header // which is being replaced. var leadingSpaces = string.Empty; // The number of spaces found so far on the current line. This will become 'leadingSpaces' if the spaces are // followed by a comment which is considered a header comment. var possibleLeadingSpaces = string.Empty; // Need to do this with index so we get the line endings correct. for (var i = 0; i < triviaList.Count; i++) { var triviaLine = triviaList[i]; if (triviaLine.RawKind == syntaxFacts.SyntaxKinds.SingleLineCommentTrivia) { if (possibleLeadingSpaces != string.Empty) { // One or more spaces precedes the comment. Keep track of these spaces so we can indent the new // header by the same amount. leadingSpaces = possibleLeadingSpaces; } removalList.Add(i); onBlankLine = false; } else if (triviaLine.RawKind == syntaxFacts.SyntaxKinds.WhitespaceTrivia) { if (leadingSpaces == string.Empty) { possibleLeadingSpaces = triviaLine.ToFullString(); } removalList.Add(i); } else if (triviaLine.RawKind == syntaxFacts.SyntaxKinds.EndOfLineTrivia) { possibleLeadingSpaces = string.Empty; removalList.Add(i); if (onBlankLine) { break; } else { onBlankLine = true; } } else { break; } } // Remove copyright lines in reverse order. for (var i = removalList.Count - 1; i >= 0; i--) { triviaList = triviaList.RemoveAt(removalList[i]); } var newHeaderTrivia = CreateNewHeader(syntaxFacts, leadingSpaces + fileHeaderHelper.CommentPrefix, expectedFileHeader, newLineTrivia.ToFullString()); // Add a blank line and any remaining preserved trivia after the header. newHeaderTrivia = newHeaderTrivia.Add(newLineTrivia).Add(newLineTrivia).AddRange(triviaList); // Insert header at top of the file. return root.WithLeadingTrivia(newHeaderTrivia); } private static SyntaxNode AddHeader(ISyntaxFacts syntaxFacts, AbstractFileHeaderHelper fileHeaderHelper, SyntaxTrivia newLineTrivia, SyntaxNode root, string expectedFileHeader) { var newTrivia = CreateNewHeader(syntaxFacts, fileHeaderHelper.CommentPrefix, expectedFileHeader, newLineTrivia.ToFullString()).Add(newLineTrivia).Add(newLineTrivia); // Skip blank lines already at the beginning of the document, since we add our own var leadingTrivia = root.GetLeadingTrivia(); var skipCount = 0; for (var i = 0; i < leadingTrivia.Count; i++) { if (leadingTrivia[i].RawKind == syntaxFacts.SyntaxKinds.EndOfLineTrivia) { skipCount = i + 1; } else if (leadingTrivia[i].RawKind != syntaxFacts.SyntaxKinds.WhitespaceTrivia) { break; } } newTrivia = newTrivia.AddRange(leadingTrivia.Skip(skipCount)); return root.WithLeadingTrivia(newTrivia); } private static SyntaxTriviaList CreateNewHeader(ISyntaxFacts syntaxFacts, string prefixWithLeadingSpaces, string expectedFileHeader, string newLineText) { var copyrightText = GetCopyrightText(prefixWithLeadingSpaces, expectedFileHeader, newLineText); var newHeader = copyrightText; return syntaxFacts.ParseLeadingTrivia(newHeader); } private static string GetCopyrightText(string prefixWithLeadingSpaces, string copyrightText, string newLineText) { copyrightText = copyrightText.Replace("\r\n", "\n"); var lines = copyrightText.Split('\n'); return string.Join(newLineText, lines.Select(line => { // Rewrite the lines of the header as comments without trailing whitespace. if (string.IsNullOrEmpty(line)) { // This is a blank line of the header. We want the prefix indicating the line is a comment, but no // additional trailing whitespace. return prefixWithLeadingSpaces; } else { // This is a normal line of the header. We want the prefix, followed by a single space, and then the // text of the header line. return prefixWithLeadingSpaces + " " + line; } })); } public override FixAllProvider GetFixAllProvider() => FixAllProvider.Create(async (context, document, diagnostics) => { if (diagnostics.IsEmpty) return null; return await this.GetTransformedDocumentAsync(document, context.CancellationToken).ConfigureAwait(false); }); }