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 0000000..d45a1fa Binary files /dev/null and b/Analyzers/ShrinkSDK.CodeGen.Analyzers.dll differ 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",