This commit is contained in:
2026-06-07 15:54:53 +07:00
parent 4ff3cc7042
commit 0591c666c2
26 changed files with 721 additions and 1169 deletions

View File

@@ -2,194 +2,208 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace G;
[Generator(LanguageNames.CSharp)]
public class CollectionsOperatorsGenerator : IIncrementalGenerator
namespace G
{
private const string AttributeShortName = "CollectionsOperatorsGenerator";
private const string AttributeFullName = AttributeShortName + "Attribute";
private const string AttributeSource = @"namespace QWERTYkez.Mensura;
[System.AttributeUsage(System.AttributeTargets.Struct | System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class CollectionsOperatorsGeneratorAttribute : System.Attribute { }";
// Какие типы коллекций поддерживаем
private static readonly (string Type, string Selector)[] CollectionTypes =
[
("T[]", "array => array.Select(u => left * u).ToArray()"), // но нужно адаптировать под конкретный тип
// Проще генерировать отдельные методы для каждого типа
];
public void Initialize(IncrementalGeneratorInitializationContext context)
[Generator(LanguageNames.CSharp)]
public class OperatorsGenerator : IIncrementalGenerator
{
context.RegisterPostInitializationOutput(ctx =>
ctx.AddSource($"{AttributeFullName}.g", SourceText.From(AttributeSource, Encoding.UTF8)));
private const string AttributeSource = @"// <auto-generated />
using System;
var operatorsPipeline = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => IsTargetType(node),
transform: static (ctx, _) => GetTypeInfo(ctx))
.Where(info => info.HasValue) // использование nullable value type
.Select((info, _) => info!.Value)
.Collect();
context.RegisterSourceOutput(operatorsPipeline, GenerateOperators);
}
private static bool IsTargetType(SyntaxNode node)
namespace QWERTYkez.Mensura
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class OperatorsGeneratorAttribute : Attribute
{
if (node is not TypeDeclarationSyntax typeDecl)
return false;
public string CoeffName { get; }
// Должен быть partial и не generic
if (!typeDecl.Modifiers.Any(SyntaxKind.PartialKeyword))
return false;
if (typeDecl.TypeParameterList != null)
return false;
// Проверяем наличие атрибута на самом типе
foreach (var attrList in typeDecl.AttributeLists)
foreach (var attr in attrList.Attributes)
{
string name = attr.Name.ToString();
if (name == AttributeShortName || name == AttributeFullName)
return true;
}
// Или на операторах внутри
foreach (var member in typeDecl.Members)
public OperatorsGeneratorAttribute()
{
if (member is OperatorDeclarationSyntax opDecl &&
opDecl.AttributeLists.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().Contains(AttributeShortName)))
return true;
}
return false;
}
private static TypeInfoData? GetTypeInfo(GeneratorSyntaxContext ctx)
{
var typeDecl = (TypeDeclarationSyntax)ctx.Node;
var semanticModel = ctx.SemanticModel;
var symbol = semanticModel.GetDeclaredSymbol(typeDecl);
if (symbol == null) return null;
string namespaceName = symbol.ContainingNamespace?.ToString() ?? "";
// Формируем заголовок (для отладочных целей)
var headerBuilder = new StringBuilder();
foreach (var modifier in typeDecl.Modifiers)
headerBuilder.Append(modifier.Text).Append(' ');
if (typeDecl is RecordDeclarationSyntax)
headerBuilder.Append("record ");
headerBuilder.Append(typeDecl.Identifier.Text);
string header = headerBuilder.ToString();
// Собираем операторы
var operators = ImmutableArray.CreateBuilder<Operation>();
foreach (var member in typeDecl.Members)
{
if (member is not OperatorDeclarationSyntax opDecl) continue;
if (opDecl.ParameterList.Parameters.Count != 2) continue;
if (!opDecl.AttributeLists.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().Contains(AttributeShortName))) continue;
operators.Add(new Operation(
opDecl.OperatorToken.Text,
opDecl.ParameterList.Parameters[0].Type!.ToString(),
opDecl.ParameterList.Parameters[1].Type!.ToString(),
opDecl.ReturnType.ToString()
));
}
if (operators.Count == 0) return null;
return new TypeInfoData(namespaceName, typeDecl.Identifier.Text, header, operators.ToImmutable());
}
private static void GenerateOperators(SourceProductionContext context, ImmutableArray<TypeInfoData> types)
{
foreach (var group in types.GroupBy(t => t.Namespace))
public OperatorsGeneratorAttribute(string coeffName)
{
var document = new StringBuilder();
document.AppendLine("namespace ").Append(group.Key).AppendLine(";");
document.AppendLine("using System.Collections.Generic;");
document.AppendLine("using System.Linq;");
var allOperators = group.SelectMany(t => t.Operators).ToList();
var multiplications = allOperators.Where(op => op.OperatorToken == "*").ToList();
var divisions = allOperators.Where(op => op.OperatorToken == "/").ToList();
// Генерация для умножения: left * collection и collection * left
foreach (var mul in multiplications)
{
// left * collection
document.AppendLine($@"
public static {mul.ReturnType}[] operator *({mul.TypeA} left, {mul.TypeB}[] right) =>
right.Select(u => left * u).ToArray();
public static List<{mul.ReturnType}> operator *({mul.TypeA} left, List<{mul.TypeB}> right) =>
right.Select(u => left * u).ToList();
public static IEnumerable<{mul.ReturnType}> operator *({mul.TypeA} left, IEnumerable<{mul.TypeB}> right) =>
right.Select(u => left * u);
");
// collection * left
document.AppendLine($@"
public static {mul.ReturnType}[] operator *({mul.TypeB}[] left, {mul.TypeA} right) =>
left.Select(u => u * right).ToArray();
public static List<{mul.ReturnType}> operator *(List<{mul.TypeB}> left, {mul.TypeA} right) =>
left.Select(u => u * right).ToList();
public static IEnumerable<{mul.ReturnType}> operator *(IEnumerable<{mul.TypeB}> left, {mul.TypeA} right) =>
left.Select(u => u * right);
");
}
// Деление: left / collection (left - тип A, collection - тип B)
foreach (var div in divisions.Where(op => op.TypeA != null)) // все деления
{
// left / collection
document.AppendLine($@"
public static {div.ReturnType}[] operator /({div.TypeA} left, {div.TypeB}[] right) =>
right.Select(u => left / u).ToArray();
public static List<{div.ReturnType}> operator /({div.TypeA} left, List<{div.TypeB}> right) =>
right.Select(u => left / u).ToList();
public static IEnumerable<{div.ReturnType}> operator /({div.TypeA} left, IEnumerable<{div.TypeB}> right) =>
right.Select(u => left / u);
");
// collection / right (где right - тип B)
document.AppendLine($@"
public static {div.ReturnType}[] operator /({div.TypeA}[] left, {div.TypeB} right) =>
left.Select(u => u / right).ToArray();
public static List<{div.ReturnType}> operator /(List<{div.TypeA}> left, {div.TypeB} right) =>
left.Select(u => u / right).ToList();
public static IEnumerable<{div.ReturnType}> operator /(IEnumerable<{div.TypeA}> left, {div.TypeB} right) =>
left.Select(u => u / right);
");
}
context.AddSource($"operations.{group.Key}.g", document.ToString());
CoeffName = coeffName;
}
}
}";
private readonly struct TypeInfoData(string ns, string name, string header, ImmutableArray<Operation> ops)
{
public string Namespace { get; } = ns;
public string TypeName { get; } = name;
public string Header { get; } = header;
public ImmutableArray<Operation> Operators { get; } = ops;
}
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 1. Внедряем атрибут
context.RegisterPostInitializationOutput(static postInitializationContext =>
{
postInitializationContext.AddSource(
".OperatorsGeneratorAttribute.g.cs",
SourceText.From(AttributeSource, Encoding.UTF8));
});
private readonly struct Operation(string token, string a, string b, string ret)
{
public string OperatorToken { get; } = token;
public string TypeA { get; } = a;
public string TypeB { get; } = b;
public string ReturnType { get; } = ret;
// 2. Ищем методы-операторы, помеченные атрибутом
IncrementalValuesProvider<OperatorInfo> operatorDeclarations = context.SyntaxProvider
.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "QWERTYkez.Mensura.OperatorsGeneratorAttribute",
predicate: static (node, _) => node is OperatorDeclarationSyntax,
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
.Where(static m => m is not null)!;
// 3. Выполняем генерацию кода
context.RegisterSourceOutput(operatorDeclarations, static (spc, opInfo) => Execute(spc, opInfo));
}
private static OperatorInfo GetSemanticTargetForGeneration(GeneratorAttributeSyntaxContext context)
{
var opDeclaration = (OperatorDeclarationSyntax)context.TargetNode;
if (opDeclaration.ParameterList.Parameters.Count < 2) return null;
if (context.SemanticModel.GetDeclaredSymbol(opDeclaration) is not IMethodSymbol symbol) return null;
var namespaceName = symbol.ContainingNamespace.ToDisplayString();
var leftType = opDeclaration.ParameterList.Parameters[0].Type?.ToString() ?? "";
var rightType = opDeclaration.ParameterList.Parameters[1].Type?.ToString() ?? "";
var returnType = opDeclaration.ReturnType.ToString();
var isMultiplication = opDeclaration.OperatorToken.IsKind(SyntaxKind.AsteriskToken);
var isDivision = opDeclaration.OperatorToken.IsKind(SyntaxKind.SlashToken);
if (!isMultiplication && !isDivision) return null;
// АВТОМАТИЧЕСКИЙ ПОИСК: Ищем содержащий тип (структуру/рекорд) по синтаксическому дереву
string containingTypeName = leftType; // Фолбэк по умолчанию
var parentTypeDeclaration = opDeclaration.Ancestors()
.OfType<TypeDeclarationSyntax>()
.FirstOrDefault();
if (parentTypeDeclaration != null)
{
containingTypeName = parentTypeDeclaration.Identifier.ToString();
}
string coeffName = null;
string coeffType = containingTypeName; // По умолчанию считаем, что коэффициент в текущем классе
// Извлекаем аргумент атрибута
var attribute = opDeclaration.AttributeLists
.SelectMany(al => al.Attributes)
.FirstOrDefault(a => a.Name.ToString().EndsWith("OperatorsGenerator") || a.Name.ToString() == "OperatorsGenerator");
if (attribute?.ArgumentList != null && attribute.ArgumentList.Arguments.Count > 0)
{
var expr = attribute.ArgumentList.Arguments[0].Expression;
// Проверяем синтаксис nameof(...)
if (expr is InvocationExpressionSyntax invocation &&
invocation.Expression.ToString() == "nameof" &&
invocation.ArgumentList.Arguments.Count > 0)
{
var nameofArg = invocation.ArgumentList.Arguments[0].Expression;
// Если все-таки написали явно nameof(Type.Property)
if (nameofArg is MemberAccessExpressionSyntax memberAccess)
{
coeffType = memberAccess.Expression.ToString();
coeffName = memberAccess.Name.ToString();
}
else
{
coeffName = nameofArg.ToString();
// coeffType остается равным содержащему классу containingTypeName
}
}
else
{
coeffName = expr.ToString().Trim('"');
// coeffType остается равным содержащему классу containingTypeName
}
}
return new OperatorInfo
{
Namespace = namespaceName,
LeftType = leftType,
RightType = rightType,
ReturnType = returnType,
IsMultiplication = isMultiplication,
IsDivision = isDivision,
CoeffName = coeffName,
CoeffType = coeffType,
HasCoeff = !string.IsNullOrWhiteSpace(coeffName)
};
}
private static void Execute(SourceProductionContext context, OperatorInfo info)
{
var opType = info.IsMultiplication ? "Mul" : "Div";
var coeffSuffix = info.HasCoeff ? ".Coeff" : "";
var hintName = $"Op.{info.LeftType}.{opType}.{info.RightType}{coeffSuffix}.g.cs";
var source = info.IsMultiplication
? GenerateMultiplicationSource(info)
: GenerateDivisionSource(info);
context.AddSource(hintName, SourceText.From(source, Encoding.UTF8));
}
private static string GenerateMultiplicationSource(OperatorInfo info)
{
return $@"// <auto-generated />
using System;
using System.Collections.Generic;
namespace {info.Namespace};
public readonly partial record struct {info.LeftType}
{{
public static {info.ReturnType}[] operator *({info.LeftType} left, {info.RightType}[] right) => {(info.HasCoeff ? $"({info.CoeffType}.{info.CoeffName} * left._Value)" : "left._Value")}.Multiply<{info.RightType}, {info.ReturnType}>(right);
public static List<{info.ReturnType}> operator *({info.LeftType} left, List<{info.RightType}> right) => {(info.HasCoeff ? $"({info.CoeffType}.{info.CoeffName} * left._Value)" : "left._Value")}.Multiply<{info.RightType}, {info.ReturnType}>(right);
public static IEnumerable<{info.ReturnType}> operator *({info.LeftType} left, IEnumerable<{info.RightType}> right) => {(info.HasCoeff ? $"({info.CoeffType}.{info.CoeffName} * left._Value)" : "left._Value")}.Multiply<{info.RightType}, {info.ReturnType}>(right);
}}
public readonly partial record struct {info.RightType}
{{
public static {info.ReturnType}[] operator *({info.LeftType}[] left, {info.RightType} right) => {(info.HasCoeff ? $"({info.CoeffType}.{info.CoeffName} * right._Value)" : "right._Value")}.Multiply<{info.LeftType}, {info.ReturnType}>(left);
public static List<{info.ReturnType}> operator *(List<{info.LeftType}> left, {info.RightType} right) => {(info.HasCoeff ? $"({info.CoeffType}.{info.CoeffName} * right._Value)" : "right._Value")}.Multiply<{info.LeftType}, {info.ReturnType}>(left);
public static IEnumerable<{info.ReturnType}> operator *(IEnumerable<{info.LeftType}> left, {info.RightType} right) => {(info.HasCoeff ? $"({info.CoeffType}.{info.CoeffName} * right._Value)" : "right._Value")}.Multiply<{info.LeftType}, {info.ReturnType}>(left);
}}";
}
private static string GenerateDivisionSource(OperatorInfo info)
{
return $@"// <auto-generated />
using System;
using System.Collections.Generic;
namespace {info.Namespace};
public readonly partial record struct {info.LeftType}
{{
public static {info.ReturnType}[] operator /({info.LeftType} left, {info.RightType}[] right) => {(info.HasCoeff ? $"({info.CoeffType}.{info.CoeffName} * left._Value)" : "left._Value")}.Divide<{info.RightType}, {info.ReturnType}>(right);
public static List<{info.ReturnType}> operator /({info.LeftType} left, List<{info.RightType}> right) => {(info.HasCoeff ? $"({info.CoeffType}.{info.CoeffName} * left._Value)" : "left._Value")}.Divide<{info.RightType}, {info.ReturnType}>(right);
public static IEnumerable<{info.ReturnType}> operator /({info.LeftType} left, IEnumerable<{info.RightType}> right) => {(info.HasCoeff ? $"({info.CoeffType}.{info.CoeffName} * left._Value)" : "left._Value")}.Divide<{info.RightType}, {info.ReturnType}>(right);
}}
public readonly partial record struct {info.RightType}
{{
public static {info.ReturnType}[] operator /({info.LeftType}[] left, {info.RightType} right) => {(info.HasCoeff ? $"left.Multiply<{info.LeftType}, {info.ReturnType}>({info.CoeffType}.{info.CoeffName} / right._Value)" : $"left.Divide<{info.LeftType}, {info.ReturnType}>(right._Value)")};
public static List<{info.ReturnType}> operator /(List<{info.LeftType}> left, {info.RightType} right) => {(info.HasCoeff ? $"left.Multiply<{info.LeftType}, {info.ReturnType}>({info.CoeffType}.{info.CoeffName} / right._Value)" : $"left.Divide<{info.LeftType}, {info.ReturnType}>(right._Value)")};
public static IEnumerable<{info.ReturnType}> operator /(IEnumerable<{info.LeftType}> left, {info.RightType} right) => {(info.HasCoeff ? $"left.Multiply<{info.LeftType}, {info.ReturnType}>({info.CoeffType}.{info.CoeffName} / right._Value)" : $"left.Divide<{info.LeftType}, {info.ReturnType}>(right._Value)")};
}}";
}
private class OperatorInfo
{
public string Namespace { get; set; } = string.Empty;
public string LeftType { get; set; } = string.Empty;
public string RightType { get; set; } = string.Empty;
public string ReturnType { get; set; } = string.Empty;
public bool IsMultiplication { get; set; }
public bool IsDivision { get; set; }
public string CoeffName { get; set; }
public string CoeffType { get; set; }
public bool HasCoeff { get; set; } }
}
}