-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/dev' into issue/LMBQ-267
- Loading branch information
Showing
12 changed files
with
410 additions
and
0 deletions.
There are no files selected for viewing
9 changes: 9 additions & 0 deletions
9
...es.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonAttribute.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
{ | ||
} | ||
} |
222 changes: 222 additions & 0 deletions
222
...es.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/ConstantFromJsonGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
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; | ||
|
||
/// <summary> | ||
/// A generator that exposes a value from a JSON file at compile time. | ||
/// The target class should be annotated with the 'Generators.ConstantFromJsonAttribute' attribute. | ||
/// </summary> | ||
[Generator] | ||
public class ConstantFromJsonGenerator : IIncrementalGenerator | ||
{ | ||
private const string AttributeName = nameof(ConstantFromJsonAttribute); | ||
private static readonly string? Namespace = typeof(ConstantFromJsonAttribute).Namespace; | ||
|
||
private readonly Dictionary<string, string> _fileContents = []; | ||
|
||
public void Initialize(IncrementalGeneratorInitializationContext context) | ||
{ | ||
// Filter classes annotated with the [ConstantFromJson] attribute. | ||
// Only filtered Syntax Nodes can trigger code generation. | ||
var provider = context.SyntaxProvider | ||
.CreateSyntaxProvider( | ||
(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)); | ||
|
||
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()), | ||
(productionContext, tuple) => GenerateCode(productionContext, tuple.Left, tuple.Right)); | ||
} | ||
|
||
/// <summary> | ||
/// Checks whether the Node is annotated with the [ConstantFromJson] attribute and maps syntax context to | ||
/// the specific node type (ClassDeclarationSyntax). | ||
/// </summary> | ||
/// <param name="context">Syntax context, based on CreateSyntaxProvider predicate.</param> | ||
/// <returns>The specific cast and whether the attribute was found.</returns> | ||
private static (ClassDeclarationSyntax Syntax, bool ReportAttributeFound, List<Dictionary<string, string>> AttributesData) | ||
GetClassDeclarationForSourceGen(GeneratorSyntaxContext context) | ||
{ | ||
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; | ||
var attributesData = classDeclarationSyntax.AttributeLists | ||
.SelectMany(list => list.Attributes) | ||
.Select(attributeSyntax => GetAttributeArguments(context, attributeSyntax)) | ||
.OfType<Dictionary<string, string>>().ToList(); | ||
|
||
return (classDeclarationSyntax, attributesData.Count > 0, attributesData); | ||
} | ||
|
||
private static Dictionary<string, string>? 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 | ||
} | ||
|
||
var attributeName = attributeSymbol.ContainingType.ToDisplayString(); | ||
// Check the full name of the [ConstantFromJson] attribute. | ||
if (attributeName != $"{Namespace}.{AttributeName}") | ||
{ | ||
return null; | ||
} | ||
|
||
var arguments = attributeSyntax.ArgumentList?.Arguments | ||
.Select(argument => argument.Expression) | ||
.OfType<LiteralExpressionSyntax>() | ||
.Select((literalExpression, index) => new | ||
{ | ||
Key = attributeSymbol.Parameters[index].Name, | ||
Value = literalExpression.Token.Text, | ||
}) | ||
.ToDictionary(keyValuePair => keyValuePair.Key, keyValuePair => keyValuePair.Value) ?? []; | ||
|
||
return arguments; | ||
} | ||
|
||
/// <summary> | ||
/// Generate code action. | ||
/// It will be executed on specific nodes (ClassDeclarationSyntax annotated with the [ConstantFromJson] attribute) | ||
/// changed by the user. | ||
/// </summary> | ||
/// <param name="context">Source generation context used to add source files.</param> | ||
/// <param name="compilation">Compilation used to provide access to the Semantic Model.</param> | ||
/// <param name="classDeclarations"> | ||
/// Nodes annotated with the [ConstantFromJson] attribute that trigger the | ||
/// generate action. | ||
/// </param> | ||
private void GenerateCode( | ||
SourceProductionContext context, | ||
Compilation compilation, | ||
ImmutableArray<(ClassDeclarationSyntax Syntax, List<Dictionary<string, string>> Dictionary)> 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; | ||
} | ||
|
||
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); | ||
|
||
// 'Identifier' means the token of the node. Get class name from the syntax node. | ||
var className = classDeclarationSyntax.Identifier.Text; | ||
|
||
var partialBody = new StringBuilder(); | ||
|
||
// It's possible that a single class is annotated with our marker attribute multiple times | ||
foreach (var dictionary in attributeData) | ||
{ | ||
// Get values from dictionary | ||
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(keyValuePair => | ||
keyValuePair.Key.EndsWith(fileName, StringComparison.OrdinalIgnoreCase)); | ||
|
||
// If the file content is empty, skip | ||
if (string.IsNullOrEmpty(fileContent.Value)) | ||
{ | ||
return; | ||
} | ||
|
||
var jsonDocument = JsonDocument.Parse(fileContent.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 | ||
var code = $@"// <auto-generated/> | ||
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)); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Find a property in a JSON document recursively. | ||
/// </summary> | ||
/// <param name="element">The JSON element to search in.</param> | ||
/// <param name="propertyName">The property name to look for.</param> | ||
private static JsonElement? FindProperty(JsonElement element, string propertyName) | ||
{ | ||
foreach (var property in element.EnumerateObject()) | ||
{ | ||
if (property.Name == propertyName) | ||
{ | ||
return property.Value; | ||
} | ||
|
||
if (property.Value.ValueKind == JsonValueKind.Object) | ||
{ | ||
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; | ||
} | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
...lLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/License.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
27 changes: 27 additions & 0 deletions
27
.../Lombiq.HelpfulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
<IsPackable>false</IsPackable> | ||
<Nullable>enable</Nullable> | ||
<LangVersion>latest</LangVersion> | ||
|
||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> | ||
<IsRoslynComponent>true</IsRoslynComponent> | ||
|
||
<RootNamespace>Lombiq.HelpfulLibraries.SourceGenerators</RootNamespace> | ||
<PackageId>Lombiq.HelpfulLibraries.SourceGenerators</PackageId> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"> | ||
<PrivateAssets>all</PrivateAssets> | ||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||
</PackageReference> | ||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/> | ||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/> | ||
<PackageReference Include="System.Text.Json" Version="7.0.3" /> | ||
</ItemGroup> | ||
|
||
|
||
</Project> |
Binary file added
BIN
+4.55 KB
...braries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/NuGetIcon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions
9
....SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Properties/launchSettings.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
...ulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators/Readme.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# 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. | ||
- [ConstantFromJsonGenerator.cs](ConstantFromJsonGenerator.cs): A source generator that creates a constant from a JSON file. | ||
|
||
For general details about and on using the Helpful Libraries see the [root Readme](../Readme.md). | ||
|
||
## Documentation | ||
|
||
### 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 | ||
<ItemGroup> | ||
<AdditionalFiles Include="package.json" /> | ||
</ItemGroup> | ||
``` | ||
|
||
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); | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using Lombiq.HelpfulLibraries.SourceGenerators; | ||
|
||
namespace Lombiq.HelpfulLibraries.Tests.Models; | ||
|
||
/// <summary> | ||
/// Shows how to use the <see cref="ConstantFromJsonAttribute" />. | ||
/// </summary> | ||
[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(); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
Lombiq.HelpfulLibraries.Tests/UnitTests/SourceGenerators/ConstantFromJsonTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
using Lombiq.HelpfulLibraries.Tests.Models; | ||
using Shouldly; | ||
using System; | ||
using Xunit; | ||
|
||
namespace Lombiq.HelpfulLibraries.Tests.UnitTests.SourceGenerators; | ||
|
||
public class ConstantFromJsonTests | ||
{ | ||
[Fact] | ||
public void TestGeneratedConstants() | ||
{ | ||
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"]); | ||
} | ||
} |
Oops, something went wrong.