diff --git a/src/StgSharp.RegularAnalysis/Text/TextRegexAttribute.cs b/src/StgSharp.RegularAnalysis/Text/TextRegexAttribute.cs new file mode 100644 index 00000000..e656ad7b --- /dev/null +++ b/src/StgSharp.RegularAnalysis/Text/TextRegexAttribute.cs @@ -0,0 +1,30 @@ +// ----------------------------------------------------------------------------- +// file="TextRegexAttribute" +// Project: StgSharp +// Copyright (c) Nitload. +// SPDX-License-Identifier: MIT +// ----------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StgSharp.RegularAnalysis.Text +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public sealed class TextRegexAttribute : Attribute + { + + public TextRegexAttribute( + string pattern + ) + { + Pattern = pattern; + } + + public string Pattern { get; } + + } +} diff --git a/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.Diagnostic.cs b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.Diagnostic.cs new file mode 100644 index 00000000..69c9e7d5 --- /dev/null +++ b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.Diagnostic.cs @@ -0,0 +1,56 @@ +// ----------------------------------------------------------------------------- +// file="TextRegexSourceGen.Diagnostic" +// Project: StgSharp +// Copyright (c) Nitload. +// SPDX-License-Identifier: MIT +// ----------------------------------------------------------------------------- + +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StgSharp.RegularAnalysis.Text +{ + internal partial class TextRegexSourceGen + { + + private static readonly DiagnosticDescriptor CanNotBeRecordDiag = new(id:"NGRA004", + title:$"Containing type cannot be record", + messageFormat:$"Containing type: {{0}} cannot be record", + defaultSeverity:DiagnosticSeverity.Error, + category:"NGRA.TextRegex", + isEnabledByDefault:true); + + private static readonly DiagnosticDescriptor IncorrectContainingTypeDiag = new(id:"NGRA005", + title:$"Containing type can only be struct or class", + messageFormat:$"Containing type: {{0}} is a {{1}}, but not class or struct", + defaultSeverity:DiagnosticSeverity.Error, + category:"NGRA.TextRegex", + isEnabledByDefault:true); + + private static readonly DiagnosticDescriptor IncorrectReturnTypeDiag = new(id:"NGRA003", + title:"Text regex binding must be partial", + messageFormat:"Method {0} must be partial to receive generated code", + defaultSeverity:DiagnosticSeverity.Error, + category:"NGRA.TextRegex", + isEnabledByDefault:true); + + private static readonly DiagnosticDescriptor NotParameterLessDiag = new(id:"NGRA002", + title:"Text regex binding must not have any parameter", + messageFormat:"Method {0} must not have any parameter", + defaultSeverity:DiagnosticSeverity.Error, + category:"NGRA.TextRegex", + isEnabledByDefault:true); + + private static readonly DiagnosticDescriptor NotPartialDiag = new(id:"NGRA001", + title:$"Returning type must be {typeof(TextRegex).FullName}", + messageFormat:$"Method {{0}} must return {typeof(TextRegex).FullName}", + defaultSeverity:DiagnosticSeverity.Error, + category:"NGRA.TextRegex", + isEnabledByDefault:true); + + } +} diff --git a/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.Source.cs b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.Source.cs new file mode 100644 index 00000000..122058b2 --- /dev/null +++ b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.Source.cs @@ -0,0 +1,116 @@ +// ----------------------------------------------------------------------------- +// file="TextRegexSourceGen.Source" +// Project: StgSharp +// Copyright (c) Nitload. +// SPDX-License-Identifier: MIT +// ----------------------------------------------------------------------------- + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StgSharp.RegularAnalysis.Text +{ + internal partial class TextRegexSourceGen + { + + private record struct SourceRecord( + + bool IsClassStatic, + bool IsMethodStatic, + string Pattern, + string MethodName, + string NamespaceName, + Accessibility MethodAccessibility, + TypeSource ContainingType + ); + + private sealed record TypeSource( + bool IsStatic, + string TypeName, + TypeKind DeclType, + Accessibility Accessibility, + TypeSource? InnerType + ); + + private struct Source + { + + public Accessibility MethodAccessibility { get; set; } + + public bool IsClassStatic { get; set; } + + public bool IsMethodStatic { get; set; } + + public string NameSpaceName { get; set; } + + public string Pattern { get; set; } + + public string MethodName { get; set; } + + public TypeSource ContainingType { get; set; } + + public readonly SourceRecord AsRecord() + { + return new SourceRecord(IsClassStatic, IsMethodStatic, Pattern, MethodName, + NameSpaceName, MethodAccessibility, ContainingType); + } + + } + + private class TextRegexBinding + { + + private readonly List _diagnostics = []; + + public bool IsValid { get; private set; } = true; + + public IReadOnlyList RegexDiagnostic => _diagnostics; + + public SourceRecord Source { get; set; } + + public void AddDiagnostic( + Diagnostic diag + ) + { + _diagnostics.Add(diag); + IsValid = IsValid && (diag.DefaultSeverity != DiagnosticSeverity.Error); + } + + public void AddDiagnostic( + DiagnosticDescriptor descriptor, + Location? location, + params object[] messageArgs + ) + { + Diagnostic diag = Diagnostic.Create(descriptor, location, messageArgs); + _diagnostics.Add(diag); + IsValid = IsValid && (diag.DefaultSeverity != DiagnosticSeverity.Error); + } + + public TextRegexBinding Clone() + { + TextRegexBinding binding = new() + { + IsValid = this.IsValid, + }; + binding._diagnostics.AddRange(this._diagnostics); + return binding; + } + + public static TextRegexBinding Invalid() + { + return new TextRegexBinding + { + IsValid = false + }; + } + + } + + } +} diff --git a/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.cs b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.cs new file mode 100644 index 00000000..848390c5 --- /dev/null +++ b/src/StgSharp.RegularAnalysis/Text/TextRegexSourceGen.cs @@ -0,0 +1,138 @@ +// ----------------------------------------------------------------------------- +// file="TextRegexSourceGen" +// Project: StgSharp +// Copyright (c) Nitload. +// SPDX-License-Identifier: MIT +// ----------------------------------------------------------------------------- + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace StgSharp.RegularAnalysis.Text +{ + internal partial class TextRegexSourceGen : IIncrementalGenerator + { + + private static readonly string TextRegexTypeName = typeof(TextRegex).FullName ?? + string.Empty; + + public void Initialize( + IncrementalGeneratorInitializationContext context + ) + { + IncrementalValuesProvider provider = context.SyntaxProvider + .ForAttributeWithMetadataName( + TextRegexTypeName, + static( + _, + _ + ) => true, BindTextRegexCandidate); + + IncrementalValuesProvider match = RegisterDiagnosticBypass(context, provider); + } + + private static TextRegexBinding BindTextRegexCandidate( + GeneratorAttributeSyntaxContext context, + CancellationToken ct + ) + { + ct.ThrowIfCancellationRequested(); + + if (context.TargetNode is not MethodDeclarationSyntax method_decl || + context.TargetSymbol is not IMethodSymbol method_symbol) { + return TextRegexBinding.Invalid(); + } + + /* + *return node is MethodDeclarationSyntax method + * && method_decl.Modifiers.Any(SyntaxKind.PartialKeyword) + * && method_decl.Modifiers.Any(SyntaxKind.StaticKeyword) + * && method_decl.ParameterList.Parameters.Count == 0 + * && method_decl.Body is null + * && method_decl.ExpressionBody is null + * && method_decl.SemicolonToken.IsKind(SyntaxKind.SemicolonToken); + */ + + TextRegexBinding bind = new(); + if (!method_decl.Modifiers.Any(SyntaxKind.PartialKeyword)) { + bind.AddDiagnostic(NotPartialDiag, method_decl.GetLocation(), method_decl.GetText()); + } + if (method_decl.ParameterList.Parameters.Count != 0) { + bind.AddDiagnostic(NotParameterLessDiag, method_decl.GetLocation(), method_decl.GetText()); + } + + + INamedTypeSymbol? expected_return_type = + context.SemanticModel.Compilation + .GetTypeByMetadataName(TextRegexTypeName + ); + ITypeSymbol ret_value = method_symbol.ReturnType; + + if (!SymbolEqualityComparer.Default.Equals(expected_return_type, ret_value)) { + bind.AddDiagnostic(IncorrectReturnTypeDiag, method_decl.ReturnType.GetLocation(), method_symbol.Name); + } + + AttributeData attribute = context.Attributes[0]; + + INamedTypeSymbol contain = method_symbol.ContainingType; + INamespaceSymbol namespace_symbol = contain.ContainingNamespace; + TypeSource type_source = null; + + while (contain is not null) + { + if (contain.IsRecord) { + bind.AddDiagnostic(CanNotBeRecordDiag, method_decl.GetLocation(), method_symbol.Name); + } + if (contain.TypeKind is not TypeKind.Class or TypeKind.Struct) { + bind.AddDiagnostic(IncorrectContainingTypeDiag, method_decl.GetLocation(), + method_symbol.Name, contain.TypeKind); + } + if (!bind.IsValid) { + return bind; + } + TypeSource t = new(contain.IsStatic, contain.Name, contain.TypeKind, + contain.DeclaredAccessibility, type_source); + type_source = t; + contain = contain.BaseType!; + } + Source s = new() + { + MethodName = method_symbol.Name, + NameSpaceName = namespace_symbol.Name, + }; + + + bind.Source = s.AsRecord(); + return bind; + } + + private static IncrementalValuesProvider RegisterDiagnosticBypass( + IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider bindings + ) + { + IncrementalValuesProvider error_path = bindings.Where(static binding => !binding.IsValid) + .SelectMany(static( + binding, + _ + ) => binding.RegexDiagnostic); + IncrementalValuesProvider correct_path = bindings.Where(static binding => binding.IsValid); + + context.RegisterSourceOutput(error_path, ( + c, + diag + ) => c.ReportDiagnostic(diag)); + + return correct_path; + } + + } +}