Skip to content

Improve contract syntax analyzer #1316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Neo.SmartContract.Analyzer/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@
| NC4026 | Usage | Error | SystemDiagnosticsUsageAnalyzer |
| NC4027 | Usage | Warning | CatchOnlySystemExceptionAnalyzer |
| NC4028 | Usage | Error | SystemThreadingUsageAnalyzer |
| NC5050 | Performance | Warning | StoragePatternAnalyzer - Repeated Access |
| NC5051 | Performance | Warning | StoragePatternAnalyzer - Large Key |
| NC5052 | Performance | Warning | StoragePatternAnalyzer - Storage In Loop |
88 changes: 88 additions & 0 deletions src/Neo.SmartContract.Analyzer/ArrayMethodsUsageAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// ArrayMethodsUsageAnalyzer.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;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Linq;

namespace Neo.SmartContract.Analyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ArrayMethodsUsageAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "NC4042";

// Array methods that are not supported in Neo smart contracts
// Based on the methods implemented in src\Neo.Compiler.CSharp\MethodConvert\System\SystemCall.Array.cs
private readonly string[] _unsupportedArrayMethods = {
"AsReadOnly", "BinarySearch", "Clear", "ConstrainedCopy", "ConvertAll",
"Copy", "CopyTo", "CreateInstance", "Exists", "Fill",
"Find", "FindAll", "FindIndex", "FindLast", "FindLastIndex",
"ForEach", "GetEnumerator", "GetHashCode", "GetLongLength", "GetLowerBound",
"GetType", "GetUpperBound", "GetValue", "IndexOf", "Initialize",
"LastIndexOf", "Resize", "SetValue", "Sort", "TrueForAll"
// Supported Array methods (not in this list):
// - Length (property)
// - Reverse
};

private static readonly DiagnosticDescriptor Rule = new(
DiagnosticId,
"Unsupported Array method is used",
"Unsupported Array method: {0}",
"Method",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.InvocationExpression);
}

private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
if (context.Node is not InvocationExpressionSyntax invocationExpression) return;

// Check if it's a static Array method
if (invocationExpression.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Expression is IdentifierNameSyntax identifier &&
identifier.Identifier.Text == "Array")
{
var methodName = memberAccess.Name.Identifier.Text;
if (_unsupportedArrayMethods.Contains(methodName))
{
var diagnostic = Diagnostic.Create(Rule, invocationExpression.GetLocation(), methodName);
context.ReportDiagnostic(diagnostic);
}
return;
}

// Check if it's an instance method on an array
var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression).Symbol as IMethodSymbol;
if (methodSymbol != null &&
methodSymbol.ReceiverType != null &&
methodSymbol.ReceiverType.TypeKind == TypeKind.Array &&
_unsupportedArrayMethods.Contains(methodSymbol.Name))
{
var diagnostic = Diagnostic.Create(Rule, invocationExpression.GetLocation(), methodSymbol.Name);
context.ReportDiagnostic(diagnostic);
}
}
}
}
86 changes: 86 additions & 0 deletions src/Neo.SmartContract.Analyzer/BitOperationsUsageAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// BitOperationsUsageAnalyzer.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;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Linq;

namespace Neo.SmartContract.Analyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class BitOperationsUsageAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "NC4043";

// BitOperations methods that are not supported in Neo smart contracts
// Based on the methods implemented in src\Neo.Compiler.CSharp\MethodConvert\System\SystemCall.Register.cs
private readonly string[] _unsupportedBitOperationsMethods = {
"TrailingZeroCount",
"IsPow2",
"RotateRight"
// Supported BitOperations methods (not in this list):
// - LeadingZeroCount
// - Log2
// - PopCount
// - RotateLeft
};

private static readonly DiagnosticDescriptor Rule = new(
DiagnosticId,
"Unsupported BitOperations method is used",
"Unsupported BitOperations method: {0}",
"Method",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.InvocationExpression);
}

private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
if (context.Node is not InvocationExpressionSyntax invocationExpression) return;

// Check if it's a BitOperations method
if (invocationExpression.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Expression is IdentifierNameSyntax identifier &&
identifier.Identifier.Text == "BitOperations")
{
var methodName = memberAccess.Name.Identifier.Text;
if (_unsupportedBitOperationsMethods.Contains(methodName))
{
var diagnostic = Diagnostic.Create(Rule, invocationExpression.GetLocation(), methodName);
context.ReportDiagnostic(diagnostic);
}
return;
}

// Check if it's a method from System.Numerics.BitOperations
var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression).Symbol as IMethodSymbol;
if (methodSymbol != null &&
methodSymbol.ContainingType?.ToString() == "System.Numerics.BitOperations" &&
_unsupportedBitOperationsMethods.Contains(methodSymbol.Name))
{
var diagnostic = Diagnostic.Create(Rule, invocationExpression.GetLocation(), methodSymbol.Name);
context.ReportDiagnostic(diagnostic);
}
}
}
}
100 changes: 100 additions & 0 deletions src/Neo.SmartContract.Analyzer/CheckWitnessUsageAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// CheckWitnessUsageAnalyzer.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;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Linq;

namespace Neo.SmartContract.Analyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CheckWitnessUsageAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "NC4029";

private static readonly LocalizableString Title = "CheckWitness result should be used";
private static readonly LocalizableString MessageFormat = "The result of CheckWitness should be used: {0}";
private static readonly LocalizableString Description = "CheckWitness results should be used to ensure proper authentication.";
private const string Category = "Security";

private static readonly DiagnosticDescriptor Rule = new(
DiagnosticId,
Title,
MessageFormat,
Category,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Description);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
}

private void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
{
var invocation = (InvocationExpressionSyntax)context.Node;

// Check if this is a call to Runtime.CheckWitness
if (IsCheckWitnessCall(invocation, context.SemanticModel))
{
// Check if the result is being used properly
if (!IsResultVerified(invocation))
{
var diagnostic = Diagnostic.Create(Rule, invocation.GetLocation(), "The result of Runtime.CheckWitness(...) should be used in a condition, assigned to a variable, passed to ExecutionEngine.Assert, or otherwise utilized");
context.ReportDiagnostic(diagnostic);
}
}
}

private bool IsCheckWitnessCall(InvocationExpressionSyntax invocation, SemanticModel semanticModel)
{
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
{
var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
if (symbol == null) return false;

// Check if it's Runtime.CheckWitness
if (symbol.Name == "CheckWitness" &&
symbol.ContainingType.Name == "Runtime" &&
symbol.ContainingNamespace.ToString() == "Neo.SmartContract.Framework.Services")
{
return true;
}
}
return false;
}

private bool IsResultVerified(InvocationExpressionSyntax invocation)
{
// Get the parent of the invocation
var parent = invocation.Parent;

// The only case where the result is not being used is when the invocation
// is directly used as a statement without capturing the result
if (parent is ExpressionStatementSyntax)
{
return false;
}

// In all other cases, the result is being used in some way
// (as a condition, in an expression, assigned to a variable, etc.)
return true;
}
}
}
Loading
Loading