diff --git a/RegExtract.SourceGenerator/RegExtract.SourceGenerator.csproj b/RegExtract.SourceGenerator/RegExtract.SourceGenerator.csproj
new file mode 100644
index 0000000..7dfe69a
--- /dev/null
+++ b/RegExtract.SourceGenerator/RegExtract.SourceGenerator.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0
+ latest
+ enable
+ false
+ false
+ true
+
+
+
+
+
+
+
+
diff --git a/RegExtract.SourceGenerator/RegExtractSourceGenerator.cs b/RegExtract.SourceGenerator/RegExtractSourceGenerator.cs
new file mode 100644
index 0000000..32116a8
--- /dev/null
+++ b/RegExtract.SourceGenerator/RegExtractSourceGenerator.cs
@@ -0,0 +1,168 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace RegExtract.SourceGenerator
+{
+ [Generator]
+ public class RegExtractSourceGenerator : ISourceGenerator
+ {
+ public void Initialize(GeneratorInitializationContext context)
+ {
+ context.RegisterForSyntaxNotifications(() => new RegExtractSyntaxReceiver());
+ }
+
+ public void Execute(GeneratorExecutionContext context)
+ {
+ // Always generate a simple test file to verify the generator is running
+ var testSource = new StringBuilder();
+ testSource.AppendLine("// ");
+ testSource.AppendLine("// This file is generated by RegExtractSourceGenerator");
+ testSource.AppendLine("namespace RegExtract.Generated");
+ testSource.AppendLine("{");
+ testSource.AppendLine(" public static class SourceGeneratorTest");
+ testSource.AppendLine(" {");
+ testSource.AppendLine(" public static string GetMessage() => \"Source generator is working!\";");
+ testSource.AppendLine(" }");
+ testSource.AppendLine("}");
+
+ context.AddSource("SourceGeneratorTest.cs", SourceText.From(testSource.ToString(), Encoding.UTF8));
+
+ if (context.SyntaxReceiver is not RegExtractSyntaxReceiver receiver)
+ return;
+
+ var sourceBuilder = new StringBuilder();
+ sourceBuilder.AppendLine("// ");
+ sourceBuilder.AppendLine("// Debug: Source generator executed");
+ sourceBuilder.AppendLine($"// Debug: Found {receiver.CandidateTypes.Count} candidate types");
+ sourceBuilder.AppendLine("using System;");
+ sourceBuilder.AppendLine("using System.Text.RegularExpressions;");
+ sourceBuilder.AppendLine("using RegExtract;");
+ sourceBuilder.AppendLine();
+ sourceBuilder.AppendLine("namespace RegExtract.Generated");
+ sourceBuilder.AppendLine("{");
+
+ bool hasGeneratedAnyCode = false;
+
+ foreach (var candidateType in receiver.CandidateTypes)
+ {
+ sourceBuilder.AppendLine($" // Debug: Processing type {candidateType.Identifier.ValueText}");
+
+ var semanticModel = context.Compilation.GetSemanticModel(candidateType.SyntaxTree);
+ var typeSymbol = semanticModel.GetDeclaredSymbol(candidateType);
+
+ if (typeSymbol is null)
+ {
+ sourceBuilder.AppendLine($" // Debug: Type symbol is null for {candidateType.Identifier.ValueText}");
+ continue;
+ }
+
+ sourceBuilder.AppendLine($" // Debug: Type symbol found: {typeSymbol.Name}");
+ sourceBuilder.AppendLine($" // Debug: Members count: {typeSymbol.GetMembers().Length}");
+
+ foreach (var member in typeSymbol.GetMembers())
+ {
+ sourceBuilder.AppendLine($" // Debug: Member: {member.Name} ({member.Kind})");
+ if (member is IFieldSymbol field)
+ {
+ sourceBuilder.AppendLine($" // Field details: IsConst={field.IsConst}, IsStatic={field.IsStatic}, Access={field.DeclaredAccessibility}, Type={field.Type.Name}");
+ }
+ }
+
+ // Find the REGEXTRACT_REGEX_PATTERN field
+ var regexPatternField = typeSymbol.GetMembers("REGEXTRACT_REGEX_PATTERN")
+ .OfType()
+ .FirstOrDefault(f => f.IsConst && f.DeclaredAccessibility == Accessibility.Public && f.Type.SpecialType == SpecialType.System_String);
+
+ if (regexPatternField is null)
+ {
+ sourceBuilder.AppendLine($" // Debug: No REGEXTRACT_REGEX_PATTERN field found for {typeSymbol.Name}");
+ continue;
+ }
+
+ sourceBuilder.AppendLine($" // Debug: Pattern field found: {regexPatternField.Name}");
+
+ var regexPattern = GetConstantValue(regexPatternField);
+ if (regexPattern is null)
+ {
+ sourceBuilder.AppendLine($" // Debug: Pattern value is null for {typeSymbol.Name}");
+ continue;
+ }
+
+ sourceBuilder.AppendLine($" // Debug: Pattern value: {regexPattern}");
+
+ // Generate the extraction plan for this type
+ GenerateExtractionPlanForType(sourceBuilder, typeSymbol, regexPattern);
+ hasGeneratedAnyCode = true;
+ }
+
+ sourceBuilder.AppendLine("}");
+
+ // Always generate the file so we can see debug output
+ context.AddSource("RegExtractGenerated.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
+ }
+
+ private string? GetConstantValue(IFieldSymbol field)
+ {
+ if (field.HasConstantValue && field.ConstantValue is string value)
+ return value;
+ return null;
+ }
+
+ private void GenerateExtractionPlanForType(StringBuilder sourceBuilder, ITypeSymbol typeSymbol, string regexPattern)
+ {
+ var typeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ var simpleTypeName = typeSymbol.Name;
+
+ sourceBuilder.AppendLine($" public static class {simpleTypeName}ExtractionPlan");
+ sourceBuilder.AppendLine(" {");
+ sourceBuilder.AppendLine($" private static readonly Regex _regex = new Regex(@\"{EscapeRegexPattern(regexPattern)}\");");
+ sourceBuilder.AppendLine($" private static readonly ExtractionPlan<{typeName}> _plan = ExtractionPlan<{typeName}>.CreatePlan(_regex);");
+ sourceBuilder.AppendLine();
+ sourceBuilder.AppendLine($" public static {typeName}? Extract(string input)");
+ sourceBuilder.AppendLine(" {");
+ sourceBuilder.AppendLine(" return _plan.Extract(input);");
+ sourceBuilder.AppendLine(" }");
+ sourceBuilder.AppendLine();
+ sourceBuilder.AppendLine($" public static {typeName}? Extract(Match match)");
+ sourceBuilder.AppendLine(" {");
+ sourceBuilder.AppendLine(" return _plan.Extract(match);");
+ sourceBuilder.AppendLine(" }");
+ sourceBuilder.AppendLine();
+ sourceBuilder.AppendLine($" public static ExtractionPlan<{typeName}> Plan => _plan;");
+ sourceBuilder.AppendLine(" }");
+ sourceBuilder.AppendLine();
+ }
+
+ private string EscapeRegexPattern(string pattern)
+ {
+ return pattern.Replace("\"", "\"\"");
+ }
+ }
+
+ internal class RegExtractSyntaxReceiver : ISyntaxReceiver
+ {
+ public List CandidateTypes { get; } = new List();
+
+ public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
+ {
+ if (syntaxNode is TypeDeclarationSyntax typeDeclaration)
+ {
+ // Check if this type has a REGEXTRACT_REGEX_PATTERN field
+ var hasRegexPatternField = typeDeclaration.Members
+ .OfType()
+ .Any(field => field.Declaration.Variables
+ .Any(variable => variable.Identifier.ValueText == "REGEXTRACT_REGEX_PATTERN"));
+
+ if (hasRegexPatternField)
+ {
+ CandidateTypes.Add(typeDeclaration);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/RegExtract.Test/RegExtract.Test.csproj b/RegExtract.Test/RegExtract.Test.csproj
index 67540f9..d5bca36 100644
--- a/RegExtract.Test/RegExtract.Test.csproj
+++ b/RegExtract.Test/RegExtract.Test.csproj
@@ -25,4 +25,10 @@
+
+
+
+
diff --git a/RegExtract.Test/SourceGeneratorTest.cs b/RegExtract.Test/SourceGeneratorTest.cs
new file mode 100644
index 0000000..cd4ac90
--- /dev/null
+++ b/RegExtract.Test/SourceGeneratorTest.cs
@@ -0,0 +1,95 @@
+using System.Linq;
+using System.Text.RegularExpressions;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace RegExtract.Test
+{
+ public class SourceGeneratorTest
+ {
+ private readonly ITestOutputHelper output;
+
+ public SourceGeneratorTest(ITestOutputHelper output)
+ {
+ this.output = output;
+ }
+
+ public record TestRecord(int Number, string Text)
+ {
+ public const string REGEXTRACT_REGEX_PATTERN = @"(\d+): (.+)";
+ }
+
+ [Fact]
+ public void SourceGeneratorShouldGenerateExtractionPlan()
+ {
+ // This test will check if the source generator created the extraction plan
+ var input = "42: Hello World";
+
+ // Try to use the generated extraction plan (if it exists)
+ // For now, we'll use the regular extraction to verify the pattern works
+ var result = input.Extract();
+
+ Assert.NotNull(result);
+ Assert.Equal(42, result.Number);
+ Assert.Equal("Hello World", result.Text);
+
+ output.WriteLine($"Extracted: {result}");
+ }
+
+ [Fact]
+ public void CheckIfGeneratedCodeExists()
+ {
+ // First check if the simple test class exists to verify the source generator is running
+ var simpleTestType = typeof(TestRecord).Assembly.GetType("RegExtract.Generated.SourceGeneratorTest");
+
+ if (simpleTestType != null)
+ {
+ output.WriteLine("Source generator is working - found SourceGeneratorTest class");
+
+ var getMessageMethod = simpleTestType.GetMethod("GetMessage");
+ if (getMessageMethod != null)
+ {
+ var message = getMessageMethod.Invoke(null, null);
+ output.WriteLine($"Message from generated code: {message}");
+ }
+ }
+ else
+ {
+ output.WriteLine("SourceGeneratorTest class not found - source generator may not be running");
+ }
+
+ // Now check if the TestRecord extraction plan exists
+ var generatedType = typeof(TestRecord).Assembly.GetType("RegExtract.Generated.TestRecordExtractionPlan");
+
+ if (generatedType != null)
+ {
+ output.WriteLine($"Generated type found: {generatedType.FullName}");
+
+ // Try to get the static Extract method
+ var extractMethod = generatedType.GetMethod("Extract", new[] { typeof(string) });
+ Assert.NotNull(extractMethod);
+
+ // Try to call the generated extraction method
+ var result = extractMethod.Invoke(null, new object[] { "42: Hello World" });
+ Assert.NotNull(result);
+
+ output.WriteLine($"Generated extraction result: {result}");
+ }
+ else
+ {
+ output.WriteLine("Generated TestRecordExtractionPlan not found - may need debugging");
+
+ // List all types in the assembly that might be generated
+ var generatedTypes = typeof(TestRecord).Assembly.GetTypes()
+ .Where(t => t.Namespace == "RegExtract.Generated")
+ .ToArray();
+
+ output.WriteLine($"Found {generatedTypes.Length} types in RegExtract.Generated namespace:");
+ foreach (var type in generatedTypes)
+ {
+ output.WriteLine($" - {type.FullName}");
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/RegExtract.sln b/RegExtract.sln
index 416e219..edc84d4 100644
--- a/RegExtract.sln
+++ b/RegExtract.sln
@@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegExtract", "RegExtract\Re
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegExtract.Test", "RegExtract.Test\RegExtract.Test.csproj", "{836F68E6-27F6-417F-B216-3B48BBE34862}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegExtract.SourceGenerator", "RegExtract.SourceGenerator\RegExtract.SourceGenerator.csproj", "{1B72EB5D-AF1E-4D51-ADEA-6C59DEA48276}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
{836F68E6-27F6-417F-B216-3B48BBE34862}.Debug|Any CPU.Build.0 = Debug|Any CPU
{836F68E6-27F6-417F-B216-3B48BBE34862}.Release|Any CPU.ActiveCfg = Release|Any CPU
{836F68E6-27F6-417F-B216-3B48BBE34862}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1B72EB5D-AF1E-4D51-ADEA-6C59DEA48276}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1B72EB5D-AF1E-4D51-ADEA-6C59DEA48276}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1B72EB5D-AF1E-4D51-ADEA-6C59DEA48276}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1B72EB5D-AF1E-4D51-ADEA-6C59DEA48276}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/RegExtract/RegExtract.csproj b/RegExtract/RegExtract.csproj
index bd4e0b0..1532050 100644
--- a/RegExtract/RegExtract.csproj
+++ b/RegExtract/RegExtract.csproj
@@ -83,4 +83,10 @@ See more docs at project page: https://github.com/sblom/RegExtract
+
+
+
+