Skip to content

Commit

Permalink
[.NET] Removed dependency on System.Text.Json for improved startup ti…
Browse files Browse the repository at this point in the history
…me (#338)
  • Loading branch information
obligaron authored Dec 24, 2024
1 parent dd2435e commit 04da830
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 3,930 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
- [c] slight update to existing CMakeFiles.txt to propagate VERSION. Close #320 ([#328](https://github.com/cucumber/gherkin/pull/328))
- [.NET] Improved parsing time
- [.NET] Use string-ordinal comparison consistently and remove old Mono workaround
- [.NET] Improved startup time

### Changed
- [cpp] add generic support for ABI versioning with VERSION ([#328](https://github.com/cucumber/gherkin/pull/328))
- [cpp] namespace was changed to 'cucumber::gherkin' to better reflect project structure and prevent clashing
- [.NET] Removed dependency on System.Text.Json and related logic in GherkinDialectProvider

## [30.0.4] - 2024-11-15
### Fixed
Expand Down
38 changes: 38 additions & 0 deletions dotnet/Gherkin.SourceGenerator/Gherkin.SourceGenerator.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>

<ItemGroup>
<EmbeddedResource Include="..\..\gherkin-languages.json" Link="gherkin-languages.json" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<!-- Use Newtonsoft because it doesn't need additional dependencies for .NET Standard 2.0 (SourceGenerator) projects -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="PolySharp" Version="*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<Target Name="GetDependencyTargetPaths">
<!-- Manually include the DLL of each NuGet package that this analyzer uses. -->
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>

</Project>
18 changes: 18 additions & 0 deletions dotnet/Gherkin.SourceGenerator/GherkinLanguageSetting.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Gherkin.SourceGenerator;

class GherkinLanguageSetting
{
public string? Name { get; set; }
public string? Native { get; set; }
public string?[]? Feature { get; set; }
public string?[]? Rule { get; set; }
public string?[]? Background { get; set; }
public string?[]? Scenario { get; set; }
public string?[]? ScenarioOutline { get; set; }
public string?[]? Examples { get; set; }
public string?[]? Given { get; set; }
public string?[]? When { get; set; }
public string?[]? Then { get; set; }
public string?[]? And { get; set; }
public string?[]? But { get; set; }
}
141 changes: 141 additions & 0 deletions dotnet/Gherkin.SourceGenerator/LanguageDialectGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace Gherkin.SourceGenerator;

[Generator]
public class LanguageDialectGenerator : IIncrementalGenerator
{
const string GeneratorVersion = "1.0.0";
record ClassToAddLanguageDialects(string? Namespace, string ClassName);

public void Initialize(IncrementalGeneratorInitializationContext context)
{
//System.Diagnostics.Debugger.Launch();

context.RegisterPostInitializationOutput(context => context.AddSource(
"LanguageDialectGeneratedAttribute.g.cs",
SourceText.From("""
[System.AttributeUsage(System.AttributeTargets.Class)]
internal sealed class LanguageDialectGeneratedAttribute : Attribute { }
""", Encoding.UTF8)));

var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "LanguageDialectGeneratedAttribute",
predicate: static (syntaxNode, cancelToken) => syntaxNode is ClassDeclarationSyntax,
transform: static (context, cancelToken) =>
{
var targetSymbol = (INamedTypeSymbol)context.TargetSymbol;
var ns = targetSymbol.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted));
var className = targetSymbol.Name;
return new ClassToAddLanguageDialects(ns, className);
});

context.RegisterSourceOutput(pipeline, static (context, classToAdd) =>
{
var allLanguageSettings = LoadLanguageSettings();

var sb = new StringBuilder();
if (classToAdd.Namespace is not null)
sb.AppendLine($"namespace {classToAdd.Namespace};");
sb.AppendLine($$"""
public partial class {{classToAdd.ClassName}}
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Gherkin.SourceGenerator", "{{GeneratorVersion}}")]
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private static GherkinDialect TryCreateGherkinDialect(string language)
{
switch (language)
{
""");
foreach (var (language, methodSuffix, _) in allLanguageSettings)
{
sb.AppendLine($$"""
case {{FormatLiteral(language)}}:
return CreateGherkinDialectFor_{{methodSuffix}}();
""");
}

sb.AppendLine($$"""
default:
return null;
}
}
""");
foreach (var (language, methodSuffix, languageSettings) in allLanguageSettings)
{
sb.AppendLine($$"""
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Gherkin.SourceGenerator", "{{GeneratorVersion}}")]
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private static GherkinDialect CreateGherkinDialectFor_{{methodSuffix}}()
{
return new GherkinDialect(
{{FormatLiteral(language)}},
{{FormatListLiteral(languageSettings.Feature)}},
{{FormatListLiteral(languageSettings.Rule)}},
{{FormatListLiteral(languageSettings.Background)}},
{{FormatListLiteral(languageSettings.Scenario)}},
{{FormatListLiteral(languageSettings.ScenarioOutline)}},
{{FormatListLiteral(languageSettings.Examples)}},
{{FormatListLiteral(languageSettings.Given)}},
{{FormatListLiteral(languageSettings.When)}},
{{FormatListLiteral(languageSettings.Then)}},
{{FormatListLiteral(languageSettings.And)}},
{{FormatListLiteral(languageSettings.But)}});
}
""");
}

sb.AppendLine(@"
}"
);

context.AddSource($"GherkinDialectProvider.LanguageDialect.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
});
}

static string FormatListLiteral(IEnumerable<string?>? items)
{
if (items is null)
return "null";
bool first = true;
var sb = new StringBuilder();
sb.Append("[");
foreach (var item in items)
{
if (first)
first = false;
else
sb.Append(", ");
if (item is null)
sb.Append("null");
else
sb.Append(FormatLiteral(item));
}
sb.Append("]");
return sb.ToString();
}

static string FormatLiteral(string value) => Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatLiteral(value, true);

static List<(string Language, string MethodSuffix, GherkinLanguageSetting Settings)> LoadLanguageSettings()
{
const string languageFileName = "gherkin-languages.json";

var assembly = typeof(LanguageDialectGenerator).Assembly;
var resourceStream = assembly.GetManifestResourceStream("Gherkin.SourceGenerator." + languageFileName);

if (resourceStream == null)
throw new InvalidOperationException("Gherkin language resource not found: " + languageFileName);
var languagesFileContent = new StreamReader(resourceStream).ReadToEnd();

var result = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, GherkinLanguageSetting>>(languagesFileContent);
if (result is null)
throw new InvalidOperationException("Gherkin language resource is empty: " + languageFileName);
return result.OrderBy(x => x.Key).Select(x => (x.Key, x.Key.Replace("-", "_"), x.Value)).ToList();
}
}
4 changes: 4 additions & 0 deletions dotnet/Gherkin.Specs/Gherkin.Specs.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Gherkin\Gherkin.csproj" />
</ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions dotnet/Gherkin.sln
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gherkin.Benchmarks", "Gherkin.Benchmarks\Gherkin.Benchmarks.csproj", "{4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gherkin.SourceGenerator", "Gherkin.SourceGenerator\Gherkin.SourceGenerator.csproj", "{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -38,6 +40,10 @@ Global
{4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Release|Any CPU.Build.0 = Release|Any CPU
{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
8 changes: 2 additions & 6 deletions dotnet/Gherkin/Gherkin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<Product>Gherkin Parser</Product>
<PackageId>Gherkin</PackageId>
<Authors>Cucumber Ltd, Gaspar Nagy</Authors>
<Copyright>Copyright &#xA9; Cucumber Ltd, Gaspar Nagy</Copyright>
<Copyright>Copyright © Cucumber Ltd, Gaspar Nagy</Copyright>
<Description>Cross-platform parser for the Gherkin language, used by Cucumber, SpecFlow and other Cucumber-based tools to parse feature files.</Description>
<PackageTags>specflow gherkin cucumber</PackageTags>
<PackageProjectUrl>https://github.com/cucumber/gherkin</PackageProjectUrl>
Expand All @@ -32,12 +32,8 @@
<PackageOutputPath>bin/$(Configuration)/NuGet</PackageOutputPath>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Text.Json" Version="8.0.5"/>
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="gherkin-languages.json"/>
<ProjectReference Include="..\Gherkin.SourceGenerator\Gherkin.SourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
Expand Down
91 changes: 4 additions & 87 deletions dotnet/Gherkin/GherkinDialectProvider.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Gherkin.Ast;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Gherkin;

Expand All @@ -10,7 +8,8 @@ public interface IGherkinDialectProvider
GherkinDialect GetDialect(string language, Location location);
}

public class GherkinDialectProvider : IGherkinDialectProvider
[LanguageDialectGenerated]
public partial class GherkinDialectProvider : IGherkinDialectProvider
{
private readonly Lazy<GherkinDialect> defaultDialect;

Expand All @@ -26,8 +25,8 @@ public GherkinDialectProvider(string defaultLanguage = "en")

protected virtual bool TryGetDialect(string language, Location location, out GherkinDialect dialect)
{
var gherkinLanguageSettings = LoadLanguageSettings();
return TryGetDialect(language, gherkinLanguageSettings, location, out dialect);
dialect = TryCreateGherkinDialect(language);
return dialect is not null;
}

public virtual GherkinDialect GetDialect(string language, Location location)
Expand All @@ -37,65 +36,6 @@ public virtual GherkinDialect GetDialect(string language, Location location)
return dialect;
}

protected virtual Dictionary<string, GherkinLanguageSetting> LoadLanguageSettings()
{
const string languageFileName = "gherkin-languages.json";

var assembly = typeof(GherkinDialectProvider).Assembly;
var resourceStream = assembly.GetManifestResourceStream("Gherkin." + languageFileName);

if (resourceStream == null)
throw new InvalidOperationException("Gherkin language resource not found: " + languageFileName);
var languagesFileContent = new StreamReader(resourceStream).ReadToEnd();

return ParseJsonContent(languagesFileContent);
}

protected virtual Dictionary<string, GherkinLanguageSetting> ParseJsonContent(string languagesFileContent)
{
return JsonSerializer.Deserialize<Dictionary<string, GherkinLanguageSetting>>(languagesFileContent, new JsonSerializerOptions(JsonSerializerDefaults.Web) { TypeInfoResolver = SourceGenerationContext.Default });
}

protected virtual bool TryGetDialect(string language, Dictionary<string, GherkinLanguageSetting> gherkinLanguageSettings, Location location, out GherkinDialect dialect)
{
if (!gherkinLanguageSettings.TryGetValue(language, out var languageSettings))
{
dialect = null;
return false;
}

dialect = CreateGherkinDialect(language, languageSettings);
return true;
}

protected GherkinDialect CreateGherkinDialect(string language, GherkinLanguageSetting languageSettings)
{
return new GherkinDialect(
language,
ParseTitleKeywords(languageSettings.Feature),
ParseTitleKeywords(languageSettings.Rule),
ParseTitleKeywords(languageSettings.Background),
ParseTitleKeywords(languageSettings.Scenario),
ParseTitleKeywords(languageSettings.ScenarioOutline),
ParseTitleKeywords(languageSettings.Examples),
ParseStepKeywords(languageSettings.Given),
ParseStepKeywords(languageSettings.When),
ParseStepKeywords(languageSettings.Then),
ParseStepKeywords(languageSettings.And),
ParseStepKeywords(languageSettings.But)
);
}

private string[] ParseStepKeywords(string[] stepKeywords)
{
return stepKeywords;
}

private string[] ParseTitleKeywords(string[] keywords)
{
return keywords;
}

protected static GherkinDialect GetFactoryDefault()
{
return new GherkinDialect(
Expand All @@ -113,26 +53,3 @@ protected static GherkinDialect GetFactoryDefault()
["* ", "But "]);
}
}

[JsonSourceGenerationOptions]
[JsonSerializable(typeof(Dictionary<string, GherkinLanguageSetting>))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}

public class GherkinLanguageSetting
{
public string Name { get; set; }
public string Native { get; set; }
public string[] Feature { get; set; }
public string[] Rule { get; set; }
public string[] Background { get; set; }
public string[] Scenario { get; set; }
public string[] ScenarioOutline { get; set; }
public string[] Examples { get; set; }
public string[] Given { get; set; }
public string[] When { get; set; }
public string[] Then { get; set; }
public string[] And { get; set; }
public string[] But { get; set; }
}
Loading

0 comments on commit 04da830

Please sign in to comment.