diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..015dd0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +[Dd]ebug/ +[Rr]elease/ +x64/ +[Bb]in/ +[Oo]bj/ + +*.*~ +.vs +.vscode +.idea +.gradle +*.DotSettings.user \ No newline at end of file diff --git a/CodeGen/AtomILPostProcessor.cs b/CodeGen/AtomILPostProcessor.cs index 5279df8..0bc7556 100644 --- a/CodeGen/AtomILPostProcessor.cs +++ b/CodeGen/AtomILPostProcessor.cs @@ -38,13 +38,12 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) var sw = Stopwatch.StartNew(); var assemblyDefinition = AssemblyDefinitionFor(compiledAssembly); - var diagnostics = new List(); - diagnostics.AddRange(new AtomWeaverV2().Weave(assemblyDefinition, out var madeAnyChange)); + var madeAnyChange = new AtomWeaverV2().Weave(assemblyDefinition); - if (!madeAnyChange || diagnostics.Any(d => d.DiagnosticType == DiagnosticType.Error)) + if (!madeAnyChange) { - return new ILPostProcessResult(null, diagnostics); + return new ILPostProcessResult(null); } var pe = new MemoryStream(); @@ -60,7 +59,7 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) UnityEngine.Debug.Log($"Weaved {compiledAssembly.Name} in {sw.ElapsedMilliseconds}ms"); #endif - return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), diagnostics); + return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray())); } private static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly) diff --git a/CodeGen/AtomWeaverV2.cs b/CodeGen/AtomWeaverV2.cs index b93aa63..6cae31f 100644 --- a/CodeGen/AtomWeaverV2.cs +++ b/CodeGen/AtomWeaverV2.cs @@ -2,12 +2,10 @@ using System; using System.Collections.Generic; -using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; using UniMob.Core; -using Unity.CompilationPipeline.Common.Diagnostics; namespace UniMob.Editor.Weaver { @@ -21,8 +19,6 @@ internal class AtomWeaverV2 private const string ThrowIfDisposedMethodName = nameof(LifetimeScopeExtension.ThrowIfDisposed); private const string KeepAliveParameterName = nameof(AtomAttribute.KeepAlive); - private List _diagnosticMessages = new List(); - private ModuleDefinition _module; private TypeReference _atomType; @@ -38,22 +34,34 @@ internal class AtomWeaverV2 private bool _generateDebugNames; - public List Weave(AssemblyDefinition assembly, out bool didAnyChange) + public bool Weave(AssemblyDefinition assembly) { - Prepare(assembly); - - var allProperties = _module - .GetAllTypes() - .SelectMany(type => type.Properties); + _module = assembly.MainModule; + _lifetimeScopeInterfaceType = _module.ImportReference(typeof(ILifetimeScope)); - didAnyChange = false; + var didAnyChange = false; + var prepared = false; - foreach (var property in allProperties) + foreach (var type in _module.GetAllTypes()) { - didAnyChange |= Weave(property); + if (!ShouldWeaveType(type)) + { + continue; + } + + if (!prepared) + { + prepared = true; + Prepare(assembly); + } + + foreach (var property in type.Properties) + { + didAnyChange |= Weave(property); + } } - return _diagnosticMessages; + return didAnyChange; } private void Prepare(AssemblyDefinition assembly) @@ -62,11 +70,8 @@ private void Prepare(AssemblyDefinition assembly) #if UNIMOB_ATOM_GENERATE_DEBUG_NAMES _generateDebugNames = true; #endif - - _module = assembly.MainModule; - + _atomType = _module.ImportReference(typeof(ComputedAtom<>)); - _lifetimeScopeInterfaceType = _module.ImportReference(typeof(ILifetimeScope)); var atomTypeDef = _atomType.Resolve(); var atomPullDef = _module.ImportReference(typeof(Func<>)).Resolve(); @@ -85,47 +90,46 @@ private void Prepare(AssemblyDefinition assembly) _module.ImportReference(lifetimeScopeExtensionsDef.FindMethod(ThrowIfDisposedMethodName, 1)); } - public bool Weave(PropertyDefinition property) + private bool ShouldWeaveType(TypeDefinition type) { - var atomAttribute = Helpers.GetCustomAttribute(property); - if (atomAttribute == null) + if (!type.IsClass) { return false; } - if (property.DeclaringType.HasGenericParameters) + if (type.HasGenericParameters) { - _diagnosticMessages.Add(UserError.AtomAttributeCannotBeUsedOnGenericClasses(property)); return false; } - if (!property.DeclaringType.IsClass || property.DeclaringType.IsValueType) + if (!type.IsInterfaceImplemented(_lifetimeScopeInterfaceType)) { - _diagnosticMessages.Add(UserError.AtomAttributeCanBeUsedOnlyOnClassMembers(property)); return false; } - if (!property.DeclaringType.IsInterfaceImplemented(_lifetimeScopeInterfaceType)) + return true; + } + + public bool Weave(PropertyDefinition property) + { + var atomAttribute = Helpers.GetCustomAttribute(property); + if (atomAttribute == null) { - _diagnosticMessages.Add(UserError.AtomAttributeCanBeUsedOnlyOnLifetimeScope(property)); return false; } if (property.GetMethod == null) { - _diagnosticMessages.Add(UserError.CannotUseAtomAttributeOnSetOnlyProperty(property)); return false; } if (property.GetMethod.IsStatic) { - _diagnosticMessages.Add(UserError.CannotUseAtomAttributeOnStaticProperty(property)); return false; } if (property.GetMethod.IsAbstract) { - _diagnosticMessages.Add(UserError.CannotUseAtomAttributeOnAbstractProperty(property)); return false; } diff --git a/CodeGen/UserError.cs b/CodeGen/UserError.cs deleted file mode 100644 index f84945a..0000000 --- a/CodeGen/UserError.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Mono.Cecil; -using Mono.Cecil.Cil; -using Unity.CompilationPipeline.Common.Diagnostics; -using DType = Unity.CompilationPipeline.Common.Diagnostics.DiagnosticType; - -namespace UniMob.Editor.Weaver -{ - internal static class UserError - { - public static DiagnosticMessage AtomAttributeCanBeUsedOnlyOnClassMembers(PropertyDefinition property) - => Make(DType.Error, Code(11), $"Atom attribute can be used only on class members", property); - - public static DiagnosticMessage AtomAttributeCanBeUsedOnlyOnLifetimeScope(PropertyDefinition property) - => Make(DType.Error, Code(12), $"Atom attribute can be used only on class that implements ILifetimeScope interface", property); - - public static DiagnosticMessage AtomAttributeCannotBeUsedOnGenericClasses(PropertyDefinition property) - => Make(DType.Error, Code(13), $"Atom attribute cannot be used on generic classes", property); - - public static DiagnosticMessage CannotUseAtomAttributeOnSetOnlyProperty(PropertyDefinition property) - => Make(DType.Error, Code(21), $"Atom attribute cannot be used on set-only property", property); - - public static DiagnosticMessage CannotUseAtomAttributeOnStaticProperty(PropertyDefinition property) - => Make(DType.Error, Code(22), $"Atom attribute cannot be used on static property", property); - - public static DiagnosticMessage CannotUseAtomAttributeOnAbstractProperty(PropertyDefinition property) - => Make(DType.Error, Code(23), $"Atom attribute cannot be used on abstract property", property); - - private static string Code(int code) - { - return "UniMob" + code.ToString().PadLeft(4, '0'); - } - - private static DiagnosticMessage Make(DiagnosticType type, string errorCode, string messageData, - PropertyDefinition property) - { - return Make(type, errorCode, messageData, property?.GetMethod ?? property?.SetMethod, null); - } - - private static DiagnosticMessage Make(DiagnosticType type, string errorCode, string messageData, - MethodDefinition method, Instruction instruction) - { - var seq = method != null ? Helpers.FindBestSequencePointFor(method, instruction) : null; - return Make(type, errorCode, messageData, seq); - } - - private static DiagnosticMessage Make(DiagnosticType type, string errorCode, string messageData, - SequencePoint seq) - { - var result = new DiagnosticMessage {Column = 0, Line = 0, DiagnosticType = type, File = ""}; - - messageData = $"error {errorCode}: {messageData}"; - if (seq != null) - { - result.File = seq.Document.Url; - result.Column = seq.StartColumn; - result.Line = seq.StartLine; -#if UNITY_EDITOR - result.MessageData = $"{seq.Document.Url}({seq.StartLine},{seq.StartColumn}): {messageData}"; -#else - result.MessageData = messageData; -#endif - } - else - { - result.MessageData = messageData; - } - - return result; - } - } -} \ No newline at end of file diff --git a/CodeGen/UserError.cs.meta b/CodeGen/UserError.cs.meta deleted file mode 100644 index eb2acec..0000000 --- a/CodeGen/UserError.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 163b9b233bbe43f2bae7ab76abc9ed90 -timeCreated: 1598198003 \ No newline at end of file diff --git a/Runtime/UniMob.Analyzers.dll b/Runtime/UniMob.Analyzers.dll new file mode 100644 index 0000000..22226fe Binary files /dev/null and b/Runtime/UniMob.Analyzers.dll differ diff --git a/Runtime/UniMob.Analyzers.dll.meta b/Runtime/UniMob.Analyzers.dll.meta new file mode 100644 index 0000000..c8c46bc --- /dev/null +++ b/Runtime/UniMob.Analyzers.dll.meta @@ -0,0 +1,88 @@ +fileFormatVersion: 2 +guid: 6fadd7579d3ef95488acf93ce5fac4ed +labels: +- RoslynAnalyzer +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 1 + Exclude iOS: 1 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + CPU: AnyCPU + DefaultValueInitialized: true + OS: AnyOS + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + iPhone: iOS + second: + enabled: 0 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Sources~/UniMob/UniMob.Analyzers/AtomAnalyzer.cs b/Sources~/UniMob/UniMob.Analyzers/AtomAnalyzer.cs new file mode 100644 index 0000000..1759ced --- /dev/null +++ b/Sources~/UniMob/UniMob.Analyzers/AtomAnalyzer.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace UniMob.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class AtomAnalyzer : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor UnhandledErrorRule = new( + id: "UniMob_000", + title: "Internal error", + messageFormat: "Internal error occurred. Please open a bug report. Message: '{0}'", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor AtomAttributeCanBeUsedOnlyOnClassMembers = new( + id: "UniMob_011", + title: "Atom attribute can be used only on class members", + messageFormat: "Atom attribute can be used only on class members", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor AtomAttributeCanBeUsedOnlyOnLifetimeScope = new( + id: "UniMob_012", + title: "Atom attribute can be used only on class that implements ILifetimeScope interface", + messageFormat: "Atom attribute can be used only on class that implements ILifetimeScope interface", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor AtomAttributeCannotBeUsedOnGenericClasses = new( + id: "UniMob_013", + title: "Atom attribute cannot be used on generic classes", + messageFormat: "Atom attribute cannot be used on generic classes", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor CannotUseAtomAttributeOnSetOnlyProperty = new( + id: "UniMob_021", + title: "Atom attribute cannot be used on set-only property", + messageFormat: "Atom attribute cannot be used on set-only property", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor CannotUseAtomAttributeOnStaticProperty = new( + id: "UniMob_022", + title: "Atom attribute cannot be used on static property", + messageFormat: "Atom attribute cannot be used on static property", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + private static readonly DiagnosticDescriptor CannotUseAtomAttributeOnAbstractProperty = new( + id: "UniMob_023", + title: "Atom attribute cannot be used on abstract property", + messageFormat: "Atom attribute cannot be used on abstract property", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + AtomAttributeCanBeUsedOnlyOnClassMembers, + AtomAttributeCannotBeUsedOnGenericClasses, + AtomAttributeCanBeUsedOnlyOnLifetimeScope, + CannotUseAtomAttributeOnSetOnlyProperty, + CannotUseAtomAttributeOnStaticProperty, + CannotUseAtomAttributeOnAbstractProperty, + UnhandledErrorRule + ); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | + GeneratedCodeAnalysisFlags.ReportDiagnostics); + + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private void OnCompilationStart(CompilationStartAnalysisContext context) + { + if (context.Compilation.GetTypeByMetadataName("UniMob.AtomAttribute") is not { } atomAttributeSymbol) + { + return; + } + + if (context.Compilation.GetTypeByMetadataName("UniMob.ILifetimeScope") is not { } lifetimeScopeSymbol) + { + return; + } + + var cache = new Cache + { + AtomAttributeTypeSymbol = atomAttributeSymbol, + LifetimeScopeTypeSymbol = lifetimeScopeSymbol, + }; + + context.RegisterSyntaxNodeAction(ctx => + { + try + { + CheckAttributeDeclaration(ctx, cache); + } + catch (Exception ex) + { + ctx.ReportDiagnostic(Diagnostic.Create(UnhandledErrorRule, ctx.Node.GetLocation(), ex.Message)); + } + }, SyntaxKind.Attribute); + } + + private void CheckAttributeDeclaration(SyntaxNodeAnalysisContext context, Cache cache) + { + if (context.Node.Ancestors().OfType().FirstOrDefault() + is not { } propertySyntax) + { + return; + } + + if (context.SemanticModel.GetDeclaredSymbol(propertySyntax) is not { } propertySymbol) + { + return; + } + + var atomAttributeData = propertySymbol.GetAttributes().FirstOrDefault(it => + SymbolEqualityComparer.Default.Equals(cache.AtomAttributeTypeSymbol, it.AttributeClass)); + + if (atomAttributeData is null) + { + return; + } + + AnalyzeAtomProperty(context, cache, propertySyntax, propertySymbol); + } + + private void AnalyzeAtomProperty(SyntaxNodeAnalysisContext context, Cache cache, + PropertyDeclarationSyntax propertySyntax, IPropertySymbol propertySymbol) + { + if (propertySymbol.IsStatic) + { + context.ReportDiagnostic( + Diagnostic.Create(CannotUseAtomAttributeOnStaticProperty, context.Node.GetLocation())); + } + + if (propertySymbol.IsAbstract) + { + context.ReportDiagnostic( + Diagnostic.Create(CannotUseAtomAttributeOnAbstractProperty, context.Node.GetLocation())); + } + + if (propertySymbol.GetMethod is null) + { + context.ReportDiagnostic( + Diagnostic.Create(CannotUseAtomAttributeOnSetOnlyProperty, context.Node.GetLocation())); + } + + if (propertySyntax.Ancestors().OfType().FirstOrDefault() + is not { } classSyntax) + { + context.ReportDiagnostic( + Diagnostic.Create(AtomAttributeCanBeUsedOnlyOnClassMembers, context.Node.GetLocation())); + return; + } + + if (classSyntax.TypeParameterList != null) + { + context.ReportDiagnostic( + Diagnostic.Create(AtomAttributeCannotBeUsedOnGenericClasses, context.Node.GetLocation())); + } + + if (context.SemanticModel.GetDeclaredSymbol(classSyntax) is not { } classSymbol) + { + return; + } + + var isLifetimeScope = classSymbol.AllInterfaces + .Any(it => SymbolEqualityComparer.Default.Equals(cache.LifetimeScopeTypeSymbol, it)); + + if (!isLifetimeScope) + { + context.ReportDiagnostic( + Diagnostic.Create(AtomAttributeCanBeUsedOnlyOnLifetimeScope, context.Node.GetLocation())); + } + } + + private class Cache + { + public INamedTypeSymbol AtomAttributeTypeSymbol; + public INamedTypeSymbol LifetimeScopeTypeSymbol; + } + } +} \ No newline at end of file diff --git a/Sources~/UniMob/UniMob.Analyzers/UniMob.Analyzers.csproj b/Sources~/UniMob/UniMob.Analyzers/UniMob.Analyzers.csproj new file mode 100644 index 0000000..c9cedf5 --- /dev/null +++ b/Sources~/UniMob/UniMob.Analyzers/UniMob.Analyzers.csproj @@ -0,0 +1,30 @@ + + + + netstandard2.0 + UniMob.Analyzers + cs + library + latest + true + false + false + true + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + $(MSBuildProjectDirectory)\..\..\..\Runtime\ + + + + + diff --git a/Sources~/UniMob/UniMob.sln b/Sources~/UniMob/UniMob.sln new file mode 100644 index 0000000..fedfd8b --- /dev/null +++ b/Sources~/UniMob/UniMob.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33516.290 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniMob.Analyzers", "UniMob.Analyzers\UniMob.Analyzers.csproj", "{8D064FAB-A280-4576-BE3D-80A43F34E5DF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8D064FAB-A280-4576-BE3D-80A43F34E5DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D064FAB-A280-4576-BE3D-80A43F34E5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D064FAB-A280-4576-BE3D-80A43F34E5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D064FAB-A280-4576-BE3D-80A43F34E5DF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {49FBB5E6-3F4E-4721-99C3-FC22481CFDDC} + EndGlobalSection +EndGlobal