Skip to content

Commit 853b99d

Browse files
committed
Feature: run Neo analyzers during nccs compilation
1 parent 23b6d67 commit 853b99d

15 files changed

Lines changed: 338 additions & 4 deletions

File tree

src/Neo.Compiler.CSharp/CompilationEngine/CompilationContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ private static void ValidateContractPermission(INamedTypeSymbol symbol, string c
165165

166166
internal void Compile()
167167
{
168+
_diagnostics.AddRange(_engine.AnalyzerDiagnostics);
169+
if (!Success) return;
170+
168171
HashSet<INamedTypeSymbol> processed = new(SymbolEqualityComparer.Default);
169172
foreach (SyntaxTree tree in _engine.Compilation!.SyntaxTrees)
170173
{

src/Neo.Compiler.CSharp/CompilationEngine/CompilationEngine.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
using Microsoft.CodeAnalysis;
1515
using Microsoft.CodeAnalysis.CSharp;
1616
using Microsoft.CodeAnalysis.CSharp.Syntax;
17+
using Microsoft.CodeAnalysis.Diagnostics;
1718
using Neo.Json;
19+
using Neo.SmartContract.Analyzer;
1820
using System;
1921
using System.Collections.Concurrent;
2022
using System.Collections.Generic;
23+
using System.Collections.Immutable;
2124
using System.ComponentModel;
2225
using System.Diagnostics;
2326
using System.IO;
@@ -34,8 +37,10 @@ public class CompilationEngine(CompilationOptions options)
3437
{
3538
internal Compilation? Compilation;
3639
internal MetadataReference? FrameworkReference;
40+
internal ImmutableArray<Diagnostic> AnalyzerDiagnostics { get; private set; } = [];
3741
internal CompilationOptions Options { get; private set; } = options;
3842
private static readonly MetadataReference[] CommonReferences;
43+
private static readonly ImmutableArray<DiagnosticAnalyzer> NeoAnalyzers = NeoAnalyzerSuite.Create();
3944
private static readonly Guid FrameworkModuleVersionId = typeof(scfx::Neo.SmartContract.Framework.Attributes.SafeAttribute).Assembly.ManifestModule.ModuleVersionId;
4045
private const string FrameworkAssemblyName = "Neo.SmartContract.Framework";
4146
private static readonly StringComparison ProjectPathComparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
@@ -154,6 +159,7 @@ internal List<CompilationContext> Compile(IEnumerable<string> sourceFiles, IEnum
154159

155160
FrameworkReference = frameworkReference ?? referenceArray.FirstOrDefault(IsTrustedFrameworkReference);
156161
Compilation = CSharpCompilation.Create(null, syntaxTrees, referenceArray, compilationOptions);
162+
AnalyzeCompilation();
157163
return CompileProjectContracts(Compilation);
158164
}
159165

@@ -224,6 +230,7 @@ private Compilation LoadProjectCompilation(string csproj, bool forceReload)
224230
ResetProjectState();
225231
Compilation = GetCompilation(projectPath);
226232
FrameworkReference = ResolveProjectFrameworkReference(Compilation);
233+
AnalyzeCompilation();
227234
ProjectPath = projectPath;
228235
return Compilation;
229236
}
@@ -232,6 +239,7 @@ private void ResetProjectState()
232239
{
233240
Compilation = null;
234241
FrameworkReference = null;
242+
AnalyzerDiagnostics = [];
235243
MetaReferences.Clear();
236244
PreparedProjectPath = null;
237245
ProjectPath = null;
@@ -272,6 +280,27 @@ internal static bool IsTrustedFrameworkReference(MetadataReference reference)
272280
}
273281
}
274282

283+
private void AnalyzeCompilation()
284+
{
285+
if (!Options.RunAnalyzers)
286+
{
287+
AnalyzerDiagnostics = [];
288+
return;
289+
}
290+
291+
AnalyzerDiagnostics = Compilation!
292+
.WithAnalyzers(NeoAnalyzers)
293+
.GetAnalyzerDiagnosticsAsync()
294+
.GetAwaiter()
295+
.GetResult()
296+
.Where(diagnostic => diagnostic.Severity != DiagnosticSeverity.Hidden)
297+
.OrderBy(diagnostic => diagnostic.Location.SourceTree?.FilePath ?? string.Empty, StringComparer.Ordinal)
298+
.ThenBy(diagnostic => diagnostic.Location.IsInSource ? diagnostic.Location.SourceSpan.Start : -1)
299+
.ThenBy(diagnostic => diagnostic.Id, StringComparer.Ordinal)
300+
.ThenBy(diagnostic => diagnostic.GetMessage(), StringComparer.Ordinal)
301+
.ToImmutableArray();
302+
}
303+
275304
public List<CompilationContext> CompileProject(string csproj, List<INamedTypeSymbol> sortedClasses, Dictionary<INamedTypeSymbol, List<INamedTypeSymbol>> classDependencies, List<INamedTypeSymbol?> allClassSymbols, string? targetContractName = null)
276305
{
277306
if (sortedClasses == null || classDependencies == null || allClassSymbols == null)

src/Neo.Compiler.CSharp/CompilationEngine/CompilationOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public enum DebugType : byte
5858
private CSharpParseOptions? parseOptions = null;
5959
public bool SkipRestoreIfAssetsPresent { get; set; }
6060

61+
/// <summary>
62+
/// Gets or sets whether the Neo contract analyzer suite runs before contract conversion.
63+
/// </summary>
64+
public bool RunAnalyzers { get; set; }
65+
6166
public CSharpParseOptions GetParseOptions()
6267
{
6368
if (parseOptions is null)

src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
</ItemGroup>
3535

3636
<ItemGroup>
37+
<ProjectReference Include="..\Neo.SmartContract.Analyzer\Neo.SmartContract.Analyzer.csproj" />
3738
<ProjectReference Include="..\Neo.SmartContract.Framework\Neo.SmartContract.Framework.csproj">
3839
<Aliases>scfx</Aliases>
3940
</ProjectReference>

src/Neo.Compiler.CSharp/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ public static int Main(string[] args)
174174
NoInline = parseResult.GetValue(noInlineOption),
175175
AddressVersion = parseResult.GetValue(addressVersionOption),
176176
PrintAbi = parseResult.GetValue(printAbiOption),
177-
Debug = parseResult.GetValue(debugOption)
177+
Debug = parseResult.GetValue(debugOption),
178+
RunAnalyzers = true
178179
};
179180
return Handle(rootCommand, options, parseResult.GetValue(pathsArgument));
180181
});

src/Neo.Compiler.CSharp/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The official C# compiler for Neo smart contracts. This compiler transforms C# co
55
## Features
66

77
- **C# to NeoVM Compilation**: Compile standard C# code to NeoVM bytecode
8+
- **Contract Diagnostics**: Run the Neo contract analyzer suite before bytecode generation
89
- **Smart Contract Support**: Full support for Neo smart contract development
910
- **Optimization**: Multiple optimization levels for bytecode efficiency
1011
- **Debug Information**: Generate debug info for testing and debugging

src/Neo.SmartContract.Analyzer/Neo.SmartContract.Analyzer.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2424
</PackageReference>
2525
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
26-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
26+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" PrivateAssets="all" />
2727
</ItemGroup>
2828

2929
<ItemGroup>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (C) 2015-2026 The Neo Project.
2+
//
3+
// NeoAnalyzerSuite.cs file belongs to the neo project and is free
4+
// software distributed under the MIT software license, see the
5+
// accompanying file LICENSE in the main directory of the
6+
// repository or http://www.opensource.org/licenses/mit-license.php
7+
// for more details.
8+
//
9+
// Redistribution and use in source and binary forms with or without
10+
// modifications are permitted.
11+
12+
using Microsoft.CodeAnalysis.Diagnostics;
13+
using System.Collections.Immutable;
14+
15+
namespace Neo.SmartContract.Analyzer;
16+
17+
public static class NeoAnalyzerSuite
18+
{
19+
/// <summary>
20+
/// Creates the complete set of Neo smart contract analyzers.
21+
/// </summary>
22+
public static ImmutableArray<DiagnosticAnalyzer> Create() =>
23+
[
24+
new BanCastMethodAnalyzer(),
25+
new BigIntegerCreationAnalyzer(),
26+
new BigIntegerUsageAnalyzer(),
27+
new BigIntegerUsingUsageAnalyzer(),
28+
new BitOperationsUsageAnalyzer(),
29+
new CatchOnlySystemExceptionAnalyzer(),
30+
new CharMethodsUsageAnalyzer(),
31+
new CollectionTypesUsageAnalyzer(),
32+
new DecimalUsageAnalyzer(),
33+
new DoubleUsageAnalyzer(),
34+
new EnumMethodsUsageAnalyzer(),
35+
new FloatUsageAnalyzer(),
36+
new InitialValueAnalyzer(),
37+
new KeywordUsageAnalyzer(),
38+
new LinqUsageAnalyzer(),
39+
new MultipleCatchBlockAnalyzer(),
40+
new NepStandardImplementationAnalyzer(),
41+
new NotifyEventNameAnalyzer(),
42+
new RefKeywordUsageAnalyzer(),
43+
new SmartContractMethodNamingAnalyzer(),
44+
new SmartContractMethodNamingAnalyzerUnderline(),
45+
new StaticFieldInitializationAnalyzer(),
46+
new StorageKeyCollisionAnalyzer(),
47+
new StringBuilderUsageAnalyzer(),
48+
new StringMethodUsageAnalyzer(),
49+
new SupportedStandardsAnalyzer(),
50+
new SystemDiagnosticsUsageAnalyzer(),
51+
new SystemMathUsageAnalyzer(),
52+
new TaskLikeTypeUsageAnalyzer(),
53+
new UnsupportedPlatformApiAnalyzer(),
54+
new UnsupportedSyntaxAnalyzer(),
55+
new VolatileKeywordUsageAnalyzer()
56+
];
57+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using Neo.Compiler;
4+
using System;
5+
using System.IO;
6+
using System.Linq;
7+
8+
namespace Neo.Compiler.CSharp.UnitTests;
9+
10+
[TestClass]
11+
public class UnitTest_AnalyzerExecution
12+
{
13+
[TestMethod]
14+
public void CompileProject_ReportsNeoAnalyzerErrorsBeforeLowering()
15+
{
16+
using var project = TempContractProject.Create("""
17+
using Neo.SmartContract.Framework;
18+
using System.Diagnostics;
19+
20+
public class Contract : SmartContract
21+
{
22+
public static void Main()
23+
{
24+
Debug.WriteLine("unsupported");
25+
}
26+
}
27+
""");
28+
29+
var result = CreateEngine().CompileProject(project.ProjectFile).Single();
30+
var diagnostics = result.Diagnostics.Where(item => item.Id == "NC4028").ToArray();
31+
32+
Assert.IsFalse(result.Success);
33+
Assert.IsTrue(diagnostics.Length > 0);
34+
Assert.IsTrue(diagnostics.All(item => item.Severity == DiagnosticSeverity.Error));
35+
Assert.IsTrue(diagnostics.All(item => item.Location.SourceTree?.FilePath == project.SourceFile));
36+
Assert.IsTrue(diagnostics.Any(item => item.Location.GetLineSpan().StartLinePosition.Line + 1 == 8));
37+
Assert.IsFalse(result.Diagnostics.Any(item => item.Id == "NC1002"));
38+
}
39+
40+
[TestMethod]
41+
public void CompileProject_PreservesSupportedContracts()
42+
{
43+
using var project = TempContractProject.Create("""
44+
using Neo.SmartContract.Framework;
45+
46+
public class Contract : SmartContract
47+
{
48+
public static int Main(int value) => value + 1;
49+
}
50+
""");
51+
52+
var result = CreateEngine().CompileProject(project.ProjectFile).Single();
53+
54+
Assert.IsTrue(result.Success, string.Join(Environment.NewLine, result.Diagnostics));
55+
Assert.IsFalse(result.Diagnostics.Any(item => item.Id.StartsWith("NC4", StringComparison.Ordinal)));
56+
}
57+
58+
[TestMethod]
59+
public void CompileProject_ReportsAnalyzerWarningsWithoutBlockingOutput()
60+
{
61+
using var project = TempContractProject.Create("""
62+
using Neo.SmartContract.Framework;
63+
64+
public class Contract : SmartContract
65+
{
66+
public static int Main(int value)
67+
{
68+
try
69+
{
70+
return value + 1;
71+
}
72+
catch (System.InvalidOperationException)
73+
{
74+
return 0;
75+
}
76+
}
77+
}
78+
""");
79+
80+
var result = CreateEngine().CompileProject(project.ProjectFile).Single();
81+
var diagnostic = result.Diagnostics.Single(item => item.Id == "NC4027");
82+
83+
Assert.IsTrue(result.Success, string.Join(Environment.NewLine, result.Diagnostics));
84+
Assert.AreEqual(DiagnosticSeverity.Warning, diagnostic.Severity);
85+
}
86+
87+
[TestMethod]
88+
public void CompileProject_DoesNotChangeLibraryBehaviorUnlessEnabled()
89+
{
90+
using var project = TempContractProject.Create("""
91+
using Neo.SmartContract.Framework;
92+
using System.Diagnostics;
93+
94+
public class Contract : SmartContract
95+
{
96+
public static void Main()
97+
{
98+
Debug.WriteLine("unsupported");
99+
}
100+
}
101+
""");
102+
103+
var result = new CompilationEngine(new CompilationOptions
104+
{
105+
SkipRestoreIfAssetsPresent = true
106+
}).CompileProject(project.ProjectFile).Single();
107+
108+
Assert.IsFalse(result.Success);
109+
Assert.IsFalse(result.Diagnostics.Any(item => item.Id == "NC4028"));
110+
Assert.IsTrue(result.Diagnostics.Any(item => item.Id == "NC1002"));
111+
}
112+
113+
[TestMethod]
114+
public void ProgramMain_RunsNeoAnalyzersByDefault()
115+
{
116+
using var project = TempContractProject.Create("""
117+
using Neo.SmartContract.Framework;
118+
using System.Diagnostics;
119+
120+
public class Contract : SmartContract
121+
{
122+
public static void Main()
123+
{
124+
Debug.WriteLine("unsupported");
125+
}
126+
}
127+
""");
128+
using var output = new StringWriter();
129+
using var error = new StringWriter();
130+
var originalOut = Console.Out;
131+
var originalError = Console.Error;
132+
int exitCode;
133+
134+
try
135+
{
136+
Console.SetOut(output);
137+
Console.SetError(error);
138+
exitCode = Program.Main([project.ProjectFile]);
139+
}
140+
finally
141+
{
142+
Console.SetOut(originalOut);
143+
Console.SetError(originalError);
144+
}
145+
146+
Assert.AreEqual(1, exitCode);
147+
StringAssert.Contains(error.ToString(), "error NC4028");
148+
Assert.IsFalse(error.ToString().Contains("NC1002", StringComparison.Ordinal));
149+
}
150+
151+
private static CompilationEngine CreateEngine() => new(new CompilationOptions
152+
{
153+
SkipRestoreIfAssetsPresent = true,
154+
RunAnalyzers = true
155+
});
156+
157+
private sealed class TempContractProject : IDisposable
158+
{
159+
private readonly string _projectDirectory;
160+
161+
public string ProjectFile { get; }
162+
public string SourceFile { get; }
163+
164+
private TempContractProject(string projectDirectory, string projectFile, string sourceFile)
165+
{
166+
_projectDirectory = projectDirectory;
167+
ProjectFile = projectFile;
168+
SourceFile = sourceFile;
169+
}
170+
171+
public static TempContractProject Create(string source)
172+
{
173+
var projectDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
174+
Directory.CreateDirectory(projectDirectory);
175+
var projectFile = Path.Combine(projectDirectory, "Contract.csproj");
176+
var sourceFile = Path.Combine(projectDirectory, "Contract.cs");
177+
var repoRoot = Syntax.SyntaxProbeLoader.GetRepositoryRoot();
178+
var frameworkProject = Path.Combine(repoRoot, "src", "Neo.SmartContract.Framework", "Neo.SmartContract.Framework.csproj");
179+
180+
File.WriteAllText(projectFile, $$"""
181+
<Project Sdk="Microsoft.NET.Sdk">
182+
<PropertyGroup>
183+
<TargetFramework>net10.0</TargetFramework>
184+
<Nullable>enable</Nullable>
185+
</PropertyGroup>
186+
<ItemGroup>
187+
<ProjectReference Include="{{frameworkProject}}" />
188+
</ItemGroup>
189+
</Project>
190+
""");
191+
File.WriteAllText(sourceFile, source);
192+
193+
return new TempContractProject(projectDirectory, projectFile, sourceFile);
194+
}
195+
196+
public void Dispose()
197+
{
198+
Directory.Delete(_projectDirectory, recursive: true);
199+
}
200+
}
201+
}

0 commit comments

Comments
 (0)