-
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.
- Loading branch information
Showing
10 changed files
with
427 additions
and
0 deletions.
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
...fulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/Examples.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,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); | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
...lLibraries.SourceGenerators.Sample/Lombiq.HelpfulLibraries.SourceGenerators.Sample.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,17 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
<RootNamespace>Lombiq.HelpfulLibraries.SourceGenerators.Sample</RootNamespace> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\Lombiq.HelpfulLibraries.SourceGenerators\Lombiq.HelpfulLibraries.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<AdditionalFiles Include="package.json"/> | ||
</ItemGroup> | ||
|
||
</Project> |
23 changes: 23 additions & 0 deletions
23
...ulLibraries.SourceGenerators/Lombiq.HelpfulLibraries.SourceGenerators.Sample/package.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,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": { } | ||
} |
259 changes: 259 additions & 0 deletions
259
...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,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; | ||
|
||
/// <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 Namespace = "Generators"; | ||
private const string AttributeName = "ConstantFromJsonAttribute"; | ||
|
||
private const string AttributeSourceCode = $@"// <auto-generated/> | ||
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<string, string> _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)); | ||
} | ||
|
||
/// <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, bool reportAttributeFound, List<Dictionary<string, string>>) | ||
GetClassDeclarationForSourceGen( | ||
GeneratorSyntaxContext context) | ||
{ | ||
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; | ||
var attributesData = new List<Dictionary<string, string>>(); | ||
|
||
// 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<string, string>(); | ||
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); | ||
} | ||
|
||
/// <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, List<Dictionary<string, string>>)> 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 = $@"// <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; | ||
} | ||
|
||
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; | ||
} | ||
} |
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" | ||
} | ||
} | ||
} |
Oops, something went wrong.