|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
#pragma warning disable IDE0010 // Add missing cases (noise)
#pragma warning disable IDE0057 // Use range operator (incorrectly reported when Range is not defined)
#pragma warning disable IDE0058 // Expression value is never used (not sure why this is enabled)
#pragma warning disable IDE0066 // Convert switch statement to expression (not always better)
namespace Microsoft.CodeAnalysis.ResxSourceGenerator
{
internal abstract class AbstractResxGenerator : IIncrementalGenerator
{
protected abstract bool SupportsNullable(Compilation compilation);
[SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Standard practice for diagnosing source generator failures.")]
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var resourceFiles = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith(".resx", StringComparison.OrdinalIgnoreCase));
var compilationInformation = context.CompilationProvider.Select(
(compilation, cancellationToken) =>
{
var methodImplOptions = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesMethodImplOptions);
var hasAggressiveInlining = methodImplOptions?.MemberNames.Contains(nameof(MethodImplOptions.AggressiveInlining)) ?? false;
var hasNotNullIfNotNull = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemDiagnosticsCodeAnalysisNotNullIfNotNullAttribute) is not null;
return new CompilationInformation(
AssemblyName: compilation.AssemblyName,
CodeLanguage: compilation.Language,
SupportsNullable: SupportsNullable(compilation),
HasAggressiveInlining: hasAggressiveInlining,
HasNotNullIfNotNull: hasNotNullIfNotNull);
});
var resourceFilesToGenerateSource = resourceFiles.Combine(context.AnalyzerConfigOptionsProvider.Combine(compilationInformation)).SelectMany(
static (resourceFileAndOptions, cancellationToken) =>
{
var (resourceFile, (optionsProvider, compilationInfo)) = resourceFileAndOptions;
var options = optionsProvider.GetOptions(resourceFile);
// Use the GenerateSource property if provided. Otherwise, the value of GenerateSource defaults to
// true for resources without an explicit culture.
var explicitGenerateSource = IsGenerateSource(options);
if (explicitGenerateSource == false)
{
// Source generation is explicitly disabled for this resource file
return Array.Empty<ResourceInformation>();
}
else if (explicitGenerateSource != true)
{
var implicitGenerateSource = !IsExplicitWithCulture(options);
if (!implicitGenerateSource)
{
// Source generation is disabled for this resource file
return Array.Empty<ResourceInformation>();
}
}
if (!optionsProvider.GlobalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace))
{
rootNamespace = compilationInfo.AssemblyName;
}
var resourceHintName = Path.GetFileNameWithoutExtension(resourceFile.Path);
var resourceName = resourceHintName;
if (options.TryGetValue("build_metadata.AdditionalFiles.RelativeDir", out var relativeDir))
{
resourceName = relativeDir.Replace(Path.DirectorySeparatorChar, '.').Replace(Path.AltDirectorySeparatorChar, '.') + resourceName;
}
options.TryGetValue("build_metadata.AdditionalFiles.ClassName", out var resourceClassName);
if (!options.TryGetValue("build_metadata.AdditionalFiles.OmitGetResourceString", out var omitGetResourceStringText)
|| !bool.TryParse(omitGetResourceStringText, out var omitGetResourceString))
{
omitGetResourceString = false;
}
if (!options.TryGetValue("build_metadata.AdditionalFiles.AsConstants", out var asConstantsText)
|| !bool.TryParse(asConstantsText, out var asConstants))
{
asConstants = false;
}
if (!options.TryGetValue("build_metadata.AdditionalFiles.IncludeDefaultValues", out var includeDefaultValuesText)
|| !bool.TryParse(includeDefaultValuesText, out var includeDefaultValues))
{
includeDefaultValues = false;
}
if (!options.TryGetValue("build_metadata.AdditionalFiles.EmitFormatMethods", out var emitFormatMethodsText)
|| !bool.TryParse(emitFormatMethodsText, out var emitFormatMethods))
{
emitFormatMethods = false;
}
if (!options.TryGetValue("build_metadata.AdditionalFiles.Public", out var publicText)
|| !bool.TryParse(publicText, out var publicResource))
{
publicResource = false;
}
var noWarn = Array.Empty<string>();
if (options.TryGetValue("build_metadata.AdditionalFiles.NoWarn", out var noWarnText))
{
noWarn = noWarnText.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries).Select(i => i.Trim()).ToArray() ?? [];
}
return new[]
{
new ResourceInformation(
CompilationInformation: compilationInfo,
ResourceFile: resourceFile,
ResourceName: string.Join(".", rootNamespace, resourceName),
ResourceHintName: resourceHintName,
ResourceClassName: resourceClassName,
OmitGetResourceString: omitGetResourceString,
AsConstants: asConstants,
IncludeDefaultValues: includeDefaultValues,
EmitFormatMethods: emitFormatMethods,
Public: publicResource,
NoWarn: noWarn)
};
});
var renameMapping = resourceFilesToGenerateSource
.Select(static (resourceFile, cancellationToken) =>
{
return (resourceFile.ResourceName, resourceFile.ResourceHintName);
})
.Collect()
.Select(static (resourceNames, cancellationToken) =>
{
var names = new HashSet<string>();
var remappedNames = ImmutableDictionary<string, string>.Empty;
foreach (var (resourceName, resourceHintName) in resourceNames.OrderBy(x => x.ResourceName, StringComparer.Ordinal))
{
for (var i = -1; true; i++)
{
if (i == -1)
{
if (names.Add(resourceHintName))
break;
}
else
{
var candidateName = resourceHintName + i;
if (names.Add(candidateName))
{
remappedNames = remappedNames.Add(resourceName, candidateName);
break;
}
}
}
}
return remappedNames;
})
.WithComparer(ImmutableDictionaryEqualityComparer<string, string>.Instance);
var resourceFilesToGenerateSourceWithNames = resourceFilesToGenerateSource.Combine(renameMapping).Select(
static (resourceFileAndRenameMapping, cancellationToken) =>
{
var (resourceFile, renameMapping) = resourceFileAndRenameMapping;
if (renameMapping.TryGetValue(resourceFile.ResourceName, out var newHintName))
{
return resourceFile with { ResourceHintName = newHintName };
}
return resourceFile;
});
context.RegisterSourceOutput(
resourceFilesToGenerateSourceWithNames,
static (context, resourceInformation) =>
{
try
{
var impl = new Impl(resourceInformation);
if (impl.Execute(context.CancellationToken))
{
context.AddSource(impl.OutputTextHintName, impl.OutputText);
}
}
catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
var exceptionLines = ex.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None);
var text = string.Join("", exceptionLines.Select(line => "#error " + line + Environment.NewLine));
var errorText = SourceText.From(text, Encoding.UTF8, SourceHashAlgorithm.Sha256);
context.AddSource($"{resourceInformation.ResourceHintName}.Error", errorText);
}
});
}
private static bool? IsGenerateSource(AnalyzerConfigOptions options)
{
if (!options.TryGetValue("build_metadata.AdditionalFiles.GenerateSource", out var generateSourceText)
|| !bool.TryParse(generateSourceText, out var generateSource))
{
// This resource did not explicitly set GenerateSource to true or false
return null;
}
return generateSource;
}
private static bool IsExplicitWithCulture(AnalyzerConfigOptions options)
{
if (!options.TryGetValue("build_metadata.AdditionalFiles.WithCulture", out var withCultureText)
|| !bool.TryParse(withCultureText, out var withCulture))
{
// Assume the resource does not have a culture when there is no indication otherwise
return false;
}
return withCulture;
}
/// <summary>
///
/// </summary>
/// <param name="AssemblyName"></param>
/// <param name="CodeLanguage">Language of source file to generate. Supported languages: CSharp, VisualBasic.</param>
/// <param name="SupportsNullable"></param>
private sealed record CompilationInformation(
string? AssemblyName,
string CodeLanguage,
bool SupportsNullable,
bool HasAggressiveInlining,
bool HasNotNullIfNotNull);
/// <summary>
///
/// </summary>
/// <param name="CompilationInformation">Information about the compilation.</param>
/// <param name="ResourceFile">Resources (resx) file.</param>
/// <param name="ResourceName">Name of the embedded resources to generate accessor class for.</param>
/// <param name="ResourceHintName">Unique identifying name for the generated resource file within the compilation. This will be the same as the last segment of <paramref name="ResourceName"/> (after the final <c>.</c>) except in the case of duplicates.</param>
/// <param name="ResourceClassName">Optionally, a <c>namespace.type</c> name for the generated Resources accessor class. Defaults to <see cref="ResourceName"/> if unspecified.</param>
/// <param name="OmitGetResourceString">If set to <see langword="true"/>, the <c>GetResourceString</c> method is not included in the generated class and must be specified in a separate source file.</param>
/// <param name="AsConstants">If set to <see langword="true"/>, emits constant key strings instead of properties that retrieve values.</param>
/// <param name="IncludeDefaultValues">If set to <see langword="true"/>, calls to <c>GetResourceString</c> receive a default resource string value.</param>
/// <param name="EmitFormatMethods">If set to <see langword="true"/>, the generated code will include <c>.FormatXYZ(...)</c> methods.</param>
/// <param name="Public">If set to <see langword="true"/>, the generated class will be declared <see langword="public"/>; otherwise, it will be declared <see langword="internal"/>.</param>
/// <param name="NoWarn">List of of disabled warnings, adding a <c>#pragma warning disable</c>.</param>
private sealed record ResourceInformation(
CompilationInformation CompilationInformation,
AdditionalText ResourceFile,
string ResourceName,
string ResourceHintName,
string? ResourceClassName,
bool OmitGetResourceString,
bool AsConstants,
bool IncludeDefaultValues,
bool EmitFormatMethods,
bool Public,
string[] NoWarn);
private sealed class ImmutableDictionaryEqualityComparer<TKey, TValue> : IEqualityComparer<ImmutableDictionary<TKey, TValue>?>
where TKey : notnull
{
public static readonly ImmutableDictionaryEqualityComparer<TKey, TValue> Instance = new();
public bool Equals(ImmutableDictionary<TKey, TValue>? x, ImmutableDictionary<TKey, TValue>? y)
{
if (ReferenceEquals(x, y))
return true;
if (x is null || y is null)
return false;
if (!Equals(x.KeyComparer, y.KeyComparer))
return false;
if (!Equals(x.ValueComparer, y.ValueComparer))
return false;
foreach (var (key, value) in x)
{
if (!y.TryGetValue(key, out var other)
|| !x.ValueComparer.Equals(value, other))
{
return false;
}
}
return true;
}
public int GetHashCode(ImmutableDictionary<TKey, TValue>? obj)
{
return obj?.Count ?? 0;
}
}
private sealed class Impl
{
private const int maxDocCommentLength = 256;
public Impl(ResourceInformation resourceInformation)
{
ResourceInformation = resourceInformation;
OutputText = SourceText.From("", Encoding.UTF8);
}
public ResourceInformation ResourceInformation { get; }
public CompilationInformation CompilationInformation => ResourceInformation.CompilationInformation;
public string? OutputTextHintName { get; private set; }
public SourceText OutputText { get; private set; }
private enum Lang
{
CSharp,
VisualBasic,
}
private void LogError(Lang language, string message)
{
var result = language switch
{
Lang.CSharp => $"#error {message}",
Lang.VisualBasic => $"#Error \"{message}\"",
_ => message,
};
OutputText = SourceText.From(result, Encoding.UTF8, SourceHashAlgorithm.Sha256);
}
[MemberNotNullWhen(true, nameof(OutputTextHintName), nameof(OutputText))]
public bool Execute(CancellationToken cancellationToken)
{
Lang language;
switch (CompilationInformation.CodeLanguage)
{
case LanguageNames.CSharp:
language = Lang.CSharp;
break;
case LanguageNames.VisualBasic:
language = Lang.VisualBasic;
break;
default:
LogError(Lang.CSharp, $"GenerateResxSource doesn't support language: '{CompilationInformation.CodeLanguage}'");
return false;
}
var extension = language switch
{
Lang.CSharp => "cs",
Lang.VisualBasic => "vb",
_ => "cs",
};
OutputTextHintName = ResourceInformation.ResourceHintName + $".Designer.{extension}";
if (string.IsNullOrEmpty(ResourceInformation.ResourceName))
{
LogError(language, "ResourceName not specified");
return false;
}
var resourceAccessName = RoslynString.IsNullOrEmpty(ResourceInformation.ResourceClassName) ? ResourceInformation.ResourceName : ResourceInformation.ResourceClassName;
SplitName(resourceAccessName, out var namespaceName, out var className);
var classIndent = namespaceName == null ? "" : " ";
var memberIndent = classIndent + " ";
var text = ResourceInformation.ResourceFile.GetText(cancellationToken);
if (text is null)
{
LogError(language, "ResourceFile was null");
return false;
}
var strings = new StringBuilder();
foreach (var node in XDocument.Load(new SourceTextReader(text)).Descendants("data"))
{
var name = node.Attribute("name")?.Value;
if (name == null)
{
LogError(language, "Missing resource name");
return false;
}
var value = node.Elements("value").FirstOrDefault()?.Value.Trim();
if (value == null)
{
LogError(language, $"Missing resource value: '{name}'");
return false;
}
if (name.Length == 0)
{
LogError(language, $"Empty resource name");
return false;
}
var docCommentString = value.Length > maxDocCommentLength ? value.Substring(0, maxDocCommentLength) + " ..." : value;
RenderDocComment(language, memberIndent, strings, docCommentString);
var identifier = GetIdentifierFromResourceName(name);
var defaultValue = ResourceInformation.IncludeDefaultValues ? ", " + CreateStringLiteral(value, language) : string.Empty;
switch (language)
{
case Lang.CSharp:
if (ResourceInformation.AsConstants)
{
strings.AppendLine($"{memberIndent}public const string @{identifier} = \"{name}\";");
}
else
{
var needSuppression = false;
if (CompilationInformation.SupportsNullable)
{
// We need a suppression unless default values are included and the NotNullIfNotNull
// attribute has been applied to eliminated the need for a suppression
if (!ResourceInformation.IncludeDefaultValues || !CompilationInformation.HasNotNullIfNotNull)
needSuppression = true;
}
strings.AppendLine($"{memberIndent}public static string @{identifier} => GetResourceString(\"{name}\"{defaultValue}){(needSuppression ? "!" : "")};");
}
if (ResourceInformation.EmitFormatMethods)
{
var resourceString = new ResourceString(name, value);
if (resourceString.HasArguments)
{
RenderDocComment(language, memberIndent, strings, docCommentString);
RenderFormatMethod(memberIndent, language, CompilationInformation.SupportsNullable, strings, resourceString);
}
}
break;
case Lang.VisualBasic:
if (ResourceInformation.AsConstants)
{
strings.AppendLine($"{memberIndent}Public Const [{identifier}] As String = \"{name}\"");
}
else
{
strings.AppendLine($"{memberIndent}Public Shared ReadOnly Property [{identifier}] As String");
strings.AppendLine($"{memberIndent} Get");
strings.AppendLine($"{memberIndent} Return GetResourceString(\"{name}\"{defaultValue})");
strings.AppendLine($"{memberIndent} End Get");
strings.AppendLine($"{memberIndent}End Property");
}
if (ResourceInformation.EmitFormatMethods)
{
throw new NotImplementedException();
}
break;
default:
throw new InvalidOperationException();
}
}
string? getStringMethod;
if (ResourceInformation.OmitGetResourceString)
{
getStringMethod = null;
}
else
{
switch (language)
{
case Lang.CSharp:
var getResourceStringAttributes = new List<string>();
if (CompilationInformation.HasAggressiveInlining)
{
getResourceStringAttributes.Add("[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]");
}
if (CompilationInformation.HasNotNullIfNotNull)
{
getResourceStringAttributes.Add("[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(\"defaultValue\")]");
}
getStringMethod = $@"{memberIndent}public static global::System.Globalization.CultureInfo{(CompilationInformation.SupportsNullable ? "?" : "")} Culture {{ get; set; }}
{string.Join(Environment.NewLine, getResourceStringAttributes.Select(attr => memberIndent + attr))}
{memberIndent}internal static {(CompilationInformation.SupportsNullable ? "string?" : "string")} GetResourceString(string resourceKey, {(CompilationInformation.SupportsNullable ? "string?" : "string")} defaultValue = null) => ResourceManager.GetString(resourceKey, Culture) ?? defaultValue;";
if (ResourceInformation.EmitFormatMethods)
{
getStringMethod += $@"
{memberIndent}private static string GetResourceString(string resourceKey, string[]? formatterNames)
{memberIndent}{{
{memberIndent} var value = GetResourceString(resourceKey) ?? """";
{memberIndent} if (formatterNames != null)
{memberIndent} {{
{memberIndent} for (var i = 0; i < formatterNames.Length; i++)
{memberIndent} {{
{memberIndent} value = value.Replace(""{{"" + formatterNames[i] + ""}}"", ""{{"" + i + ""}}"");
{memberIndent} }}
{memberIndent} }}
{memberIndent} return value;
{memberIndent}}}
";
}
break;
case Lang.VisualBasic:
getStringMethod = $@"{memberIndent}Public Shared Property Culture As Global.System.Globalization.CultureInfo
{memberIndent}<Global.System.Runtime.CompilerServices.MethodImpl(Global.System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)>
{memberIndent}Friend Shared Function GetResourceString(ByVal resourceKey As String, Optional ByVal defaultValue As String = Nothing) As String
{memberIndent} Return ResourceManager.GetString(resourceKey, Culture)
{memberIndent}End Function";
if (ResourceInformation.EmitFormatMethods)
{
throw new NotImplementedException();
}
break;
default:
throw new InvalidOperationException();
}
}
string? namespaceStart, namespaceEnd;
if (namespaceName == null)
{
namespaceStart = namespaceEnd = null;
}
else
{
switch (language)
{
case Lang.CSharp:
namespaceStart = $@"namespace {namespaceName}{Environment.NewLine}{{";
namespaceEnd = "}";
break;
case Lang.VisualBasic:
namespaceStart = $"Namespace Global.{namespaceName}";
namespaceEnd = "End Namespace";
break;
default:
throw new InvalidOperationException();
}
}
string resourceTypeName;
string? resourceTypeDefinition;
if (string.IsNullOrEmpty(ResourceInformation.ResourceClassName) || ResourceInformation.ResourceName == ResourceInformation.ResourceClassName)
{
// resource name is same as accessor, no need for a second type.
resourceTypeName = className;
resourceTypeDefinition = null;
}
else
{
// resource name differs from the access class, need a type for specifying the resources
// this empty type must remain as it is required by the .NETNative toolchain for locating resources
// once assemblies have been merged into the application
resourceTypeName = ResourceInformation.ResourceName;
SplitName(resourceTypeName, out var resourceNamespaceName, out var resourceClassName);
var resourceClassIndent = resourceNamespaceName == null ? "" : " ";
switch (language)
{
case Lang.CSharp:
resourceTypeDefinition = $"{resourceClassIndent}internal static class {resourceClassName} {{ }}";
if (resourceNamespaceName != null)
{
resourceTypeDefinition = $@"namespace {resourceNamespaceName}
{{
{resourceTypeDefinition}
}}";
}
break;
case Lang.VisualBasic:
resourceTypeDefinition = $@"{resourceClassIndent}Friend Class {resourceClassName}
{resourceClassIndent}End Class";
if (resourceNamespaceName != null)
{
resourceTypeDefinition = $@"Namespace {resourceNamespaceName}
{resourceTypeDefinition}
End Namespace";
}
break;
default:
throw new InvalidOperationException();
}
}
// List of NoWarn
string? noWarnDisabled = null;
string? noWarnRestored = null;
if (ResourceInformation.NoWarn.Length > 0)
{
var noWarnList = string.Join(", ", ResourceInformation.NoWarn);
var crLf = Environment.NewLine;
switch (language)
{
case Lang.CSharp:
noWarnDisabled = $"{crLf}{crLf}#pragma warning disable {noWarnList}";
noWarnRestored = $"{crLf}{crLf}#pragma warning restore {noWarnList}";
break;
case Lang.VisualBasic:
noWarnDisabled = $"{crLf}{crLf}#Disable Warning {noWarnList}";
noWarnRestored = $"{crLf}{crLf}#Enable Warning {noWarnList}";
break;
default:
throw new InvalidOperationException();
}
}
// The ResourceManager property being initialized lazily is an important optimization that lets .NETNative
// completely remove the ResourceManager class if the disk space saving optimization to strip resources
// (/DisableExceptionMessages) is turned on in the compiler.
string result;
switch (language)
{
case Lang.CSharp:
result = $@"// <auto-generated/>{noWarnDisabled}
{(CompilationInformation.SupportsNullable ? "#nullable enable" : "")}
using System.Reflection;
{resourceTypeDefinition}
{namespaceStart}
{classIndent}{(ResourceInformation.Public ? "public" : "internal")} static partial class {className}
{classIndent}{{
{memberIndent}private static global::System.Resources.ResourceManager{(CompilationInformation.SupportsNullable ? "?" : "")} s_resourceManager;
{memberIndent}public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager(typeof({resourceTypeName})));
{getStringMethod}
{strings}
{classIndent}}}
{namespaceEnd}{noWarnRestored}
";
break;
case Lang.VisualBasic:
result = $@"' <auto-generated/>{noWarnDisabled}
Imports System.Reflection
{resourceTypeDefinition}
{namespaceStart}
{classIndent}{(ResourceInformation.Public ? "Public" : "Friend")} Partial Class {className}
{memberIndent}Private Sub New
{memberIndent}End Sub
{memberIndent}
{memberIndent}Private Shared s_resourceManager As Global.System.Resources.ResourceManager
{memberIndent}Public Shared ReadOnly Property ResourceManager As Global.System.Resources.ResourceManager
{memberIndent} Get
{memberIndent} If s_resourceManager Is Nothing Then
{memberIndent} s_resourceManager = New Global.System.Resources.ResourceManager(GetType({resourceTypeName}))
{memberIndent} End If
{memberIndent} Return s_resourceManager
{memberIndent} End Get
{memberIndent}End Property
{getStringMethod}
{strings}
{classIndent}End Class
{namespaceEnd}{noWarnRestored}
";
break;
default:
throw new InvalidOperationException();
}
OutputText = SourceText.From(result, Encoding.UTF8, SourceHashAlgorithm.Sha256);
return true;
}
internal static string GetIdentifierFromResourceName(string name)
{
if (name.All(IsIdentifierPartCharacter))
{
return IsIdentifierStartCharacter(name[0]) ? name : "_" + name;
}
var builder = new StringBuilder(name.Length);
var f = name[0];
if (IsIdentifierPartCharacter(f) && !IsIdentifierStartCharacter(f))
{
builder.Append('_');
}
foreach (var c in name)
{
builder.Append(IsIdentifierPartCharacter(c) ? c : '_');
}
return builder.ToString();
static bool IsIdentifierStartCharacter(char ch)
=> ch == '_' || IsLetterChar(CharUnicodeInfo.GetUnicodeCategory(ch));
static bool IsIdentifierPartCharacter(char ch)
{
var cat = CharUnicodeInfo.GetUnicodeCategory(ch);
return IsLetterChar(cat)
|| cat == UnicodeCategory.DecimalDigitNumber
|| cat == UnicodeCategory.ConnectorPunctuation
|| cat == UnicodeCategory.Format
|| cat == UnicodeCategory.NonSpacingMark
|| cat == UnicodeCategory.SpacingCombiningMark;
}
static bool IsLetterChar(UnicodeCategory cat)
{
switch (cat)
{
case UnicodeCategory.UppercaseLetter:
case UnicodeCategory.LowercaseLetter:
case UnicodeCategory.TitlecaseLetter:
case UnicodeCategory.ModifierLetter:
case UnicodeCategory.OtherLetter:
case UnicodeCategory.LetterNumber:
return true;
}
return false;
}
}
private static void RenderDocComment(Lang language, string memberIndent, StringBuilder strings, string value)
{
var docCommentStart = language == Lang.CSharp
? "///"
: "'''";
var escapedTrimmedValue = new XElement("summary", value).ToString();
foreach (var line in escapedTrimmedValue.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None))
{
strings.Append(memberIndent).Append(docCommentStart).Append(' ');
strings.AppendLine(line);
}
}
private static string CreateStringLiteral(string original, Lang lang)
{
var stringLiteral = new StringBuilder(original.Length + 3);
if (lang == Lang.CSharp)
{
stringLiteral.Append('@');
}
stringLiteral.Append('\"');
for (var i = 0; i < original.Length; i++)
{
// duplicate '"' for VB and C#
if (original[i] == '\"')
{
stringLiteral.Append('"');
}
stringLiteral.Append(original[i]);
}
stringLiteral.Append('\"');
return stringLiteral.ToString();
}
private static void SplitName(string fullName, out string? namespaceName, out string className)
{
var lastDot = fullName.LastIndexOf('.');
if (lastDot == -1)
{
namespaceName = null;
className = fullName;
}
else
{
namespaceName = fullName.Substring(0, lastDot);
className = fullName.Substring(lastDot + 1);
}
}
private static void RenderFormatMethod(string indent, Lang language, bool supportsNullable, StringBuilder strings, ResourceString resourceString)
{
strings.AppendLine($"{indent}internal static string Format{resourceString.Name}({resourceString.GetMethodParameters(language, supportsNullable)})");
if (resourceString.UsingNamedArgs)
{
strings.AppendLine($@"{indent} => string.Format(Culture, GetResourceString(""{resourceString.Name}"", new[] {{ {resourceString.GetArgumentNames()} }}), {resourceString.GetArguments()});");
}
else
{
strings.AppendLine($@"{indent} => string.Format(Culture, GetResourceString(""{resourceString.Name}"") ?? """", {resourceString.GetArguments()});");
}
strings.AppendLine();
}
private class ResourceString
{
private static readonly Regex _namedParameterMatcher = new(@"\{([a-z]\w*)\}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex _numberParameterMatcher = new(@"\{(\d+)\}", RegexOptions.Compiled);
private readonly IReadOnlyList<string> _arguments;
public ResourceString(string name, string value)
{
Name = name;
Value = value;
var match = _namedParameterMatcher.Matches(value);
UsingNamedArgs = match.Count > 0;
if (!UsingNamedArgs)
{
match = _numberParameterMatcher.Matches(value);
}
var arguments = match.Cast<Match>()
.Select(m => m.Groups[1].Value)
.Distinct();
if (!UsingNamedArgs)
{
arguments = arguments.OrderBy(Convert.ToInt32);
}
_arguments = arguments.ToList();
}
public string Name { get; }
public string Value { get; }
public bool UsingNamedArgs { get; }
public bool HasArguments => _arguments.Count > 0;
public string GetArgumentNames() => string.Join(", ", _arguments.Select(a => "\"" + a + "\""));
public string GetArguments() => string.Join(", ", _arguments.Select(GetArgName));
public string GetMethodParameters(Lang language, bool supportsNullable)
{
switch (language)
{
case Lang.CSharp:
return string.Join(", ", _arguments.Select(a => $"object{(supportsNullable ? "?" : "")} " + GetArgName(a)));
case Lang.VisualBasic:
return string.Join(", ", _arguments.Select(GetArgName));
default:
throw new NotImplementedException();
}
}
private string GetArgName(string name) => UsingNamedArgs ? name : 'p' + name;
}
}
private sealed class SourceTextReader : TextReader
{
private readonly SourceText _text;
private int _position;
public SourceTextReader(SourceText text)
{
_text = text;
}
public override int Read(char[] buffer, int index, int count)
{
var remaining = _text.Length - _position;
var charactersToRead = Math.Min(remaining, count);
_text.CopyTo(_position, buffer, index, charactersToRead);
_position += charactersToRead;
return charactersToRead;
}
}
}
}
|