I've written about it before.Two use cases for generating source code with the Roslyn source generatorCode Fixer for Roslyn today.CodeFixProvider
Implementing a cs file header comment function.
The code fixer will involve bothCodeFixProvider
cap (a poem)DiagnosticAnalyzer
,
Implementing FileHeaderAnalyzer
First of all, we know that the prerequisite for the fixer is the analyzer, for example, here, if you want to add header comments to the code, then the analyzer must give the corresponding analysis of the reminder.
We start by implementing an implementation calledFileHeaderAnalyzer
The analyzer.
[DiagnosticAnalyzer()]
public class FileHeaderAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "GEN050";
private static readonly LocalizableString Title = "File missing header information";
private static readonly LocalizableString MessageFormat = "File missing header information";
private static readonly LocalizableString Description = "Each file should contain header information.";
private const string Category = "Document";
private static readonly DiagnosticDescriptor Rule = new(
DiagnosticId, Title, MessageFormat, Category, , isEnabledByDefault: true, description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
public override void Initialize(AnalysisContext context)
{
if (context is null)
return;
();
();
(AnalyzeSyntaxTree);
}
private static void AnalyzeSyntaxTree(SyntaxTreeAnalysisContext context)
{
var root = ();
var firstToken = ();
// Check if the file starts with a comment
var hasHeaderComment = (trivia => () || ());
if (!hasHeaderComment)
{
var diagnostic = (Rule, (, (0, 0)));
(diagnostic);
}
}
}
FileHeaderAnalyzer analyzer principle is very simple, you need to overload a few methods, focusing on theInitialize
method, where theRegisterSyntaxTreeAction
i.e., the core code.SyntaxTreeAnalysisContext
object fetches the current source code of theSyntaxNode
root node, and then determine the first of TA'sSyntaxToken
Whether or not it is a comment line (|)
If it's not a comment line, then notify the parser!
Having implemented the above code let's look at the effect.
and the analyzer will display a list of warnings in the error panel when compiling:.
Implementing CodeFixProvider
The analyzer is done, and now we'll implement the program calledAddFileHeaderCodeFixProvider
The restorers, the
/// <summary>
/// Automatically add header comments to documents
/// </summary>
[ExportCodeFixProvider(, Name = nameof(AddFileHeaderCodeFixProvider))]
[Shared]
public class AddFileHeaderCodeFixProvider : CodeFixProvider
{
private const string Title = "Add file header information";
//Name of the convention template file
private const string ConfigFileName = "";
private const string VarPrefix = "$";//prefix of a variable
//Default comment text if the template does not exist
private const string DefaultComment = """
// Licensed to the {Product} under one or more agreements.
// The {Product} licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
""";
#region regex
private const RegexOptions ROptions = | ;
private static readonly Regex VersionRegex = new(@"<Version>(.*?)</Version>", ROptions);
private static readonly Regex CopyrightRegex = new(@"<Copyright>(.*?)</Copyright>", ROptions);
private static readonly Regex CompanyRegex = new(@"<Company>(.*?)</Company>", ROptions);
private static readonly Regex DescriptionRegex = new(@"<Description>(.*?)</Description>", ROptions);
private static readonly Regex AuthorsRegex = new(@"<Authors>(.*?)</Authors>", ROptions);
private static readonly Regex ProductRegex = new(@"<Product>(.*?)</Product>", ROptions);
private static readonly Regex TargetFrameworkRegex = new(@"<TargetFramework>(.*?)</TargetFramework>", ROptions);
private static readonly Regex TargetFrameworksRegex = new(@"<TargetFrameworks>(.*?)</TargetFrameworks>", ROptions);
private static readonly Regex ImportRegex = new(@"<Import Project=""(.*?)""", ROptions);
#endregion
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
//rewriteFixableDiagnosticIds,Return to Analyzer ReportId,Indicates that the current repairer is capable of repairing the correspondingId
get { return []; }
}
public sealed override FixAllProvider GetFixAllProvider()
{
return ;
}
public override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diagnostic = [0];
var diagnosticSpan = ;
(
(
title: Title,
createChangedDocument: c => FixDocumentAsync(, diagnosticSpan, c),
equivalenceKey: Title),
diagnostic);
return ;
}
private static async Task<Document> FixDocumentAsync(Document document, TextSpan span, CancellationToken ct)
{
var root = await (ct).ConfigureAwait(false);
//Getting file header information from the project configuration
var projFilePath = ?? "C:\\";//No file path for unit test,So use the default path
var projectDirectory = (projFilePath);
var configFilePath = (projectDirectory, ConfigFileName);
var comment = DefaultComment;
string? copyright = "MIT";
string? author = ;
string? company = ;
string? description = ;
string? title = ;
string? version = ();
string? product = ;
string? file = ();
string? targetFramework = ;
#pragma warning disable CA1305 // indicate clearly and with certainty IFormatProvider
string? date = ("yyyy-MM-dd HH:mm:ss");
#pragma warning restore CA1305 // indicate clearly and with certainty IFormatProvider
if ((configFilePath))
{
comment = (configFilePath, .UTF8);
}
#region Finding assembly metadata
// Loading project files:
var text = (projFilePath, .UTF8);
// go down (in history)Importdocuments,for example : <Import Project="..\" />
// Matching with regular expressionsProject:
var importMatchs = (text);
foreach (Match importMatch in importMatchs)
{
var importFile = (projectDirectory, [1].Value);
if ((importFile))
{
text += (importFile);
}
}
//Presence of variable references,Need to parse
string RawVal(string old, string @default)
{
if (old == null)
return @default;
//When the obtained version number is a variable reference:$(Version)when,Need to parse again
if ((VarPrefix, ))
{
var varName = (2, - 3);
var varMatch = new Regex($@"<{varName}>(.*?)</{varName}>", ).Match(text);
if ()
{
return [1].Value;
}
//Variable reference not found,Return to Silent
return @default;
}
return old;
}
var versionMatch = (text);
var copyrightMath = (text);
var companyMatch = (text);
var descriptionMatch = (text);
var authorsMatch = (text);
var productMatch = (text);
var targetFrameworkMatch = (text);
var targetFrameworksMatch = (text);
if ()
{
version = RawVal([1].Value, version);
}
if ()
{
copyright = RawVal([1].Value, copyright);
}
if ()
{
company = RawVal([1].Value, company);
}
if ()
{
description = RawVal([1].Value, description);
}
if ()
{
author = RawVal([1].Value, author);
}
if ()
{
product = RawVal([1].Value, product);
}
if ()
{
targetFramework = RawVal([1].Value, targetFramework);
}
if ()
{
targetFramework = RawVal([1].Value, targetFramework);
}
#endregion
//Use regular expressions to replace
comment = (comment, @"\{(?<key>[^}]+)\}", m =>
{
var key = ["key"].Value;
return key switch
{
"Product" => product,
"Title" => title,
"Version" => version,
"Date" => date,
"Author" => author,
"Company" => company,
"Copyright" => copyright,
"File" => file,
"Description" => description,
"TargetFramework" => targetFramework,
_ => ,
};
}, );
var headerComment = (comment + );
var newRoot = root?.WithLeadingTrivia(headerComment);
if (newRoot == null)
{
return document;
}
var newDocument = (newRoot);
return newDocument;
}
}
Code Fixer's most important overloaded methodsRegisterCodeFixesAsync
, ObjectCodeFixContext
Contains information about the project and source code and the corresponding analyzer.
For example.indicates the corresponding source code, the
indicates the corresponding item.
Just what I need in the code.
*.csproj
the project file, the
When we fetch the project file, we can then read the information configured in the project file, for exampleCompany
,Authors
,Description
and even useful information like the version number we mentioned in the previous post, I'm currently using regular expressions, but of course you can also useXPath
,
Then replace the template with useful data to get the desired annotated code snippet!
For example, my Comment template file
// Licensed to the {Product} under one or more agreements.
// The {Product} licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// {Product} Author: Wan Yashoo (1933-), PRC politician, prime minister from 2008 Github: /vipwan
// {Description}
// Modify Date: {Date} {File}
The replacement will generate the following code.
// Licensed to the under one or more agreements.
// The licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
// Author: Wan Yashoo (1933-), PRC politician, prime minister from 2008 Github: /vipwan
// ,NET9+ MinimalApi CQRS
// Modify Date: 2024-09-07 15:22:42
final use(comment)
method generates an annotatedSyntaxTrivia
and appends it to the current root syntax tree, and finally returns the newDocument
I'll do it!
Now that it's done, let's see what it looks like.
The above code completes the entire source generation step, and finally you can use the nuget package I released to experience.
dotnet add package
I posted the source code to GitHub, welcome to star!/vipwan/
/vipwan//blob/master//CodeFixProviders/