mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
1.8.3 package
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
desktop.ini
|
||||
|
||||
*.csproj.user
|
||||
*.pubxml
|
||||
*.DotSettings.user
|
||||
|
||||
.vs/
|
||||
@@ -20,4 +19,5 @@ src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
|
||||
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao.Test/bin/
|
||||
src/Snap.Hutao/Snap.Hutao.Test/obj/
|
||||
src/Snap.Hutao/Snap.Hutao.Test/obj/
|
||||
src/Snap.Hutao/Snap.Hutao/Properties/PublishProfiles/FolderProfile.pubxml.user
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class AttributeGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(GenerateAllAttributes);
|
||||
}
|
||||
|
||||
public static void GenerateAllAttributes(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
string coreAnnotations = """
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Core.Annotation;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
|
||||
internal sealed class CommandAttribute : Attribute
|
||||
{
|
||||
public CommandAttribute(string name)
|
||||
{
|
||||
}
|
||||
|
||||
public bool AllowConcurrentExecutions { get; set; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class ConstructorGeneratedAttribute : Attribute
|
||||
{
|
||||
public ConstructorGeneratedAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
public bool CallBaseConstructor { get; set; }
|
||||
public bool ResolveHttpClient { get; set; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class DependencyPropertyAttribute : Attribute
|
||||
{
|
||||
public DependencyPropertyAttribute(string name, Type type)
|
||||
{
|
||||
}
|
||||
|
||||
public DependencyPropertyAttribute(string name, Type type, object defaultValue)
|
||||
{
|
||||
}
|
||||
|
||||
public DependencyPropertyAttribute(string name, Type type, object defaultValue, string valueChangedCallbackName)
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsAttached { get; set; }
|
||||
public Type AttachedType { get; set; } = default;
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
[Conditional("DEBUG")]
|
||||
internal sealed class HighQualityAttribute : Attribute
|
||||
{
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.Annotation.Attributes.g.cs", coreAnnotations);
|
||||
|
||||
string coreDependencyInjectionAnnotationHttpClients = """
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class HttpClientAttribute : Attribute
|
||||
{
|
||||
public HttpClientAttribute(HttpClientConfiguration configuration)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpClientAttribute(HttpClientConfiguration configuration, Type interfaceType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal enum HttpClientConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认配置
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// 米游社请求配置
|
||||
/// </summary>
|
||||
XRpc,
|
||||
|
||||
/// <summary>
|
||||
/// 米游社登录请求配置
|
||||
/// </summary>
|
||||
XRpc2,
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab app
|
||||
/// </summary>
|
||||
XRpc3,
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
internal sealed class PrimaryHttpMessageHandlerAttribute : Attribute
|
||||
{
|
||||
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.MaxConnectionsPerServer"/>
|
||||
public int MaxConnectionsPerServer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="System.Net.Http.HttpClientHandler.UseCookies"/>
|
||||
/// </summary>
|
||||
public bool UseCookies { get; set; }
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.Attributes.g.cs", coreDependencyInjectionAnnotationHttpClients);
|
||||
|
||||
string coreDependencyInjectionAnnotations = """
|
||||
namespace Snap.Hutao.Core.DependencyInjection.Annotation;
|
||||
|
||||
internal enum InjectAs
|
||||
{
|
||||
Singleton,
|
||||
Transient,
|
||||
Scoped,
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
internal sealed class InjectionAttribute : Attribute
|
||||
{
|
||||
public InjectionAttribute(InjectAs injectAs)
|
||||
{
|
||||
}
|
||||
|
||||
public InjectionAttribute(InjectAs injectAs, Type interfaceType)
|
||||
{
|
||||
}
|
||||
|
||||
public object Key { get; set; }
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Core.DependencyInjection.Annotation.Attributes.g.cs", coreDependencyInjectionAnnotations);
|
||||
|
||||
string resourceLocalization = """
|
||||
namespace Snap.Hutao.Resource.Localization;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Enum)]
|
||||
internal sealed class LocalizationAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
internal sealed class LocalizationKeyAttribute : Attribute
|
||||
{
|
||||
public LocalizationKeyAttribute(string key)
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
context.AddSource("Snap.Hutao.Resource.Localization.Attributes.g.cs", resourceLocalization);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class CommandGenerator : IIncrementalGenerator
|
||||
{
|
||||
public const string AttributeName = "Snap.Hutao.Core.Annotation.CommandAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>>> commands =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedMethods, CommandMethod)
|
||||
.Where(GeneratorSyntaxContext2<IMethodSymbol>.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(commands, GenerateCommandImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedMethods(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is MethodDeclarationSyntax methodDeclarationSyntax
|
||||
&& methodDeclarationSyntax.Parent is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& methodDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2<IMethodSymbol> CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out IMethodSymbol? methodSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, methodSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateCommandImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2<IMethodSymbol>> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2<IMethodSymbol> context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateCommandImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateCommandImplementation(SourceProductionContext production, GeneratorSyntaxContext2<IMethodSymbol> context2)
|
||||
{
|
||||
INamedTypeSymbol classSymbol = context2.Symbol.ContainingType;
|
||||
|
||||
AttributeData commandInfo = context2.SingleAttribute(AttributeName);
|
||||
string commandName = (string)commandInfo.ConstructorArguments[0].Value!;
|
||||
|
||||
string commandType = context2.Symbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task")
|
||||
? "AsyncRelayCommand"
|
||||
: "RelayCommand";
|
||||
|
||||
string genericParameter = context2.Symbol.Parameters.ElementAtOrDefault(0) is IParameterSymbol parameter
|
||||
? $"<{parameter.Type.ToDisplayString(SymbolDisplayFormats.FullyQualifiedNonNullableFormat)}>"
|
||||
: string.Empty;
|
||||
|
||||
string concurrentExecution = commandInfo.HasNamedArgumentWith<bool>("AllowConcurrentExecutions", value => value)
|
||||
? ", AsyncRelayCommandOptions.AllowConcurrentExecutions"
|
||||
: string.Empty;
|
||||
|
||||
string className = classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
|
||||
string code = $$"""
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace {{classSymbol.ContainingNamespace}};
|
||||
|
||||
partial class {{className}}
|
||||
{
|
||||
private ICommand _{{commandName}};
|
||||
|
||||
public ICommand {{commandName}}
|
||||
{
|
||||
get => _{{commandName}} ??= new {{commandType}}{{genericParameter}}({{context2.Symbol.Name}}{{concurrentExecution}});
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
string normalizedClassName = classSymbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{commandName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class ConstructorGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.ConstructorGeneratedAttribute";
|
||||
private const string CompilerGenerated = "System.Runtime.CompilerServices.CompilerGeneratedAttribute";
|
||||
|
||||
//private static readonly DiagnosticDescriptor genericTypeNotSupportedDescriptor = new("SH102", "Generic type is not supported to generate .ctor", "Type [{0}] is not supported", "Quality", DiagnosticSeverity.Error, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, ConstructorGeneratedClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(injectionClasses, GenerateConstructorImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 ConstructorGeneratedClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateConstructorImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateConstructorImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateConstructorImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
AttributeData constructorInfo = context2.SingleAttribute(AttributeName);
|
||||
|
||||
bool resolveHttpClient = constructorInfo.HasNamedArgumentWith<bool>("ResolveHttpClient", value => value);
|
||||
bool callBaseConstructor = constructorInfo.HasNamedArgumentWith<bool>("CallBaseConstructor", value => value);
|
||||
string httpclient = resolveHttpClient ? ", System.Net.Http.HttpClient httpClient" : string.Empty;
|
||||
|
||||
FieldValueAssignmentOptions options = new(resolveHttpClient, callBaseConstructor);
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(ConstructorGenerator)}}", "1.0.0.0")]
|
||||
partial class {{context2.Symbol.ToDisplayString(SymbolDisplayFormats.QualifiedNonNullableFormat)}}
|
||||
{
|
||||
public {{context2.Symbol.Name}}(System.IServiceProvider serviceProvider{{httpclient}}){{(options.CallBaseConstructor ? " : base(serviceProvider)" : string.Empty)}}
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithFieldValueAssignment(sourceBuilder, context2, options);
|
||||
|
||||
sourceBuilder.Append("""
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.ctor.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithFieldValueAssignment(StringBuilder builder, GeneratorSyntaxContext2 context2, FieldValueAssignmentOptions options)
|
||||
{
|
||||
IEnumerable<IFieldSymbol> fields = context2.Symbol.GetMembers()
|
||||
.Where(m => m.Kind == SymbolKind.Field)
|
||||
.OfType<IFieldSymbol>();
|
||||
|
||||
foreach (IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
if (fieldSymbol.Name.AsSpan()[0] is '<')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool shoudSkip = false;
|
||||
foreach (SyntaxReference syntaxReference in fieldSymbol.DeclaringSyntaxReferences)
|
||||
{
|
||||
if (syntaxReference.GetSyntax() is VariableDeclaratorSyntax declarator)
|
||||
{
|
||||
if (declarator.Initializer is not null)
|
||||
{
|
||||
// Skip field with initializer
|
||||
builder.Append(" // Skip field with initializer: ").AppendLine(fieldSymbol.Name);
|
||||
shoudSkip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shoudSkip)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fieldSymbol.IsReadOnly && !fieldSymbol.IsStatic)
|
||||
{
|
||||
switch (fieldSymbol.Type.ToDisplayString())
|
||||
{
|
||||
case "System.IServiceProvider":
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.AppendLine(" = serviceProvider;");
|
||||
break;
|
||||
|
||||
case "System.Net.Http.HttpClient":
|
||||
if (options.ResolveHttpClient)
|
||||
{
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.AppendLine(" = httpClient;");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.Append(" = serviceProvider.GetRequiredService<System.Net.Http.IHttpClientFactory>().CreateClient(nameof(")
|
||||
.Append(context2.Symbol.Name)
|
||||
.AppendLine("));");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
builder
|
||||
.Append(" this.")
|
||||
.Append(fieldSymbol.Name)
|
||||
.Append(" = serviceProvider.GetRequiredService<")
|
||||
.Append(fieldSymbol.Type)
|
||||
.AppendLine(">();");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (INamedTypeSymbol interfaceSymbol in context2.Symbol.Interfaces)
|
||||
{
|
||||
if (interfaceSymbol.Name == "IRecipient")
|
||||
{
|
||||
builder
|
||||
.Append(" CommunityToolkit.Mvvm.Messaging.IMessengerExtensions.Register<")
|
||||
.Append(interfaceSymbol.TypeArguments[0])
|
||||
.AppendLine(">(serviceProvider.GetRequiredService<CommunityToolkit.Mvvm.Messaging.IMessenger>(), this);");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct FieldValueAssignmentOptions
|
||||
{
|
||||
public readonly bool ResolveHttpClient;
|
||||
public readonly bool CallBaseConstructor;
|
||||
|
||||
public FieldValueAssignmentOptions(bool resolveHttpClient, bool callBaseConstructor)
|
||||
{
|
||||
ResolveHttpClient = resolveHttpClient;
|
||||
CallBaseConstructor = callBaseConstructor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class DependencyPropertyGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.Annotation.DependencyPropertyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> commands =
|
||||
context.SyntaxProvider.CreateSyntaxProvider(FilterAttributedClasses, CommandMethod)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(commands, GenerateDependencyPropertyImplementations);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.Modifiers.Count > 1
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 CommandMethod(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? methodSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = methodSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, methodSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementations(SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
foreach (GeneratorSyntaxContext2 context2 in context2s.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
GenerateDependencyPropertyImplementation(production, context2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateDependencyPropertyImplementation(SourceProductionContext production, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
foreach (AttributeData propertyInfo in context2.Attributes.Where(attr => attr.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
string owner = context2.Symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
Dictionary<string, TypedConstant> namedArguments = propertyInfo.NamedArguments.ToDictionary();
|
||||
bool isAttached = namedArguments.TryGetValue("IsAttached", out TypedConstant constant) && (bool)constant.Value!;
|
||||
string register = isAttached ? "RegisterAttached" : "Register";
|
||||
|
||||
ImmutableArray<TypedConstant> arguments = propertyInfo.ConstructorArguments;
|
||||
|
||||
string propertyName = (string)arguments[0].Value!;
|
||||
string propertyType = arguments[1].Value!.ToString();
|
||||
string defaultValue = arguments.ElementAtOrDefault(2).ToCSharpString() ?? "default";
|
||||
defaultValue = defaultValue == "null" ? "default" : defaultValue;
|
||||
string propertyChangedCallback = arguments.ElementAtOrDefault(3) is { IsNull: false } arg3 ? $", {arg3.Value}" : string.Empty;
|
||||
|
||||
string code;
|
||||
if (isAttached)
|
||||
{
|
||||
string objType = namedArguments.TryGetValue("AttachedType", out TypedConstant attachedType)
|
||||
? attachedType.Value!.ToString()
|
||||
: "object";
|
||||
|
||||
code = $$"""
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
partial class {{owner}}
|
||||
{
|
||||
private static readonly DependencyProperty {{propertyName}}Property =
|
||||
DependencyProperty.RegisterAttached("{{propertyName}}", typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
|
||||
|
||||
public static {{propertyType}} Get{{propertyName}}({{objType}} obj)
|
||||
{
|
||||
return ({{propertyType}})obj?.GetValue({{propertyName}}Property);
|
||||
}
|
||||
|
||||
public static void Set{{propertyName}}({{objType}} obj, {{propertyType}} value)
|
||||
{
|
||||
obj.SetValue({{propertyName}}Property, value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
else
|
||||
{
|
||||
code = $$"""
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace {{context2.Symbol.ContainingNamespace}};
|
||||
|
||||
partial class {{owner}}
|
||||
{
|
||||
private static readonly DependencyProperty {{propertyName}}Property =
|
||||
DependencyProperty.Register(nameof({{propertyName}}), typeof({{propertyType}}), typeof({{owner}}), new PropertyMetadata(({{propertyType}}){{defaultValue}}{{propertyChangedCallback}}));
|
||||
|
||||
public {{propertyType}} {{propertyName}}
|
||||
{
|
||||
get => ({{propertyType}})GetValue({{propertyName}}Property);
|
||||
set => SetValue({{propertyName}}Property, value);
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
string normalizedClassName = context2.Symbol.ToDisplayString().Replace('<', '{').Replace('>', '}');
|
||||
production.AddSource($"{normalizedClassName}.{propertyName}.g.cs", code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Automation;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class SaltConstantGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static readonly HttpClient httpClient;
|
||||
private static readonly Lazy<Response<SaltLatest>> lazySaltInfo;
|
||||
|
||||
static SaltConstantGenerator()
|
||||
{
|
||||
httpClient = new();
|
||||
lazySaltInfo = new Lazy<Response<SaltLatest>>(() =>
|
||||
{
|
||||
string body = httpClient.GetStringAsync("https://internal.snapgenshin.cn/Archive/Salt/Latest").GetAwaiter().GetResult();
|
||||
return JsonParser.FromJson<Response<SaltLatest>>(body)!;
|
||||
});
|
||||
}
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
context.RegisterPostInitializationOutput(GenerateSaltContstants);
|
||||
}
|
||||
|
||||
private static void GenerateSaltContstants(IncrementalGeneratorPostInitializationContext context)
|
||||
{
|
||||
Response<SaltLatest> saltInfo = lazySaltInfo.Value;
|
||||
string code = $$"""
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
internal sealed class SaltConstants
|
||||
{
|
||||
public const string CNVersion = "{{saltInfo.Data.CNVersion}}";
|
||||
public const string CNK2 = "{{saltInfo.Data.CNK2}}";
|
||||
public const string CNLK2 = "{{saltInfo.Data.CNLK2}}";
|
||||
|
||||
public const string OSVersion = "{{saltInfo.Data.OSVersion}}";
|
||||
public const string OSK2 = "{{saltInfo.Data.OSK2}}";
|
||||
public const string OSLK2 = "{{saltInfo.Data.OSLK2}}";
|
||||
}
|
||||
""";
|
||||
context.AddSource("SaltConstants.g.cs", code);
|
||||
}
|
||||
|
||||
private sealed class Response<T>
|
||||
{
|
||||
[DataMember(Name = "data")]
|
||||
public T Data { get; set; } = default!;
|
||||
}
|
||||
|
||||
internal sealed class SaltLatest
|
||||
{
|
||||
public string CNVersion { get; set; } = default!;
|
||||
|
||||
public string CNK2 { get; set; } = default!;
|
||||
|
||||
public string CNLK2 { get; set; } = default!;
|
||||
|
||||
public string OSVersion { get; set; } = default!;
|
||||
|
||||
public string OSK2 { get; set; } = default!;
|
||||
|
||||
public string OSLK2 { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace System.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
|
||||
internal sealed class NotNullWhenAttribute : Attribute
|
||||
{
|
||||
/// <summary>Initializes the attribute with the specified return value condition.</summary>
|
||||
/// <param name="returnValue">
|
||||
/// The return value condition. If the method returns this value, the associated parameter will not be null.
|
||||
/// </param>
|
||||
public NotNullWhenAttribute(bool returnValue)
|
||||
{
|
||||
ReturnValue = returnValue;
|
||||
}
|
||||
|
||||
/// <summary>Gets the return value condition.</summary>
|
||||
public bool ReturnValue { get; }
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class HttpClientGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientAttribute";
|
||||
|
||||
private const string HttpClientConfiguration = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfiguration.";
|
||||
private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute";
|
||||
private const string CRLF = "\r\n";
|
||||
|
||||
private static readonly DiagnosticDescriptor injectionShouldOmitDescriptor = new("SH201", "Injection 特性可以省略", "HttpClient 特性已将 {0} 注册为 Transient 服务", "Quality", DiagnosticSeverity.Warning, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddHttpClientsImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateAddHttpClientsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class IocHttpClientConfiguration
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(HttpClientGenerator)}}", "1.0.0.0")]
|
||||
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
FillUpWithAddHttpClient(sourceBuilder, context, context2s);
|
||||
|
||||
sourceBuilder.Append("""
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("IocHttpClientConfiguration.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithAddHttpClient(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = [];
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
if (context.SingleOrDefaultAttribute(InjectionGenerator.AttributeName) is AttributeData injectionData)
|
||||
{
|
||||
if (injectionData.ConstructorArguments[0].ToCSharpString() == InjectionGenerator.InjectAsTransientName)
|
||||
{
|
||||
if (injectionData.ConstructorArguments.Length < 2)
|
||||
{
|
||||
production.ReportDiagnostic(Diagnostic.Create(injectionShouldOmitDescriptor, context.Context.Node.GetLocation(), context.Context.Node));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lineBuilder.Clear().Append(CRLF);
|
||||
lineBuilder.Append(@" services.AddHttpClient<");
|
||||
|
||||
AttributeData httpClientData = context.SingleAttribute(AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = httpClientData.ConstructorArguments;
|
||||
|
||||
if (arguments.Length == 2)
|
||||
{
|
||||
lineBuilder.Append($"{arguments[1].Value}, ");
|
||||
}
|
||||
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>(");
|
||||
lineBuilder.Append(arguments[0].ToCSharpString().Substring(HttpClientConfiguration.Length)).Append("Configuration)");
|
||||
|
||||
if (context.SingleOrDefaultAttribute(PrimaryHttpMessageHandlerAttributeName) is AttributeData handlerData)
|
||||
{
|
||||
ImmutableArray<KeyValuePair<string, TypedConstant>> properties = handlerData.NamedArguments;
|
||||
lineBuilder.Append(@".ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() {");
|
||||
|
||||
foreach (KeyValuePair<string, TypedConstant> property in properties)
|
||||
{
|
||||
lineBuilder.Append(' ');
|
||||
lineBuilder.Append(property.Key);
|
||||
lineBuilder.Append(" = ");
|
||||
lineBuilder.Append(property.Value.ToCSharpString());
|
||||
lineBuilder.Append(',');
|
||||
}
|
||||
|
||||
lineBuilder.Append(" })");
|
||||
}
|
||||
|
||||
lineBuilder.Append(';');
|
||||
|
||||
lines.Add(lineBuilder.ToString());
|
||||
}
|
||||
|
||||
foreach (string line in lines.OrderBy(x => x))
|
||||
{
|
||||
sourceBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class InjectionGenerator : IIncrementalGenerator
|
||||
{
|
||||
public const string AttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectionAttribute";
|
||||
public const string InjectAsSingletonName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Singleton";
|
||||
public const string InjectAsTransientName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Transient";
|
||||
public const string InjectAsScopedName = "Snap.Hutao.Core.DependencyInjection.Annotation.InjectAs.Scoped";
|
||||
|
||||
private static readonly DiagnosticDescriptor invalidInjectionDescriptor = new("SH101", "无效的 InjectAs 枚举值", "尚未支持生成 {0} 配置", "Quality", DiagnosticSeverity.Error, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<GeneratorSyntaxContext2>> injectionClasses = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedClasses, HttpClientClass)
|
||||
.Where(GeneratorSyntaxContext2.NotNull)
|
||||
.Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(injectionClasses, GenerateAddInjectionsImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedClasses(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is ClassDeclarationSyntax classDeclarationSyntax
|
||||
&& classDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 HttpClientClass(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.TryGetDeclaredSymbol(token, out INamedTypeSymbol? classSymbol))
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = classSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, classSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateAddInjectionsImplementation(SourceProductionContext context, ImmutableArray<GeneratorSyntaxContext2> context2s)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.DependencyInjection;
|
||||
|
||||
internal static partial class ServiceCollectionExtension
|
||||
{
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(InjectionGenerator)}}", "1.0.0.0")]
|
||||
public static partial IServiceCollection AddInjections(this IServiceCollection services)
|
||||
{
|
||||
""");
|
||||
|
||||
FillUpWithAddServices(sourceBuilder, context, context2s);
|
||||
sourceBuilder.Append("""
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource("ServiceCollectionExtension.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithAddServices(StringBuilder sourceBuilder, SourceProductionContext production, ImmutableArray<GeneratorSyntaxContext2> contexts)
|
||||
{
|
||||
List<string> lines = [];
|
||||
StringBuilder lineBuilder = new();
|
||||
|
||||
foreach (GeneratorSyntaxContext2 context in contexts.DistinctBy(c => c.Symbol.ToDisplayString()))
|
||||
{
|
||||
lineBuilder.Clear().AppendLine();
|
||||
|
||||
AttributeData injectionInfo = context.SingleAttribute(AttributeName);
|
||||
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
|
||||
|
||||
string injectAsName = arguments[0].ToCSharpString();
|
||||
|
||||
bool hasKey = injectionInfo.TryGetNamedArgumentValue("Key", out TypedConstant key);
|
||||
|
||||
switch (injectAsName, hasKey)
|
||||
{
|
||||
case (InjectAsSingletonName, false):
|
||||
lineBuilder.Append(" services.AddSingleton<");
|
||||
break;
|
||||
case (InjectAsSingletonName, true):
|
||||
lineBuilder.Append(" services.AddKeyedSingleton<");
|
||||
break;
|
||||
case (InjectAsTransientName, false):
|
||||
lineBuilder.Append(" services.AddTransient<");
|
||||
break;
|
||||
case (InjectAsTransientName, true):
|
||||
lineBuilder.Append(" services.AddKeyedTransient<");
|
||||
break;
|
||||
case (InjectAsScopedName, false):
|
||||
lineBuilder.Append(" services.AddScoped<");
|
||||
break;
|
||||
case (InjectAsScopedName, true):
|
||||
lineBuilder.Append(" services.AddKeyedScoped<");
|
||||
break;
|
||||
default:
|
||||
production.ReportDiagnostic(Diagnostic.Create(invalidInjectionDescriptor, context.Context.Node.GetLocation(), injectAsName));
|
||||
break;
|
||||
}
|
||||
|
||||
if (arguments.Length == 2)
|
||||
{
|
||||
lineBuilder.Append($"{arguments[1].Value}, ");
|
||||
}
|
||||
|
||||
if (hasKey)
|
||||
{
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>({key.ToCSharpString()});");
|
||||
}
|
||||
else
|
||||
{
|
||||
lineBuilder.Append($"{context.Symbol.ToDisplayString()}>();");
|
||||
}
|
||||
|
||||
lines.Add(lineBuilder.ToString());
|
||||
}
|
||||
|
||||
foreach (string line in lines.OrderBy(x => x))
|
||||
{
|
||||
sourceBuilder.Append(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.DependencyInjection;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal class ServiceAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor NonSingletonUseServiceProviderDescriptor = new("SH301", "Non Singleton service should avoid direct use of IServiceProvider", "Non Singleton service should avoid direct use of IServiceProvider", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor SingletonServiceCaptureNonSingletonServiceDescriptor = new("SH302", "Singleton service should avoid keep reference of non singleton service", "Singleton service should avoid keep reference of non singleton service", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
{
|
||||
get => new DiagnosticDescriptor[]
|
||||
{
|
||||
NonSingletonUseServiceProviderDescriptor,
|
||||
SingletonServiceCaptureNonSingletonServiceDescriptor,
|
||||
}.ToImmutableArray();
|
||||
}
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private static void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
context.RegisterSyntaxNodeAction(HandleNonSingletonUseServiceProvider, SyntaxKind.ClassDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleSingletonServiceCaptureNonSingletonService, SyntaxKind.ClassDeclaration);
|
||||
}
|
||||
|
||||
private static void HandleNonSingletonUseServiceProvider(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
ClassDeclarationSyntax classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
|
||||
if (classDeclarationSyntax.HasAttributeLists())
|
||||
{
|
||||
INamedTypeSymbol? classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
|
||||
if (classSymbol is not null)
|
||||
{
|
||||
foreach (AttributeData attributeData in classSymbol.GetAttributes())
|
||||
{
|
||||
if (attributeData.AttributeClass!.ToDisplayString() is InjectionGenerator.AttributeName)
|
||||
{
|
||||
string serviceType = attributeData.ConstructorArguments[0].ToCSharpString();
|
||||
if (serviceType is InjectionGenerator.InjectAsTransientName or InjectionGenerator.InjectAsScopedName)
|
||||
{
|
||||
HandleNonSingletonUseServiceProviderActual(context, classSymbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleNonSingletonUseServiceProviderActual(SyntaxNodeAnalysisContext context, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
ISymbol? symbol = classSymbol.GetMembers().Where(m => m is IFieldSymbol f && f.Type.ToDisplayString() == "System.IServiceProvider").SingleOrDefault();
|
||||
|
||||
if (symbol is not null)
|
||||
{
|
||||
Diagnostic diagnostic = Diagnostic.Create(NonSingletonUseServiceProviderDescriptor, symbol.Locations.FirstOrDefault());
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleSingletonServiceCaptureNonSingletonService(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
//classSymbol.GetMembers().Where(m => m is IFieldSymbol { IsReadOnly: true, DeclaredAccessibility: Accessibility.Private } f);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Enum;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal class LocalizedEnumGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string AttributeName = "Snap.Hutao.Resource.Localization.LocalizationAttribute";
|
||||
private const string LocalizationKeyName = "Snap.Hutao.Resource.Localization.LocalizationKeyAttribute";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValuesProvider<GeneratorSyntaxContext2> localizationEnums = context.SyntaxProvider
|
||||
.CreateSyntaxProvider(FilterAttributedEnums, LocalizationEnum)
|
||||
.Where(GeneratorSyntaxContext2.NotNull);
|
||||
|
||||
context.RegisterSourceOutput(localizationEnums, GenerateGetLocalizedDescriptionImplementation);
|
||||
}
|
||||
|
||||
private static bool FilterAttributedEnums(SyntaxNode node, CancellationToken token)
|
||||
{
|
||||
return node is EnumDeclarationSyntax enumDeclarationSyntax
|
||||
&& enumDeclarationSyntax.HasAttributeLists();
|
||||
}
|
||||
|
||||
private static GeneratorSyntaxContext2 LocalizationEnum(GeneratorSyntaxContext context, CancellationToken token)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(context.Node, token) is INamedTypeSymbol enumSymbol)
|
||||
{
|
||||
ImmutableArray<AttributeData> attributes = enumSymbol.GetAttributes();
|
||||
if (attributes.Any(data => data.AttributeClass!.ToDisplayString() == AttributeName))
|
||||
{
|
||||
return new(context, enumSymbol, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void GenerateGetLocalizedDescriptionImplementation(SourceProductionContext context, GeneratorSyntaxContext2 context2)
|
||||
{
|
||||
StringBuilder sourceBuilder = new StringBuilder().Append($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace Snap.Hutao.Resource.Localization;
|
||||
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(LocalizedEnumGenerator)}}", "1.0.0.0")]
|
||||
internal static class {{context2.Symbol.Name}}Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
public static string GetLocalizedDescription(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return Enum.GetName(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return SH.ResourceManager.GetString(key, CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化的描述
|
||||
/// </summary>
|
||||
/// <param name="value">枚举值</param>
|
||||
/// <returns>本地化的描述</returns>
|
||||
[return:MaybeNull]
|
||||
public static string GetLocalizedDescriptionOrDefault(this {{context2.Symbol}} value)
|
||||
{
|
||||
string key = value switch
|
||||
{
|
||||
|
||||
""");
|
||||
|
||||
FillUpWithSwitchBranches(sourceBuilder, context2);
|
||||
|
||||
sourceBuilder.Append($$"""
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
return SH.ResourceManager.GetString(key, CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
context.AddSource($"{context2.Symbol.Name}Extension.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private static void FillUpWithSwitchBranches(StringBuilder sourceBuilder, GeneratorSyntaxContext2 context)
|
||||
{
|
||||
IEnumerable<IFieldSymbol> fields = context.Symbol.GetMembers()
|
||||
.Where(m => m.Kind == SymbolKind.Field)
|
||||
.Cast<IFieldSymbol>();
|
||||
|
||||
foreach (IFieldSymbol fieldSymbol in fields)
|
||||
{
|
||||
AttributeData? localizationKeyInfo = fieldSymbol.GetAttributes()
|
||||
.SingleOrDefault(data => data.AttributeClass!.ToDisplayString() == LocalizationKeyName);
|
||||
if (localizationKeyInfo != null)
|
||||
{
|
||||
sourceBuilder
|
||||
.Append(" ")
|
||||
.Append(fieldSymbol)
|
||||
.Append(" => \"")
|
||||
.Append(localizationKeyInfo.ConstructorArguments[0].Value)
|
||||
.AppendLine("\",");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("", "RS2008")]
|
||||
@@ -1,202 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Identity;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
internal sealed class IdentityGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string FileName = "IdentityStructs.json";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<ImmutableArray<AdditionalText>> provider = context.AdditionalTextsProvider.Where(MatchFileName).Collect();
|
||||
|
||||
context.RegisterImplementationSourceOutput(provider, GenerateIdentityStructs);
|
||||
}
|
||||
|
||||
private static bool MatchFileName(AdditionalText text)
|
||||
{
|
||||
return Path.GetFileName(text.Path) == FileName;
|
||||
}
|
||||
|
||||
private static void GenerateIdentityStructs(SourceProductionContext context, ImmutableArray<AdditionalText> texts)
|
||||
{
|
||||
AdditionalText jsonFile = texts.Single();
|
||||
|
||||
string identityJson = jsonFile.GetText(context.CancellationToken)!.ToString();
|
||||
List<IdentityStructMetadata> identities = identityJson.FromJson<List<IdentityStructMetadata>>()!;
|
||||
|
||||
if (identities.Any())
|
||||
{
|
||||
foreach (IdentityStructMetadata identityStruct in identities)
|
||||
{
|
||||
GenerateIdentityStruct(context, identityStruct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateIdentityStruct(SourceProductionContext context, IdentityStructMetadata metadata)
|
||||
{
|
||||
string name = metadata.Name;
|
||||
|
||||
StringBuilder sourceBuilder = new StringBuilder().AppendLine($$"""
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Primitive.Converter;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Snap.Hutao.Model.Primitive;
|
||||
|
||||
/// <summary>
|
||||
/// {{metadata.Documentation}}
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(IdentityConverter<{{name}}>))]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(IdentityGenerator)}}","1.0.0.0")]
|
||||
internal readonly partial struct {{name}}
|
||||
{
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
public readonly uint Value;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="{{name}}"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="value">value</param>
|
||||
public {{name}}(uint value)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public static implicit operator uint({{name}} value)
|
||||
{
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public static implicit operator {{name}}(uint value)
|
||||
{
|
||||
return new(value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Value.GetHashCode();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return Value.ToString();
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
if (metadata.Equatable)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IEquatable<{{name}}>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is {{name}} other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals({{name}} other)
|
||||
{
|
||||
return Value == other.Value;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.EqualityOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IEqualityOperators<{{name}}, {{name}}, bool>, IEqualityOperators<{{name}}, uint, bool>
|
||||
{
|
||||
public static bool operator ==({{name}} left, {{name}} right)
|
||||
{
|
||||
return left.Value == right.Value;
|
||||
}
|
||||
|
||||
public static bool operator ==({{name}} left, uint right)
|
||||
{
|
||||
return left.Value == right;
|
||||
}
|
||||
|
||||
public static bool operator !=({{name}} left, {{name}} right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public static bool operator !=({{name}} left, uint right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.AdditionOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IAdditionOperators<{{name}}, {{name}}, {{name}}>, IAdditionOperators<{{name}}, uint, {{name}}>
|
||||
{
|
||||
public static {{name}} operator +({{name}} left, {{name}} right)
|
||||
{
|
||||
return left.Value + right.Value;
|
||||
}
|
||||
|
||||
public static {{name}} operator +({{name}} left, uint right)
|
||||
{
|
||||
return left.Value + right;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
if (metadata.IncrementOperators)
|
||||
{
|
||||
sourceBuilder.AppendLine($$"""
|
||||
|
||||
internal readonly partial struct {{name}} : IIncrementOperators<{{name}}>
|
||||
{
|
||||
public static unsafe {{name}} operator ++({{name}} value)
|
||||
{
|
||||
++*(uint*)&value;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
context.AddSource($"{name}.g.cs", sourceBuilder.ToString());
|
||||
}
|
||||
|
||||
private sealed class IdentityStructMetadata
|
||||
{
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
public string? Documentation { get; set; }
|
||||
|
||||
public bool Equatable { get; set; }
|
||||
|
||||
public bool EqualityOperators { get; set; }
|
||||
|
||||
public bool AdditionOperators { get; set; }
|
||||
|
||||
public bool IncrementOperators { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
// Really simple JSON parser in ~300 lines
|
||||
// - Attempts to parse JSON files with minimal GC allocation
|
||||
// - Nice and simple "[1,2,3]".FromJson<List<int>>() API
|
||||
// - Classes and structs can be parsed too!
|
||||
// class Foo { public int Value; }
|
||||
// "{\"Value\":10}".FromJson<Foo>()
|
||||
// - Can parse JSON without type information into Dictionary<string,object> and List<object> e.g.
|
||||
// "[1,2,3]".FromJson<object>().GetType() == typeof(List<object>)
|
||||
// "{\"Value\":10}".FromJson<object>().GetType() == typeof(Dictionary<string,object>)
|
||||
// - No JIT Emit support to support AOT compilation on iOS
|
||||
// - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead.
|
||||
// - Only public fields and property setters on classes/structs will be written to
|
||||
//
|
||||
// Limitations:
|
||||
// - No JIT Emit support to parse structures quickly
|
||||
// - Limited to parsing <2GB JSON files (due to int.MaxValue)
|
||||
// - Parsing of abstract classes or interfaces is NOT supported and will throw an exception.
|
||||
public static class JsonParser
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static Stack<List<string>>? splitArrayPool;
|
||||
|
||||
[ThreadStatic]
|
||||
private static StringBuilder? stringBuilder;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, Dictionary<string, FieldInfo>>? fieldInfoCache;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, Dictionary<string, PropertyInfo>>? propertyInfoCache;
|
||||
|
||||
public static T? FromJson<T>(this string json)
|
||||
{
|
||||
// Initialize, if needed, the ThreadStatic variables
|
||||
propertyInfoCache ??= [];
|
||||
fieldInfoCache ??= [];
|
||||
stringBuilder ??= new();
|
||||
splitArrayPool ??= [];
|
||||
|
||||
// Remove all whitespace not within strings to make parsing simpler
|
||||
stringBuilder.Length = 0;
|
||||
for (int i = 0; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (c == '"')
|
||||
{
|
||||
i = AppendUntilStringEnd(true, i, json);
|
||||
continue;
|
||||
}
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
// Parse the thing!
|
||||
return (T?)ParseValue(typeof(T), stringBuilder.ToString());
|
||||
}
|
||||
|
||||
private static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json)
|
||||
{
|
||||
stringBuilder!.Append(json[startIdx]);
|
||||
for (int i = startIdx + 1; i < json.Length; i++)
|
||||
{
|
||||
if (json[i] == '\\')
|
||||
{
|
||||
if (appendEscapeCharacter)
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
|
||||
stringBuilder.Append(json[i + 1]);
|
||||
i++;//Skip next character as it is escaped
|
||||
}
|
||||
else if (json[i] == '"')
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
return i;
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
}
|
||||
return json.Length - 1;
|
||||
}
|
||||
|
||||
// Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
|
||||
private static List<string> Split(string json)
|
||||
{
|
||||
List<string> splitArray = splitArrayPool!.Count > 0 ? splitArrayPool.Pop() : [];
|
||||
splitArray.Clear();
|
||||
if (json.Length == 2)
|
||||
{
|
||||
return splitArray;
|
||||
}
|
||||
|
||||
int parseDepth = 0;
|
||||
stringBuilder!.Length = 0;
|
||||
for (int i = 1; i < json.Length - 1; i++)
|
||||
{
|
||||
switch (json[i])
|
||||
{
|
||||
case '[':
|
||||
case '{':
|
||||
parseDepth++;
|
||||
break;
|
||||
case ']':
|
||||
case '}':
|
||||
parseDepth--;
|
||||
break;
|
||||
case '"':
|
||||
i = AppendUntilStringEnd(true, i, json);
|
||||
continue;
|
||||
case ',':
|
||||
case ':':
|
||||
if (parseDepth == 0)
|
||||
{
|
||||
splitArray.Add(stringBuilder.ToString());
|
||||
stringBuilder.Length = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
|
||||
splitArray.Add(stringBuilder.ToString());
|
||||
|
||||
return splitArray;
|
||||
}
|
||||
|
||||
internal static object? ParseValue(Type type, string json)
|
||||
{
|
||||
if (type == typeof(string))
|
||||
{
|
||||
if (json.Length <= 2)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
StringBuilder parseStringBuilder = new(json.Length);
|
||||
for (int i = 1; i < json.Length - 1; ++i)
|
||||
{
|
||||
if (json[i] == '\\' && i + 1 < json.Length - 1)
|
||||
{
|
||||
int j = "\"\\nrtbf/".IndexOf(json[i + 1]);
|
||||
if (j >= 0)
|
||||
{
|
||||
parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
if (json[i + 1] == 'u' && i + 5 < json.Length - 1)
|
||||
{
|
||||
if (uint.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out uint c))
|
||||
{
|
||||
parseStringBuilder.Append((char)c);
|
||||
i += 5;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
parseStringBuilder.Append(json[i]);
|
||||
}
|
||||
return parseStringBuilder.ToString();
|
||||
}
|
||||
if (type.IsPrimitive)
|
||||
{
|
||||
object result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture);
|
||||
return result;
|
||||
}
|
||||
if (type == typeof(decimal))
|
||||
{
|
||||
decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out decimal result);
|
||||
return result;
|
||||
}
|
||||
if (type == typeof(DateTime))
|
||||
{
|
||||
DateTime.TryParse(json.Replace("\"", ""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime result);
|
||||
return result;
|
||||
}
|
||||
if (json == "null")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (type.IsEnum)
|
||||
{
|
||||
if (json[0] == '"')
|
||||
{
|
||||
json = json.Substring(1, json.Length - 2);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return System.Enum.Parse(type, json, false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (type.IsArray)
|
||||
{
|
||||
Type arrayType = type.GetElementType();
|
||||
if (json[0] != '[' || json[json.Length - 1] != ']')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> elems = Split(json);
|
||||
Array newArray = Array.CreateInstance(arrayType, elems.Count);
|
||||
for (int i = 0; i < elems.Count; i++)
|
||||
{
|
||||
newArray.SetValue(ParseValue(arrayType, elems[i]), i);
|
||||
}
|
||||
|
||||
splitArrayPool!.Push(elems);
|
||||
return newArray;
|
||||
}
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
Type listType = type.GetGenericArguments()[0];
|
||||
if (json[0] != '[' || json[json.Length - 1] != ']')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> elems = Split(json);
|
||||
IList list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count });
|
||||
for (int i = 0; i < elems.Count; i++)
|
||||
{
|
||||
list.Add(ParseValue(listType, elems[i]));
|
||||
}
|
||||
|
||||
splitArrayPool!.Push(elems);
|
||||
return list;
|
||||
}
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
Type keyType, valueType;
|
||||
{
|
||||
Type[] args = type.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
valueType = args[1];
|
||||
}
|
||||
|
||||
//Refuse to parse dictionary keys that aren't of type string
|
||||
if (keyType != typeof(string))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
//Must be a valid dictionary element
|
||||
if (json[0] != '{' || json[json.Length - 1] != '}')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IDictionary dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 });
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
if (elems[i].Length <= 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string keyValue = elems[i].Substring(1, elems[i].Length - 2);
|
||||
object? val = ParseValue(valueType, elems[i + 1]);
|
||||
dictionary[keyValue] = val;
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
if (type == typeof(object))
|
||||
{
|
||||
return ParseAnonymousValue(json);
|
||||
}
|
||||
if (json[0] == '{' && json[json.Length - 1] == '}')
|
||||
{
|
||||
return ParseObject(type, json);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static object? ParseAnonymousValue(string json)
|
||||
{
|
||||
if (json.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (json[0] == '{' && json[json.Length - 1] == '}')
|
||||
{
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Dictionary<string, object?> dict = new(elems.Count / 2);
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
if (json[0] == '[' && json[json.Length - 1] == ']')
|
||||
{
|
||||
List<string> items = Split(json);
|
||||
List<object?> finalList = new(items.Count);
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
finalList.Add(ParseAnonymousValue(items[i]));
|
||||
}
|
||||
|
||||
return finalList;
|
||||
}
|
||||
if (json[0] == '"' && json[json.Length - 1] == '"')
|
||||
{
|
||||
string str = json.Substring(1, json.Length - 2);
|
||||
return str.Replace("\\", string.Empty);
|
||||
}
|
||||
if (char.IsDigit(json[0]) || json[0] == '-')
|
||||
{
|
||||
if (json.Contains("."))
|
||||
{
|
||||
double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double result);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
int.TryParse(json, out int result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (json == "true")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (json == "false")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// handles json == "null" as well as invalid JSON
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, T> CreateMemberNameDictionary<T>(T[] members) where T : MemberInfo
|
||||
{
|
||||
Dictionary<string, T> nameToMember = new(StringComparer.OrdinalIgnoreCase);
|
||||
for (int i = 0; i < members.Length; i++)
|
||||
{
|
||||
T member = members[i];
|
||||
if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = member.Name;
|
||||
if (member.IsDefined(typeof(DataMemberAttribute), true))
|
||||
{
|
||||
DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true);
|
||||
if (!string.IsNullOrEmpty(dataMemberAttribute.Name))
|
||||
{
|
||||
name = dataMemberAttribute.Name;
|
||||
}
|
||||
}
|
||||
|
||||
nameToMember.Add(name, member);
|
||||
}
|
||||
|
||||
return nameToMember;
|
||||
}
|
||||
|
||||
private static object ParseObject(Type type, string json)
|
||||
{
|
||||
object instance = FormatterServices.GetUninitializedObject(type);
|
||||
|
||||
// The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (!fieldInfoCache!.TryGetValue(type, out Dictionary<string, FieldInfo> nameToField))
|
||||
{
|
||||
nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
|
||||
fieldInfoCache.Add(type, nameToField);
|
||||
}
|
||||
if (!propertyInfoCache!.TryGetValue(type, out Dictionary<string, PropertyInfo> nameToProperty))
|
||||
{
|
||||
nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
|
||||
propertyInfoCache.Add(type, nameToProperty);
|
||||
}
|
||||
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
if (elems[i].Length <= 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = elems[i].Substring(1, elems[i].Length - 2);
|
||||
string value = elems[i + 1];
|
||||
|
||||
if (nameToField.TryGetValue(key, out FieldInfo fieldInfo))
|
||||
{
|
||||
fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value));
|
||||
}
|
||||
else if (nameToProperty.TryGetValue(key, out PropertyInfo propertyInfo))
|
||||
{
|
||||
propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null);
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class AttributeDataExtension
|
||||
{
|
||||
public static bool HasNamedArgumentWith<TValue>(this AttributeData data, string key, Func<TValue, bool> predicate)
|
||||
{
|
||||
return data.NamedArguments.Any(a => a.Key == key && predicate((TValue)a.Value.Value!));
|
||||
}
|
||||
|
||||
public static bool TryGetNamedArgumentValue(this AttributeData data, string key, out TypedConstant value)
|
||||
{
|
||||
foreach (KeyValuePair<string, TypedConstant> pair in data.NamedArguments)
|
||||
{
|
||||
if (pair.Key == key)
|
||||
{
|
||||
value = pair.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class EnumerableExtension
|
||||
{
|
||||
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
return DistinctByIterator(source, keySelector);
|
||||
}
|
||||
|
||||
private static IEnumerable<TSource> DistinctByIterator<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
|
||||
{
|
||||
using IEnumerator<TSource> enumerator = source.GetEnumerator();
|
||||
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
HashSet<TKey> set = [];
|
||||
|
||||
do
|
||||
{
|
||||
TSource element = enumerator.Current;
|
||||
if (set.Add(keySelector(element)))
|
||||
{
|
||||
yield return element;
|
||||
}
|
||||
}
|
||||
while (enumerator.MoveNext());
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source)
|
||||
{
|
||||
return source.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal readonly struct GeneratorSyntaxContext2
|
||||
{
|
||||
public readonly GeneratorSyntaxContext Context;
|
||||
public readonly INamedTypeSymbol Symbol;
|
||||
public readonly ImmutableArray<AttributeData> Attributes;
|
||||
public readonly bool HasValue = false;
|
||||
|
||||
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, INamedTypeSymbol symbol, ImmutableArray<AttributeData> attributes)
|
||||
{
|
||||
Context = context;
|
||||
Symbol = symbol;
|
||||
Attributes = attributes;
|
||||
HasValue = true;
|
||||
}
|
||||
|
||||
public static bool NotNull(GeneratorSyntaxContext2 context)
|
||||
{
|
||||
return context.HasValue;
|
||||
}
|
||||
|
||||
public bool HasAttributeWithName(string name)
|
||||
{
|
||||
return Attributes.Any(attr => attr.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public AttributeData SingleAttribute(string name)
|
||||
{
|
||||
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public AttributeData? SingleOrDefaultAttribute(string name)
|
||||
{
|
||||
return Attributes.SingleOrDefault(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
|
||||
public TSyntaxNode Node<TSyntaxNode>()
|
||||
where TSyntaxNode : SyntaxNode
|
||||
{
|
||||
return (TSyntaxNode)Context.Node;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct GeneratorSyntaxContext2<TSymbol>
|
||||
where TSymbol : ISymbol
|
||||
{
|
||||
public readonly GeneratorSyntaxContext Context;
|
||||
public readonly TSymbol Symbol;
|
||||
public readonly ImmutableArray<AttributeData> Attributes;
|
||||
public readonly bool HasValue = false;
|
||||
|
||||
public GeneratorSyntaxContext2(GeneratorSyntaxContext context, TSymbol symbol, ImmutableArray<AttributeData> attributes)
|
||||
{
|
||||
Context = context;
|
||||
Symbol = symbol;
|
||||
Attributes = attributes;
|
||||
HasValue = true;
|
||||
}
|
||||
|
||||
public static bool NotNull(GeneratorSyntaxContext2<TSymbol> context)
|
||||
{
|
||||
return context.HasValue;
|
||||
}
|
||||
|
||||
public AttributeData SingleAttribute(string name)
|
||||
{
|
||||
return Attributes.Single(attribute => attribute.AttributeClass!.ToDisplayString() == name);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class GeneratorSyntaxContextExtension
|
||||
{
|
||||
public static bool TryGetDeclaredSymbol<TSymbol>(this GeneratorSyntaxContext context, System.Threading.CancellationToken token, [NotNullWhen(true)] out TSymbol? symbol)
|
||||
where TSymbol : class, ISymbol
|
||||
{
|
||||
symbol = context.SemanticModel.GetDeclaredSymbol(context.Node, token) as TSymbol;
|
||||
return symbol != null;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class SymbolDisplayFormats
|
||||
{
|
||||
public static SymbolDisplayFormat FullyQualifiedNonNullableFormat { get; } = new(
|
||||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
|
||||
|
||||
public static SymbolDisplayFormat QualifiedNonNullableFormat { get; } = new(
|
||||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class SyntaxExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether a given <see cref="MemberDeclarationSyntax"/> has or could potentially have any attribute lists.
|
||||
/// </summary>
|
||||
/// <param name="declaration">The input <see cref="MemberDeclarationSyntax"/> to check.</param>
|
||||
/// <returns>Whether <paramref name="declaration"/> has or potentially has any attribute lists.</returns>
|
||||
public static bool HasAttributeLists<TSyntax>(this TSyntax declaration)
|
||||
where TSyntax : MemberDeclarationSyntax
|
||||
{
|
||||
return declaration.AttributeLists.Count > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Primitive;
|
||||
|
||||
internal static class TypeSymbolExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether or not a given <see cref="ITypeSymbol"/> has or inherits from a specified type.
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
|
||||
/// <param name="name">The full name of the type to check for inheritance.</param>
|
||||
/// <returns>Whether or not <paramref name="typeSymbol"/> is or inherits from <paramref name="name"/>.</returns>
|
||||
public static bool IsOrInheritsFrom(this ITypeSymbol typeSymbol, string name)
|
||||
{
|
||||
for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType)
|
||||
{
|
||||
if (currentType.ToDisplayString() == name)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,629 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.XPath;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Resx;
|
||||
|
||||
[Generator]
|
||||
public sealed class ResxGenerator : IIncrementalGenerator
|
||||
{
|
||||
private static readonly DiagnosticDescriptor InvalidResx = new("SH401", "Couldn't parse Resx file", "Couldn't parse Resx file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
private static readonly DiagnosticDescriptor InvalidPropertiesForNamespace = new("SH402", "Couldn't compute namespace", "Couldn't compute namespace for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
private static readonly DiagnosticDescriptor InvalidPropertiesForResourceName = new("SH403", "Couldn't compute resource name", "Couldn't compute resource name for file '{0}'", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
private static readonly DiagnosticDescriptor InconsistentProperties = new("SH404", "Inconsistent properties", "Property '{0}' values for '{1}' are inconsistent", "ResxGenerator", DiagnosticSeverity.Warning, true);
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValueProvider<(string? AssemblyName, bool SupportNullableReferenceTypes)> compilationProvider = context.CompilationProvider
|
||||
.Select(static (compilation, cancellationToken) => (compilation.AssemblyName, SupportNullableReferenceTypes: compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute") is not null));
|
||||
|
||||
IncrementalValueProvider<ImmutableArray<AdditionalText>> resxProvider = context.AdditionalTextsProvider
|
||||
.Where(text => text.Path.EndsWith(".resx", StringComparison.OrdinalIgnoreCase))
|
||||
.Collect();
|
||||
|
||||
context.RegisterSourceOutput(
|
||||
source: context.AnalyzerConfigOptionsProvider.Combine(compilationProvider.Combine(resxProvider)),
|
||||
action: (ctx, source) => Execute(ctx, source.Left, source.Right.Left.AssemblyName, source.Right.Left.SupportNullableReferenceTypes, source.Right.Right));
|
||||
}
|
||||
|
||||
private static void Execute(SourceProductionContext context, AnalyzerConfigOptionsProvider options, string? assemblyName, bool supportNullableReferenceTypes, ImmutableArray<AdditionalText> files)
|
||||
{
|
||||
// Group additional file by resource kind ((a.resx, a.en.resx, a.en-us.resx), (b.resx, b.en-us.resx))
|
||||
IOrderedEnumerable<IGrouping<string, AdditionalText>> group = files
|
||||
.GroupBy(file => GetResourceName(file.Path), StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x.Key, StringComparer.Ordinal);
|
||||
List<IGrouping<string, AdditionalText>> resxGroups = [.. group];
|
||||
|
||||
foreach (IGrouping<string, AdditionalText>? resxGroup in resxGroups)
|
||||
{
|
||||
string? rootNamespaceConfiguration = GetMetadataValue(context, options, "RootNamespace", resxGroup);
|
||||
string? projectDirConfiguration = GetMetadataValue(context, options, "ProjectDir", resxGroup);
|
||||
string? namespaceConfiguration = GetMetadataValue(context, options, "Namespace", "DefaultResourcesNamespace", resxGroup);
|
||||
string? resourceNameConfiguration = GetMetadataValue(context, options, "ResourceName", globalName: null, resxGroup);
|
||||
string? classNameConfiguration = GetMetadataValue(context, options, "ClassName", globalName: null, resxGroup);
|
||||
|
||||
string rootNamespace = rootNamespaceConfiguration ?? assemblyName ?? "";
|
||||
string projectDir = projectDirConfiguration ?? assemblyName ?? "";
|
||||
string? defaultResourceName = ComputeResourceName(rootNamespace, projectDir, resxGroup.Key);
|
||||
string? defaultNamespace = ComputeNamespace(rootNamespace, projectDir, resxGroup.Key);
|
||||
|
||||
string? ns = namespaceConfiguration ?? defaultNamespace;
|
||||
string? resourceName = resourceNameConfiguration ?? defaultResourceName;
|
||||
string className = classNameConfiguration ?? ToCSharpNameIdentifier(Path.GetFileName(resxGroup.Key));
|
||||
|
||||
if (ns == null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForNamespace, location: null, resxGroup.First().Path));
|
||||
}
|
||||
|
||||
if (resourceName == null)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InvalidPropertiesForResourceName, location: null, resxGroup.First().Path));
|
||||
}
|
||||
|
||||
List<ResxEntry>? entries = LoadResourceFiles(context, resxGroup);
|
||||
|
||||
string content = $"""
|
||||
// Debug info:
|
||||
// key: {resxGroup.Key}
|
||||
// files: {string.Join(", ", resxGroup.Select(f => f.Path))}
|
||||
// RootNamespace (metadata): {rootNamespaceConfiguration}
|
||||
// ProjectDir (metadata): {projectDirConfiguration}
|
||||
// Namespace / DefaultResourcesNamespace (metadata): {namespaceConfiguration}
|
||||
// ResourceName (metadata): {resourceNameConfiguration}
|
||||
// ClassName (metadata): {classNameConfiguration}
|
||||
// AssemblyName: {assemblyName}
|
||||
// RootNamespace (computed): {rootNamespace}
|
||||
// ProjectDir (computed): {projectDir}
|
||||
// defaultNamespace: {defaultNamespace}
|
||||
// defaultResourceName: {defaultResourceName}
|
||||
// Namespace: {ns}
|
||||
// ResourceName: {resourceName}
|
||||
// ClassName: {className}
|
||||
""";
|
||||
|
||||
if (resourceName != null && entries != null)
|
||||
{
|
||||
content += GenerateCode(ns, className, resourceName, entries, supportNullableReferenceTypes);
|
||||
}
|
||||
|
||||
context.AddSource($"{Path.GetFileName(resxGroup.Key)}.resx.g.cs", SourceText.From(content, Encoding.UTF8));
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateCode(string? ns, string className, string resourceName, List<ResxEntry> entries, bool enableNullableAttributes)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#nullable enable");
|
||||
|
||||
if (ns != null)
|
||||
{
|
||||
sb.AppendLine($$"""
|
||||
|
||||
namespace {{ns}};
|
||||
|
||||
""");
|
||||
}
|
||||
|
||||
sb.AppendLine($$"""
|
||||
internal partial class {{className}}
|
||||
{
|
||||
private static global::System.Resources.ResourceManager? resourceMan;
|
||||
|
||||
public {{className}}()
|
||||
{
|
||||
}
|
||||
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (resourceMan is null)
|
||||
{
|
||||
resourceMan = new global::System.Resources.ResourceManager("{{resourceName}}", typeof({{className}}).Assembly);
|
||||
}
|
||||
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo? Culture { get; set; }
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static object? GetObject(global::System.Globalization.CultureInfo? culture, string name, object? defaultValue)
|
||||
{
|
||||
culture ??= Culture;
|
||||
object? obj = ResourceManager.GetObject(name, culture);
|
||||
if (obj == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static object? GetObject(global::System.Globalization.CultureInfo? culture, string name)
|
||||
{
|
||||
return GetObject(culture: culture, name: name, defaultValue: null);
|
||||
}
|
||||
|
||||
public static object? GetObject(string name)
|
||||
{
|
||||
return GetObject(culture: null, name: name, defaultValue: null);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static object? GetObject(string name, object? defaultValue)
|
||||
{
|
||||
return GetObject(culture: null, name: name, defaultValue: defaultValue);
|
||||
}
|
||||
|
||||
public static global::System.IO.Stream? GetStream(string name)
|
||||
{
|
||||
return GetStream(culture: null, name: name);
|
||||
}
|
||||
|
||||
public static global::System.IO.Stream? GetStream(global::System.Globalization.CultureInfo? culture, string name)
|
||||
{
|
||||
culture ??= Culture;
|
||||
return ResourceManager.GetStream(name, culture);
|
||||
}
|
||||
|
||||
public static string? GetString(global::System.Globalization.CultureInfo? culture, string name)
|
||||
{
|
||||
return GetString(culture: culture, name: name, args: null);
|
||||
}
|
||||
|
||||
public static string? GetString(global::System.Globalization.CultureInfo? culture, string name, params object?[]? args)
|
||||
{
|
||||
culture ??= Culture;
|
||||
string? str = ResourceManager.GetString(name, culture);
|
||||
if (str == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args != null)
|
||||
{
|
||||
return string.Format(culture, str, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
public static string? GetString(string name, params object?[]? args)
|
||||
{
|
||||
return GetString(culture: null, name: name, args: args);
|
||||
}
|
||||
|
||||
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetString(string name, string? defaultValue)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: null);
|
||||
}
|
||||
|
||||
public static string? GetString(string name)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: null, args: null);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(global::System.Globalization.CultureInfo? culture, string name, string? defaultValue)
|
||||
{
|
||||
return GetStringOrDefault(culture: culture, name: name, defaultValue: defaultValue, args: null);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(global::System.Globalization.CultureInfo? culture, string name, string? defaultValue, params object?[]? args)
|
||||
{
|
||||
culture ??= Culture;
|
||||
string? str = ResourceManager.GetString(name, culture);
|
||||
if (str == null)
|
||||
{
|
||||
if (defaultValue == null || args == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Format(culture, defaultValue, args);
|
||||
}
|
||||
}
|
||||
|
||||
if (args != null)
|
||||
{
|
||||
return string.Format(culture, str, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(string name, string? defaultValue, params object?[]? args)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: args);
|
||||
}
|
||||
|
||||
[return:global::System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute("defaultValue")]
|
||||
public static string? GetStringOrDefault(string name, string? defaultValue)
|
||||
{
|
||||
return GetStringOrDefault(culture: null, name: name, defaultValue: defaultValue, args: null);
|
||||
}
|
||||
""");
|
||||
|
||||
foreach (ResxEntry? entry in entries.OrderBy(e => e.Name, StringComparer.Ordinal))
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.IsText)
|
||||
{
|
||||
XElement summary = new("summary", new XElement("para", $"Looks up a localized string for \"{entry.Name}\"."));
|
||||
if (!string.IsNullOrWhiteSpace(entry.Comment))
|
||||
{
|
||||
summary.Add(new XElement("para", entry.Comment));
|
||||
}
|
||||
|
||||
if (!entry.IsFileRef)
|
||||
{
|
||||
foreach((string? each, string locale) in entry.Values.Zip(entry.Locales,(x,y)=>(x,y)))
|
||||
{
|
||||
summary.Add(new XElement("para", $"{GetStringWithPadding(locale, 8)} Value: \"{each}\""));
|
||||
}
|
||||
}
|
||||
|
||||
string comment = summary.ToString().Replace("\r\n", "\r\n /// ", StringComparison.Ordinal);
|
||||
|
||||
sb.AppendLine($$"""
|
||||
/// {{comment}}
|
||||
public static string {{ToCSharpNameIdentifier(entry.Name!)}}
|
||||
{
|
||||
get => GetString("{{entry.Name}}")!;
|
||||
}
|
||||
|
||||
""");
|
||||
|
||||
if (entry.Values.FirstOrDefault() is string value)
|
||||
{
|
||||
int args = Regex.Matches(value, "\\{(?<num>[0-9]+)(\\:[^}]*)?\\}", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant)
|
||||
.Cast<Match>()
|
||||
.Select(m => int.Parse(m.Groups["num"].Value, CultureInfo.InvariantCulture))
|
||||
.Distinct()
|
||||
.DefaultIfEmpty(-1)
|
||||
.Max();
|
||||
|
||||
if (args >= 0)
|
||||
{
|
||||
string inParams = string.Join(", ", Enumerable.Range(0, args + 1).Select(arg => "object? arg" + arg.ToString(CultureInfo.InvariantCulture)));
|
||||
string callParams = string.Join(", ", Enumerable.Range(0, args + 1).Select(arg => "arg" + arg.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
sb.AppendLine($$"""
|
||||
/// {{comment}}
|
||||
public static string Format{{ToCSharpNameIdentifier(entry.Name!)}}({{inParams}})
|
||||
{
|
||||
return GetString("{{entry.Name}}", {{callParams}})!;
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($$"""
|
||||
public static global::{{entry.FullTypeName}}? {{ToCSharpNameIdentifier(entry.Name!)}}
|
||||
{
|
||||
get => (global::{{entry.FullTypeName}}?)GetObject("{{entry.Name}}");
|
||||
}
|
||||
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($$"""
|
||||
}
|
||||
|
||||
internal partial class {{className}}Names
|
||||
{
|
||||
""");
|
||||
|
||||
foreach (ResxEntry entry in entries)
|
||||
{
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.AppendLine($$"""
|
||||
public const string {{ToCSharpNameIdentifier(entry.Name!)}} = "entry.Name";
|
||||
""");
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GetStringWithPadding(string source, int length)
|
||||
{
|
||||
if (source.Length >= length)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
return source + new string('_', length - source.Length);
|
||||
}
|
||||
|
||||
private static string? ComputeResourceName(string rootNamespace, string projectDir, string resourcePath)
|
||||
{
|
||||
string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir));
|
||||
string fullResourcePath = Path.GetFullPath(resourcePath);
|
||||
|
||||
if (fullProjectDir == fullResourcePath)
|
||||
{
|
||||
return rootNamespace;
|
||||
}
|
||||
|
||||
if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal))
|
||||
{
|
||||
string relativePath = fullResourcePath.Substring(fullProjectDir.Length);
|
||||
return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ComputeNamespace(string rootNamespace, string projectDir, string resourcePath)
|
||||
{
|
||||
string fullProjectDir = EnsureEndSeparator(Path.GetFullPath(projectDir));
|
||||
string fullResourcePath = EnsureEndSeparator(Path.GetDirectoryName(Path.GetFullPath(resourcePath))!);
|
||||
|
||||
if (fullProjectDir == fullResourcePath)
|
||||
{
|
||||
return rootNamespace;
|
||||
}
|
||||
|
||||
if (fullResourcePath.StartsWith(fullProjectDir, StringComparison.Ordinal))
|
||||
{
|
||||
string relativePath = fullResourcePath.Substring(fullProjectDir.Length);
|
||||
return rootNamespace + '.' + relativePath.Replace('/', '.').Replace('\\', '.').TrimEnd('.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<ResxEntry>? LoadResourceFiles(SourceProductionContext context, IGrouping<string, AdditionalText> resxGroug)
|
||||
{
|
||||
List<ResxEntry> entries = [];
|
||||
foreach (AdditionalText? entry in resxGroug.OrderBy(file => file.Path, StringComparer.Ordinal))
|
||||
{
|
||||
SourceText? content = entry.GetText(context.CancellationToken);
|
||||
if (content is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
XDocument document = XDocument.Parse(content.ToString());
|
||||
foreach (XElement? element in document.XPathSelectElements("/root/data"))
|
||||
{
|
||||
string? name = element.Attribute("name")?.Value;
|
||||
string? type = element.Attribute("type")?.Value;
|
||||
string? comment = element.Attribute("comment")?.Value;
|
||||
string? value = element.Element("value")?.Value;
|
||||
|
||||
ResxEntry existingEntry = entries.Find(e => e.Name == name);
|
||||
if (existingEntry != null)
|
||||
{
|
||||
existingEntry.Comment ??= comment;
|
||||
existingEntry.Values.Add(value);
|
||||
existingEntry.Locales.Add(GetLocaleName(entry.Path));
|
||||
}
|
||||
else
|
||||
{
|
||||
entries.Add(new() { Name = name, Values = [value], Locales = [GetLocaleName(entry.Path)], Comment = comment, Type = type });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InvalidResx, location: null, entry.Path));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static string? GetMetadataValue(SourceProductionContext context, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string name, IEnumerable<AdditionalText> additionalFiles)
|
||||
{
|
||||
return GetMetadataValue(context, analyzerConfigOptionsProvider, name, name, additionalFiles);
|
||||
}
|
||||
|
||||
private static string? GetMetadataValue(SourceProductionContext context, AnalyzerConfigOptionsProvider analyzerConfigOptionsProvider, string name, string? globalName, IEnumerable<AdditionalText> additionalFiles)
|
||||
{
|
||||
string? result = null;
|
||||
foreach (AdditionalText file in additionalFiles)
|
||||
{
|
||||
if (analyzerConfigOptionsProvider.GetOptions(file).TryGetValue("build_metadata.AdditionalFiles." + name, out string? value))
|
||||
{
|
||||
if (result != null && value != result)
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(InconsistentProperties, location: null, name, file.Path));
|
||||
return null;
|
||||
}
|
||||
|
||||
result = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (globalName != null && analyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property." + globalName, out string? globalValue) && !string.IsNullOrEmpty(globalValue))
|
||||
{
|
||||
return globalValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ToCSharpNameIdentifier(string name)
|
||||
{
|
||||
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#identifiers
|
||||
// https://docs.microsoft.com/en-us/dotnet/api/system.globalization.unicodecategory?view=net-5.0
|
||||
StringBuilder sb = new();
|
||||
foreach (char c in name)
|
||||
{
|
||||
UnicodeCategory category = char.GetUnicodeCategory(c);
|
||||
switch (category)
|
||||
{
|
||||
case UnicodeCategory.UppercaseLetter:
|
||||
case UnicodeCategory.LowercaseLetter:
|
||||
case UnicodeCategory.TitlecaseLetter:
|
||||
case UnicodeCategory.ModifierLetter:
|
||||
case UnicodeCategory.OtherLetter:
|
||||
case UnicodeCategory.LetterNumber:
|
||||
sb.Append(c);
|
||||
break;
|
||||
|
||||
case UnicodeCategory.DecimalDigitNumber:
|
||||
case UnicodeCategory.ConnectorPunctuation:
|
||||
case UnicodeCategory.Format:
|
||||
if (sb.Length == 0)
|
||||
{
|
||||
sb.Append('_');
|
||||
}
|
||||
sb.Append(c);
|
||||
break;
|
||||
|
||||
default:
|
||||
sb.Append('_');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string EnsureEndSeparator(string path)
|
||||
{
|
||||
if (path[path.Length - 1] == Path.DirectorySeparatorChar)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return path + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
private static string GetResourceName(string path)
|
||||
{
|
||||
string pathWithoutExtension = Path.Combine(Path.GetDirectoryName(path)!, Path.GetFileNameWithoutExtension(path));
|
||||
int indexOf = pathWithoutExtension.LastIndexOf('.');
|
||||
if (indexOf < 0)
|
||||
{
|
||||
return pathWithoutExtension;
|
||||
}
|
||||
|
||||
return Regex.IsMatch(pathWithoutExtension.Substring(indexOf + 1), "^[a-zA-Z]{2}(-[a-zA-Z]{2,4})?$", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1))
|
||||
? pathWithoutExtension.Substring(0, indexOf)
|
||||
: pathWithoutExtension;
|
||||
}
|
||||
|
||||
private static string GetLocaleName(string path)
|
||||
{
|
||||
string fileName = Path.GetFileNameWithoutExtension(path);
|
||||
int indexOf = fileName.LastIndexOf('.');
|
||||
if (indexOf < 0)
|
||||
{
|
||||
return "Neutral";
|
||||
}
|
||||
|
||||
return fileName.Substring(indexOf + 1);
|
||||
}
|
||||
|
||||
private sealed class ResxEntry
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public List<string?> Values { get; set; } = default!;
|
||||
public List<string> Locales { get; set; } = default!;
|
||||
public string? Comment { get; set; }
|
||||
public string? Type { get; set; }
|
||||
|
||||
public bool IsText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Type == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Values.FirstOrDefault() is string value)
|
||||
{
|
||||
string[] parts = value.Split(';');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
string type = parts[1];
|
||||
if (type.StartsWith("System.String,", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string? FullTypeName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsText)
|
||||
{
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (Values.FirstOrDefault() is string value)
|
||||
{
|
||||
string[] parts = value.Split(';');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
string type = parts[1];
|
||||
return type.Split(',')[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsFileRef
|
||||
{
|
||||
get => Type != null && Type.StartsWith("System.Resources.ResXFileRef,", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration.Resx;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
|
||||
int previousIndex = 0;
|
||||
int index = str.IndexOf(oldValue, comparison);
|
||||
while (index is not -1)
|
||||
{
|
||||
sb.Append(str, previousIndex, index - previousIndex);
|
||||
sb.Append(newValue);
|
||||
index += oldValue.Length;
|
||||
|
||||
previousIndex = index;
|
||||
index = str.IndexOf(oldValue, index, comparison);
|
||||
}
|
||||
|
||||
sb.Append(str, previousIndex, str.Length - previousIndex);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" Version="3.3.4" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,302 +0,0 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Snap.Hutao.SourceGeneration.Primitive;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Snap.Hutao.SourceGeneration;
|
||||
|
||||
/// <summary>
|
||||
/// 通用分析器
|
||||
/// </summary>
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
internal sealed class UniversalAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly DiagnosticDescriptor typeInternalDescriptor = new("SH001", "Type should be internal", "Type [{0}] should be internal", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor readOnlyStructRefDescriptor = new("SH002", "ReadOnly struct should be passed with ref-like key word", "ReadOnly Struct [{0}] should be passed with ref-like key word", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useValueTaskIfPossibleDescriptor = new("SH003", "Use ValueTask instead of Task whenever possible", "Use ValueTask instead of Task", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsNotNullPatternMatchingDescriptor = new("SH004", "Use \"is not null\" instead of \"!= null\" whenever possible", "Use \"is not null\" instead of \"!= null\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsNullPatternMatchingDescriptor = new("SH005", "Use \"is null\" instead of \"== null\" whenever possible", "Use \"is null\" instead of \"== null\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useIsPatternRecursiveMatchingDescriptor = new("SH006", "Use \"is { } obj\" whenever possible", "Use \"is {{ }} {0}\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
private static readonly DiagnosticDescriptor useArgumentNullExceptionThrowIfNullDescriptor = new("SH007", "Use \"ArgumentNullException.ThrowIfNull()\" instead of \"!\"", "Use \"ArgumentNullException.ThrowIfNull()\"", "Quality", DiagnosticSeverity.Info, true);
|
||||
|
||||
|
||||
private static readonly ImmutableHashSet<string> RefLikeKeySkipTypes = new HashSet<string>()
|
||||
{
|
||||
"System.Threading.CancellationToken",
|
||||
"System.Guid"
|
||||
}.ToImmutableHashSet();
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DiagnosticDescriptor[]
|
||||
{
|
||||
typeInternalDescriptor,
|
||||
readOnlyStructRefDescriptor,
|
||||
useValueTaskIfPossibleDescriptor,
|
||||
useIsNotNullPatternMatchingDescriptor,
|
||||
useIsNullPatternMatchingDescriptor,
|
||||
useIsPatternRecursiveMatchingDescriptor,
|
||||
useArgumentNullExceptionThrowIfNullDescriptor
|
||||
}.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterCompilationStartAction(CompilationStart);
|
||||
}
|
||||
|
||||
private static void CompilationStart(CompilationStartAnalysisContext context)
|
||||
{
|
||||
SyntaxKind[] types = [SyntaxKind.ClassDeclaration, SyntaxKind.InterfaceDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.EnumDeclaration,];
|
||||
context.RegisterSyntaxNodeAction(HandleTypeShouldBeInternal, types);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodParameterShouldUseRefLikeKeyword, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask, SyntaxKind.MethodDeclaration);
|
||||
context.RegisterSyntaxNodeAction(HandleConstructorParameterShouldUseRefLikeKeyword, SyntaxKind.ConstructorDeclaration);
|
||||
|
||||
SyntaxKind[] expressions = [SyntaxKind.EqualsExpression, SyntaxKind.NotEqualsExpression,];
|
||||
context.RegisterSyntaxNodeAction(HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching, expressions);
|
||||
context.RegisterSyntaxNodeAction(HandleIsPatternShouldUseRecursivePattern, SyntaxKind.IsPatternExpression);
|
||||
context.RegisterSyntaxNodeAction(HandleArgumentNullExceptionThrowIfNull, SyntaxKind.SuppressNullableWarningExpression);
|
||||
|
||||
// TODO add analyzer for unnecessary IServiceProvider registration
|
||||
// TODO add analyzer for Singlton service use Scoped or Transient services
|
||||
}
|
||||
|
||||
private static void HandleTypeShouldBeInternal(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
BaseTypeDeclarationSyntax syntax = (BaseTypeDeclarationSyntax)context.Node;
|
||||
|
||||
bool privateExists = false;
|
||||
bool internalExists = false;
|
||||
bool fileExists = false;
|
||||
|
||||
foreach (SyntaxToken token in syntax.Modifiers)
|
||||
{
|
||||
if (token.IsKind(SyntaxKind.PrivateKeyword))
|
||||
{
|
||||
privateExists = true;
|
||||
}
|
||||
|
||||
if (token.IsKind(SyntaxKind.InternalKeyword))
|
||||
{
|
||||
internalExists = true;
|
||||
}
|
||||
|
||||
if (token.IsKind(SyntaxKind.FileKeyword))
|
||||
{
|
||||
fileExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!privateExists && !internalExists && !fileExists)
|
||||
{
|
||||
Location location = syntax.Identifier.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(typeInternalDescriptor, location, syntax.Identifier);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleMethodReturnTypeShouldUseValueTaskInsteadOfTask(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
|
||||
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
|
||||
|
||||
// 跳过重载方法
|
||||
if (methodSyntax.Modifiers.Any(token => token.IsKind(SyntaxKind.OverrideKeyword)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ICommand can only use Task or Task<T>
|
||||
if (methodSymbol.GetAttributes().Any(attr => attr.AttributeClass!.ToDisplayString() == Automation.CommandGenerator.AttributeName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
|
||||
{
|
||||
Location location = methodSyntax.ReturnType.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useValueTaskIfPossibleDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleMethodParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
MethodDeclarationSyntax methodSyntax = (MethodDeclarationSyntax)context.Node;
|
||||
|
||||
// 跳过方法定义 如 接口
|
||||
if (methodSyntax.Body == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IMethodSymbol methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax)!;
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.Task"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodSymbol.ReturnType.IsOrInheritsFrom("System.Threading.Tasks.ValueTask"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (SyntaxToken token in methodSyntax.Modifiers)
|
||||
{
|
||||
// 跳过异步方法,因为异步方法无法使用 ref/in/out
|
||||
if (token.IsKind(SyntaxKind.AsyncKeyword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过重载方法
|
||||
if (token.IsKind(SyntaxKind.OverrideKeyword))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ParameterSyntax parameter in methodSyntax.ParameterList.Parameters)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
|
||||
{
|
||||
if (IsBuiltInType(symbol.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RefLikeKeySkipTypes.Contains(symbol.Type.ToDisplayString()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
|
||||
{
|
||||
Location location = parameter.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleConstructorParameterShouldUseRefLikeKeyword(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
ConstructorDeclarationSyntax constructorSyntax = (ConstructorDeclarationSyntax)context.Node;
|
||||
|
||||
foreach (ParameterSyntax parameter in constructorSyntax.ParameterList.Parameters)
|
||||
{
|
||||
if (context.SemanticModel.GetDeclaredSymbol(parameter) is IParameterSymbol symbol)
|
||||
{
|
||||
if (IsBuiltInType(symbol.Type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过 CancellationToken
|
||||
if (symbol.Type.ToDisplayString() == "System.Threading.CancellationToken")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (symbol.Type.IsReadOnly && symbol.RefKind == RefKind.None)
|
||||
{
|
||||
Location location = parameter.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(readOnlyStructRefDescriptor, location, symbol.Type);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandleEqualsAndNotEqualsExpressionShouldUsePatternMatching(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
BinaryExpressionSyntax syntax = (BinaryExpressionSyntax)context.Node;
|
||||
if (syntax.IsKind(SyntaxKind.NotEqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
|
||||
{
|
||||
Location location = syntax.OperatorToken.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsNotNullPatternMatchingDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
else if (syntax.IsKind(SyntaxKind.EqualsExpression) && syntax.Right.IsKind(SyntaxKind.NullLiteralExpression))
|
||||
{
|
||||
Location location = syntax.OperatorToken.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsNullPatternMatchingDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleIsPatternShouldUseRecursivePattern(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
IsPatternExpressionSyntax syntax = (IsPatternExpressionSyntax)context.Node;
|
||||
if (syntax.Pattern is DeclarationPatternSyntax declaration)
|
||||
{
|
||||
ITypeSymbol? leftType = context.SemanticModel.GetTypeInfo(syntax.Expression).ConvertedType;
|
||||
ITypeSymbol? rightType = context.SemanticModel.GetTypeInfo(declaration).ConvertedType;
|
||||
if (SymbolEqualityComparer.Default.Equals(leftType, rightType))
|
||||
{
|
||||
Location location = declaration.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useIsPatternRecursiveMatchingDescriptor, location, declaration.Designation);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleArgumentNullExceptionThrowIfNull(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
PostfixUnaryExpressionSyntax syntax = (PostfixUnaryExpressionSyntax)context.Node;
|
||||
|
||||
if (syntax.Operand is LiteralExpressionSyntax literal)
|
||||
{
|
||||
if (literal.IsKind(SyntaxKind.DefaultLiteralExpression))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (syntax.Operand is DefaultExpressionSyntax expression)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Location location = syntax.GetLocation();
|
||||
Diagnostic diagnostic = Diagnostic.Create(useArgumentNullExceptionThrowIfNullDescriptor, location);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
private static bool IsBuiltInType(ITypeSymbol symbol)
|
||||
{
|
||||
return symbol.SpecialType switch
|
||||
{
|
||||
SpecialType.System_Boolean => true,
|
||||
SpecialType.System_Char => true,
|
||||
SpecialType.System_SByte => true,
|
||||
SpecialType.System_Byte => true,
|
||||
SpecialType.System_Int16 => true,
|
||||
SpecialType.System_UInt16 => true,
|
||||
SpecialType.System_Int32 => true,
|
||||
SpecialType.System_UInt32 => true,
|
||||
SpecialType.System_Int64 => true,
|
||||
SpecialType.System_UInt64 => true,
|
||||
SpecialType.System_Decimal => true,
|
||||
SpecialType.System_Single => true,
|
||||
SpecialType.System_Double => true,
|
||||
SpecialType.System_IntPtr => true,
|
||||
SpecialType.System_UIntPtr => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
|
||||
"settings": {
|
||||
"documentationRules": {
|
||||
"companyName": "DGP Studio",
|
||||
"copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license.",
|
||||
"xmlHeader": false,
|
||||
"variables": {
|
||||
"licenseName": "MIT"
|
||||
}
|
||||
},
|
||||
"orderingRules": {
|
||||
"elementOrder": [
|
||||
"kind",
|
||||
"accessibility",
|
||||
"constant",
|
||||
"static",
|
||||
"readonly"
|
||||
],
|
||||
"usingDirectivesPlacement": "outsideNamespace"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Win32", "Snap.Hutao.Win32\Snap.Hutao.Win32.csproj", "{0F7ABEB2-5107-4037-B9DC-84D288FB0801}"
|
||||
@@ -52,22 +50,6 @@ Global
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.ActiveCfg = Release|x86
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Build.0 = Release|x86
|
||||
{AAAB7CF0-F299-49B8-BDB4-4C320B3EC2C7}.Release|x86.Deploy.0 = Release|x86
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|Any CPU.Build.0 = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|arm64.Build.0 = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x64.Build.0 = Debug|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|arm64.Build.0 = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.ActiveCfg = Release|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.ActiveCfg = Debug|Any CPU
|
||||
@@ -105,11 +87,11 @@ Global
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_SortFileContentOnSave = True
|
||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||
RESX_NeutralResourcesLanguage = zh-CN
|
||||
RESX_AutoApplyExistingTranslations = False
|
||||
RESX_NeutralResourcesLanguage = zh-CN
|
||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||
RESX_SortFileContentOnSave = True
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutao"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.8.2.0" />
|
||||
Version="1.8.3.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao</DisplayName>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutaoDev"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.8.2.0" />
|
||||
Version="1.8.3.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao Dev</DisplayName>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
<PublishDir>bin\Release\Publish</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<_TargetId>Folder</_TargetId>
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -5,12 +5,10 @@
|
||||
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Snap.Hutao</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platform>x64</Platform>
|
||||
<Platforms>x64</Platforms>
|
||||
<!-- https://github.com/dotnet/docs/issues/36527 -->
|
||||
<UseRidGraph>true</UseRidGraph>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<UseWPF>False</UseWPF>
|
||||
<UseWindowsForms>False</UseWindowsForms>
|
||||
@@ -23,7 +21,7 @@
|
||||
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
||||
<PackageCertificateThumbprint>D15C4474363D2AF157C5CC6F230C7BFF2CF19B25</PackageCertificateThumbprint>
|
||||
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
||||
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
|
||||
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
|
||||
<AppxSymbolPackageEnabled>False</AppxSymbolPackageEnabled>
|
||||
<GenerateTestArtifacts>False</GenerateTestArtifacts>
|
||||
<AppxBundle>Never</AppxBundle>
|
||||
@@ -37,6 +35,15 @@
|
||||
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
|
||||
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<!--
|
||||
Required for .NET 8 MSIX packaging
|
||||
|
||||
10.2.4.1 Security - Software Dependencies
|
||||
Products may depend on non-integrated software (such as another product or module)
|
||||
to deliver primary functionality only as long as the additional required software
|
||||
is disclosed within the first two lines of the description in the Store.
|
||||
-->
|
||||
<SelfContained>true</SelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -295,6 +302,10 @@
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
|
||||
<PackageReference Include="Snap.Discord.GameSDK" Version="1.5.0" />
|
||||
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.507">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -306,10 +317,9 @@
|
||||
|
||||
<!-- Projects -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" />
|
||||
<ProjectReference Include="..\Snap.Hutao.Win32\Snap.Hutao.Win32.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<!-- Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||
package has not yet been restored -->
|
||||
|
||||
Reference in New Issue
Block a user