From 853b99db8eb741acbacde56a182ea42f2847f1cd Mon Sep 17 00:00:00 2001 From: Jim8y Date: Tue, 14 Jul 2026 13:34:02 +0800 Subject: [PATCH 1/3] Feature: run Neo analyzers during nccs compilation --- .../CompilationEngine/CompilationContext.cs | 3 + .../CompilationEngine/CompilationEngine.cs | 29 +++ .../CompilationEngine/CompilationOptions.cs | 5 + .../Neo.Compiler.CSharp.csproj | 1 + src/Neo.Compiler.CSharp/Program.cs | 3 +- src/Neo.Compiler.CSharp/README.md | 1 + .../Neo.SmartContract.Analyzer.csproj | 2 +- .../NeoAnalyzerSuite.cs | 57 +++++ .../UnitTest_AnalyzerExecution.cs | 201 ++++++++++++++++++ .../UnitTest_ProjectBoundaries.cs | 6 +- ...eo.SmartContract.Analyzer.UnitTests.csproj | 4 + .../NeoAnalyzerSuiteTests.cs | 27 +++ ...o.SmartContract.Framework.UnitTests.csproj | 1 + ...eo.SmartContract.Template.UnitTests.csproj | 1 + ...Neo.SmartContract.Testing.UnitTests.csproj | 1 + 15 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 src/Neo.SmartContract.Analyzer/NeoAnalyzerSuite.cs create mode 100644 tests/Neo.Compiler.CSharp.UnitTests/UnitTest_AnalyzerExecution.cs create mode 100644 tests/Neo.SmartContract.Analyzer.UnitTests/NeoAnalyzerSuiteTests.cs diff --git a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs index 2053fee69..30db93ec8 100644 --- a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs +++ b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs @@ -165,6 +165,9 @@ private static void ValidateContractPermission(INamedTypeSymbol symbol, string c internal void Compile() { + _diagnostics.AddRange(_engine.AnalyzerDiagnostics); + if (!Success) return; + HashSet processed = new(SymbolEqualityComparer.Default); foreach (SyntaxTree tree in _engine.Compilation!.SyntaxTrees) { diff --git a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationEngine.cs b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationEngine.cs index 52e095fe5..c8f6c3115 100644 --- a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationEngine.cs +++ b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationEngine.cs @@ -14,10 +14,13 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; using Neo.Json; +using Neo.SmartContract.Analyzer; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; using System.IO; @@ -34,8 +37,10 @@ public class CompilationEngine(CompilationOptions options) { internal Compilation? Compilation; internal MetadataReference? FrameworkReference; + internal ImmutableArray AnalyzerDiagnostics { get; private set; } = []; internal CompilationOptions Options { get; private set; } = options; private static readonly MetadataReference[] CommonReferences; + private static readonly ImmutableArray NeoAnalyzers = NeoAnalyzerSuite.Create(); private static readonly Guid FrameworkModuleVersionId = typeof(scfx::Neo.SmartContract.Framework.Attributes.SafeAttribute).Assembly.ManifestModule.ModuleVersionId; private const string FrameworkAssemblyName = "Neo.SmartContract.Framework"; private static readonly StringComparison ProjectPathComparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; @@ -154,6 +159,7 @@ internal List Compile(IEnumerable sourceFiles, IEnum FrameworkReference = frameworkReference ?? referenceArray.FirstOrDefault(IsTrustedFrameworkReference); Compilation = CSharpCompilation.Create(null, syntaxTrees, referenceArray, compilationOptions); + AnalyzeCompilation(); return CompileProjectContracts(Compilation); } @@ -224,6 +230,7 @@ private Compilation LoadProjectCompilation(string csproj, bool forceReload) ResetProjectState(); Compilation = GetCompilation(projectPath); FrameworkReference = ResolveProjectFrameworkReference(Compilation); + AnalyzeCompilation(); ProjectPath = projectPath; return Compilation; } @@ -232,6 +239,7 @@ private void ResetProjectState() { Compilation = null; FrameworkReference = null; + AnalyzerDiagnostics = []; MetaReferences.Clear(); PreparedProjectPath = null; ProjectPath = null; @@ -272,6 +280,27 @@ internal static bool IsTrustedFrameworkReference(MetadataReference reference) } } + private void AnalyzeCompilation() + { + if (!Options.RunAnalyzers) + { + AnalyzerDiagnostics = []; + return; + } + + AnalyzerDiagnostics = Compilation! + .WithAnalyzers(NeoAnalyzers) + .GetAnalyzerDiagnosticsAsync() + .GetAwaiter() + .GetResult() + .Where(diagnostic => diagnostic.Severity != DiagnosticSeverity.Hidden) + .OrderBy(diagnostic => diagnostic.Location.SourceTree?.FilePath ?? string.Empty, StringComparer.Ordinal) + .ThenBy(diagnostic => diagnostic.Location.IsInSource ? diagnostic.Location.SourceSpan.Start : -1) + .ThenBy(diagnostic => diagnostic.Id, StringComparer.Ordinal) + .ThenBy(diagnostic => diagnostic.GetMessage(), StringComparer.Ordinal) + .ToImmutableArray(); + } + public List CompileProject(string csproj, List sortedClasses, Dictionary> classDependencies, List allClassSymbols, string? targetContractName = null) { if (sortedClasses == null || classDependencies == null || allClassSymbols == null) diff --git a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationOptions.cs b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationOptions.cs index 45105e4ac..75742ff48 100644 --- a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationOptions.cs +++ b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationOptions.cs @@ -58,6 +58,11 @@ public enum DebugType : byte private CSharpParseOptions? parseOptions = null; public bool SkipRestoreIfAssetsPresent { get; set; } + /// + /// Gets or sets whether the Neo contract analyzer suite runs before contract conversion. + /// + public bool RunAnalyzers { get; set; } + public CSharpParseOptions GetParseOptions() { if (parseOptions is null) diff --git a/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj b/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj index 7cef5a8cd..754334edb 100644 --- a/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj +++ b/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj @@ -34,6 +34,7 @@ + scfx diff --git a/src/Neo.Compiler.CSharp/Program.cs b/src/Neo.Compiler.CSharp/Program.cs index 84dcd8f32..b104fd41a 100644 --- a/src/Neo.Compiler.CSharp/Program.cs +++ b/src/Neo.Compiler.CSharp/Program.cs @@ -174,7 +174,8 @@ public static int Main(string[] args) NoInline = parseResult.GetValue(noInlineOption), AddressVersion = parseResult.GetValue(addressVersionOption), PrintAbi = parseResult.GetValue(printAbiOption), - Debug = parseResult.GetValue(debugOption) + Debug = parseResult.GetValue(debugOption), + RunAnalyzers = true }; return Handle(rootCommand, options, parseResult.GetValue(pathsArgument)); }); diff --git a/src/Neo.Compiler.CSharp/README.md b/src/Neo.Compiler.CSharp/README.md index 5bedfa794..2d571196d 100644 --- a/src/Neo.Compiler.CSharp/README.md +++ b/src/Neo.Compiler.CSharp/README.md @@ -5,6 +5,7 @@ The official C# compiler for Neo smart contracts. This compiler transforms C# co ## Features - **C# to NeoVM Compilation**: Compile standard C# code to NeoVM bytecode +- **Contract Diagnostics**: Run the Neo contract analyzer suite before bytecode generation - **Smart Contract Support**: Full support for Neo smart contract development - **Optimization**: Multiple optimization levels for bytecode efficiency - **Debug Information**: Generate debug info for testing and debugging diff --git a/src/Neo.SmartContract.Analyzer/Neo.SmartContract.Analyzer.csproj b/src/Neo.SmartContract.Analyzer/Neo.SmartContract.Analyzer.csproj index effd63a2f..fdbb4a5bb 100644 --- a/src/Neo.SmartContract.Analyzer/Neo.SmartContract.Analyzer.csproj +++ b/src/Neo.SmartContract.Analyzer/Neo.SmartContract.Analyzer.csproj @@ -23,7 +23,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Neo.SmartContract.Analyzer/NeoAnalyzerSuite.cs b/src/Neo.SmartContract.Analyzer/NeoAnalyzerSuite.cs new file mode 100644 index 000000000..cc430d0e8 --- /dev/null +++ b/src/Neo.SmartContract.Analyzer/NeoAnalyzerSuite.cs @@ -0,0 +1,57 @@ +// Copyright (C) 2015-2026 The Neo Project. +// +// NeoAnalyzerSuite.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; + +namespace Neo.SmartContract.Analyzer; + +public static class NeoAnalyzerSuite +{ + /// + /// Creates the complete set of Neo smart contract analyzers. + /// + public static ImmutableArray Create() => + [ + new BanCastMethodAnalyzer(), + new BigIntegerCreationAnalyzer(), + new BigIntegerUsageAnalyzer(), + new BigIntegerUsingUsageAnalyzer(), + new BitOperationsUsageAnalyzer(), + new CatchOnlySystemExceptionAnalyzer(), + new CharMethodsUsageAnalyzer(), + new CollectionTypesUsageAnalyzer(), + new DecimalUsageAnalyzer(), + new DoubleUsageAnalyzer(), + new EnumMethodsUsageAnalyzer(), + new FloatUsageAnalyzer(), + new InitialValueAnalyzer(), + new KeywordUsageAnalyzer(), + new LinqUsageAnalyzer(), + new MultipleCatchBlockAnalyzer(), + new NepStandardImplementationAnalyzer(), + new NotifyEventNameAnalyzer(), + new RefKeywordUsageAnalyzer(), + new SmartContractMethodNamingAnalyzer(), + new SmartContractMethodNamingAnalyzerUnderline(), + new StaticFieldInitializationAnalyzer(), + new StorageKeyCollisionAnalyzer(), + new StringBuilderUsageAnalyzer(), + new StringMethodUsageAnalyzer(), + new SupportedStandardsAnalyzer(), + new SystemDiagnosticsUsageAnalyzer(), + new SystemMathUsageAnalyzer(), + new TaskLikeTypeUsageAnalyzer(), + new UnsupportedPlatformApiAnalyzer(), + new UnsupportedSyntaxAnalyzer(), + new VolatileKeywordUsageAnalyzer() + ]; +} diff --git a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_AnalyzerExecution.cs b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_AnalyzerExecution.cs new file mode 100644 index 000000000..a67c6dbed --- /dev/null +++ b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_AnalyzerExecution.cs @@ -0,0 +1,201 @@ +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Compiler; +using System; +using System.IO; +using System.Linq; + +namespace Neo.Compiler.CSharp.UnitTests; + +[TestClass] +public class UnitTest_AnalyzerExecution +{ + [TestMethod] + public void CompileProject_ReportsNeoAnalyzerErrorsBeforeLowering() + { + using var project = TempContractProject.Create(""" +using Neo.SmartContract.Framework; +using System.Diagnostics; + +public class Contract : SmartContract +{ + public static void Main() + { + Debug.WriteLine("unsupported"); + } +} +"""); + + var result = CreateEngine().CompileProject(project.ProjectFile).Single(); + var diagnostics = result.Diagnostics.Where(item => item.Id == "NC4028").ToArray(); + + Assert.IsFalse(result.Success); + Assert.IsTrue(diagnostics.Length > 0); + Assert.IsTrue(diagnostics.All(item => item.Severity == DiagnosticSeverity.Error)); + Assert.IsTrue(diagnostics.All(item => item.Location.SourceTree?.FilePath == project.SourceFile)); + Assert.IsTrue(diagnostics.Any(item => item.Location.GetLineSpan().StartLinePosition.Line + 1 == 8)); + Assert.IsFalse(result.Diagnostics.Any(item => item.Id == "NC1002")); + } + + [TestMethod] + public void CompileProject_PreservesSupportedContracts() + { + using var project = TempContractProject.Create(""" +using Neo.SmartContract.Framework; + +public class Contract : SmartContract +{ + public static int Main(int value) => value + 1; +} +"""); + + var result = CreateEngine().CompileProject(project.ProjectFile).Single(); + + Assert.IsTrue(result.Success, string.Join(Environment.NewLine, result.Diagnostics)); + Assert.IsFalse(result.Diagnostics.Any(item => item.Id.StartsWith("NC4", StringComparison.Ordinal))); + } + + [TestMethod] + public void CompileProject_ReportsAnalyzerWarningsWithoutBlockingOutput() + { + using var project = TempContractProject.Create(""" +using Neo.SmartContract.Framework; + +public class Contract : SmartContract +{ + public static int Main(int value) + { + try + { + return value + 1; + } + catch (System.InvalidOperationException) + { + return 0; + } + } +} +"""); + + var result = CreateEngine().CompileProject(project.ProjectFile).Single(); + var diagnostic = result.Diagnostics.Single(item => item.Id == "NC4027"); + + Assert.IsTrue(result.Success, string.Join(Environment.NewLine, result.Diagnostics)); + Assert.AreEqual(DiagnosticSeverity.Warning, diagnostic.Severity); + } + + [TestMethod] + public void CompileProject_DoesNotChangeLibraryBehaviorUnlessEnabled() + { + using var project = TempContractProject.Create(""" +using Neo.SmartContract.Framework; +using System.Diagnostics; + +public class Contract : SmartContract +{ + public static void Main() + { + Debug.WriteLine("unsupported"); + } +} +"""); + + var result = new CompilationEngine(new CompilationOptions + { + SkipRestoreIfAssetsPresent = true + }).CompileProject(project.ProjectFile).Single(); + + Assert.IsFalse(result.Success); + Assert.IsFalse(result.Diagnostics.Any(item => item.Id == "NC4028")); + Assert.IsTrue(result.Diagnostics.Any(item => item.Id == "NC1002")); + } + + [TestMethod] + public void ProgramMain_RunsNeoAnalyzersByDefault() + { + using var project = TempContractProject.Create(""" +using Neo.SmartContract.Framework; +using System.Diagnostics; + +public class Contract : SmartContract +{ + public static void Main() + { + Debug.WriteLine("unsupported"); + } +} +"""); + using var output = new StringWriter(); + using var error = new StringWriter(); + var originalOut = Console.Out; + var originalError = Console.Error; + int exitCode; + + try + { + Console.SetOut(output); + Console.SetError(error); + exitCode = Program.Main([project.ProjectFile]); + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalError); + } + + Assert.AreEqual(1, exitCode); + StringAssert.Contains(error.ToString(), "error NC4028"); + Assert.IsFalse(error.ToString().Contains("NC1002", StringComparison.Ordinal)); + } + + private static CompilationEngine CreateEngine() => new(new CompilationOptions + { + SkipRestoreIfAssetsPresent = true, + RunAnalyzers = true + }); + + private sealed class TempContractProject : IDisposable + { + private readonly string _projectDirectory; + + public string ProjectFile { get; } + public string SourceFile { get; } + + private TempContractProject(string projectDirectory, string projectFile, string sourceFile) + { + _projectDirectory = projectDirectory; + ProjectFile = projectFile; + SourceFile = sourceFile; + } + + public static TempContractProject Create(string source) + { + var projectDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(projectDirectory); + var projectFile = Path.Combine(projectDirectory, "Contract.csproj"); + var sourceFile = Path.Combine(projectDirectory, "Contract.cs"); + var repoRoot = Syntax.SyntaxProbeLoader.GetRepositoryRoot(); + var frameworkProject = Path.Combine(repoRoot, "src", "Neo.SmartContract.Framework", "Neo.SmartContract.Framework.csproj"); + + File.WriteAllText(projectFile, $$""" + + + net10.0 + enable + + + + + +"""); + File.WriteAllText(sourceFile, source); + + return new TempContractProject(projectDirectory, projectFile, sourceFile); + } + + public void Dispose() + { + Directory.Delete(_projectDirectory, recursive: true); + } + } +} diff --git a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_ProjectBoundaries.cs b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_ProjectBoundaries.cs index ae5434c22..e77b6fbbe 100644 --- a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_ProjectBoundaries.cs +++ b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_ProjectBoundaries.cs @@ -51,6 +51,7 @@ public void CompilerToolPackage_Includes_ArtifactLibraryDependency() RedirectStandardError = true, UseShellExecute = false }; + startInfo.Environment["MSBUILDDISABLENODEREUSE"] = "1"; startInfo.ArgumentList.Add("pack"); startInfo.ArgumentList.Add(GetCompilerProjectPath()); startInfo.ArgumentList.Add("--configuration"); @@ -73,14 +74,15 @@ public void CompilerToolPackage_Includes_ArtifactLibraryDependency() string packagePath = Directory.GetFiles(outputPath, "*.nupkg") .Single(path => !path.EndsWith(".snupkg", StringComparison.OrdinalIgnoreCase)); using var package = ZipFile.OpenRead(packagePath); - string[] artifactMetadataAssemblies = + string[] runtimeAssemblies = [ "Neo.dll", "Neo.IO.dll", + "Neo.SmartContract.Analyzer.dll", "Neo.SmartContract.Testing.dll" ]; - foreach (string assembly in artifactMetadataAssemblies) + foreach (string assembly in runtimeAssemblies) { Assert.IsTrue( package.Entries.Any(entry => entry.FullName == $"tools/net10.0/any/{assembly}"), diff --git a/tests/Neo.SmartContract.Analyzer.UnitTests/Neo.SmartContract.Analyzer.UnitTests.csproj b/tests/Neo.SmartContract.Analyzer.UnitTests/Neo.SmartContract.Analyzer.UnitTests.csproj index 1ec4a2b31..6b50c3121 100644 --- a/tests/Neo.SmartContract.Analyzer.UnitTests/Neo.SmartContract.Analyzer.UnitTests.csproj +++ b/tests/Neo.SmartContract.Analyzer.UnitTests/Neo.SmartContract.Analyzer.UnitTests.csproj @@ -13,6 +13,10 @@ + + + + diff --git a/tests/Neo.SmartContract.Analyzer.UnitTests/NeoAnalyzerSuiteTests.cs b/tests/Neo.SmartContract.Analyzer.UnitTests/NeoAnalyzerSuiteTests.cs new file mode 100644 index 000000000..f77ddbe65 --- /dev/null +++ b/tests/Neo.SmartContract.Analyzer.UnitTests/NeoAnalyzerSuiteTests.cs @@ -0,0 +1,27 @@ +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; + +namespace Neo.SmartContract.Analyzer.UnitTests; + +[TestClass] +public class NeoAnalyzerSuiteTests +{ + [TestMethod] + public void SuiteContainsEveryConcreteAnalyzerExactlyOnce() + { + var expected = typeof(NeoAnalyzerSuite).Assembly + .GetTypes() + .Where(type => !type.IsAbstract && typeof(DiagnosticAnalyzer).IsAssignableFrom(type)) + .OrderBy(type => type.FullName, StringComparer.Ordinal) + .ToArray(); + var actual = NeoAnalyzerSuite.Create() + .Select(analyzer => analyzer.GetType()) + .OrderBy(type => type.FullName, StringComparer.Ordinal) + .ToArray(); + + CollectionAssert.AreEqual(expected, actual); + Assert.AreEqual(actual.Length, actual.Distinct().Count()); + } +} diff --git a/tests/Neo.SmartContract.Framework.UnitTests/Neo.SmartContract.Framework.UnitTests.csproj b/tests/Neo.SmartContract.Framework.UnitTests/Neo.SmartContract.Framework.UnitTests.csproj index f04bc1d05..d1139deea 100644 --- a/tests/Neo.SmartContract.Framework.UnitTests/Neo.SmartContract.Framework.UnitTests.csproj +++ b/tests/Neo.SmartContract.Framework.UnitTests/Neo.SmartContract.Framework.UnitTests.csproj @@ -7,6 +7,7 @@ false true CS0067 + **/src/Neo.SmartContract.Analyzer/**/*.cs diff --git a/tests/Neo.SmartContract.Template.UnitTests/Neo.SmartContract.Template.UnitTests.csproj b/tests/Neo.SmartContract.Template.UnitTests/Neo.SmartContract.Template.UnitTests.csproj index 91442164d..5a12901d5 100644 --- a/tests/Neo.SmartContract.Template.UnitTests/Neo.SmartContract.Template.UnitTests.csproj +++ b/tests/Neo.SmartContract.Template.UnitTests/Neo.SmartContract.Template.UnitTests.csproj @@ -7,6 +7,7 @@ false true CS0067 + **/src/Neo.SmartContract.Analyzer/**/*.cs diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Neo.SmartContract.Testing.UnitTests.csproj b/tests/Neo.SmartContract.Testing.UnitTests/Neo.SmartContract.Testing.UnitTests.csproj index 34b1c1e79..1eb820cca 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Neo.SmartContract.Testing.UnitTests.csproj +++ b/tests/Neo.SmartContract.Testing.UnitTests/Neo.SmartContract.Testing.UnitTests.csproj @@ -6,6 +6,7 @@ false true enable + **/src/Neo.SmartContract.Analyzer/**/*.cs From 7ca9489257d185d76c6bb36526a4a7daec781a58 Mon Sep 17 00:00:00 2001 From: Jim8y Date: Sat, 18 Jul 2026 20:11:54 +0800 Subject: [PATCH 2/3] Fix: preserve C# diagnostics with analyzer failures --- .../CompilationEngine/CompilationContext.cs | 1 - .../UnitTest_AnalyzerExecution.cs | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs index 30db93ec8..6df4a6050 100644 --- a/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs +++ b/src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs @@ -166,7 +166,6 @@ private static void ValidateContractPermission(INamedTypeSymbol symbol, string c internal void Compile() { _diagnostics.AddRange(_engine.AnalyzerDiagnostics); - if (!Success) return; HashSet processed = new(SymbolEqualityComparer.Default); foreach (SyntaxTree tree in _engine.Compilation!.SyntaxTrees) diff --git a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_AnalyzerExecution.cs b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_AnalyzerExecution.cs index a67c6dbed..8b184fc0d 100644 --- a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_AnalyzerExecution.cs +++ b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_AnalyzerExecution.cs @@ -37,6 +37,31 @@ public static void Main() Assert.IsFalse(result.Diagnostics.Any(item => item.Id == "NC1002")); } + [TestMethod] + public void CompileProject_ReportsAnalyzerAndCSharpErrorsTogetherBeforeLowering() + { + using var project = TempContractProject.Create(""" +using Neo.SmartContract.Framework; +using System.Diagnostics; + +public class Contract : SmartContract +{ + public static void Main() + { + Debug.WriteLine("unsupported"); + MissingMethod(); + } +} +"""); + + var result = CreateEngine().CompileProject(project.ProjectFile).Single(); + + Assert.IsFalse(result.Success); + Assert.IsTrue(result.Diagnostics.Any(item => item.Id == "NC4028")); + Assert.IsTrue(result.Diagnostics.Any(item => item.Id == "CS0103")); + Assert.IsFalse(result.Diagnostics.Any(item => item.Id == "NC1002")); + } + [TestMethod] public void CompileProject_PreservesSupportedContracts() { From 286bc2810fb3752d8535e1338a27452ce8a4bdb9 Mon Sep 17 00:00:00 2001 From: Jim8y Date: Mon, 20 Jul 2026 09:45:05 +0800 Subject: [PATCH 3/3] Discover Neo analyzers by reflection --- .../NeoAnalyzerSuite.cs | 55 +++++++------------ 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/src/Neo.SmartContract.Analyzer/NeoAnalyzerSuite.cs b/src/Neo.SmartContract.Analyzer/NeoAnalyzerSuite.cs index cc430d0e8..dea58332e 100644 --- a/src/Neo.SmartContract.Analyzer/NeoAnalyzerSuite.cs +++ b/src/Neo.SmartContract.Analyzer/NeoAnalyzerSuite.cs @@ -10,7 +10,11 @@ // modifications are permitted. using Microsoft.CodeAnalysis.Diagnostics; +using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; +using System.Reflection; namespace Neo.SmartContract.Analyzer; @@ -20,38 +24,21 @@ public static class NeoAnalyzerSuite /// Creates the complete set of Neo smart contract analyzers. /// public static ImmutableArray Create() => - [ - new BanCastMethodAnalyzer(), - new BigIntegerCreationAnalyzer(), - new BigIntegerUsageAnalyzer(), - new BigIntegerUsingUsageAnalyzer(), - new BitOperationsUsageAnalyzer(), - new CatchOnlySystemExceptionAnalyzer(), - new CharMethodsUsageAnalyzer(), - new CollectionTypesUsageAnalyzer(), - new DecimalUsageAnalyzer(), - new DoubleUsageAnalyzer(), - new EnumMethodsUsageAnalyzer(), - new FloatUsageAnalyzer(), - new InitialValueAnalyzer(), - new KeywordUsageAnalyzer(), - new LinqUsageAnalyzer(), - new MultipleCatchBlockAnalyzer(), - new NepStandardImplementationAnalyzer(), - new NotifyEventNameAnalyzer(), - new RefKeywordUsageAnalyzer(), - new SmartContractMethodNamingAnalyzer(), - new SmartContractMethodNamingAnalyzerUnderline(), - new StaticFieldInitializationAnalyzer(), - new StorageKeyCollisionAnalyzer(), - new StringBuilderUsageAnalyzer(), - new StringMethodUsageAnalyzer(), - new SupportedStandardsAnalyzer(), - new SystemDiagnosticsUsageAnalyzer(), - new SystemMathUsageAnalyzer(), - new TaskLikeTypeUsageAnalyzer(), - new UnsupportedPlatformApiAnalyzer(), - new UnsupportedSyntaxAnalyzer(), - new VolatileKeywordUsageAnalyzer() - ]; + GetLoadableTypes() + .Where(type => !type.IsAbstract && typeof(DiagnosticAnalyzer).IsAssignableFrom(type)) + .OrderBy(type => type.FullName, StringComparer.Ordinal) + .Select(type => (DiagnosticAnalyzer)Activator.CreateInstance(type)!) + .ToImmutableArray(); + + private static IEnumerable GetLoadableTypes() + { + try + { + return typeof(NeoAnalyzerSuite).Assembly.GetTypes(); + } + catch (ReflectionTypeLoadException exception) + { + return exception.Types.OfType(); + } + } }