Skip to content

Commit 04da830

Browse files
authored
[.NET] Removed dependency on System.Text.Json for improved startup time (#338)
1 parent dd2435e commit 04da830

File tree

10 files changed

+218
-3930
lines changed

10 files changed

+218
-3930
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
1414
- [c] slight update to existing CMakeFiles.txt to propagate VERSION. Close #320 ([#328](https://github.com/cucumber/gherkin/pull/328))
1515
- [.NET] Improved parsing time
1616
- [.NET] Use string-ordinal comparison consistently and remove old Mono workaround
17+
- [.NET] Improved startup time
1718

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

2224
## [30.0.4] - 2024-11-15
2325
### Fixed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<LangVersion>latest</LangVersion>
6+
<Nullable>enable</Nullable>
7+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
8+
<IsRoslynComponent>true</IsRoslynComponent>
9+
<IncludeBuildOutput>false</IncludeBuildOutput>
10+
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<EmbeddedResource Include="..\..\gherkin-languages.json" Link="gherkin-languages.json" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
19+
<PrivateAssets>all</PrivateAssets>
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
</PackageReference>
22+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
23+
<!-- Use Newtonsoft because it doesn't need additional dependencies for .NET Standard 2.0 (SourceGenerator) projects -->
24+
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" GeneratePathProperty="true" PrivateAssets="all" />
25+
<PackageReference Include="PolySharp" Version="*">
26+
<PrivateAssets>all</PrivateAssets>
27+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
28+
</PackageReference>
29+
</ItemGroup>
30+
31+
<Target Name="GetDependencyTargetPaths">
32+
<!-- Manually include the DLL of each NuGet package that this analyzer uses. -->
33+
<ItemGroup>
34+
<TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />
35+
</ItemGroup>
36+
</Target>
37+
38+
</Project>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Gherkin.SourceGenerator;
2+
3+
class GherkinLanguageSetting
4+
{
5+
public string? Name { get; set; }
6+
public string? Native { get; set; }
7+
public string?[]? Feature { get; set; }
8+
public string?[]? Rule { get; set; }
9+
public string?[]? Background { get; set; }
10+
public string?[]? Scenario { get; set; }
11+
public string?[]? ScenarioOutline { get; set; }
12+
public string?[]? Examples { get; set; }
13+
public string?[]? Given { get; set; }
14+
public string?[]? When { get; set; }
15+
public string?[]? Then { get; set; }
16+
public string?[]? And { get; set; }
17+
public string?[]? But { get; set; }
18+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp.Syntax;
3+
using Microsoft.CodeAnalysis.Text;
4+
using System.Text;
5+
6+
namespace Gherkin.SourceGenerator;
7+
8+
[Generator]
9+
public class LanguageDialectGenerator : IIncrementalGenerator
10+
{
11+
const string GeneratorVersion = "1.0.0";
12+
record ClassToAddLanguageDialects(string? Namespace, string ClassName);
13+
14+
public void Initialize(IncrementalGeneratorInitializationContext context)
15+
{
16+
//System.Diagnostics.Debugger.Launch();
17+
18+
context.RegisterPostInitializationOutput(context => context.AddSource(
19+
"LanguageDialectGeneratedAttribute.g.cs",
20+
SourceText.From("""
21+
[System.AttributeUsage(System.AttributeTargets.Class)]
22+
internal sealed class LanguageDialectGeneratedAttribute : Attribute { }
23+
""", Encoding.UTF8)));
24+
25+
var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(
26+
fullyQualifiedMetadataName: "LanguageDialectGeneratedAttribute",
27+
predicate: static (syntaxNode, cancelToken) => syntaxNode is ClassDeclarationSyntax,
28+
transform: static (context, cancelToken) =>
29+
{
30+
var targetSymbol = (INamedTypeSymbol)context.TargetSymbol;
31+
var ns = targetSymbol.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted));
32+
var className = targetSymbol.Name;
33+
return new ClassToAddLanguageDialects(ns, className);
34+
});
35+
36+
context.RegisterSourceOutput(pipeline, static (context, classToAdd) =>
37+
{
38+
var allLanguageSettings = LoadLanguageSettings();
39+
40+
var sb = new StringBuilder();
41+
if (classToAdd.Namespace is not null)
42+
sb.AppendLine($"namespace {classToAdd.Namespace};");
43+
sb.AppendLine($$"""
44+
public partial class {{classToAdd.ClassName}}
45+
{
46+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Gherkin.SourceGenerator", "{{GeneratorVersion}}")]
47+
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
48+
private static GherkinDialect TryCreateGherkinDialect(string language)
49+
{
50+
switch (language)
51+
{
52+
""");
53+
foreach (var (language, methodSuffix, _) in allLanguageSettings)
54+
{
55+
sb.AppendLine($$"""
56+
case {{FormatLiteral(language)}}:
57+
return CreateGherkinDialectFor_{{methodSuffix}}();
58+
""");
59+
}
60+
61+
sb.AppendLine($$"""
62+
default:
63+
return null;
64+
}
65+
}
66+
67+
""");
68+
foreach (var (language, methodSuffix, languageSettings) in allLanguageSettings)
69+
{
70+
sb.AppendLine($$"""
71+
72+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Gherkin.SourceGenerator", "{{GeneratorVersion}}")]
73+
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
74+
private static GherkinDialect CreateGherkinDialectFor_{{methodSuffix}}()
75+
{
76+
return new GherkinDialect(
77+
{{FormatLiteral(language)}},
78+
{{FormatListLiteral(languageSettings.Feature)}},
79+
{{FormatListLiteral(languageSettings.Rule)}},
80+
{{FormatListLiteral(languageSettings.Background)}},
81+
{{FormatListLiteral(languageSettings.Scenario)}},
82+
{{FormatListLiteral(languageSettings.ScenarioOutline)}},
83+
{{FormatListLiteral(languageSettings.Examples)}},
84+
{{FormatListLiteral(languageSettings.Given)}},
85+
{{FormatListLiteral(languageSettings.When)}},
86+
{{FormatListLiteral(languageSettings.Then)}},
87+
{{FormatListLiteral(languageSettings.And)}},
88+
{{FormatListLiteral(languageSettings.But)}});
89+
}
90+
""");
91+
}
92+
93+
sb.AppendLine(@"
94+
}"
95+
);
96+
97+
context.AddSource($"GherkinDialectProvider.LanguageDialect.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
98+
});
99+
}
100+
101+
static string FormatListLiteral(IEnumerable<string?>? items)
102+
{
103+
if (items is null)
104+
return "null";
105+
bool first = true;
106+
var sb = new StringBuilder();
107+
sb.Append("[");
108+
foreach (var item in items)
109+
{
110+
if (first)
111+
first = false;
112+
else
113+
sb.Append(", ");
114+
if (item is null)
115+
sb.Append("null");
116+
else
117+
sb.Append(FormatLiteral(item));
118+
}
119+
sb.Append("]");
120+
return sb.ToString();
121+
}
122+
123+
static string FormatLiteral(string value) => Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatLiteral(value, true);
124+
125+
static List<(string Language, string MethodSuffix, GherkinLanguageSetting Settings)> LoadLanguageSettings()
126+
{
127+
const string languageFileName = "gherkin-languages.json";
128+
129+
var assembly = typeof(LanguageDialectGenerator).Assembly;
130+
var resourceStream = assembly.GetManifestResourceStream("Gherkin.SourceGenerator." + languageFileName);
131+
132+
if (resourceStream == null)
133+
throw new InvalidOperationException("Gherkin language resource not found: " + languageFileName);
134+
var languagesFileContent = new StreamReader(resourceStream).ReadToEnd();
135+
136+
var result = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, GherkinLanguageSetting>>(languagesFileContent);
137+
if (result is null)
138+
throw new InvalidOperationException("Gherkin language resource is empty: " + languageFileName);
139+
return result.OrderBy(x => x.Key).Select(x => (x.Key, x.Key.Replace("-", "_"), x.Value)).ToList();
140+
}
141+
}

dotnet/Gherkin.Specs/Gherkin.Specs.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
1818
</ItemGroup>
1919

20+
<ItemGroup>
21+
<PackageReference Include="System.Text.Json" Version="8.0.5" />
22+
</ItemGroup>
23+
2024
<ItemGroup>
2125
<ProjectReference Include="..\Gherkin\Gherkin.csproj" />
2226
</ItemGroup>

dotnet/Gherkin.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
2020
EndProject
2121
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gherkin.Benchmarks", "Gherkin.Benchmarks\Gherkin.Benchmarks.csproj", "{4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}"
2222
EndProject
23+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gherkin.SourceGenerator", "Gherkin.SourceGenerator\Gherkin.SourceGenerator.csproj", "{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}"
24+
EndProject
2325
Global
2426
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2527
Debug|Any CPU = Debug|Any CPU
@@ -38,6 +40,10 @@ Global
3840
{4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
3941
{4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
4042
{4DC5C858-3F32-44E7-8FF6-7D85A16F7FF7}.Release|Any CPU.Build.0 = Release|Any CPU
43+
{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
44+
{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
45+
{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
46+
{0DF5A047-E6CB-44FE-9A79-AB55DF5C87D6}.Release|Any CPU.Build.0 = Release|Any CPU
4147
EndGlobalSection
4248
GlobalSection(SolutionProperties) = preSolution
4349
HideSolutionNode = FALSE

dotnet/Gherkin/Gherkin.csproj

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<Product>Gherkin Parser</Product>
2020
<PackageId>Gherkin</PackageId>
2121
<Authors>Cucumber Ltd, Gaspar Nagy</Authors>
22-
<Copyright>Copyright &#xA9; Cucumber Ltd, Gaspar Nagy</Copyright>
22+
<Copyright>Copyright © Cucumber Ltd, Gaspar Nagy</Copyright>
2323
<Description>Cross-platform parser for the Gherkin language, used by Cucumber, SpecFlow and other Cucumber-based tools to parse feature files.</Description>
2424
<PackageTags>specflow gherkin cucumber</PackageTags>
2525
<PackageProjectUrl>https://github.com/cucumber/gherkin</PackageProjectUrl>
@@ -32,12 +32,8 @@
3232
<PackageOutputPath>bin/$(Configuration)/NuGet</PackageOutputPath>
3333
</PropertyGroup>
3434

35-
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
36-
<PackageReference Include="System.Text.Json" Version="8.0.5"/>
37-
</ItemGroup>
38-
3935
<ItemGroup>
40-
<EmbeddedResource Include="gherkin-languages.json"/>
36+
<ProjectReference Include="..\Gherkin.SourceGenerator\Gherkin.SourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
4137
</ItemGroup>
4238

4339
<ItemGroup>
Lines changed: 4 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
using Gherkin.Ast;
2-
using System.Text.Json;
3-
using System.Text.Json.Serialization;
42

53
namespace Gherkin;
64

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

13-
public class GherkinDialectProvider : IGherkinDialectProvider
11+
[LanguageDialectGenerated]
12+
public partial class GherkinDialectProvider : IGherkinDialectProvider
1413
{
1514
private readonly Lazy<GherkinDialect> defaultDialect;
1615

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

2726
protected virtual bool TryGetDialect(string language, Location location, out GherkinDialect dialect)
2827
{
29-
var gherkinLanguageSettings = LoadLanguageSettings();
30-
return TryGetDialect(language, gherkinLanguageSettings, location, out dialect);
28+
dialect = TryCreateGherkinDialect(language);
29+
return dialect is not null;
3130
}
3231

3332
public virtual GherkinDialect GetDialect(string language, Location location)
@@ -37,65 +36,6 @@ public virtual GherkinDialect GetDialect(string language, Location location)
3736
return dialect;
3837
}
3938

40-
protected virtual Dictionary<string, GherkinLanguageSetting> LoadLanguageSettings()
41-
{
42-
const string languageFileName = "gherkin-languages.json";
43-
44-
var assembly = typeof(GherkinDialectProvider).Assembly;
45-
var resourceStream = assembly.GetManifestResourceStream("Gherkin." + languageFileName);
46-
47-
if (resourceStream == null)
48-
throw new InvalidOperationException("Gherkin language resource not found: " + languageFileName);
49-
var languagesFileContent = new StreamReader(resourceStream).ReadToEnd();
50-
51-
return ParseJsonContent(languagesFileContent);
52-
}
53-
54-
protected virtual Dictionary<string, GherkinLanguageSetting> ParseJsonContent(string languagesFileContent)
55-
{
56-
return JsonSerializer.Deserialize<Dictionary<string, GherkinLanguageSetting>>(languagesFileContent, new JsonSerializerOptions(JsonSerializerDefaults.Web) { TypeInfoResolver = SourceGenerationContext.Default });
57-
}
58-
59-
protected virtual bool TryGetDialect(string language, Dictionary<string, GherkinLanguageSetting> gherkinLanguageSettings, Location location, out GherkinDialect dialect)
60-
{
61-
if (!gherkinLanguageSettings.TryGetValue(language, out var languageSettings))
62-
{
63-
dialect = null;
64-
return false;
65-
}
66-
67-
dialect = CreateGherkinDialect(language, languageSettings);
68-
return true;
69-
}
70-
71-
protected GherkinDialect CreateGherkinDialect(string language, GherkinLanguageSetting languageSettings)
72-
{
73-
return new GherkinDialect(
74-
language,
75-
ParseTitleKeywords(languageSettings.Feature),
76-
ParseTitleKeywords(languageSettings.Rule),
77-
ParseTitleKeywords(languageSettings.Background),
78-
ParseTitleKeywords(languageSettings.Scenario),
79-
ParseTitleKeywords(languageSettings.ScenarioOutline),
80-
ParseTitleKeywords(languageSettings.Examples),
81-
ParseStepKeywords(languageSettings.Given),
82-
ParseStepKeywords(languageSettings.When),
83-
ParseStepKeywords(languageSettings.Then),
84-
ParseStepKeywords(languageSettings.And),
85-
ParseStepKeywords(languageSettings.But)
86-
);
87-
}
88-
89-
private string[] ParseStepKeywords(string[] stepKeywords)
90-
{
91-
return stepKeywords;
92-
}
93-
94-
private string[] ParseTitleKeywords(string[] keywords)
95-
{
96-
return keywords;
97-
}
98-
9939
protected static GherkinDialect GetFactoryDefault()
10040
{
10141
return new GherkinDialect(
@@ -113,26 +53,3 @@ protected static GherkinDialect GetFactoryDefault()
11353
["* ", "But "]);
11454
}
11555
}
116-
117-
[JsonSourceGenerationOptions]
118-
[JsonSerializable(typeof(Dictionary<string, GherkinLanguageSetting>))]
119-
internal partial class SourceGenerationContext : JsonSerializerContext
120-
{
121-
}
122-
123-
public class GherkinLanguageSetting
124-
{
125-
public string Name { get; set; }
126-
public string Native { get; set; }
127-
public string[] Feature { get; set; }
128-
public string[] Rule { get; set; }
129-
public string[] Background { get; set; }
130-
public string[] Scenario { get; set; }
131-
public string[] ScenarioOutline { get; set; }
132-
public string[] Examples { get; set; }
133-
public string[] Given { get; set; }
134-
public string[] When { get; set; }
135-
public string[] Then { get; set; }
136-
public string[] And { get; set; }
137-
public string[] But { get; set; }
138-
}

0 commit comments

Comments
 (0)