From 0188e84170b676dc0a45a8921ab19fbcc9c377f5 Mon Sep 17 00:00:00 2001 From: Rasmus Melchior Jacobsen Date: Tue, 4 Jun 2024 15:21:59 +0200 Subject: [PATCH] Add StringEmbed attribute to embed strings as literals from files --- src/EmbedResourceCSharp/DiagnosticsHelper.cs | 8 ++ src/EmbedResourceCSharp/Generator.cs | 109 ++++++++++++++++++- src/EmbedResourceCSharp/Utility.cs | 34 +++++- tests/FileTests/EmbedTests.cs | 15 +++ 4 files changed, 163 insertions(+), 3 deletions(-) diff --git a/src/EmbedResourceCSharp/DiagnosticsHelper.cs b/src/EmbedResourceCSharp/DiagnosticsHelper.cs index e4b3f2f..6b489bc 100644 --- a/src/EmbedResourceCSharp/DiagnosticsHelper.cs +++ b/src/EmbedResourceCSharp/DiagnosticsHelper.cs @@ -19,4 +19,12 @@ internal sealed class DiagnosticsHelper category: "ResourceEmbedCSharp", DiagnosticSeverity.Error, true); + + internal static readonly DiagnosticDescriptor EncodingNotFoundError = new( + id: "EMBED003", + title: "Encoding Not Found", + messageFormat: "Encoding '{0}' is not a supported encoding name", + category: "ResourceEmbedCSharp", + DiagnosticSeverity.Error, + true); } diff --git a/src/EmbedResourceCSharp/Generator.cs b/src/EmbedResourceCSharp/Generator.cs index fbc0fd0..dedf269 100644 --- a/src/EmbedResourceCSharp/Generator.cs +++ b/src/EmbedResourceCSharp/Generator.cs @@ -26,6 +26,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return compilation.GetTypeByMetadataName("EmbedResourceCSharp.FileEmbedAttribute"); }) .WithComparer(SymbolEqualityComparer.Default); + var str = context.CompilationProvider + .Select(static (compilation, token) => + { + token.ThrowIfCancellationRequested(); + return compilation.GetTypeByMetadataName("EmbedResourceCSharp.StringEmbedAttribute"); + }) + .WithComparer(SymbolEqualityComparer.Default); var folder = context.CompilationProvider .Select(static (compilation, token) => { @@ -37,8 +44,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .CreateSyntaxProvider(Predicate, Transform) .Combine(file) .Select(PostTransformFile) - .Where(x => x.Method is not null && x.Path is not null)! - .WithComparer(FileAttributeComparer.Instance); + .Where(x => x.Method is not null && x.Path is not null)!; + var strings = context.SyntaxProvider + .CreateSyntaxProvider(Predicate, Transform) + .Combine(str) + .Select(PostTransformString) + .Where(x => x.Method is not null && x.Path is not null); var folders = context.SyntaxProvider .CreateSyntaxProvider(Predicate, Transform) .Combine(folder) @@ -46,6 +57,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Where(x => x.Method is not null); context.RegisterSourceOutput(files.Combine(options), GenerateFileEmbed); + context.RegisterSourceOutput(strings.Combine(options), GenerateStringEmbed); context.RegisterSourceOutput(folders.Combine(options), GenerateFolderEmbed!); } @@ -95,6 +107,36 @@ private bool Predicate(SyntaxNode node, CancellationToken token) return default; } + private (IMethodSymbol? Method, string? Path, string? EncodingName) PostTransformString((IMethodSymbol? Method, INamedTypeSymbol? Type) pair, CancellationToken token) + { + var type = pair.Type; + if (type is null) + { + return default; + } + + var method = pair.Method; + if (method is null) + { + return default; + } + + foreach (var attribute in method.GetAttributes()) + { + token.ThrowIfCancellationRequested(); + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, type)) + { + var path = attribute.ConstructorArguments[0].Value as string; + var encodingName = attribute.ConstructorArguments.Length == 2 + ? attribute.ConstructorArguments[1].Value as string + : null; + return (method, path, encodingName); + } + } + + return default; + } + private (IMethodSymbol? Method, AttributeData? Data) PostTransform((IMethodSymbol? Method, INamedTypeSymbol? Type) pair, CancellationToken token) { var type = pair.Type; @@ -207,6 +249,69 @@ private void GenerateFileEmbed(SourceProductionContext context, ((IMethodSymbol context.AddSource(hintName, source); } + private void GenerateStringEmbed(SourceProductionContext context, ((IMethodSymbol Method, string Path, string? EncodingName) Left, Options Options) pair) + { + if (string.IsNullOrWhiteSpace(pair.Options.ProjectDir)) + { + return; + } + + StringBuilder builder; + + var token = context.CancellationToken; + token.ThrowIfCancellationRequested(); + var method = pair.Left.Method; + var path = pair.Left.Path; + var encodingName = pair.Left.EncodingName; + + var filePath = Path.Combine(pair.Options.ProjectDir, path); + if (!File.Exists(filePath)) + { + var location = Location.None; + if (method.AssociatedSymbol is { Locations: { Length: > 0 } locations }) + { + location = locations[0]; + } + + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsHelper.FileNotFoundError, location, filePath)); + return; + } + + Encoding? encoding = null; + if (encodingName is not null) + { + try + { + encoding = Encoding.GetEncoding(encodingName); + } + catch (ArgumentException) + { + var location = Location.None; + if (method.AssociatedSymbol is { Locations: { Length: > 0 } locations }) + { + location = locations[0]; + } + + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsHelper.EncodingNotFoundError, location, encodingName)); + return; + } + } + + builder = new StringBuilder(); + if (pair.Options.IsDesignTimeBuild) + { + Utility.ProcessFileDesignTimeBuild(builder, method); + } + else + { + Utility.ProcessString(builder, method, filePath, encoding, token); + } + + var source = builder.ToString(); + var hintName = Utility.CalcHintName(builder, method, ".string.g.cs"); + context.AddSource(hintName, source); + } + private sealed class FileAttributeComparer : IEqualityComparer> { public static readonly FileAttributeComparer Instance = new(); diff --git a/src/EmbedResourceCSharp/Utility.cs b/src/EmbedResourceCSharp/Utility.cs index 15f5c0a..233da1d 100644 --- a/src/EmbedResourceCSharp/Utility.cs +++ b/src/EmbedResourceCSharp/Utility.cs @@ -31,6 +31,24 @@ public FileEmbedAttribute(string path) } } + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)] + internal sealed class StringEmbedAttribute : global::System.Attribute + { + public string Path { get; } + public string? EncodingName { get; } + + public StringEmbedAttribute(string path) + { + Path = path; + } + + public StringEmbedAttribute(string path, string encodingName) + { + Path = path; + EncodingName = encodingName; + } + } + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false)] internal sealed class FolderEmbedAttribute : global::System.Attribute { @@ -210,6 +228,20 @@ public static void ProcessFile(StringBuilder buffer, IMethodSymbol method, strin Footer(buffer); } + public static void ProcessString(StringBuilder buffer, IMethodSymbol method, string filePath, Encoding? encoding, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var content = encoding is null ? File.ReadAllText(filePath) : File.ReadAllText(filePath, encoding); + Header(buffer, method); + buffer.Append("()").AppendLine(); + buffer.Append(" {").AppendLine(); + buffer.Append(" return \"\"\"\"\"\"").AppendLine(); + buffer.Append(content).AppendLine(); + buffer.Append("\"\"\"\"\"\";").AppendLine(); + buffer.Append(" }"); + Footer(buffer); + } + private static void Footer(StringBuilder buffer) { buffer.AppendLine().Append(" }").AppendLine().Append('}').AppendLine().AppendLine(); @@ -241,7 +273,7 @@ private static void Header(StringBuilder buffer, IMethodSymbol method) buffer.Append(" {").AppendLine(); buffer.Append(" "); PrintAccessibility(buffer, method.DeclaredAccessibility); - buffer.Append(" static partial global::System.ReadOnlySpan "); + buffer.Append($" static partial {method.ReturnType.ToDisplayString()} "); buffer.Append(method.Name); } diff --git a/tests/FileTests/EmbedTests.cs b/tests/FileTests/EmbedTests.cs index 78b0f79..5bf0044 100644 --- a/tests/FileTests/EmbedTests.cs +++ b/tests/FileTests/EmbedTests.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Runtime.CompilerServices; +using System.Text; using EmbedResourceCSharp; using Xunit; @@ -9,11 +10,18 @@ namespace FileTests public partial class EmbedTests { private static string GetCurrentFilePath([CallerFilePath] string path = "") => path; + private readonly string currentFolder; [FileEmbed("a.txt")] private static partial ReadOnlySpan GetA(); + [StringEmbed("a.txt")] + private static partial string GetStringDefaultEncoding(); + + [StringEmbed("a.txt", "utf-8")] + private static partial string GetStringUtf8(); + public EmbedTests() { currentFolder = Path.GetDirectoryName(GetCurrentFilePath()) ?? ""; @@ -26,6 +34,13 @@ public void FileEmbedTest() Assert.True(GetA().SequenceEqual(original.AsSpan())); } + [Fact] + public void StringEmbedTest() + { + var original = File.ReadAllText(Path.Combine(currentFolder, "./a.txt")); + Assert.Equal(original, GetStringDefaultEncoding()); + } + [Fact] public void FileNotFoundTest() {