Skip to content

Conversation

walterlv
Copy link
Member

@walterlv walterlv commented Sep 9, 2025

来自 https://github.com/dotnet-campus/dotnetCampus.CommandLine 的代码:

var builder = new SourceTextBuilder(model.Namespace)
    .Using("System")
    .Using("DotNetCampus.Cli.Compiler")
    .AddTypeDeclaration(GenerateBuilderTypeDeclarationLine(model), t => t
        .WithSummaryComment($"""辅助 <see cref="{model.CommandObjectType.ToUsingString()}"/> 生成命令行选项、子命令或处理函数的创建。""")
        .AddMethodDeclaration(
            $"public static {model.CommandObjectType.ToUsingString()} CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine)",
            m => m
                .AddRawStatements($"return new {model.Namespace}.{model.GetBuilderTypeName()}(commandLine).Build();"))
        .AddRawMembers(model.OptionProperties.Select(GenerateArgumentPropertyCode))
        .AddRawMembers(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateArgumentPropertyCode))
        .AddRawText(GenerateBuildCode(model))
        .AddMethodDeclaration(
            "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan<char> longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy)",
            m => GenerateMatchLongOptionCode(m, model))
        .AddMethodDeclaration(
            "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan<char> shortOption, bool defaultCaseSensitive)",
            m => GenerateMatchShortOptionCode(m, model))
        .AddMethodDeclaration(
            "private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan<char> value, int argumentIndex)",
            m => m
                .AddRawStatements(GenerateMatchPositionalArgumentsCode(model))
                .AddRawStatements("return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;"))
        .AddMethodDeclaration(
            "private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan<char> key, ReadOnlySpan<char> value)",
            m => m
                .AddBracketScope("switch (propertyIndex)", l => l
                    .AddRawStatements(model.OptionProperties.Select(GenerateAssignPropertyValueCode))
                    .AddRawStatements(model.EnumeratePositionalArgumentPropertiesExcludingSameNameOptions().Select(GenerateAssignPropertyValueCode))))
        .AddMethodDeclaration(
            $"private {model.CommandObjectType.ToUsingString()} BuildCore(global::DotNetCampus.Cli.CommandLine commandLine)",
            m => GenerateBuildCoreCode(m, model))
        .AddMethodDeclaration(
            $"private {model.CommandObjectType.ToUsingString()} BuildDefault()",
            m => GenerateBuildDefaultCode(m, model))
        .AddRawMembers(model.EnumerateEnumPropertyTypes().Select(GenerateEnumDeclarationCode))
    );
return builder.ToString();

能生成下面这样复杂的代码:

#nullable enable
using global::System;
using global::DotNetCampus.Cli.Compiler;

namespace DotNetCampus.Cli.Performance.Fakes;

/// <summary>
/// 辅助 <see cref="global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions4"/> 生成命令行选项、子命令或处理函数的创建。
/// </summary>
public sealed class BenchmarkOptions4Builder(global::DotNetCampus.Cli.CommandLine commandLine)
{
    public static global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions4 CreateInstance(global::DotNetCampus.Cli.CommandLine commandLine)
    {
        return new DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions4Builder(commandLine).Build();
    }

    private global::DotNetCampus.Cli.Compiler.BooleanArgument IsDebugMode = new();

    private global::DotNetCampus.Cli.Compiler.NumberArgument TestCount = new();

    private global::DotNetCampus.Cli.Compiler.StringArgument TestName = new();

    private global::DotNetCampus.Cli.Compiler.StringArgument TestCategory = new();

    private __GeneratedEnumArgument__DotNetCampus_Cli_Performance_Fakes_DetailLevel__ DetailLevel = new();

    private global::DotNetCampus.Cli.Compiler.StringListArgument TestItems = new();

    public global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions4 Build()
    {
        if (commandLine.RawArguments.Count is 0)
        {
            return BuildDefault();
        }

        var parser = new global::DotNetCampus.Cli.Utils.Parsers.CommandLineParser(commandLine, "BenchmarkOptions4", 0)
        {
            MatchLongOption = MatchLongOption,
            MatchShortOption = MatchShortOption,
            MatchPositionalArguments = MatchPositionalArguments,
            AssignPropertyValue = AssignPropertyValue,
        };
        parser.Parse().ThrowIfError();
        return BuildCore(commandLine);
    }

    private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan<char> longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy)
    {
        // 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。
        switch (longOption)
        {
            case "debug":
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(IsDebugMode), 0, global::DotNetCampus.Cli.OptionValueType.Boolean);
            case "count":
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal);
            case "test-name":
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal);
            case "test-category":
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCategory), 3, global::DotNetCampus.Cli.OptionValueType.Normal);
            case "detail-level":
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal);
        }

        var defaultComparison = defaultCaseSensitive
            ? global::System.StringComparison.Ordinal
            : global::System.StringComparison.OrdinalIgnoreCase;

        // 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。
        if (namingPolicy.SupportsOrdinal())
        {
            if (longOption.Equals("debug".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(IsDebugMode), 0, global::DotNetCampus.Cli.OptionValueType.Boolean);
            }
            if (longOption.Equals("count".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal);
            }
            if (longOption.Equals("test-name".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal);
            }
            if (longOption.Equals("test-category".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCategory), 3, global::DotNetCampus.Cli.OptionValueType.Normal);
            }
            if (longOption.Equals("detail-level".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal);
            }
        }

        // 3. 最后根据其他命名法匹配一遍(能应对所有不规范命令行大小写,并支持所有风格)。
        if (namingPolicy.SupportsPascalCase())
        {
            if (longOption.Equals("Debug".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(IsDebugMode), 0, global::DotNetCampus.Cli.OptionValueType.Boolean);
            }
            if (longOption.Equals("Count".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal);
            }
            if (longOption.Equals("TestName".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal);
            }
            if (longOption.Equals("TestCategory".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCategory), 3, global::DotNetCampus.Cli.OptionValueType.Normal);
            }
            if (longOption.Equals("DetailLevel".AsSpan(), defaultComparison))
            {
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal);
            }
        }

        return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;
    }

    private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan<char> shortOption, bool defaultCaseSensitive)
    {
        // 1. 先快速原字符匹配一遍(能应对规范命令行大小写,并优化 DotNet / GNU 风格的性能)。
        switch (shortOption)
        {
            // 属性 IsDebugMode 没有短名称,无需匹配。
            case "c":
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal);
            case "n":
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal);
            // 属性 TestCategory 没有短名称,无需匹配。
            case "d":
                return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal);
        }

        var defaultComparison = defaultCaseSensitive
            ? global::System.StringComparison.Ordinal
            : global::System.StringComparison.OrdinalIgnoreCase;

        // 2. 再按指定大小写指定命名法匹配一遍(能应对不规范命令行大小写)。
        // 属性 IsDebugMode 没有短名称,无需匹配。
        if (shortOption.Equals("c".AsSpan(), defaultComparison))
        {
            return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestCount), 1, global::DotNetCampus.Cli.OptionValueType.Normal);
        }
        if (shortOption.Equals("n".AsSpan(), defaultComparison))
        {
            return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(TestName), 2, global::DotNetCampus.Cli.OptionValueType.Normal);
        }
        // 属性 TestCategory 没有短名称,无需匹配。
        if (shortOption.Equals("d".AsSpan(), defaultComparison))
        {
            return new global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch(nameof(DetailLevel), 4, global::DotNetCampus.Cli.OptionValueType.Normal);
        }

        return global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch.NotMatch;
    }

    private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan<char> value, int argumentIndex)
    {
        if (argumentIndex is >= 0 and < 2147483647)
        {
            return new global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch("TestItems", 5, global::DotNetCampus.Cli.PositionalArgumentValueType.Normal);
        }
        return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;
    }

    private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan<char> key, ReadOnlySpan<char> value)
    {
        switch (propertyIndex)
        {
            case 0:
                IsDebugMode = IsDebugMode.Assign(value);
                break;
            case 1:
                TestCount = TestCount.Assign(value);
                break;
            case 2:
                TestName = TestName.Assign(value);
                break;
            case 3:
                TestCategory = TestCategory.Assign(value);
                break;
            case 4:
                DetailLevel = DetailLevel.Assign(value);
                break;
            case 5:
                TestItems = TestItems.Append(value);
                break;
        }
    }

    private global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions4 BuildCore(global::DotNetCampus.Cli.CommandLine commandLine)
    {
        var result = new global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions4
        {
            // 1. There is no [RawArguments] property to be initialized.

            // 2. [Option]
            IsDebugMode = IsDebugMode.ToBoolean() ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain a required option 'debug'. Command line: {commandLine}", "IsDebugMode"),
            TestCount = TestCount.ToInt32() ?? throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain a required option 'count'. Command line: {commandLine}", "TestCount"),

            // 3. [Value]
            TestItems = TestItems.ToList(),
        };

        // 1. There is no [RawArguments] property to be assigned.

        // 2. [Option]
        if (TestName.ToString() is { } o0)
        {
            result.TestName = o0;
        }
        if (TestCategory.ToString() is { } o1)
        {
            result.TestCategory = o1;
        }
        if (DetailLevel.ToDetailLevel() is { } o2)
        {
            result.DetailLevel = o2;
        }

        // 3. There is no [Value] property to be assigned.

        return result;
    }

    private global::DotNetCampus.Cli.Performance.Fakes.BenchmarkOptions4 BuildDefault()
    {
        throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain any required option or positional argument. Command line: {commandLine}", null!);
    }

    /// <summary>
    /// Provides parsing and assignment for the enum type <see cref="global::DotNetCampus.Cli.Performance.Fakes.DetailLevel"/>.
    /// </summary>
    private readonly record struct __GeneratedEnumArgument__DotNetCampus_Cli_Performance_Fakes_DetailLevel__
    {
        /// <summary>
        /// Indicates whether to ignore exceptions when parsing fails.
        /// </summary>
        public bool IgnoreExceptions { get; init; }

        /// <summary>
        /// Stores the parsed enum value.
        /// </summary>
        private global::DotNetCampus.Cli.Performance.Fakes.DetailLevel? Value { get; init; }

        /// <summary>
        /// Assigns a value when a command line input is parsed.
        /// </summary>
        /// <param name="value">The parsed string value.</param>
        public __GeneratedEnumArgument__DotNetCampus_Cli_Performance_Fakes_DetailLevel__ Assign(ReadOnlySpan<char> value)
        {
            Span<char> lowerValue = stackalloc char[value.Length];
            for (var i = 0; i < value.Length; i++)
            {
                lowerValue[i] = char.ToLowerInvariant(value[i]);
            }
            global::DotNetCampus.Cli.Performance.Fakes.DetailLevel? newValue = lowerValue switch
            {
                "low" => global::DotNetCampus.Cli.Performance.Fakes.DetailLevel.Low,
                "medium" => global::DotNetCampus.Cli.Performance.Fakes.DetailLevel.Medium,
                "high" => global::DotNetCampus.Cli.Performance.Fakes.DetailLevel.High,
                _ when IgnoreExceptions => null,
                _ => throw new global::DotNetCampus.Cli.Exceptions.CommandLineParseValueException($"Cannot convert '{value.ToString()}' to enum type 'DotNetCampus.Cli.Performance.Fakes.DetailLevel'."),
            };
            return this with { Value = newValue };
        }

        /// <summary>
        /// Converts the parsed value to the enum type.
        /// </summary>
        public global::DotNetCampus.Cli.Performance.Fakes.DetailLevel? ToDetailLevel() => Value;
    }
}

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces the first batch of source generator utility methods for code analysis tools. The changes add a comprehensive set of helper utilities for building and working with source generators in .NET projects.

  • Adds core utility classes for source generation including identifier handling, source text building, and embedded file management
  • Implements source generator infrastructure with template support and MSBuild property integration
  • Sets up project structure with proper packaging configuration for NuGet distribution

Reviewed Changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/dotnetCampus.CodeAnalysisUtils/DotNetCampus.CodeAnalysisUtils.csproj Main project file with analyzer/generator configuration and NuGet packaging setup
src/dotnetCampus.CodeAnalysisUtils/IdentifierHelper.g.cs Utility for creating safe C# identifiers from arbitrary strings
src/dotnetCampus.CodeAnalysisUtils/Generators/Builders/SourceTextBuilder.g.cs Comprehensive builder for generating C# source code with proper formatting
src/dotnetCampus.CodeAnalysisUtils/Generators/EmbeddedGenerator.cs Source generator that processes embedded template files
src/dotnetCampus.CodeAnalysisUtils/CodeAnalysis/AnalyzerConfigOptionsExtensions.g.cs Extensions for accessing MSBuild properties in source generators
src/dotnetCampus.CodeAnalysisUtils/GeneratorInfo.g.cs Utility for generator metadata and symbol display formatting
tests/DotNetCampus.CodeAnalysis.Tests/DotNetCampus.CodeAnalysis.Tests.csproj Test project configuration for debugging source generators
Directory.Build.props Solution-wide build configuration and package metadata
Comments suppressed due to low confidence (1)

src/dotnetCampus.CodeAnalysisUtils/Generators/Builders/SourceTextBuilder.g.cs:1

  • The Chinese comment is incomplete. It should be '使其可以在异步上下文中安全地访问、修改和清除' (so that it can be safely accessed, modified and cleared in asynchronous contexts).
#nullable enable

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@lindexi lindexi assigned walterlv and unassigned lindexi Sep 18, 2025
@lindexi lindexi merged commit 3d5292e into main Sep 19, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants