Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions RegExtract.SourceGenerator/RegExtract.SourceGenerator.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="all" />
</ItemGroup>

</Project>
168 changes: 168 additions & 0 deletions RegExtract.SourceGenerator/RegExtractSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -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("// <auto-generated />");
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("// <auto-generated />");
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<IFieldSymbol>()
.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<TypeDeclarationSyntax> CandidateTypes { get; } = new List<TypeDeclarationSyntax>();

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<FieldDeclarationSyntax>()
.Any(field => field.Declaration.Variables
.Any(variable => variable.Identifier.ValueText == "REGEXTRACT_REGEX_PATTERN"));

if (hasRegexPatternField)
{
CandidateTypes.Add(typeDeclaration);
}
}
}
}
}
6 changes: 6 additions & 0 deletions RegExtract.Test/RegExtract.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@
<ProjectReference Include="..\RegExtract\RegExtract.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net452'">
<ProjectReference Include="..\RegExtract.SourceGenerator\RegExtract.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
95 changes: 95 additions & 0 deletions RegExtract.Test/SourceGeneratorTest.cs
Original file line number Diff line number Diff line change
@@ -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<TestRecord>();

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}");
}
}
}
}
}
6 changes: 6 additions & 0 deletions RegExtract.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions RegExtract/RegExtract.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,10 @@ See more docs at project page: https://github.com/sblom/RegExtract
</PackageReference>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net40'">
<ProjectReference Include="..\RegExtract.SourceGenerator\RegExtract.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>