From 6e6577e760961a66cac327c9c9be7c483c84d2e7 Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Sun, 10 Mar 2024 11:05:27 +0100 Subject: [PATCH 01/12] Added ConstantFromJson for https://github.com/Lombiq/Helpful-Libraries/issues/238 --- .../Examples.cs | 16 ++ ...ulLibraries.SourceGenerators.Sample.csproj | 17 ++ .../package.json | 23 ++ .../ConstantFromJsonGenerator.cs | 259 ++++++++++++++++++ .../License.md | 13 + ...q.HelpfulLibraries.SourceGenerators.csproj | 27 ++ .../NuGetIcon.png | Bin 0 -> 4657 bytes .../Properties/launchSettings.json | 9 + .../Readme.md | 51 ++++ Lombiq.HelpfulLibraries.sln | 12 + 10 files changed, 427 insertions(+) create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/package.json create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/License.md create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/NuGetIcon.png create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs new file mode 100644 index 00000000..9357c7cd --- /dev/null +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs @@ -0,0 +1,16 @@ +using Generators; +using System; + +namespace Lombiq.HelpfulLibraries.SourceGenerators.Sample; + +[ConstantFromJson("GulpUglifyVersion", "package.json", "gulp-uglify")] +[ConstantFromJson("GulpVersion", "package.json", "gulp")] +public partial class Examples +{ + // Show usage of the generated constants + public void LogVersions() + { + Console.WriteLine(GulpUglifyVersion); + Console.WriteLine(GulpVersion); + } +} diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj new file mode 100644 index 00000000..cc00a341 --- /dev/null +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + Lombiq.HelpfulLibraries.SourceGenerators.Sample + + + + + + + + + + + diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/package.json b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/package.json new file mode 100644 index 00000000..9d038ca3 --- /dev/null +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/package.json @@ -0,0 +1,23 @@ +{ + "private": true, + "devDependencies": { + "fs": "0.0.2", + "glob": "5.0.15", + "path-posix": "1.0.0", + "merge-stream": "1.0.0", + "gulp-if": "2.0.0", + "gulp": "3.9.0", + "gulp-newer": "0.5.1", + "gulp-plumber": "1.0.1", + "gulp-sourcemaps": "1.6.0", + "gulp-less": "3.0.3", + "gulp-autoprefixer": "2.2.0", + "gulp-minify-css": "1.2.1", + "gulp-typescript": "2.9.2", + "gulp-uglify": "1.4.1", + "gulp-rename": "1.2.2", + "gulp-concat": "2.6.0", + "gulp-header": "1.7.1" + }, + "dependencies": { } +} \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs new file mode 100644 index 00000000..8dd20eab --- /dev/null +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs @@ -0,0 +1,259 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using System.Text.Json; + +namespace Lombiq.HelpfulLibraries.SourceGenerators; + +/// +/// A generator that exposes a value from a JSON file at compile time. +/// The target class should be annotated with the 'Generators.ConstantFromJsonAttribute' attribute. +/// +[Generator] +public class ConstantFromJsonGenerator : IIncrementalGenerator +{ + private const string Namespace = "Generators"; + private const string AttributeName = "ConstantFromJsonAttribute"; + + private const string AttributeSourceCode = $@"// + +namespace {Namespace} +{{ + [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] + public class {AttributeName} : System.Attribute + {{ + public string Value {{ get; }} + public {AttributeName}(string constantName, string fileName, string jsonPath) + {{ + Value = ""testvaluetje""; + }} + }} +}}"; + + private readonly Dictionary _fileContents = []; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Add the marker attribute to the compilation. + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + $"{AttributeName}.g.cs", + SourceText.From(AttributeSourceCode, Encoding.UTF8))); + + // Filter classes annotated with the [ConstantFromJson] attribute. + // Only filtered Syntax Nodes can trigger code generation. + var provider = context.SyntaxProvider + .CreateSyntaxProvider( + (s, _) => s is ClassDeclarationSyntax, + (ctx, _) => GetClassDeclarationForSourceGen(ctx)) + .Where(t => t.reportAttributeFound) + .Select((t, _) => (t.Item1, t.Item3)); + + var additionalFiles = context.AdditionalTextsProvider + .Where(static file => file.Path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)); + + var namesAndContents = additionalFiles + .Select((file, cancellationToken) => + (Content: file.GetText(cancellationToken)?.ToString(), + file.Path)); + + context.RegisterSourceOutput(namesAndContents.Collect(), (_, contents) => + { + foreach ((string? content, string path) in contents) + { + // Add to the dictionary + _fileContents.Add(path, content ?? string.Empty); + } + }); + + // Generate the source code. + context.RegisterSourceOutput( + context.CompilationProvider.Combine(provider.Collect()), + (ctx, t) => GenerateCode(ctx, t.Left, t.Right)); + } + + /// + /// Checks whether the Node is annotated with the [ConstantFromJson] attribute and maps syntax context to + /// the specific node type (ClassDeclarationSyntax). + /// + /// Syntax context, based on CreateSyntaxProvider predicate + /// The specific cast and whether the attribute was found. + private static (ClassDeclarationSyntax, bool reportAttributeFound, List>) + GetClassDeclarationForSourceGen( + GeneratorSyntaxContext context) + { + var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; + var attributesData = new List>(); + + // Go through all attributes of the class. + foreach (var attributeListSyntax in classDeclarationSyntax.AttributeLists) + foreach (var attributeSyntax in attributeListSyntax.Attributes) + { + if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) + { + continue; // if we can't get the symbol, ignore it + } + + string attributeName = attributeSymbol.ContainingType.ToDisplayString(); + // Check the full name of the [ConstantFromJson] attribute. + if (attributeName != $"{Namespace}.{AttributeName}") + { + continue; + } + + var arguments = new Dictionary(); + int idx = 0; + foreach (var argumentSyntax in attributeSyntax.ArgumentList?.Arguments!) + { + if (argumentSyntax.Expression is LiteralExpressionSyntax literalExpression) + arguments.Add(attributeSymbol.Parameters[idx].Name, literalExpression.Token.Text); + + idx += 1; + } + + attributesData.Add(arguments); + } + + return (classDeclarationSyntax, attributesData.Count > 0, attributesData); + } + + /// + /// Generate code action. + /// It will be executed on specific nodes (ClassDeclarationSyntax annotated with the [ConstantFromJson] attribute) + /// changed by the user. + /// + /// Source generation context used to add source files. + /// Compilation used to provide access to the Semantic Model. + /// + /// Nodes annotated with the [ConstantFromJson] attribute that trigger the + /// generate action. + /// + private void GenerateCode(SourceProductionContext context, Compilation compilation, + ImmutableArray<(ClassDeclarationSyntax, List>)> classDeclarations) + { + // Go through all filtered class declarations. + foreach (var (classDeclarationSyntax, attributeData) in classDeclarations) + { + // We need to get semantic model of the class to retrieve metadata. + var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree); + + // Symbols allow us to get the compile-time information. + if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax, cancellationToken: context.CancellationToken) + is not INamedTypeSymbol classSymbol) + { + continue; + } + + string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + + // 'Identifier' means the token of the node. Get class name from the syntax node. + string className = classDeclarationSyntax.Identifier.Text; + + string partialBody = string.Empty; + + // It's possible that a single class is annotated with our marker attribute multiple times + foreach (var dictionary in attributeData) + { + // Get values from dictionary + string? constantName = dictionary["constantName"]; + string? fileName = dictionary["fileName"]; + string? jsonPath = dictionary["jsonPath"]; + + // Try get content of file from dictionary where key ends with filename + var fileContent = _fileContents + .FirstOrDefault(kvp => + kvp.Key.EndsWith(fileName.Replace($"\"", string.Empty), StringComparison.Ordinal)); + + // If the file content is empty, skip + if (string.IsNullOrEmpty(fileContent.Value)) + { + return; + } + + var jsonDocument = JsonDocument.Parse(fileContent.Value); + + // try to find the value in the jsonDocument + var jsonValue = FindProperty(jsonDocument.RootElement, jsonPath.Replace("\"", "")); + + if (jsonValue == null) + { + return; + } + + partialBody += $""" + public const string {constantName.Replace("\"", "")} = "{jsonValue.Value}"; + """; + } + + // Create a new partial class with the same name as the original class. + // Build up the source code + string code = $@"// + +using System; +using System.Collections.Generic; + +namespace {namespaceName}; + +partial class {className} +{{ + {partialBody} +}} +"; + // Add the source code to the compilation. + context.AddSource($"{className}.g.cs", SourceText.From(code, Encoding.UTF8)); + } + } + + /// + /// Find a property in a JSON document recursively. + /// + /// The JSON element to search in. + /// The property name to look for + private static JsonElement? FindProperty(JsonElement element, string propertyName) + { + foreach (var property in element.EnumerateObject()) + { + if (property.Name == propertyName) + { + return property.Value; + } + + switch (property.Value.ValueKind) + { + case JsonValueKind.Object: + { + var result = FindProperty(property.Value, propertyName); + if (result != null) + { + return result; + } + + break; + } + + case JsonValueKind.Array: + { + foreach (var arrayElement in property.Value.EnumerateArray()) + { + if (arrayElement.ValueKind == JsonValueKind.Object) + { + var result = FindProperty(arrayElement, propertyName); + if (result != null) + { + return result; + } + } + } + + break; + } + } + } + + return null; + } +} diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/License.md b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/License.md new file mode 100644 index 00000000..f516dbf7 --- /dev/null +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/License.md @@ -0,0 +1,13 @@ +Copyright © 2011, [Lombiq Technologies Ltd.](https://lombiq.com) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +- Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj new file mode 100644 index 00000000..133e66a9 --- /dev/null +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + false + enable + latest + + true + true + + Lombiq.HelpfulLibraries.SourceGenerators + Lombiq.HelpfulLibraries.SourceGenerators + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/NuGetIcon.png b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/NuGetIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..162a00508d8041833604d427216238c1b23b2d47 GIT binary patch literal 4657 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 literal 0 HcmV?d00001 diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json new file mode 100644 index 00000000..30b5e4c6 --- /dev/null +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "DebugRoslynSourceGenerator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj" + } + } +} \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md new file mode 100644 index 00000000..3bee310d --- /dev/null +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md @@ -0,0 +1,51 @@ +# Lombiq.HelpfulLibraries.SourceGenerators +A collection of helpful source generators. +**When using one of the generators you must run a build before errors will go away** + +- [ConstantFromJsonGenerator.cs](ConstantFromJsonGenerator.cs): A source generator that creates a constant from a JSON file. + +### Lombiq.HelpfulLibraries.SourceGenerators.Sample +A project that references source generators. It is used to test and showcase the source generators. + +### Lombiq.HelpfulLibraries.SourceGenerators.Tests +Unit tests for source generators. + +## How To? +### How to use the `ConstantFromJsonGenerator`? + +1. Add a JSON file to your project. +2. Set the `Build Action` of the JSON file to `AdditionalFiles` for example: + ```xml + + + + ``` +3. Wherever you want to use the JSON file, make sure to use a `partial class` and add the `ConstantFromJsonGenerator` attribute to it. + Where the first parameter is the name of the constant and the second parameter is the path to the JSON file, the last parameter is the name or 'key' for the value we are looking for. + ```csharp + [ConstantFromJson("GulpVersion", "package.json", "gulp")] + public partial class YourClass + { + + } + ``` +4. Run a build and the constant will be generated . +5. Use the constant in your code, full example: + ```csharp + using System; + using Generators; + + namespace Lombiq.HelpfulLibraries.SourceGenerators.Sample; + + [ConstantFromJson("GulpUglifyVersion", "package.json", "gulp-uglify")] + [ConstantFromJson("GulpVersion", "package.json", "gulp")] + public partial class Examples + { + // Show usage of the generated constants + public void LogVersions() + { + Console.WriteLine(GulpUglifyVersion); + Console.WriteLine(GulpVersion); + } + } + ``` \ No newline at end of file diff --git a/Lombiq.HelpfulLibraries.sln b/Lombiq.HelpfulLibraries.sln index 0d83cdf7..da10c232 100644 --- a/Lombiq.HelpfulLibraries.sln +++ b/Lombiq.HelpfulLibraries.sln @@ -21,6 +21,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.HelpfulLibraries.Cli EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.HelpfulLibraries.Refit", "Lombiq.HelpfulLibraries.Refit\Lombiq.HelpfulLibraries.Refit.csproj", "{5DC1A3D5-0626-4258-95C6-7E5CA5495A80}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.HelpfulLibraries.SourceGenerators", "Lombiq.HelpfulLibraries.SourceGenerators\Lombiq.HelpfulLibraries.SourceGenerators\Lombiq.HelpfulLibraries.SourceGenerators.csproj", "{FB53DB02-5C86-44C9-A88E-294B95C1F979}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.HelpfulLibraries.SourceGenerators.Sample", "Lombiq.HelpfulLibraries.SourceGenerators\Lombiq.HelpfulLibraries.SourceGenerators.Sample\Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj", "{DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +67,14 @@ Global {5DC1A3D5-0626-4258-95C6-7E5CA5495A80}.Debug|Any CPU.Build.0 = Debug|Any CPU {5DC1A3D5-0626-4258-95C6-7E5CA5495A80}.Release|Any CPU.ActiveCfg = Release|Any CPU {5DC1A3D5-0626-4258-95C6-7E5CA5495A80}.Release|Any CPU.Build.0 = Release|Any CPU + {FB53DB02-5C86-44C9-A88E-294B95C1F979}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB53DB02-5C86-44C9-A88E-294B95C1F979}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB53DB02-5C86-44C9-A88E-294B95C1F979}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB53DB02-5C86-44C9-A88E-294B95C1F979}.Release|Any CPU.Build.0 = Release|Any CPU + {DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 05393d68b216a301e9b897505660ccbdd3afcd59 Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Sun, 10 Mar 2024 11:26:21 +0100 Subject: [PATCH 02/12] changes for spellcheck --- .../ConstantFromJsonGenerator.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs index 8dd20eab..12692327 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs @@ -27,10 +27,8 @@ namespace {Namespace} [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] public class {AttributeName} : System.Attribute {{ - public string Value {{ get; }} public {AttributeName}(string constantName, string fileName, string jsonPath) {{ - Value = ""testvaluetje""; }} }} }}"; @@ -165,8 +163,8 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati // Try get content of file from dictionary where key ends with filename var fileContent = _fileContents - .FirstOrDefault(kvp => - kvp.Key.EndsWith(fileName.Replace($"\"", string.Empty), StringComparison.Ordinal)); + .FirstOrDefault(k => + k.Key.EndsWith(fileName.Replace($"\"", string.Empty), StringComparison.Ordinal)); // If the file content is empty, skip if (string.IsNullOrEmpty(fileContent.Value)) From 71445d1d1baff01f5fdd35869553a656e7b07550 Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Sun, 10 Mar 2024 13:42:48 +0100 Subject: [PATCH 03/12] Update Readme.md --- .../Readme.md | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md index 3bee310d..a8acedc9 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md @@ -1,27 +1,34 @@ # Lombiq.HelpfulLibraries.SourceGenerators + A collection of helpful source generators. **When using one of the generators you must run a build before errors will go away** -- [ConstantFromJsonGenerator.cs](ConstantFromJsonGenerator.cs): A source generator that creates a constant from a JSON file. +- [ConstantFromJsonGenerator.cs](ConstantFromJsonGenerator.cs): A source generator that creates a constant from a JSON file. + +## Lombiq.HelpfulLibraries.SourceGenerators.Sample + +A project that references source generators. It is used to test and showcase the source generators. -### Lombiq.HelpfulLibraries.SourceGenerators.Sample -A project that references source generators. It is used to test and showcase the source generators. +## Lombiq.HelpfulLibraries.SourceGenerators.Tests -### Lombiq.HelpfulLibraries.SourceGenerators.Tests Unit tests for source generators. ## How To? + ### How to use the `ConstantFromJsonGenerator`? 1. Add a JSON file to your project. 2. Set the `Build Action` of the JSON file to `AdditionalFiles` for example: + ```xml ``` -3. Wherever you want to use the JSON file, make sure to use a `partial class` and add the `ConstantFromJsonGenerator` attribute to it. - Where the first parameter is the name of the constant and the second parameter is the path to the JSON file, the last parameter is the name or 'key' for the value we are looking for. + +3. Wherever you want to use the JSON file, make sure to use a `partial class` and add the `ConstantFromJsonGenerator` attribute to it. +Where the first parameter is the name of the constant and the second parameter is the path to the JSON file, the last parameter is the name or 'key' for the value we are looking for. + ```csharp [ConstantFromJson("GulpVersion", "package.json", "gulp")] public partial class YourClass @@ -29,8 +36,10 @@ Unit tests for source generators. } ``` + 4. Run a build and the constant will be generated . 5. Use the constant in your code, full example: + ```csharp using System; using Generators; @@ -48,4 +57,4 @@ Unit tests for source generators. Console.WriteLine(GulpVersion); } } - ``` \ No newline at end of file + ``` From 67e12884f31c85b445ff8e14792a4d200d10ea9e Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Sun, 10 Mar 2024 14:11:49 +0100 Subject: [PATCH 04/12] Refactor to comply with formatting rules --- .../Examples.cs | 9 +- .../ConstantFromJsonGenerator.cs | 151 ++++++++++-------- 2 files changed, 92 insertions(+), 68 deletions(-) diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs index 9357c7cd..89ec0c69 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs @@ -1,5 +1,4 @@ using Generators; -using System; namespace Lombiq.HelpfulLibraries.SourceGenerators.Sample; @@ -8,9 +7,11 @@ namespace Lombiq.HelpfulLibraries.SourceGenerators.Sample; public partial class Examples { // Show usage of the generated constants - public void LogVersions() + public string ReturnVersions() { - Console.WriteLine(GulpUglifyVersion); - Console.WriteLine(GulpVersion); + var stringBuilder = new System.Text.StringBuilder(); + stringBuilder.AppendLine($"Gulp version: {GulpVersion}"); + stringBuilder.AppendLine($"Gulp-uglify version: {GulpUglifyVersion}"); + return stringBuilder.ToString(); } } diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs index 12692327..cc506363 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs @@ -48,8 +48,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .CreateSyntaxProvider( (s, _) => s is ClassDeclarationSyntax, (ctx, _) => GetClassDeclarationForSourceGen(ctx)) - .Where(t => t.reportAttributeFound) - .Select((t, _) => (t.Item1, t.Item3)); + .Where(t => t.ReportAttributeFound) + .Select((t, _) => (t.Syntax, t.AttributesData)); var additionalFiles = context.AdditionalTextsProvider .Where(static file => file.Path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)); @@ -78,45 +78,61 @@ public void Initialize(IncrementalGeneratorInitializationContext context) /// Checks whether the Node is annotated with the [ConstantFromJson] attribute and maps syntax context to /// the specific node type (ClassDeclarationSyntax). /// - /// Syntax context, based on CreateSyntaxProvider predicate + /// Syntax context, based on CreateSyntaxProvider predicate. /// The specific cast and whether the attribute was found. - private static (ClassDeclarationSyntax, bool reportAttributeFound, List>) - GetClassDeclarationForSourceGen( - GeneratorSyntaxContext context) + private static (ClassDeclarationSyntax Syntax, bool ReportAttributeFound, List> AttributesData) + GetClassDeclarationForSourceGen(GeneratorSyntaxContext context) { var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; + var attributesData = GetAttributesData(context, classDeclarationSyntax); + + return (classDeclarationSyntax, attributesData.Count > 0, attributesData); + } + + private static List> GetAttributesData(GeneratorSyntaxContext context, MemberDeclarationSyntax classDeclarationSyntax) + { var attributesData = new List>(); - // Go through all attributes of the class. foreach (var attributeListSyntax in classDeclarationSyntax.AttributeLists) + { foreach (var attributeSyntax in attributeListSyntax.Attributes) { - if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) + var attributeArguments = GetAttributeArguments(context, attributeSyntax); + if (attributeArguments != null) { - continue; // if we can't get the symbol, ignore it + attributesData.Add(attributeArguments); } + } + } - string attributeName = attributeSymbol.ContainingType.ToDisplayString(); - // Check the full name of the [ConstantFromJson] attribute. - if (attributeName != $"{Namespace}.{AttributeName}") - { - continue; - } + return attributesData; + } - var arguments = new Dictionary(); - int idx = 0; - foreach (var argumentSyntax in attributeSyntax.ArgumentList?.Arguments!) - { - if (argumentSyntax.Expression is LiteralExpressionSyntax literalExpression) - arguments.Add(attributeSymbol.Parameters[idx].Name, literalExpression.Token.Text); + private static Dictionary? GetAttributeArguments(GeneratorSyntaxContext context, AttributeSyntax attributeSyntax) + { + if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) + { + return null; // if we can't get the symbol, ignore it + } - idx += 1; - } + string attributeName = attributeSymbol.ContainingType.ToDisplayString(); + // Check the full name of the [ConstantFromJson] attribute. + if (attributeName != $"{Namespace}.{AttributeName}") + { + return null; + } - attributesData.Add(arguments); - } + var arguments = new Dictionary(); + int idx = 0; + foreach (var argumentSyntax in attributeSyntax.ArgumentList?.Arguments!) + { + if (argumentSyntax.Expression is LiteralExpressionSyntax literalExpression) + arguments.Add(attributeSymbol.Parameters[idx].Name, literalExpression.Token.Text); - return (classDeclarationSyntax, attributesData.Count > 0, attributesData); + idx += 1; + } + + return arguments; } /// @@ -130,8 +146,10 @@ private static (ClassDeclarationSyntax, bool reportAttributeFound, List - private void GenerateCode(SourceProductionContext context, Compilation compilation, - ImmutableArray<(ClassDeclarationSyntax, List>)> classDeclarations) + private void GenerateCode( + SourceProductionContext context, + Compilation compilation, + ImmutableArray<(ClassDeclarationSyntax Syntax, List> Dictionary)> classDeclarations) { // Go through all filtered class declarations. foreach (var (classDeclarationSyntax, attributeData) in classDeclarations) @@ -151,7 +169,7 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati // 'Identifier' means the token of the node. Get class name from the syntax node. string className = classDeclarationSyntax.Identifier.Text; - string partialBody = string.Empty; + var partialBody = new StringBuilder(); // It's possible that a single class is annotated with our marker attribute multiple times foreach (var dictionary in attributeData) @@ -175,16 +193,14 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati var jsonDocument = JsonDocument.Parse(fileContent.Value); // try to find the value in the jsonDocument - var jsonValue = FindProperty(jsonDocument.RootElement, jsonPath.Replace("\"", "")); + var jsonValue = FindProperty(jsonDocument.RootElement, jsonPath.Replace("\"", string.Empty)); if (jsonValue == null) { return; } - partialBody += $""" - public const string {constantName.Replace("\"", "")} = "{jsonValue.Value}"; - """; + partialBody.AppendLine($"public const string {constantName.Replace("\"", string.Empty)} = \"{jsonValue.Value}\";"); } // Create a new partial class with the same name as the original class. @@ -210,45 +226,52 @@ partial class {className} /// Find a property in a JSON document recursively. /// /// The JSON element to search in. - /// The property name to look for + /// The property name to look for. private static JsonElement? FindProperty(JsonElement element, string propertyName) { foreach (var property in element.EnumerateObject()) { - if (property.Name == propertyName) + var result = ProcessProperty(property, propertyName); + if (result != null) { - return property.Value; + return result; } + } + + return null; + } + + private static JsonElement? ProcessProperty(JsonProperty property, string propertyName) + { + if (property.Name == propertyName) + { + return property.Value; + } + + if (property.Value.ValueKind == JsonValueKind.Object) + { + return FindProperty(property.Value, propertyName); + } + + if (property.Value.ValueKind == JsonValueKind.Array) + { + return ProcessArrayProperty(property.Value.EnumerateArray(), propertyName); + } - switch (property.Value.ValueKind) + return null; + } + + private static JsonElement? ProcessArrayProperty(JsonElement.ArrayEnumerator array, string propertyName) + { + foreach (var arrayElement in array) + { + if (arrayElement.ValueKind == JsonValueKind.Object) { - case JsonValueKind.Object: - { - var result = FindProperty(property.Value, propertyName); - if (result != null) - { - return result; - } - - break; - } - - case JsonValueKind.Array: - { - foreach (var arrayElement in property.Value.EnumerateArray()) - { - if (arrayElement.ValueKind == JsonValueKind.Object) - { - var result = FindProperty(arrayElement, propertyName); - if (result != null) - { - return result; - } - } - } - - break; - } + var result = FindProperty(arrayElement, propertyName); + if (result != null) + { + return result; + } } } From 99951d3f40e74aedd9f411071a38bcfab052b4dd Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Mon, 11 Mar 2024 17:49:20 +0100 Subject: [PATCH 05/12] Address PR comments --- .../Examples.cs | 17 -- ...ulLibraries.SourceGenerators.Sample.csproj | 17 -- .../ConstantFromJsonAttribute.cs | 9 + .../ConstantFromJsonGenerator.cs | 190 +++++++----------- ...q.HelpfulLibraries.SourceGenerators.csproj | 3 + .../Readme.md | 16 +- .../Lombiq.HelpfulLibraries.Tests.csproj | 6 + .../Models/Examples.cs | 19 ++ .../SourceGenerators/ConstantFromJsonTests.cs | 62 ++++++ .../Utils/TestAdditionalFile.cs | 14 ++ .../package.json | 0 Lombiq.HelpfulLibraries.sln | 6 - 12 files changed, 191 insertions(+), 168 deletions(-) delete mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs delete mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj create mode 100644 Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonAttribute.cs create mode 100644 Lombiq.HelpfulLibraries.Tests/Models/Examples.cs create mode 100644 Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs create mode 100644 Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs rename {Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample => Lombiq.HelpfulLibraries.Tests}/package.json (100%) diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs deleted file mode 100644 index 89ec0c69..00000000 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Generators; - -namespace Lombiq.HelpfulLibraries.SourceGenerators.Sample; - -[ConstantFromJson("GulpUglifyVersion", "package.json", "gulp-uglify")] -[ConstantFromJson("GulpVersion", "package.json", "gulp")] -public partial class Examples -{ - // Show usage of the generated constants - public string ReturnVersions() - { - var stringBuilder = new System.Text.StringBuilder(); - stringBuilder.AppendLine($"Gulp version: {GulpVersion}"); - stringBuilder.AppendLine($"Gulp-uglify version: {GulpUglifyVersion}"); - return stringBuilder.ToString(); - } -} diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj deleted file mode 100644 index cc00a341..00000000 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net8.0 - enable - Lombiq.HelpfulLibraries.SourceGenerators.Sample - - - - - - - - - - - diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonAttribute.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonAttribute.cs new file mode 100644 index 00000000..0679458c --- /dev/null +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonAttribute.cs @@ -0,0 +1,9 @@ +namespace Lombiq.HelpfulLibraries.SourceGenerators; + +[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] +public sealed class ConstantFromJsonAttribute : System.Attribute +{ + public ConstantFromJsonAttribute(string constantName, string fileName, string propertyName) + { + } +} diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs index cc506363..0191d8cf 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs @@ -11,45 +11,28 @@ namespace Lombiq.HelpfulLibraries.SourceGenerators; /// -/// A generator that exposes a value from a JSON file at compile time. -/// The target class should be annotated with the 'Generators.ConstantFromJsonAttribute' attribute. +/// A generator that exposes a value from a JSON file at compile time. +/// The target class should be annotated with the 'Generators.ConstantFromJsonAttribute' attribute. /// [Generator] public class ConstantFromJsonGenerator : IIncrementalGenerator { - private const string Namespace = "Generators"; - private const string AttributeName = "ConstantFromJsonAttribute"; - - private const string AttributeSourceCode = $@"// - -namespace {Namespace} -{{ - [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] - public class {AttributeName} : System.Attribute - {{ - public {AttributeName}(string constantName, string fileName, string jsonPath) - {{ - }} - }} -}}"; + // Get current file namespace + private const string Namespace = "Lombiq.HelpfulLibraries.SourceGenerators"; + private const string AttributeName = nameof(ConstantFromJsonAttribute); private readonly Dictionary _fileContents = []; public void Initialize(IncrementalGeneratorInitializationContext context) { - // Add the marker attribute to the compilation. - context.RegisterPostInitializationOutput(ctx => ctx.AddSource( - $"{AttributeName}.g.cs", - SourceText.From(AttributeSourceCode, Encoding.UTF8))); - // Filter classes annotated with the [ConstantFromJson] attribute. // Only filtered Syntax Nodes can trigger code generation. var provider = context.SyntaxProvider .CreateSyntaxProvider( - (s, _) => s is ClassDeclarationSyntax, - (ctx, _) => GetClassDeclarationForSourceGen(ctx)) - .Where(t => t.ReportAttributeFound) - .Select((t, _) => (t.Syntax, t.AttributesData)); + (node, _) => node is ClassDeclarationSyntax, + (syntaxContext, _) => GetClassDeclarationForSourceGen(syntaxContext)) + .Where(tuple => tuple.ReportAttributeFound) + .Select((tuple, _) => (tuple.Syntax, tuple.AttributesData)); var additionalFiles = context.AdditionalTextsProvider .Where(static file => file.Path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)); @@ -71,12 +54,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Generate the source code. context.RegisterSourceOutput( context.CompilationProvider.Combine(provider.Collect()), - (ctx, t) => GenerateCode(ctx, t.Left, t.Right)); + (productionContext, tuple) => GenerateCode(productionContext, tuple.Left, tuple.Right)); } /// - /// Checks whether the Node is annotated with the [ConstantFromJson] attribute and maps syntax context to - /// the specific node type (ClassDeclarationSyntax). + /// Checks whether the Node is annotated with the [ConstantFromJson] attribute and maps syntax context to + /// the specific node type (ClassDeclarationSyntax). /// /// Syntax context, based on CreateSyntaxProvider predicate. /// The specific cast and whether the attribute was found. @@ -89,24 +72,11 @@ private static (ClassDeclarationSyntax Syntax, bool ReportAttributeFound, List 0, attributesData); } - private static List> GetAttributesData(GeneratorSyntaxContext context, MemberDeclarationSyntax classDeclarationSyntax) - { - var attributesData = new List>(); - - foreach (var attributeListSyntax in classDeclarationSyntax.AttributeLists) - { - foreach (var attributeSyntax in attributeListSyntax.Attributes) - { - var attributeArguments = GetAttributeArguments(context, attributeSyntax); - if (attributeArguments != null) - { - attributesData.Add(attributeArguments); - } - } - } - - return attributesData; - } + private static List> + GetAttributesData(GeneratorSyntaxContext context, MemberDeclarationSyntax classDeclarationSyntax) => + classDeclarationSyntax.AttributeLists.SelectMany(list => list.Attributes) + .Select(attributeSyntax => GetAttributeArguments(context, attributeSyntax)) + .OfType>().ToList(); private static Dictionary? GetAttributeArguments(GeneratorSyntaxContext context, AttributeSyntax attributeSyntax) { @@ -115,36 +85,36 @@ private static List> GetAttributesData(GeneratorSynta return null; // if we can't get the symbol, ignore it } - string attributeName = attributeSymbol.ContainingType.ToDisplayString(); + var attributeName = attributeSymbol.ContainingType.ToDisplayString(); // Check the full name of the [ConstantFromJson] attribute. if (attributeName != $"{Namespace}.{AttributeName}") { return null; } - var arguments = new Dictionary(); - int idx = 0; - foreach (var argumentSyntax in attributeSyntax.ArgumentList?.Arguments!) - { - if (argumentSyntax.Expression is LiteralExpressionSyntax literalExpression) - arguments.Add(attributeSymbol.Parameters[idx].Name, literalExpression.Token.Text); - - idx += 1; - } + var arguments = attributeSyntax.ArgumentList?.Arguments + .Select(argument => argument.Expression) + .OfType() + .Select((literalExpression, index) => new + { + Key = attributeSymbol.Parameters[index].Name, + Value = literalExpression.Token.Text, + }) + .ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value) ?? []; return arguments; } /// - /// Generate code action. - /// It will be executed on specific nodes (ClassDeclarationSyntax annotated with the [ConstantFromJson] attribute) - /// changed by the user. + /// Generate code action. + /// It will be executed on specific nodes (ClassDeclarationSyntax annotated with the [ConstantFromJson] attribute) + /// changed by the user. /// /// Source generation context used to add source files. /// Compilation used to provide access to the Semantic Model. /// - /// Nodes annotated with the [ConstantFromJson] attribute that trigger the - /// generate action. + /// Nodes annotated with the [ConstantFromJson] attribute that trigger the + /// generate action. /// private void GenerateCode( SourceProductionContext context, @@ -164,10 +134,10 @@ private void GenerateCode( continue; } - string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); // 'Identifier' means the token of the node. Get class name from the syntax node. - string className = classDeclarationSyntax.Identifier.Text; + var className = classDeclarationSyntax.Identifier.Text; var partialBody = new StringBuilder(); @@ -175,14 +145,14 @@ private void GenerateCode( foreach (var dictionary in attributeData) { // Get values from dictionary - string? constantName = dictionary["constantName"]; - string? fileName = dictionary["fileName"]; - string? jsonPath = dictionary["jsonPath"]; + var constantName = dictionary["constantName"].Trim('"'); + var fileName = dictionary["fileName"].Trim('"'); + var propertyName = dictionary["propertyName"].Trim('"'); // Try get content of file from dictionary where key ends with filename var fileContent = _fileContents - .FirstOrDefault(k => - k.Key.EndsWith(fileName.Replace($"\"", string.Empty), StringComparison.Ordinal)); + .FirstOrDefault(keyValuePair => + keyValuePair.Key.EndsWith(fileName, StringComparison.OrdinalIgnoreCase)); // If the file content is empty, skip if (string.IsNullOrEmpty(fileContent.Value)) @@ -192,20 +162,13 @@ private void GenerateCode( var jsonDocument = JsonDocument.Parse(fileContent.Value); - // try to find the value in the jsonDocument - var jsonValue = FindProperty(jsonDocument.RootElement, jsonPath.Replace("\"", string.Empty)); - - if (jsonValue == null) - { - return; - } - - partialBody.AppendLine($"public const string {constantName.Replace("\"", string.Empty)} = \"{jsonValue.Value}\";"); + if (FindProperty(jsonDocument.RootElement, propertyName) is { } jsonValue) + partialBody.AppendLine($"public const string {constantName} = \"{jsonValue}\";"); } // Create a new partial class with the same name as the original class. // Build up the source code - string code = $@"// + var code = $@"// using System; using System.Collections.Generic; @@ -223,55 +186,46 @@ partial class {className} } /// - /// Find a property in a JSON document recursively. + /// Find a property in a JSON document recursively. /// /// The JSON element to search in. - /// The property name to look for. + /// The property name to look for private static JsonElement? FindProperty(JsonElement element, string propertyName) { foreach (var property in element.EnumerateObject()) { - var result = ProcessProperty(property, propertyName); - if (result != null) + if (property.Name == propertyName) { - return result; + return property.Value; } - } - - return null; - } - - private static JsonElement? ProcessProperty(JsonProperty property, string propertyName) - { - if (property.Name == propertyName) - { - return property.Value; - } - - if (property.Value.ValueKind == JsonValueKind.Object) - { - return FindProperty(property.Value, propertyName); - } - - if (property.Value.ValueKind == JsonValueKind.Array) - { - return ProcessArrayProperty(property.Value.EnumerateArray(), propertyName); - } - return null; - } - - private static JsonElement? ProcessArrayProperty(JsonElement.ArrayEnumerator array, string propertyName) - { - foreach (var arrayElement in array) - { - if (arrayElement.ValueKind == JsonValueKind.Object) + switch (property.Value.ValueKind) { - var result = FindProperty(arrayElement, propertyName); - if (result != null) - { - return result; - } + case JsonValueKind.Object: + { + var result = FindProperty(property.Value, propertyName); + if (result != null) + { + return result; + } + + break; + } + + case JsonValueKind.Array: + { + var result = property.Value.EnumerateArray() + .Where(arrayElement => arrayElement.ValueKind == JsonValueKind.Object) + .Select(arrayElement => FindProperty(arrayElement, propertyName)) + .FirstOrDefault(jsonProperty => jsonProperty != null); + + if (result != null) + { + return result; + } + + break; + } } } diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj index 133e66a9..425c7faa 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj @@ -11,6 +11,9 @@ Lombiq.HelpfulLibraries.SourceGenerators Lombiq.HelpfulLibraries.SourceGenerators + + true + Generated diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md index a8acedc9..33f025fc 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md @@ -1,19 +1,15 @@ -# Lombiq.HelpfulLibraries.SourceGenerators +# Lombiq HelpfulLibraries - Source Generators + +## About A collection of helpful source generators. -**When using one of the generators you must run a build before errors will go away** +> ⚠ When using one of the generators you must run a build before errors will go away. - [ConstantFromJsonGenerator.cs](ConstantFromJsonGenerator.cs): A source generator that creates a constant from a JSON file. -## Lombiq.HelpfulLibraries.SourceGenerators.Sample - -A project that references source generators. It is used to test and showcase the source generators. - -## Lombiq.HelpfulLibraries.SourceGenerators.Tests - -Unit tests for source generators. +For general details about and on using the Helpful Libraries see the [root Readme](../Readme.md). -## How To? +## Documentation ### How to use the `ConstantFromJsonGenerator`? diff --git a/Lombiq.HelpfulLibraries.Tests/Lombiq.HelpfulLibraries.Tests.csproj b/Lombiq.HelpfulLibraries.Tests/Lombiq.HelpfulLibraries.Tests.csproj index c508da1f..c1af1677 100644 --- a/Lombiq.HelpfulLibraries.Tests/Lombiq.HelpfulLibraries.Tests.csproj +++ b/Lombiq.HelpfulLibraries.Tests/Lombiq.HelpfulLibraries.Tests.csproj @@ -15,6 +15,12 @@ + + + + + + diff --git a/Lombiq.HelpfulLibraries.Tests/Models/Examples.cs b/Lombiq.HelpfulLibraries.Tests/Models/Examples.cs new file mode 100644 index 00000000..9949e5d8 --- /dev/null +++ b/Lombiq.HelpfulLibraries.Tests/Models/Examples.cs @@ -0,0 +1,19 @@ +using Lombiq.HelpfulLibraries.SourceGenerators; + +namespace Lombiq.HelpfulLibraries.Tests.Models; + +/// +/// Shows how to use the . +/// +[ConstantFromJson(constantName: "GulpUglifyVersion", fileName: "package.json", propertyName: "gulp-uglify")] +[ConstantFromJson(constantName: "GulpVersion", fileName: "package.json", propertyName: "gulp")] +public partial class Examples +{ + public string ReturnVersions() + { + var stringBuilder = new System.Text.StringBuilder(); + stringBuilder.AppendLine($"Gulp version: {GulpVersion}"); + stringBuilder.AppendLine($"Gulp-uglify version: {GulpUglifyVersion}"); + return stringBuilder.ToString(); + } +} diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs new file mode 100644 index 00000000..73c81555 --- /dev/null +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs @@ -0,0 +1,62 @@ +using Lombiq.HelpfulLibraries.SourceGenerators; +using Lombiq.HelpfulLibraries.Tests.UnitTests.SourceGenerators.Utils; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Collections.Immutable; +using Xunit; + +namespace Lombiq.HelpfulLibraries.Tests.UnitTests.SourceGenerators; + +public class ConstantFromJsonTests +{ + private const string TestClassText = """ +using Lombiq.HelpfulLibraries.SourceGenerators; + +namespace TestNamespace; + +[ConstantFromJson(constantName: "GulpVersion", fileName: "package.json", jsonPath: "gulp")] +public partial class SourceGeneratorTest +{ +} +"""; + + private const string JsonText = """ +{ + "private": true, + "devDependencies": { + "gulp": "3.9.0", + "gulp-uglify": "1.4.1", + }, + "dependencies": { } +} +"""; + + [Fact] + public void TestGeneratedConstants() + { + // Create an instance of the source generator. + var generator = new ConstantFromJsonGenerator(); + + var driver = CSharpGeneratorDriver.Create(generator); + + var updatedDriver = driver.AddAdditionalTexts([new InMemoryAdditionalText("./package.json", JsonText)]); + // We need to create a compilation with the required source code. + var compilation = CSharpCompilation.Create( + nameof(ConstantFromJsonTests), + new[] { CSharpSyntaxTree.ParseText(TestClassText) }, + new[] + { + // To support 'System.Attribute' inheritance, add reference to 'System.Private.CoreLib'. + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + }); + + compilation.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + + // Run generators. Don't forget to use the new compilation rather than the previous one. + updatedDriver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _); + + // Retrieve all files in the compilation. + var generatedFiles = newCompilation.SyntaxTrees; + Assert.True(generatedFiles.ToImmutableArray().Length == 2); + } +} diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs new file mode 100644 index 00000000..18992961 --- /dev/null +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using System.Threading; + +namespace Lombiq.HelpfulLibraries.Tests.UnitTests.SourceGenerators.Utils; + +internal class InMemoryAdditionalText(string path, string text) : AdditionalText +{ + private readonly SourceText _text = SourceText.From(text); + + public override SourceText GetText(CancellationToken cancellationToken = default) => _text; + + public override string Path { get; } = path; +} diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/package.json b/Lombiq.HelpfulLibraries.Tests/package.json similarity index 100% rename from Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/package.json rename to Lombiq.HelpfulLibraries.Tests/package.json diff --git a/Lombiq.HelpfulLibraries.sln b/Lombiq.HelpfulLibraries.sln index da10c232..d43a03dd 100644 --- a/Lombiq.HelpfulLibraries.sln +++ b/Lombiq.HelpfulLibraries.sln @@ -23,8 +23,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.HelpfulLibraries.Ref EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.HelpfulLibraries.SourceGenerators", "Lombiq.HelpfulLibraries.SourceGenerators\Lombiq.HelpfulLibraries.SourceGenerators\Lombiq.HelpfulLibraries.SourceGenerators.csproj", "{FB53DB02-5C86-44C9-A88E-294B95C1F979}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lombiq.HelpfulLibraries.SourceGenerators.Sample", "Lombiq.HelpfulLibraries.SourceGenerators\Lombiq.HelpfulLibraries.SourceGenerators.Sample\Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj", "{DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -71,10 +69,6 @@ Global {FB53DB02-5C86-44C9-A88E-294B95C1F979}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB53DB02-5C86-44C9-A88E-294B95C1F979}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB53DB02-5C86-44C9-A88E-294B95C1F979}.Release|Any CPU.Build.0 = Release|Any CPU - {DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DD8D992A-43C8-4E9B-8A40-B57F0D5B4217}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From fa3d67d52d8e36072ab2f5a53c94b3a7dcd41d44 Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Mon, 11 Mar 2024 17:51:26 +0100 Subject: [PATCH 06/12] Property was renamed --- .../UnitTests/SourceGenerators/ConstantFromJsonTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs index 73c81555..56d5c80c 100644 --- a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs @@ -14,7 +14,7 @@ public class ConstantFromJsonTests namespace TestNamespace; -[ConstantFromJson(constantName: "GulpVersion", fileName: "package.json", jsonPath: "gulp")] +[ConstantFromJson(constantName: "GulpVersion", fileName: "package.json", propertyName: "gulp")] public partial class SourceGeneratorTest { } @@ -57,6 +57,6 @@ public void TestGeneratedConstants() // Retrieve all files in the compilation. var generatedFiles = newCompilation.SyntaxTrees; - Assert.True(generatedFiles.ToImmutableArray().Length == 2); + Assert.True(generatedFiles.ToImmutableArray().Length == 1); } } From 2715cb7fb32c41c1b8832f60ea6fb64f9468cb84 Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Mon, 11 Mar 2024 18:22:25 +0100 Subject: [PATCH 07/12] Replace switch because build error --- .../ConstantFromJsonGenerator.cs | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs index 0191d8cf..71a51284 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs @@ -189,7 +189,7 @@ partial class {className} /// Find a property in a JSON document recursively. /// /// The JSON element to search in. - /// The property name to look for + /// The property name to look for. private static JsonElement? FindProperty(JsonElement element, string propertyName) { foreach (var property in element.EnumerateObject()) @@ -199,33 +199,25 @@ partial class {className} return property.Value; } - switch (property.Value.ValueKind) + if (property.Value.ValueKind == JsonValueKind.Object) { - case JsonValueKind.Object: - { - var result = FindProperty(property.Value, propertyName); - if (result != null) - { - return result; - } - - break; - } - - case JsonValueKind.Array: - { - var result = property.Value.EnumerateArray() - .Where(arrayElement => arrayElement.ValueKind == JsonValueKind.Object) - .Select(arrayElement => FindProperty(arrayElement, propertyName)) - .FirstOrDefault(jsonProperty => jsonProperty != null); - - if (result != null) - { - return result; - } - - break; - } + var result = FindProperty(property.Value, propertyName); + if (result != null) + { + return result; + } + } + else if (property.Value.ValueKind == JsonValueKind.Array) + { + var result = property.Value.EnumerateArray() + .Where(arrayElement => arrayElement.ValueKind == JsonValueKind.Object) + .Select(arrayElement => FindProperty(arrayElement, propertyName)) + .FirstOrDefault(jsonProperty => jsonProperty != null); + + if (result != null) + { + return result; + } } } From 671b7c9faabbb62278abdd254d3c67807ea17bd4 Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Mon, 11 Mar 2024 18:54:10 +0100 Subject: [PATCH 08/12] Fix build errors --- ...q.HelpfulLibraries.SourceGenerators.csproj | 3 --- .../SourceGenerators/ConstantFromJsonTests.cs | 24 ++++++++----------- .../Utils/TestAdditionalFile.cs | 2 +- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj index 425c7faa..133e66a9 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj @@ -11,9 +11,6 @@ Lombiq.HelpfulLibraries.SourceGenerators Lombiq.HelpfulLibraries.SourceGenerators - - true - Generated diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs index 56d5c80c..60d19f79 100644 --- a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs @@ -9,27 +9,23 @@ namespace Lombiq.HelpfulLibraries.Tests.UnitTests.SourceGenerators; public class ConstantFromJsonTests { - private const string TestClassText = """ -using Lombiq.HelpfulLibraries.SourceGenerators; + private const string TestClassText = @"using Lombiq.HelpfulLibraries.SourceGenerators; namespace TestNamespace; -[ConstantFromJson(constantName: "GulpVersion", fileName: "package.json", propertyName: "gulp")] +[ConstantFromJson(constantName: ""GulpVersion"", fileName: ""package.json"", propertyName: ""gulp"")] public partial class SourceGeneratorTest { -} -"""; +}"; - private const string JsonText = """ -{ - "private": true, - "devDependencies": { - "gulp": "3.9.0", - "gulp-uglify": "1.4.1", + private const string JsonText = @"{ + ""private"": true, + ""devDependencies"": { + ""gulp"": ""3.9.0"", + ""gulp-uglify"": ""1.4.1"", }, - "dependencies": { } -} -"""; + ""dependencies"": { } +}"; [Fact] public void TestGeneratedConstants() diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs index 18992961..d8657915 100644 --- a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs @@ -4,7 +4,7 @@ namespace Lombiq.HelpfulLibraries.Tests.UnitTests.SourceGenerators.Utils; -internal class InMemoryAdditionalText(string path, string text) : AdditionalText +internal sealed class InMemoryAdditionalText(string path, string text) : AdditionalText { private readonly SourceText _text = SourceText.From(text); From 0c8a25bf52d016d536536491b5c6173f0719c4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 12 Mar 2024 18:04:48 +0100 Subject: [PATCH 09/12] Update Lombiq.HelpfulLibraries.Tests/package.json --- Lombiq.HelpfulLibraries.Tests/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.Tests/package.json b/Lombiq.HelpfulLibraries.Tests/package.json index 9d038ca3..2662e523 100644 --- a/Lombiq.HelpfulLibraries.Tests/package.json +++ b/Lombiq.HelpfulLibraries.Tests/package.json @@ -20,4 +20,4 @@ "gulp-header": "1.7.1" }, "dependencies": { } -} \ No newline at end of file +} From 9c1c57e8b696f98ad82cd1946906623df7dfb5a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ra=20El-Saig?= Date: Tue, 12 Mar 2024 18:19:10 +0100 Subject: [PATCH 10/12] Update Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json --- .../Properties/launchSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json index 30b5e4c6..d2da76e1 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json @@ -6,4 +6,4 @@ "targetProject": "../Lombiq.HelpfulLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.csproj" } } -} \ No newline at end of file +} From ae32b17eb15dcc27acf6cffda924d1ec4c5440ee Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Wed, 13 Mar 2024 10:11:04 +0100 Subject: [PATCH 11/12] Fix test and small cleanup --- .../ConstantFromJsonGenerator.cs | 14 ++--- .../SourceGenerators/ConstantFromJsonTests.cs | 56 +++---------------- 2 files changed, 14 insertions(+), 56 deletions(-) diff --git a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs index 71a51284..f7395b6d 100644 --- a/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs +++ b/Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs @@ -17,9 +17,8 @@ namespace Lombiq.HelpfulLibraries.SourceGenerators; [Generator] public class ConstantFromJsonGenerator : IIncrementalGenerator { - // Get current file namespace - private const string Namespace = "Lombiq.HelpfulLibraries.SourceGenerators"; private const string AttributeName = nameof(ConstantFromJsonAttribute); + private static readonly string? Namespace = typeof(ConstantFromJsonAttribute).Namespace; private readonly Dictionary _fileContents = []; @@ -67,17 +66,14 @@ private static (ClassDeclarationSyntax Syntax, bool ReportAttributeFound, List list.Attributes) + .Select(attributeSyntax => GetAttributeArguments(context, attributeSyntax)) + .OfType>().ToList(); return (classDeclarationSyntax, attributesData.Count > 0, attributesData); } - private static List> - GetAttributesData(GeneratorSyntaxContext context, MemberDeclarationSyntax classDeclarationSyntax) => - classDeclarationSyntax.AttributeLists.SelectMany(list => list.Attributes) - .Select(attributeSyntax => GetAttributeArguments(context, attributeSyntax)) - .OfType>().ToList(); - private static Dictionary? GetAttributeArguments(GeneratorSyntaxContext context, AttributeSyntax attributeSyntax) { if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs index 60d19f79..64a904ca 100644 --- a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs +++ b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs @@ -1,58 +1,20 @@ -using Lombiq.HelpfulLibraries.SourceGenerators; -using Lombiq.HelpfulLibraries.Tests.UnitTests.SourceGenerators.Utils; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using System.Collections.Immutable; +using Lombiq.HelpfulLibraries.Tests.Models; +using Shouldly; +using System; using Xunit; namespace Lombiq.HelpfulLibraries.Tests.UnitTests.SourceGenerators; public class ConstantFromJsonTests { - private const string TestClassText = @"using Lombiq.HelpfulLibraries.SourceGenerators; - -namespace TestNamespace; - -[ConstantFromJson(constantName: ""GulpVersion"", fileName: ""package.json"", propertyName: ""gulp"")] -public partial class SourceGeneratorTest -{ -}"; - - private const string JsonText = @"{ - ""private"": true, - ""devDependencies"": { - ""gulp"": ""3.9.0"", - ""gulp-uglify"": ""1.4.1"", - }, - ""dependencies"": { } -}"; - [Fact] public void TestGeneratedConstants() { - // Create an instance of the source generator. - var generator = new ConstantFromJsonGenerator(); - - var driver = CSharpGeneratorDriver.Create(generator); - - var updatedDriver = driver.AddAdditionalTexts([new InMemoryAdditionalText("./package.json", JsonText)]); - // We need to create a compilation with the required source code. - var compilation = CSharpCompilation.Create( - nameof(ConstantFromJsonTests), - new[] { CSharpSyntaxTree.ParseText(TestClassText) }, - new[] - { - // To support 'System.Attribute' inheritance, add reference to 'System.Private.CoreLib'. - MetadataReference.CreateFromFile(typeof(object).Assembly.Location), - }); - - compilation.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); - - // Run generators. Don't forget to use the new compilation rather than the previous one. - updatedDriver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _); - - // Retrieve all files in the compilation. - var generatedFiles = newCompilation.SyntaxTrees; - Assert.True(generatedFiles.ToImmutableArray().Length == 1); + Examples.GulpVersion.ShouldBe("3.9.0"); + Examples.GulpUglifyVersion.ShouldBe("1.4.1"); + new Examples() + .ReturnVersions() + .Split(["\n", "\r"], StringSplitOptions.RemoveEmptyEntries) + .ShouldBe(["Gulp version: 3.9.0", "Gulp-uglify version: 1.4.1"]); } } From 7970140614fb7595d3f97e82ec8f3aa095f61fcf Mon Sep 17 00:00:00 2001 From: Aydin Erdas Date: Wed, 13 Mar 2024 23:27:59 +0100 Subject: [PATCH 12/12] Remove unneeded file --- .../SourceGenerators/Utils/TestAdditionalFile.cs | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs diff --git a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs b/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs deleted file mode 100644 index d8657915..00000000 --- a/Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/Utils/TestAdditionalFile.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Text; -using System.Threading; - -namespace Lombiq.HelpfulLibraries.Tests.UnitTests.SourceGenerators.Utils; - -internal sealed class InMemoryAdditionalText(string path, string text) : AdditionalText -{ - private readonly SourceText _text = SourceText.From(text); - - public override SourceText GetText(CancellationToken cancellationToken = default) => _text; - - public override string Path { get; } = path; -}