Skip to content

Commit

Permalink
Add support for ThisAssemblyNamespace to change root namespace
Browse files Browse the repository at this point in the history
This is consistently added across all packages.
  • Loading branch information
kzu committed Jul 22, 2024
1 parent ccc5f4a commit 2d8949b
Show file tree
Hide file tree
Showing 33 changed files with 430 additions and 347 deletions.
125 changes: 65 additions & 60 deletions src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,79 +10,84 @@
using Microsoft.CodeAnalysis.Text;
using Scriban;

namespace ThisAssembly
namespace ThisAssembly;

[Generator(LanguageNames.CSharp)]
public class AssemblyInfoGenerator : IIncrementalGenerator
{
[Generator]
public class AssemblyInfoGenerator : IIncrementalGenerator
static readonly HashSet<string> attributes =
[
nameof(AssemblyConfigurationAttribute),
nameof(AssemblyCompanyAttribute),
nameof(AssemblyCopyrightAttribute),
nameof(AssemblyTitleAttribute),
nameof(AssemblyDescriptionAttribute),
nameof(AssemblyProductAttribute),
nameof(AssemblyVersionAttribute),
nameof(AssemblyInformationalVersionAttribute),
nameof(AssemblyFileVersionAttribute),
];

public void Initialize(IncrementalGeneratorInitializationContext context)
{
static readonly HashSet<string> attributes =
[
nameof(AssemblyConfigurationAttribute),
nameof(AssemblyCompanyAttribute),
nameof(AssemblyCopyrightAttribute),
nameof(AssemblyTitleAttribute),
nameof(AssemblyDescriptionAttribute),
nameof(AssemblyProductAttribute),
nameof(AssemblyVersionAttribute),
nameof(AssemblyInformationalVersionAttribute),
nameof(AssemblyFileVersionAttribute),
];
var metadata = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => s is AttributeSyntax,
transform: static (ctx, token) => GetAttributes(ctx, token))
.Where(static m => m is not null)
.Select(static (m, _) => m!.Value)
.Collect();

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var metadata = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => s is AttributeSyntax,
transform: static (ctx, token) => GetAttributes(ctx, token))
.Where(static m => m is not null)
.Select(static (m, _) => m!.Value)
.Collect();
// Read the ThisAssemblyNamespace property or default to null
var right = context.AnalyzerConfigOptionsProvider
.Select((c, t) => c.GlobalOptions.TryGetValue("build_property.ThisAssemblyNamespace", out var ns) && !string.IsNullOrEmpty(ns) ? ns : null)
.Combine(context.ParseOptionsProvider);

context.RegisterSourceOutput(
metadata.Combine(context.ParseOptionsProvider),
GenerateSource);
}
context.RegisterSourceOutput(
metadata.Combine(right),
GenerateSource);
}

static KeyValuePair<string, string>? GetAttributes(GeneratorSyntaxContext ctx, CancellationToken token)
{
var attributeNode = (AttributeSyntax)ctx.Node;
static KeyValuePair<string, string>? GetAttributes(GeneratorSyntaxContext ctx, CancellationToken token)
{
var attributeNode = (AttributeSyntax)ctx.Node;

if (attributeNode.ArgumentList?.Arguments.Count != 1)
return null;
if (attributeNode.ArgumentList?.Arguments.Count != 1)
return null;

if (ctx.SemanticModel.GetSymbolInfo(attributeNode, token).Symbol is not IMethodSymbol ctor)
return null;
if (ctx.SemanticModel.GetSymbolInfo(attributeNode, token).Symbol is not IMethodSymbol ctor)
return null;

var attributeType = ctor.ContainingType;
if (attributeType == null)
return null;
var attributeType = ctor.ContainingType;
if (attributeType == null)
return null;

if (!attributes.Contains(attributeType.Name))
return null;
if (!attributes.Contains(attributeType.Name))
return null;

// Remove the "Assembly" prefix and "Attribute" suffix.
var key = attributeType.Name[8..^9];
var expr = attributeNode.ArgumentList!.Arguments[0].Expression;
var value = ctx.SemanticModel.GetConstantValue(expr, token).ToString();
// KeyValuePair is a struct and properly equatable for optimal caching in the generator.
return new KeyValuePair<string, string>(key, value);
}
// Remove the "Assembly" prefix and "Attribute" suffix.
var key = attributeType.Name[8..^9];
var expr = attributeNode.ArgumentList!.Arguments[0].Expression;
var value = ctx.SemanticModel.GetConstantValue(expr, token).ToString();
// KeyValuePair is a struct and properly equatable for optimal caching in the generator.
return new KeyValuePair<string, string>(key, value);
}

static void GenerateSource(SourceProductionContext spc, (ImmutableArray<KeyValuePair<string, string>> attributes, ParseOptions parse) arg)
{
var (attributes, parse) = arg;
static void GenerateSource(SourceProductionContext spc,
(ImmutableArray<KeyValuePair<string, string>> attributes, (string? ns, ParseOptions parse)) arg)
{
var (attributes, (ns, parse)) = arg;

var model = new Model(attributes.ToList());
if (parse is CSharpParseOptions cs && (int)cs.LanguageVersion >= 1100)
model.RawStrings = true;
var model = new Model(attributes.ToList(), ns);
if (parse is CSharpParseOptions cs && (int)cs.LanguageVersion >= 1100)
model.RawStrings = true;

var file = parse.Language.Replace("#", "Sharp") + ".sbntxt";
var template = Template.Parse(EmbeddedResource.GetContent(file), file);
var output = template.Render(model, member => member.Name);
var file = parse.Language.Replace("#", "Sharp") + ".sbntxt";
var template = Template.Parse(EmbeddedResource.GetContent(file), file);
var output = template.Render(model, member => member.Name);

spc.AddSource(
"ThisAssembly.AssemblyInfo.g.cs",
SourceText.From(output, Encoding.UTF8));
}
spc.AddSource(
"ThisAssembly.AssemblyInfo.g.cs",
SourceText.From(output, Encoding.UTF8));
}
}
6 changes: 4 additions & 2 deletions src/ThisAssembly.AssemblyInfo/CSharp.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@

using System.CodeDom.Compiler;
using System.Runtime.CompilerServices;
{{ if Namespace }}
namespace {{ Namespace }};
{{~ end ~}}

/// <summary>
/// Provides access to the current assembly information as pure constants,
/// without requiring reflection.
/// Provides access to the current assembly information.
/// </summary>
partial class ThisAssembly
{
Expand Down
18 changes: 10 additions & 8 deletions src/ThisAssembly.AssemblyInfo/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
using System.Linq;
using System.Reflection;

namespace ThisAssembly
namespace ThisAssembly;

public class Model
{
public class Model
{
public Model(IEnumerable<KeyValuePair<string, string>> properties) => Properties = properties.ToList();
public Model(IEnumerable<KeyValuePair<string, string>> properties, string? ns)
=> (Properties, Namespace)
= (properties.ToList(), ns);

public bool RawStrings { get; set; } = false;
public string Version => Assembly.GetExecutingAssembly().GetName().Version.ToString(3);
public string? Namespace { get; }
public bool RawStrings { get; set; } = false;
public string Version => Assembly.GetExecutingAssembly().GetName().Version.ToString(3);

public List<KeyValuePair<string, string>> Properties { get; }
}
public List<KeyValuePair<string, string>> Properties { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<!-- This generator supports VB too -->
<PackFolder>analyzers/dotnet/roslyn$(ThisAssemblyMinimumRoslynVersion)</PackFolder>
<CustomAfterMicrosoftCSharpTargets>$(MSBuildThisFileDirectory)..\SponsorLink\SponsorLink.Analyzer.targets</CustomAfterMicrosoftCSharpTargets>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<Import Project="..\..\buildTransitive\Devlooped.Sponsors.targets" Condition="Exists('..\..\buildTransitive\Devlooped.Sponsors.targets')"/>

<ItemGroup>
<CompilerVisibleProperty Include="ThisAssemblyNamespace" />

<!-- Make sure we're always private to the referencing project.
Prevents analyzers from "flowing out" of the referencing project. -->
<PackageReference Update="ThisAssembly.AssemblyInfo" PrivateAssets="all" PackTransitive="false" />
Expand Down
4 changes: 4 additions & 0 deletions src/ThisAssembly.AssemblyInfo/Visual Basic.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
''' </auto-generated>
'''------------------------------------------------------------------------------

{{ if Namespace }}
Namespace {{ Namespace }}
{{ else }}
Namespace Global
{{ end }}
''' <summary>
''' Provides access to the current assembly information as pure constants,
''' without requiring reflection.
Expand Down
3 changes: 3 additions & 0 deletions src/ThisAssembly.AssemblyInfo/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ on the `ThisAssembly.Info` class.

![](https://raw.githubusercontent.com/devlooped/ThisAssembly/main/img/ThisAssembly.AssemblyInfo.png)

Set the `$(ThisAssemblyNamespace)` MSBuild property to set the root namespace of the
generated `ThisAssembly` class. Otherwise, it will be generated in the global namespace.

<!-- #content -->
<!-- include https://github.com/devlooped/sponsors/raw/main/footer.md -->
# Sponsors
Expand Down
9 changes: 9 additions & 0 deletions src/ThisAssembly.Constants/CSharp.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,17 @@

using System;
using System.Globalization;
{{ if Namespace }}
namespace {{ Namespace }};
{{~ end ~}}

/// <summary>
/// Provides access to the current assembly information.
/// </summary>
partial class ThisAssembly
{
/// <summary>
/// Provides access project-defined constants.
/// </summary>
{{ render RootArea }}
}
127 changes: 71 additions & 56 deletions src/ThisAssembly.Constants/ConstantsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,88 @@
using Microsoft.CodeAnalysis.Text;
using Scriban;

namespace ThisAssembly
namespace ThisAssembly;

[Generator(LanguageNames.CSharp)]
public class ConstantsGenerator : IIncrementalGenerator
{
[Generator]
public class ConstantsGenerator : IIncrementalGenerator
public void Initialize(IncrementalGeneratorInitializationContext context)
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var files = context.AdditionalTextsProvider
.Combine(context.AnalyzerConfigOptionsProvider)
.Where(x =>
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.ItemType", out var itemType)
&& itemType == "Constant")
.Select((x, ct) =>
{
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Value", out var value);
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Comment", out var comment);
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Root", out var root);
var files = context.AdditionalTextsProvider
.Combine(context.AnalyzerConfigOptionsProvider)
.Where(x =>
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.ItemType", out var itemType)
&& itemType == "Constant")
.Select((x, ct) =>
{
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Value", out var value);
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Comment", out var comment);
x.Right.GetOptions(x.Left).TryGetValue("build_metadata.Constant.Root", out var root);

// Revert auto-escaping due to https://github.com/dotnet/roslyn/issues/51692
if (value != null && value.StartsWith("|") && value.EndsWith("|"))
value = value[1..^1].Replace('|', ';');
// Revert auto-escaping due to https://github.com/dotnet/roslyn/issues/51692
if (value != null && value.StartsWith("|") && value.EndsWith("|"))
value = value[1..^1].Replace('|', ';');

return (
name: Path.GetFileName(x.Left.Path),
value: value ?? "",
comment: string.IsNullOrWhiteSpace(comment) ? null : comment,
root: string.IsNullOrWhiteSpace(root) ? "Constants" : root!);
})
.Combine(context.ParseOptionsProvider);
return (
name: Path.GetFileName(x.Left.Path),
value: value ?? "",
comment: string.IsNullOrWhiteSpace(comment) ? null : comment,
root: string.IsNullOrWhiteSpace(root) ? "Constants" : root!);
});

context.RegisterSourceOutput(
files,
GenerateConstant);
// Read the ThisAssemblyNamespace property or default to null
var right = context.AnalyzerConfigOptionsProvider
.Select((c, t) => c.GlobalOptions.TryGetValue("build_property.ThisAssemblyNamespace", out var ns) && !string.IsNullOrEmpty(ns) ? ns : null)
.Combine(context.ParseOptionsProvider);

}
context.RegisterSourceOutput(
files.Combine(right),
GenerateConstant);

void GenerateConstant(SourceProductionContext spc, ((string name, string value, string? comment, string root), ParseOptions parse) args)
{
var ((name, value, comment, root), parse) = args;
}

var rootArea = Area.Load(new List<Constant> { new Constant(name, value, comment), }, root);
var file = parse.Language.Replace("#", "Sharp") + ".sbntxt";
var template = Template.Parse(EmbeddedResource.GetContent(file), file);
var model = new Model(rootArea);
if (parse is CSharpParseOptions cs && (int)cs.LanguageVersion >= 1100)
model.RawStrings = true;
void GenerateConstant(SourceProductionContext spc, ((string name, string value, string? comment, string root), (string? ns, ParseOptions parse)) args)
{
var ((name, value, comment, root), (ns, parse)) = args;
var cs = (CSharpParseOptions)parse;

var output = template.Render(model, member => member.Name);
if (!string.IsNullOrWhiteSpace(ns) &&
cs.LanguageVersion < LanguageVersion.CSharp10)
{
spc.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor("TA002", "ThisAssemblyNamespace requires C# 8.0 or higher",
"ThisAssemblyNamespace requires C# 8.0 or higher", "ThisAssembly", DiagnosticSeverity.Error, true),
Location.None));
return;
}

// Apply formatting since indenting isn't that nice in Scriban when rendering nested
// structures via functions.
if (parse.Language == LanguageNames.CSharp)
{
output = SyntaxFactory.ParseCompilationUnit(output)
.NormalizeWhitespace()
.GetText()
.ToString();
}
//else if (language == LanguageNames.VisualBasic)
//{
// output = Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory.ParseCompilationUnit(output)
// .NormalizeWhitespace()
// .GetText()
// .ToString();
//}
var rootArea = Area.Load(new List<Constant> { new Constant(name, value, comment), }, root);
// For now, we only support C# though
var file = parse.Language.Replace("#", "Sharp") + ".sbntxt";
var template = Template.Parse(EmbeddedResource.GetContent(file), file);
var model = new Model(rootArea, ns);
if ((int)cs.LanguageVersion >= 1100)
model.RawStrings = true;

spc.AddSource($"{name}.g.cs", SourceText.From(output, Encoding.UTF8));
var output = template.Render(model, member => member.Name);

// Apply formatting since indenting isn't that nice in Scriban when rendering nested
// structures via functions.
if (parse.Language == LanguageNames.CSharp)
{
output = SyntaxFactory.ParseCompilationUnit(output)
.NormalizeWhitespace()
.GetText()
.ToString();
}
//else if (language == LanguageNames.VisualBasic)
//{
// output = Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory.ParseCompilationUnit(output)
// .NormalizeWhitespace()
// .GetText()
// .ToString();
//}

spc.AddSource($"{name}.g.cs", SourceText.From(output, Encoding.UTF8));
}
}
Loading

0 comments on commit 2d8949b

Please sign in to comment.