From c669ee5956c65a386e7fa185032b452a1ef3d986 Mon Sep 17 00:00:00 2001 From: cneicy Date: Tue, 29 Sep 2026 10:14:49 +0800 Subject: [PATCH] feat(codegen): validate contracts and unify Unity weaving --- Analyzers.meta | 8 +++ Analyzers/ShrinkSDK.CodeGen.Analyzers.dll | Bin 0 -> 16896 bytes .../ShrinkSDK.CodeGen.Analyzers.dll.meta | 35 ++++++++++ .../AnalyzerReleases.Shipped.md | 8 +++ .../ShrinkCodeGenAnalyzer.cs | 64 ++++++++++++++++-- .../ShrinkSDK.CodeGen.Analyzers.csproj | 3 +- .../ShrinkSDK.CodeGen.Task.csproj | 2 +- Editor/Core/ShrinkAssemblyWeaver.cs | 25 +++++-- Editor/ShrinkRegistryILPostProcessor.cs | 3 +- README.md | 16 ++++- package.json | 2 +- 11 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 Analyzers.meta create mode 100644 Analyzers/ShrinkSDK.CodeGen.Analyzers.dll create mode 100644 Analyzers/ShrinkSDK.CodeGen.Analyzers.dll.meta diff --git a/Analyzers.meta b/Analyzers.meta new file mode 100644 index 0000000..183b45c --- /dev/null +++ b/Analyzers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1b080310f7f6e52449c95e2b87bd3e0b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Analyzers/ShrinkSDK.CodeGen.Analyzers.dll b/Analyzers/ShrinkSDK.CodeGen.Analyzers.dll new file mode 100644 index 0000000000000000000000000000000000000000..d45a1fac9f002253c092f7b70a612204220a01bc GIT binary patch literal 16896 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 literal 0 HcmV?d00001 diff --git a/Analyzers/ShrinkSDK.CodeGen.Analyzers.dll.meta b/Analyzers/ShrinkSDK.CodeGen.Analyzers.dll.meta new file mode 100644 index 0000000..ffbc5ec --- /dev/null +++ b/Analyzers/ShrinkSDK.CodeGen.Analyzers.dll.meta @@ -0,0 +1,35 @@ +fileFormatVersion: 2 +guid: 6ecf86bfca6de8b46ae42b0e17af19ad +labels: +- RoslynAnalyzer +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/DotNet~/ShrinkSDK.CodeGen.Analyzers/AnalyzerReleases.Shipped.md b/DotNet~/ShrinkSDK.CodeGen.Analyzers/AnalyzerReleases.Shipped.md index 332da7f..8ed460f 100644 --- a/DotNet~/ShrinkSDK.CodeGen.Analyzers/AnalyzerReleases.Shipped.md +++ b/DotNet~/ShrinkSDK.CodeGen.Analyzers/AnalyzerReleases.Shipped.md @@ -7,3 +7,11 @@ Rule ID | Category | Severity | Notes SHRINK001 | ShrinkSDK.CodeGen | Error | Invalid event subscriber signature SHRINK002 | ShrinkSDK.CodeGen | Error | Duplicate network opcode and route SHRINK003 | ShrinkSDK.CodeGen | Error | Invalid application installer + +## Release 0.2.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +SHRINK004 | ShrinkSDK.CodeGen | Error | Invalid constant Context key constructor arguments diff --git a/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkCodeGenAnalyzer.cs b/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkCodeGenAnalyzer.cs index 64ae6e9..3a1173e 100644 --- a/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkCodeGenAnalyzer.cs +++ b/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkCodeGenAnalyzer.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; namespace ShrinkSDK.CodeGen.Analyzers; @@ -17,24 +18,45 @@ public sealed class ShrinkCodeGenAnalyzer : DiagnosticAnalyzer "Method '{0}' must use void(TEvent), UniTask(TEvent), or UniTask(TEvent, CancellationToken)", "ShrinkSDK.CodeGen", DiagnosticSeverity.Error, true); private static readonly DiagnosticDescriptor DuplicateNetworkContract = new( - "SHRINK002", "Duplicate network opcode and route", + "SHRINK002", "Duplicate network contract", "Network opcode/route '{0}' is already declared by '{1}'", "ShrinkSDK.CodeGen", DiagnosticSeverity.Error, true); private static readonly DiagnosticDescriptor InvalidInstaller = new( "SHRINK003", "Invalid application installer", "Type '{0}' has ShrinkAppModuleInstaller but does not implement IShrinkAppModuleInstaller", "ShrinkSDK.CodeGen", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor InvalidKey = new( + "SHRINK004", "Invalid Context key declaration", + "ShrinkKey argument '{0}' is invalid. Use nonempty package/name and a positive major version, e.g. new ShrinkKey(\"game\", \"rage\", 1).", + "ShrinkSDK.CodeGen", DiagnosticSeverity.Error, true); public override ImmutableArray SupportedDiagnostics => - ImmutableArray.Create(InvalidEventHandler, DuplicateNetworkContract, InvalidInstaller); + ImmutableArray.Create(InvalidEventHandler, DuplicateNetworkContract, InvalidInstaller, InvalidKey); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); + context.RegisterOperationAction(AnalyzeKey, OperationKind.ObjectCreation); context.RegisterCompilationStartAction(start => { var networkContracts = new ConcurrentDictionary(StringComparer.Ordinal); + var referencedConflicts = new System.Collections.Generic.List(); + foreach (var assembly in start.Compilation.SourceModule.ReferencedAssemblySymbols) + foreach (var type in Types(assembly.GlobalNamespace)) + { + var attribute = type.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "ShrinkNetwork.ShrinkNetworkMessageAttribute"); + if (attribute == null || attribute.ConstructorArguments.Length == 0) continue; + AddReference("opcode:" + attribute.ConstructorArguments[0].Value, type); + var route = attribute.ConstructorArguments.Length > 1 ? attribute.ConstructorArguments[1].Value as string : null; + if (!string.IsNullOrWhiteSpace(route)) AddReference("route:" + route!.Trim(), type); + } + void AddReference(string key, INamedTypeSymbol type) + { + if (!networkContracts.TryAdd(key, type) && networkContracts.TryGetValue(key, out var previous) && !SymbolEqualityComparer.Default.Equals(type, previous)) + referencedConflicts.Add(Diagnostic.Create(DuplicateNetworkContract, Location.None, key, previous.ToDisplayString() + " and " + type.ToDisplayString())); + } + start.RegisterCompilationEndAction(end => { foreach (var diagnostic in referencedConflicts) end.ReportDiagnostic(diagnostic); }); start.RegisterSymbolAction(symbolContext => AnalyzeMethod(symbolContext), SymbolKind.Method); start.RegisterSymbolAction(symbolContext => AnalyzeType(symbolContext, networkContracts), SymbolKind.NamedType); }); @@ -49,10 +71,26 @@ public sealed class ShrinkCodeGenAnalyzer : DiagnosticAnalyzer var returnName = method.ReturnType.ToDisplayString(); var returnValid = method.Parameters.Length == 1 && (method.ReturnsVoid || returnName == "Cysharp.Threading.Tasks.UniTask") || method.Parameters.Length == 2 && returnName == "Cysharp.Threading.Tasks.UniTask"; - if (!parametersValid || !returnValid) + var eventType = method.Parameters.FirstOrDefault()?.Type; + var eventValid = eventType != null && (eventType.ToDisplayString() == "ShrinkEventBus.IShrinkEvent" || + eventType.AllInterfaces.Any(i => i.ToDisplayString() == "ShrinkEventBus.IShrinkEvent")); + if (!parametersValid || !returnValid || !eventValid || method.IsGenericMethod || method.Parameters.Any(p => p.RefKind != RefKind.None)) context.ReportDiagnostic(Diagnostic.Create(InvalidEventHandler, method.Locations.FirstOrDefault(), method.Name)); } + private static void AnalyzeKey(OperationAnalysisContext context) + { + var creation = (IObjectCreationOperation)context.Operation; + if (creation.Type?.OriginalDefinition.ToDisplayString() != "ShrinkContext.ShrinkKey") return; + foreach (var argument in creation.Arguments) + { + if (!argument.Value.ConstantValue.HasValue) continue; + var value = argument.Value.ConstantValue.Value; + var invalid = argument.Parameter?.Ordinal < 2 ? value == null || value is string text && string.IsNullOrWhiteSpace(text) : value is int version && version <= 0; + if (invalid) context.ReportDiagnostic(Diagnostic.Create(InvalidKey, argument.Syntax.GetLocation(), argument.Parameter?.Name)); + } + } + private static void AnalyzeType(SymbolAnalysisContext context, ConcurrentDictionary networkContracts) { var type = (INamedTypeSymbol)context.Symbol; @@ -65,9 +103,23 @@ public sealed class ShrinkCodeGenAnalyzer : DiagnosticAnalyzer if (network == null || network.ConstructorArguments.Length == 0) return; var opcode = network.ConstructorArguments[0].Value?.ToString() ?? string.Empty; var route = network.ConstructorArguments.Length > 1 ? network.ConstructorArguments[1].Value as string ?? string.Empty : string.Empty; - var key = opcode + ":" + route; - if (!networkContracts.TryAdd(key, type) && networkContracts.TryGetValue(key, out var previous)) - context.ReportDiagnostic(Diagnostic.Create(DuplicateNetworkContract, type.Locations.FirstOrDefault(), key, previous.Name)); + Check("opcode:" + opcode); + if (!string.IsNullOrWhiteSpace(route)) Check("route:" + route.Trim()); + void Check(string key) + { + if (!networkContracts.TryAdd(key, type) && networkContracts.TryGetValue(key, out var previous) && + !SymbolEqualityComparer.Default.Equals(previous, type)) + context.ReportDiagnostic(Diagnostic.Create(DuplicateNetworkContract, type.Locations.FirstOrDefault(), key, previous.ToDisplayString())); + } + } + + private static System.Collections.Generic.IEnumerable Types(INamespaceOrTypeSymbol container) + { + foreach (var member in container.GetMembers()) + { + if (member is INamedTypeSymbol type) { yield return type; foreach (var child in Types(type)) yield return child; } + else if (member is INamespaceSymbol ns) foreach (var child in Types(ns)) yield return child; + } } private static bool HasAttribute(ISymbol symbol, string fullName) => diff --git a/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj b/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj index 78c53ff..ebec188 100644 --- a/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj +++ b/DotNet~/ShrinkSDK.CodeGen.Analyzers/ShrinkSDK.CodeGen.Analyzers.csproj @@ -6,8 +6,9 @@ false false true + 4.14.0 - + diff --git a/DotNet~/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj b/DotNet~/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj index d89da22..a47eeba 100644 --- a/DotNet~/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj +++ b/DotNet~/ShrinkSDK.CodeGen.Task/ShrinkSDK.CodeGen.Task.csproj @@ -4,7 +4,7 @@ ShrinkSDK.CodeGen.Task ShrinkSDK.CodeGen ShrinkSDK.CodeGen - 0.1.0 + 0.2.0 MSBuild integration for ShrinkSDK Cecil weaving. true tools diff --git a/Editor/Core/ShrinkAssemblyWeaver.cs b/Editor/Core/ShrinkAssemblyWeaver.cs index ebb5af2..cff8715 100644 --- a/Editor/Core/ShrinkAssemblyWeaver.cs +++ b/Editor/Core/ShrinkAssemblyWeaver.cs @@ -57,6 +57,14 @@ public static class ShrinkAssemblyWeaver var module = assembly.MainModule; var marker = FindType(module, "ShrinkSDK.Runtime.ShrinkCodeGenWovenAttribute", "ShrinkRuntime.Abstractions"); + // Unity passes transitive reference paths even when C# emitted no direct reference. + // All Unity postprocessors must see the same marker before another pass runs. + if (marker == null) + { + var runtime = resolver.Resolve(new AssemblyNameReference("ShrinkRuntime.Abstractions", new System.Version(0, 0, 0, 0))); + var definition = runtime?.MainModule.GetType("ShrinkSDK.Runtime.ShrinkCodeGenWovenAttribute"); + if (definition != null) marker = module.ImportReference(definition); + } var existingMarker = marker == null ? null : assembly.CustomAttributes.FirstOrDefault(a => a.AttributeType.FullName == marker.FullName); if (existingMarker != null) { @@ -140,14 +148,13 @@ public static class ShrinkAssemblyWeaver if (type.Fields.Any(field => field.Name == bindingFieldName)) throw new InvalidOperationException($"Reserved generated field already exists on {type.FullName}: {bindingFieldName}."); - var disposableType = module.ImportReference(typeof(IDisposable)); + var disposableType = new TypeReference("System", "IDisposable", module, module.TypeSystem.CoreLibrary); var bindingField = new FieldDefinition(bindingFieldName, FieldAttributes.Private, disposableType); type.Fields.Add(bindingField); var eventBusType = RequireType(module, "ShrinkEventBus.EventBus", EventRuntime); var attachMethod = ImportMethod(module, eventBusType, method => method.Name == "Attach" && method.IsStatic && method.Parameters.Count == 2); - var disposeMethod = module.ImportReference(typeof(IDisposable).GetMethod(nameof(IDisposable.Dispose)) - ?? throw new InvalidOperationException("IDisposable.Dispose was not found.")); + var disposeMethod = new MethodReference("Dispose", module.TypeSystem.Void, disposableType) { HasThis = true }; InjectAwake(type, module, bindingField, attachMethod); InjectOnDestroy(type, module, bindingField, disposeMethod); } @@ -638,10 +645,14 @@ public static class ShrinkAssemblyWeaver { var opcode = Convert.ToInt32(attribute.ConstructorArguments[0].Value); var route = attribute.ConstructorArguments.Count > 1 ? attribute.ConstructorArguments[1].Value as string ?? string.Empty : string.Empty; - var key = $"{opcode}:{route}"; - if (seen.TryGetValue(key, out var previous)) - throw new InvalidOperationException($"Duplicate network opcode/route {key}: {previous} and {type.FullName}."); - seen[key] = type.FullName; + Check("opcode:" + opcode); + if (!string.IsNullOrWhiteSpace(route)) Check("route:" + route.Trim()); + void Check(string key) + { + if (seen.TryGetValue(key, out var previous)) + throw new InvalidOperationException($"SHRINK002 Duplicate {key}: {previous} and {type.FullName}. Assign unique opcodes and nonempty routes."); + seen[key] = type.FullName; + } } } diff --git a/Editor/ShrinkRegistryILPostProcessor.cs b/Editor/ShrinkRegistryILPostProcessor.cs index d1e1b48..158e400 100644 --- a/Editor/ShrinkRegistryILPostProcessor.cs +++ b/Editor/ShrinkRegistryILPostProcessor.cs @@ -12,7 +12,8 @@ namespace ShrinkShared.CodeGen public override bool WillProcess(ICompiledAssembly compiledAssembly) => UnityShrinkCodeGenAdapter.ReferencesAny(compiledAssembly, - "ShrinkCommand.Runtime", "ShrinkNetwork.Runtime", "ShrinkApp.Core.Runtime"); + "ShrinkCommand.Runtime", "ShrinkNetwork.Runtime", "ShrinkApp.Core.Runtime", + "ShrinkEventBus.Runtime", "ShrinkNetwork.Integration.EventBus"); public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) => WillProcess(compiledAssembly) diff --git a/README.md b/README.md index 7a3baf7..64df25e 100644 --- a/README.md +++ b/README.md @@ -7,5 +7,19 @@ Unity Editor UPM 包,为引用 ShrinkCommand.Runtime、ShrinkNetwork.Runtime `DotNet~` 包含同一 Cecil 织入核心、Roslyn Analyzer 和 `ShrinkSDK.CodeGen` MSBuild 包。Godot 或普通 .NET 项目引用 EventBus、Command、Network、App 等包后,会通过 `buildTransitive` 自动启用织入。 ```powershell -dotnet add package ShrinkSDK.CodeGen --version 0.1.0 +dotnet add package ShrinkSDK.CodeGen --version 0.2.0 ``` + +## 可复用能力 + + +Capability: 编译期织入事件订阅、命令和网络注册表,提供规范诊断 +Aliases: 生成 织入 analyzer 注册 IL +Limits: 不是运行时业务模块;Unity 织入与外部 Roslyn 分别构建 +Extension: 特性声明及 MSBuild/Unity 构建入口 +Evidence: [ShrinkAssemblyWeaver](Editor/Core/ShrinkAssemblyWeaver.cs) + + +Unity 2022.3 的分析器 DLL 使用 Roslyn 3.8 构建,位于 `Analyzers/`,由 `RoslynAnalyzer` 标签加载,禁用普通运行时插件导入。外部 CLI/NuGet 使用 Roslyn 4.14,二者不能直接互换。SDK 仓库通过 `Tools/AgentSupport/Build-UnityAnalyzer.ps1` 重建 Unity 版本。 + +诊断:SHRINK001 订阅签名,SHRINK002 opcode/非空 route 冲突(包括引用程序集),SHRINK003 Installer 接口,SHRINK004 常量 key 声明。生命周期是否正确仍由行为测试验证。Unity 的统一 IL 后处理入口为 ShrinkRegistryILPostProcessor,避免同一程序集重复织入。 diff --git a/package.json b/package.json index 2c6e868..00ad1d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.cneicy.shrink-shared-codegen", - "version": "0.1.1", + "version": "0.2.1", "displayName": "Shrink Shared CodeGen", "description": "ShrinkApp、ShrinkCommand 与 ShrinkNetwork 共用的 Unity IL 后处理注册表生成器。", "unity": "2022.3",