diff --git a/src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs b/src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs index da34752b..ffbe6e28 100644 --- a/src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs +++ b/src/ThisAssembly.AssemblyInfo/AssemblyInfoGenerator.cs @@ -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 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 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? GetAttributes(GeneratorSyntaxContext ctx, CancellationToken token) - { - var attributeNode = (AttributeSyntax)ctx.Node; + static KeyValuePair? 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(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(key, value); + } - static void GenerateSource(SourceProductionContext spc, (ImmutableArray> attributes, ParseOptions parse) arg) - { - var (attributes, parse) = arg; + static void GenerateSource(SourceProductionContext spc, + (ImmutableArray> 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)); } } diff --git a/src/ThisAssembly.AssemblyInfo/CSharp.sbntxt b/src/ThisAssembly.AssemblyInfo/CSharp.sbntxt index d4e035e9..c4c7ed08 100644 --- a/src/ThisAssembly.AssemblyInfo/CSharp.sbntxt +++ b/src/ThisAssembly.AssemblyInfo/CSharp.sbntxt @@ -9,10 +9,12 @@ using System.CodeDom.Compiler; using System.Runtime.CompilerServices; +{{ if Namespace }} +namespace {{ Namespace }}; +{{~ end ~}} /// -/// Provides access to the current assembly information as pure constants, -/// without requiring reflection. +/// Provides access to the current assembly information. /// partial class ThisAssembly { diff --git a/src/ThisAssembly.AssemblyInfo/Model.cs b/src/ThisAssembly.AssemblyInfo/Model.cs index 0e00aef9..da0ab9c2 100644 --- a/src/ThisAssembly.AssemblyInfo/Model.cs +++ b/src/ThisAssembly.AssemblyInfo/Model.cs @@ -2,15 +2,17 @@ using System.Linq; using System.Reflection; -namespace ThisAssembly +namespace ThisAssembly; + +public class Model { - public class Model - { - public Model(IEnumerable> properties) => Properties = properties.ToList(); + public Model(IEnumerable> 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> Properties { get; } - } + public List> Properties { get; } } diff --git a/src/ThisAssembly.AssemblyInfo/ThisAssembly.AssemblyInfo.csproj b/src/ThisAssembly.AssemblyInfo/ThisAssembly.AssemblyInfo.csproj index 75754930..abe5ba3d 100644 --- a/src/ThisAssembly.AssemblyInfo/ThisAssembly.AssemblyInfo.csproj +++ b/src/ThisAssembly.AssemblyInfo/ThisAssembly.AssemblyInfo.csproj @@ -4,6 +4,8 @@ netstandard2.0 latest true + + analyzers/dotnet/roslyn$(ThisAssemblyMinimumRoslynVersion) $(MSBuildThisFileDirectory)..\SponsorLink\SponsorLink.Analyzer.targets diff --git a/src/ThisAssembly.AssemblyInfo/ThisAssembly.AssemblyInfo.targets b/src/ThisAssembly.AssemblyInfo/ThisAssembly.AssemblyInfo.targets index acaa1d99..2b93a728 100644 --- a/src/ThisAssembly.AssemblyInfo/ThisAssembly.AssemblyInfo.targets +++ b/src/ThisAssembly.AssemblyInfo/ThisAssembly.AssemblyInfo.targets @@ -2,6 +2,8 @@ + + diff --git a/src/ThisAssembly.AssemblyInfo/Visual Basic.sbntxt b/src/ThisAssembly.AssemblyInfo/Visual Basic.sbntxt index dce6c308..510363f9 100644 --- a/src/ThisAssembly.AssemblyInfo/Visual Basic.sbntxt +++ b/src/ThisAssembly.AssemblyInfo/Visual Basic.sbntxt @@ -7,7 +7,11 @@ ''' '''------------------------------------------------------------------------------ +{{ if Namespace }} +Namespace {{ Namespace }} +{{ else }} Namespace Global +{{ end }} ''' ''' Provides access to the current assembly information as pure constants, ''' without requiring reflection. diff --git a/src/ThisAssembly.AssemblyInfo/readme.md b/src/ThisAssembly.AssemblyInfo/readme.md index c37070b4..4a9b9db8 100644 --- a/src/ThisAssembly.AssemblyInfo/readme.md +++ b/src/ThisAssembly.AssemblyInfo/readme.md @@ -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. + # Sponsors diff --git a/src/ThisAssembly.Constants/CSharp.sbntxt b/src/ThisAssembly.Constants/CSharp.sbntxt index 888927d2..a5af0287 100644 --- a/src/ThisAssembly.Constants/CSharp.sbntxt +++ b/src/ThisAssembly.Constants/CSharp.sbntxt @@ -40,8 +40,17 @@ using System; using System.Globalization; +{{ if Namespace }} +namespace {{ Namespace }}; +{{~ end ~}} +/// +/// Provides access to the current assembly information. +/// partial class ThisAssembly { + /// + /// Provides access project-defined constants. + /// {{ render RootArea }} } \ No newline at end of file diff --git a/src/ThisAssembly.Constants/ConstantsGenerator.cs b/src/ThisAssembly.Constants/ConstantsGenerator.cs index 2efc6121..9cd784ac 100644 --- a/src/ThisAssembly.Constants/ConstantsGenerator.cs +++ b/src/ThisAssembly.Constants/ConstantsGenerator.cs @@ -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 { 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 { 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)); } } diff --git a/src/ThisAssembly.Constants/Model.cs b/src/ThisAssembly.Constants/Model.cs index b48f7631..8945f776 100644 --- a/src/ThisAssembly.Constants/Model.cs +++ b/src/ThisAssembly.Constants/Model.cs @@ -6,8 +6,10 @@ using System.Text.RegularExpressions; using Microsoft.CodeAnalysis.CSharp; +namespace ThisAssembly; + [DebuggerDisplay("Values = {RootArea.Values.Count}")] -record Model(Area RootArea) +record Model(Area RootArea, string? Namespace) { public bool RawStrings { get; set; } = false; public string Version => Assembly.GetExecutingAssembly().GetName().Version.ToString(3); diff --git a/src/ThisAssembly.Constants/ThisAssembly.Constants.targets b/src/ThisAssembly.Constants/ThisAssembly.Constants.targets index 046a8f76..ca59b16d 100644 --- a/src/ThisAssembly.Constants/ThisAssembly.Constants.targets +++ b/src/ThisAssembly.Constants/ThisAssembly.Constants.targets @@ -2,6 +2,8 @@ + + diff --git a/src/ThisAssembly.Constants/readme.md b/src/ThisAssembly.Constants/readme.md index 121e389e..74890a96 100644 --- a/src/ThisAssembly.Constants/readme.md +++ b/src/ThisAssembly.Constants/readme.md @@ -25,6 +25,9 @@ Which results in: ![](https://raw.githubusercontent.com/devlooped/ThisAssembly/main/img/ThisAssembly.Constants2.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. + # Sponsors diff --git a/src/ThisAssembly.Git/readme.md b/src/ThisAssembly.Git/readme.md index b04fa938..02794172 100644 --- a/src/ThisAssembly.Git/readme.md +++ b/src/ThisAssembly.Git/readme.md @@ -66,6 +66,9 @@ packaging experience possible: ``` +Set the `$(ThisAssemblyNamespace)` MSBuild property to set the root namespace of the +generated `ThisAssembly` class. Otherwise, it will be generated in the global namespace. + diff --git a/src/ThisAssembly.Metadata/readme.md b/src/ThisAssembly.Metadata/readme.md index b5bd068a..dc6a5dae 100644 --- a/src/ThisAssembly.Metadata/readme.md +++ b/src/ThisAssembly.Metadata/readme.md @@ -21,6 +21,9 @@ The metadata attribute can alternatively be declared using MSBuild syntax in the ``` +Set the `$(ThisAssemblyNamespace)` MSBuild property to set the root namespace of the +generated `ThisAssembly` class. Otherwise, it will be generated in the global namespace. + # Sponsors diff --git a/src/ThisAssembly.Project/CSharp.sbntxt b/src/ThisAssembly.Project/CSharp.sbntxt index b01110e5..ed3ef12e 100644 --- a/src/ThisAssembly.Project/CSharp.sbntxt +++ b/src/ThisAssembly.Project/CSharp.sbntxt @@ -9,10 +9,12 @@ using System.CodeDom.Compiler; using System.Runtime.CompilerServices; +{{ if Namespace }} +namespace {{ Namespace }}; +{{~ end ~}} /// -/// Provides access to the current assembly information as pure constants, -/// without requiring reflection. +/// Provides access to the current assembly information. /// partial class ThisAssembly { diff --git a/src/ThisAssembly.Project/Model.cs b/src/ThisAssembly.Project/Model.cs index 0e00aef9..da0ab9c2 100644 --- a/src/ThisAssembly.Project/Model.cs +++ b/src/ThisAssembly.Project/Model.cs @@ -2,15 +2,17 @@ using System.Linq; using System.Reflection; -namespace ThisAssembly +namespace ThisAssembly; + +public class Model { - public class Model - { - public Model(IEnumerable> properties) => Properties = properties.ToList(); + public Model(IEnumerable> 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> Properties { get; } - } + public List> Properties { get; } } diff --git a/src/ThisAssembly.Project/ProjectPropertyGenerator.cs b/src/ThisAssembly.Project/ProjectPropertyGenerator.cs index 85d16d64..e7da9984 100644 --- a/src/ThisAssembly.Project/ProjectPropertyGenerator.cs +++ b/src/ThisAssembly.Project/ProjectPropertyGenerator.cs @@ -8,48 +8,52 @@ using Microsoft.CodeAnalysis.Text; using Scriban; -namespace ThisAssembly +namespace ThisAssembly; + +[Generator] +public class ProjectPropertyGenerator : IIncrementalGenerator { - [Generator] - public class ProjectPropertyGenerator : IIncrementalGenerator + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var properties = context.AnalyzerConfigOptionsProvider + .SelectMany((p, ct) => + { + var go = p.GlobalOptions; + if (!go.TryGetValue("build_property.ThisAssemblyProject", out var values)) + return Array.Empty>(); + + return values.Split('|') + .Select(prop => new KeyValuePair( + prop, + go.TryGetValue("build_property." + prop, out var value) ? value : null)) + .Where(pair => pair.Value != null); + }) + .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( + properties.Combine(right), + GenerateSource); + } + + void GenerateSource(SourceProductionContext spc, (ImmutableArray> properties, (string? ns, ParseOptions parse)) arg) { - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var properties = context.AnalyzerConfigOptionsProvider - .SelectMany((p, ct) => - { - var go = p.GlobalOptions; - if (!go.TryGetValue("build_property.ThisAssemblyProject", out var values)) - return Array.Empty>(); - - return values.Split('|') - .Select(prop => new KeyValuePair( - prop, - go.TryGetValue("build_property." + prop, out var value) ? value : null)) - .Where(pair => pair.Value != null); - }) - .Collect(); - - context.RegisterSourceOutput( - properties.Combine(context.ParseOptionsProvider), - GenerateSource); - } - - void GenerateSource(SourceProductionContext spc, (ImmutableArray> properties, ParseOptions parse) arg) - { - var (properties, parse) = arg; - - var model = new Model(properties); - 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); - - spc.AddSource( - "ThisAssembly.Property.g.cs", - SourceText.From(output, Encoding.UTF8)); - } + var (properties, (ns, parse)) = arg; + + var model = new Model(properties, 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); + + spc.AddSource( + "ThisAssembly.Property.g.cs", + SourceText.From(output, Encoding.UTF8)); } } diff --git a/src/ThisAssembly.Project/ThisAssembly.Project.targets b/src/ThisAssembly.Project/ThisAssembly.Project.targets index 483e80f7..bafda7ad 100644 --- a/src/ThisAssembly.Project/ThisAssembly.Project.targets +++ b/src/ThisAssembly.Project/ThisAssembly.Project.targets @@ -2,6 +2,8 @@ + + diff --git a/src/ThisAssembly.Project/Visual Basic.sbntxt b/src/ThisAssembly.Project/Visual Basic.sbntxt index 88cc3b09..6d3c4d52 100644 --- a/src/ThisAssembly.Project/Visual Basic.sbntxt +++ b/src/ThisAssembly.Project/Visual Basic.sbntxt @@ -7,10 +7,13 @@ ''' '''------------------------------------------------------------------------------ +{{ if Namespace }} +Namespace {{ Namespace }} +{{ else }} Namespace Global +{{ end }} ''' - ''' Provides access to the current assembly information as pure constants, - ''' without requiring reflection. + ''' Provides access to the current assembly information. ''' Partial Class ThisAssembly ''' diff --git a/src/ThisAssembly.Project/readme.md b/src/ThisAssembly.Project/readme.md index b81f3a77..5a308aa0 100644 --- a/src/ThisAssembly.Project/readme.md +++ b/src/ThisAssembly.Project/readme.md @@ -16,6 +16,9 @@ them as `ProjectProperty` MSBuild items in the project file, such as: ![](https://raw.githubusercontent.com/devlooped/ThisAssembly/main/img/ThisAssembly.Project.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. + # Sponsors diff --git a/src/ThisAssembly.Resources/CSharp.sbntxt b/src/ThisAssembly.Resources/CSharp.sbntxt index eeee15a4..c376a41d 100644 --- a/src/ThisAssembly.Resources/CSharp.sbntxt +++ b/src/ThisAssembly.Resources/CSharp.sbntxt @@ -45,8 +45,17 @@ using System; using System.IO; +{{ if Namespace }} +namespace {{ Namespace }}; +{{~ end ~}} +/// +/// Provides access to the current assembly information. +/// partial class ThisAssembly { + /// + /// Provides access to assembly resources. + /// {{ render RootArea }} } diff --git a/src/ThisAssembly.Resources/Model.cs b/src/ThisAssembly.Resources/Model.cs index da189b71..47333a1b 100644 --- a/src/ThisAssembly.Resources/Model.cs +++ b/src/ThisAssembly.Resources/Model.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; + +namespace ThisAssembly; [DebuggerDisplay("Values = {RootArea.Values.Count}")] -record Model(Area RootArea) +record Model(Area RootArea, string? Namespace) { public string Version => Assembly.GetExecutingAssembly().GetName().Version.ToString(3); } diff --git a/src/ThisAssembly.Resources/ResourcesGenerator.cs b/src/ThisAssembly.Resources/ResourcesGenerator.cs index 0e0d4e28..ddff68e8 100644 --- a/src/ThisAssembly.Resources/ResourcesGenerator.cs +++ b/src/ThisAssembly.Resources/ResourcesGenerator.cs @@ -7,99 +7,100 @@ using Microsoft.CodeAnalysis.Text; using Scriban; -namespace ThisAssembly +namespace ThisAssembly; + +[Generator(LanguageNames.CSharp)] +public class ResourcesGenerator : IIncrementalGenerator { - [Generator(LanguageNames.CSharp)] - public class ResourcesGenerator : IIncrementalGenerator + public void Initialize(IncrementalGeneratorInitializationContext context) { - public void Initialize(IncrementalGeneratorInitializationContext context) - { - context.RegisterPostInitializationOutput( - spc => spc.AddSource( - "ThisAssembly.EmbeddedResource.cs", - SourceText.From(EmbeddedResource.GetContent("EmbeddedResource.cs"), Encoding.UTF8))); + context.RegisterPostInitializationOutput( + spc => spc.AddSource( + "ThisAssembly.EmbeddedResource.cs", + SourceText.From(EmbeddedResource.GetContent("EmbeddedResource.cs"), Encoding.UTF8))); - var files = context.AdditionalTextsProvider - .Combine(context.AnalyzerConfigOptionsProvider) - .Where(x => - x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.ThisAssemblyResource", out var assemblyResource) - && bool.TryParse(assemblyResource, out var isAssemblyResource) && isAssemblyResource) - .Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var value) && value != null) - .Select((x, ct) => + var files = context.AdditionalTextsProvider + .Combine(context.AnalyzerConfigOptionsProvider) + .Where(x => + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.ThisAssemblyResource", out var assemblyResource) + && bool.TryParse(assemblyResource, out var isAssemblyResource) && isAssemblyResource) + .Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var value) && value != null) + .Select((x, ct) => + { + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var resourceName); + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Kind", out var kind); + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Comment", out var comment); + return (resourceName: resourceName!, kind, comment: string.IsNullOrWhiteSpace(comment) ? null : comment); + }) + .Collect() + .Combine(context.AnalyzerConfigOptionsProvider + .SelectMany((p, _) => { - x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Value", out var resourceName); - x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Kind", out var kind); - x.Right.GetOptions(x.Left).TryGetValue("build_metadata.EmbeddedResource.Comment", out var comment); - return (resourceName: resourceName!, kind, comment: string.IsNullOrWhiteSpace(comment) ? null : comment); + if (!p.GlobalOptions.TryGetValue("build_property.EmbeddedResourceStringExtensions", out var extensions) || + extensions == null) + return Array.Empty(); + + return extensions.Split('|'); }) - .Collect() - .Combine(context.AnalyzerConfigOptionsProvider - .SelectMany((p, _) => - { - if (!p.GlobalOptions.TryGetValue("build_property.EmbeddedResourceStringExtensions", out var extensions) || - extensions == null) - return Array.Empty(); + .WithComparer(StringComparer.OrdinalIgnoreCase) + .Collect()); - return extensions.Split('|'); - }) - .WithComparer(StringComparer.OrdinalIgnoreCase) - .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); - context.RegisterSourceOutput( - files, - GenerateSource); - } + context.RegisterSourceOutput( + files.Combine(right), + GenerateSource); + } - static void GenerateSource( - SourceProductionContext spc, - ( - ImmutableArray<(string resourceName, string? kind, string? comment)> files, - ImmutableArray extensions) arg2) - { - var (files, extensions) = arg2; + static void GenerateSource(SourceProductionContext spc, + ((ImmutableArray<(string resourceName, string? kind, string? comment)> files, + ImmutableArray extensions), string? ns) args) + { + var ((files, extensions), ns) = args; - var file = "CSharp.sbntxt"; - var template = Template.Parse(EmbeddedResource.GetContent(file), file); + var file = "CSharp.sbntxt"; + var template = Template.Parse(EmbeddedResource.GetContent(file), file); - var groupsWithoutExtensions = files - .GroupBy(f => Path.Combine( - Path.GetDirectoryName(f.resourceName), - Path.GetFileNameWithoutExtension(f.resourceName))); - foreach (var group in groupsWithoutExtensions) - { - var basePath = group.Key; - var resources = group - .Select(f => - { - var isText = f.kind?.Equals("text", StringComparison.OrdinalIgnoreCase) == true || - extensions.Contains(Path.GetExtension(f.resourceName)); - var name = group.Count() == 1 - ? Path.GetFileNameWithoutExtension(f.resourceName) - : Path.GetExtension(f.resourceName)[1..]; - return new Resource( - Name: name, - Comment: f.comment, - IsText: isText, - Path: f.resourceName); - }) - .ToList(); + var groupsWithoutExtensions = files + .GroupBy(f => Path.Combine( + Path.GetDirectoryName(f.resourceName), + Path.GetFileNameWithoutExtension(f.resourceName))); + foreach (var group in groupsWithoutExtensions) + { + var basePath = group.Key; + var resources = group + .Select(f => + { + var isText = f.kind?.Equals("text", StringComparison.OrdinalIgnoreCase) == true || + extensions.Contains(Path.GetExtension(f.resourceName)); + var name = group.Count() == 1 + ? Path.GetFileNameWithoutExtension(f.resourceName) + : Path.GetExtension(f.resourceName)[1..]; + return new Resource( + Name: name, + Comment: f.comment, + IsText: isText, + Path: f.resourceName); + }) + .ToList(); - var root = Area.Load(basePath, resources); - var model = new Model(root); + var root = Area.Load(basePath, resources); + var model = new Model(root, ns); - var output = template.Render(model, member => member.Name); + var output = template.Render(model, member => member.Name); - // Apply formatting since indenting isn't that nice in Scriban when rendering nested - // structures via functions. - output = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParseCompilationUnit(output) - .NormalizeWhitespace() - .GetText() - .ToString(); + // Apply formatting since indenting isn't that nice in Scriban when rendering nested + // structures via functions. + output = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParseCompilationUnit(output) + .NormalizeWhitespace() + .GetText() + .ToString(); - spc.AddSource( - $"{basePath.Replace('\\', '.').Replace('/', '.')}.g.cs", - SourceText.From(output, Encoding.UTF8)); - } + spc.AddSource( + $"{basePath.Replace('\\', '.').Replace('/', '.')}.g.cs", + SourceText.From(output, Encoding.UTF8)); } } } diff --git a/src/ThisAssembly.Resources/readme.md b/src/ThisAssembly.Resources/readme.md index b3c7d317..8af58892 100644 --- a/src/ThisAssembly.Resources/readme.md +++ b/src/ThisAssembly.Resources/readme.md @@ -42,6 +42,9 @@ treated as a text file: You can also add a `Comment` item metadata attribute, which will be used as the `` XML doc for the generated member. +Set the `$(ThisAssemblyNamespace)` MSBuild property to set the root namespace of the +generated `ThisAssembly` class. Otherwise, it will be generated in the global namespace. + # Sponsors diff --git a/src/ThisAssembly.Strings/CSharp.sbntxt b/src/ThisAssembly.Strings/CSharp.sbntxt index df7ba476..039d67b6 100644 --- a/src/ThisAssembly.Strings/CSharp.sbntxt +++ b/src/ThisAssembly.Strings/CSharp.sbntxt @@ -68,8 +68,17 @@ using System; using System.Globalization; +{{ if Namespace }} +namespace {{ Namespace }}; +{{~ end ~}} +/// +/// Provides access to the current assembly information. +/// partial class ThisAssembly { + /// + /// Provides access to the assembly strings. + /// {{ render RootArea ResourceName }} } \ No newline at end of file diff --git a/src/ThisAssembly.Strings/Model.cs b/src/ThisAssembly.Strings/Model.cs index 561c74ba..46475a61 100644 --- a/src/ThisAssembly.Strings/Model.cs +++ b/src/ThisAssembly.Strings/Model.cs @@ -7,7 +7,7 @@ using System.Xml.Linq; [DebuggerDisplay("ResourceName = {ResourceName}, Values = {RootArea.Values.Count}")] -record Model(ResourceArea RootArea, string ResourceName) +record Model(ResourceArea RootArea, string ResourceName, string? Namespace) { public string? Version => Assembly.GetExecutingAssembly().GetName().Version?.ToString(3); } diff --git a/src/ThisAssembly.Strings/StringsGenerator.cs b/src/ThisAssembly.Strings/StringsGenerator.cs index f00b642f..73aae59f 100644 --- a/src/ThisAssembly.Strings/StringsGenerator.cs +++ b/src/ThisAssembly.Strings/StringsGenerator.cs @@ -1,83 +1,73 @@ using System; -using System.Collections.Generic; -using System.Collections.Immutable; using System.IO; using System.Linq; +using System.Resources; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using Scriban; -namespace ThisAssembly +namespace ThisAssembly; + +[Generator(LanguageNames.CSharp)] +public class StringsGenerator : IIncrementalGenerator { - [Generator] - public class StringsGenerator : IIncrementalGenerator + public void Initialize(IncrementalGeneratorInitializationContext context) { - public void Initialize(IncrementalGeneratorInitializationContext context) - { - context.RegisterSourceOutput( - context.CompilationProvider.Select((c, _) => c.Language), - (spc, l) => - { - var extension = l switch - { - LanguageNames.CSharp => "cs", - LanguageNames.VisualBasic => "vb", - _ => throw new NotSupportedException() - }; + // 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.CompilationProvider.Select((s, _) => s.Language)); + + context.RegisterSourceOutput( + right, + (spc, args) => + { + var (ns, _) = args; + + var strings = EmbeddedResource.GetContent($"ThisAssembly.Strings.sbntxt"); + var template = Template.Parse(strings); + var output = template.Render(new { Namespace = ns }, member => member.Name); + spc.AddSource("ThisAssembly.Strings.g.cs", SourceText.From(output, Encoding.UTF8)); + }); - var strings = EmbeddedResource.GetContent($"ThisAssembly.Strings.{extension}"); - spc.AddSource($"ThisAssembly.Strings.g.{extension}", SourceText.From(strings, Encoding.UTF8)); - }); + var files = context.AdditionalTextsProvider + .Combine(context.AnalyzerConfigOptionsProvider) + .Where(x => + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.ResxCode.ThisAssemblyStrings", out var resourceString) + && bool.TryParse(resourceString, out var isResourceString) && isResourceString) + .Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.ResxCode.ManifestResourceName", out var value) && value != null) + .Select((x, ct) => + { + x.Right.GetOptions(x.Left).TryGetValue("build_metadata.ResxCode.ManifestResourceName", out var resourceName); + return (fileName: Path.GetFileName(x.Left.Path), text: x.Left.GetText(ct), resourceName!); + }) + .Where(x => x.text != null); - var files = context.AdditionalTextsProvider - .Combine(context.AnalyzerConfigOptionsProvider) - .Where(x => - x.Right.GetOptions(x.Left).TryGetValue("build_metadata.ResxCode.ThisAssemblyStrings", out var resourceString) - && bool.TryParse(resourceString, out var isResourceString) && isResourceString) - .Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.ResxCode.ManifestResourceName", out var value) && value != null) - .Select((x, ct) => - { - x.Right.GetOptions(x.Left).TryGetValue("build_metadata.ResxCode.ManifestResourceName", out var resourceName); - return (fileName: Path.GetFileName(x.Left.Path), text: x.Left.GetText(ct), resourceName!); - }) - .Where(x => x.text != null); - context.RegisterSourceOutput( - files.Combine(context.CompilationProvider.Select((s, _) => s.Language)), - GenerateSource); - } + context.RegisterSourceOutput( + files.Combine(right), + GenerateSource); + } - static void GenerateSource(SourceProductionContext spc, ((string fileName, SourceText? text, string resourceName), string language) arg2) - { - var ((fileName, resourceText, resourceName), language) = arg2; + static void GenerateSource(SourceProductionContext spc, + ((string fileName, SourceText? text, string resourceName), (string? ns, string language)) arg) + { + var ((fileName, resourceText, resourceName), (ns, language)) = arg; - var file = language.Replace("#", "Sharp") + ".sbntxt"; - var template = Template.Parse(EmbeddedResource.GetContent(file), file); + var file = language.Replace("#", "Sharp") + ".sbntxt"; + var template = Template.Parse(EmbeddedResource.GetContent(file), file); - var rootArea = ResourceFile.LoadText(resourceText!.ToString(), "Strings"); - var model = new Model(rootArea, resourceName); + var rootArea = ResourceFile.LoadText(resourceText!.ToString(), "Strings"); + var model = new Model(rootArea, resourceName, ns); - var output = template.Render(model, member => member.Name); + 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 (language == LanguageNames.CSharp) - { - output = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParseCompilationUnit(output) - .NormalizeWhitespace() - .GetText() - .ToString(); - } - //else if (language == LanguageNames.VisualBasic) - //{ - // output = Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory.ParseCompilationUnit(output) - // .NormalizeWhitespace() - // .GetText() - // .ToString(); - //} + output = Microsoft.CodeAnalysis.CSharp.SyntaxFactory.ParseCompilationUnit(output) + .NormalizeWhitespace() + .GetText() + .ToString(); - spc.AddSource(resourceName, SourceText.From(output, Encoding.UTF8)); - } + spc.AddSource(resourceName, SourceText.From(output, Encoding.UTF8)); } } diff --git a/src/ThisAssembly.Strings/ThisAssembly.Strings.csproj b/src/ThisAssembly.Strings/ThisAssembly.Strings.csproj index 735378bb..44dcb871 100644 --- a/src/ThisAssembly.Strings/ThisAssembly.Strings.csproj +++ b/src/ThisAssembly.Strings/ThisAssembly.Strings.csproj @@ -35,15 +35,6 @@ such as "Hello {name}". - - - - - - - - - diff --git a/src/ThisAssembly.Strings/ThisAssembly.Strings.cs b/src/ThisAssembly.Strings/ThisAssembly.Strings.sbntxt similarity index 52% rename from src/ThisAssembly.Strings/ThisAssembly.Strings.cs rename to src/ThisAssembly.Strings/ThisAssembly.Strings.sbntxt index 4338747d..a48df3a4 100644 --- a/src/ThisAssembly.Strings/ThisAssembly.Strings.cs +++ b/src/ThisAssembly.Strings/ThisAssembly.Strings.sbntxt @@ -1,10 +1,22 @@ -using System.Collections.Concurrent; +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// ThisAssembly.Strings: {{ Version }} +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +using System.Collections.Concurrent; using System.Resources; using System.Runtime.CompilerServices; +{{ if Namespace }} +namespace {{ Namespace }}; +{{~ end ~}} /// -/// Provides access to the current assembly information as pure constants, -/// without requiring reflection. +/// Provides access to the current assembly information. /// partial class ThisAssembly { diff --git a/src/ThisAssembly.Strings/Visual Basic.sbntxt b/src/ThisAssembly.Strings/Visual Basic.sbntxt deleted file mode 100644 index 12e57cb0..00000000 --- a/src/ThisAssembly.Strings/Visual Basic.sbntxt +++ /dev/null @@ -1,10 +0,0 @@ -'''------------------------------------------------------------------------------ -''' -''' This code was generated by a tool. -''' -''' Changes to this file may cause incorrect behavior And will be lost if -''' the code Is regenerated. -''' -'''------------------------------------------------------------------------------ - -' TODO \ No newline at end of file diff --git a/src/ThisAssembly.Strings/readme.md b/src/ThisAssembly.Strings/readme.md index 3587f8f3..1746a286 100644 --- a/src/ThisAssembly.Strings/readme.md +++ b/src/ThisAssembly.Strings/readme.md @@ -67,6 +67,9 @@ partial class ThisAssembly } ``` +Set the `$(ThisAssemblyNamespace)` MSBuild property to set the root namespace of the +generated `ThisAssembly` class. Otherwise, it will be generated in the global namespace. + # Sponsors diff --git a/src/ThisAssembly.Tests/Tests.cs b/src/ThisAssembly.Tests/Tests.cs index f2376293..b29f5a85 100644 --- a/src/ThisAssembly.Tests/Tests.cs +++ b/src/ThisAssembly.Tests/Tests.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO; +using Devlooped; using Xunit; using Xunit.Abstractions; diff --git a/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj b/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj index bf4b66e4..6e72472b 100644 --- a/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj +++ b/src/ThisAssembly.Tests/ThisAssembly.Tests.csproj @@ -3,6 +3,7 @@ false net8.0 + Devlooped A Description with a newline and