Skip to content

Commit

Permalink
[Analyzer] update analyzer and use it in test contracts (#1149)
Browse files Browse the repository at this point in the history
* update analyzer and use it in test contracts

* Update tests/Neo.Compiler.CSharp.TestContracts/Contract_TryCatch.cs

* Update tests/Neo.Compiler.CSharp.TestContracts/Contract_TryCatch.cs

* fix test

* fix analyzer

* update format

* remove test that is not useful anymore
  • Loading branch information
Jim8y authored Aug 24, 2024
1 parent afbc58b commit 9c19cd7
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 458 deletions.
3 changes: 1 addition & 2 deletions src/Neo.SmartContract.Analyzer/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ NC4005 | Method | Error | SystemMathUsageAnalyzer
NC4006 | Method | Error | BigIntegerUsageAnalyzer
NC4007 | Method | Error | StringMethodUsageAnalyzer
NC4008 | Usage | Error | BigIntegerCreationAnalyzer
NC4009 | Usage | Error | OutKeywordUsageAnalyzer
NC4009 | Usage | Info | InitialValueAnalyzer
NC4010 | Usage | Warning | RefKeywordUsageAnalyzer
NC4011 | Usage | Error | LinqUsageAnalyzer
NC4012 | Method | Error | CharMethodsUsageAnalyzer
NC4013 | Type | Error | CollectionTypesUsageAnalyzer
NC4014 | Usage | Warning | VolatileKeywordUsageAnalyzer
NC4015 | Usage | Error | KeywordUsageAnalyzer
NC4016 | Usage | Error | AnonymousFunctionUsageAnalyzer
NC4017 | Usage | Error | BanCastMethodAnalyzer
NC4018 | Naming | Error | SmartContractMethodNamingAnalyzer
NC4019 | Usage | Error | NotifyEventNameAnalyzer
Expand Down
116 changes: 0 additions & 116 deletions src/Neo.SmartContract.Analyzer/AnonymousFunctionUsageAnalyzer.cs

This file was deleted.

10 changes: 4 additions & 6 deletions src/Neo.SmartContract.Analyzer/BigIntegerUsageAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ public class BigIntegerUsageAnalyzer : DiagnosticAnalyzer

private readonly string[] _unsupportedBigIntegerMethods = {
"BitwiseAnd", "BitwiseOr",
"CompareTo", "DivRem",
"Exp", "GreatestCommonDivisor", "LeftShift", "Log",
"Log10", "OnesComplement", "Parse",
"RightShift", "Subtract",
"TryParse", "Xor",
"IsPowerOfTwo",
"Exp", "LeftShift",
"Log10", "OnesComplement",
"RightShift",
"Xor"
};

private static readonly DiagnosticDescriptor Rule = new(
Expand Down
2 changes: 1 addition & 1 deletion src/Neo.SmartContract.Analyzer/CharMethodsUsageAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class CharMethodsUsageAnalyzer : DiagnosticAnalyzer
"ToLowerInvariant",
"ToUpperInvariant", "TryParse",
"ConvertFromUtf32", "ConvertToUtf32",
"GetNumericValue", "GetUnicodeCategory",
"GetUnicodeCategory",
"IsSurrogatePair"
};

Expand Down
128 changes: 128 additions & 0 deletions src/Neo.SmartContract.Analyzer/InitialValueAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Editing;

namespace Neo.SmartContract.Analyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class InitialValueAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "NC4009";
private static readonly LocalizableString Title = "Convert attribute to literal initialization";
private static readonly LocalizableString MessageFormat = "Convert '{0}' attribute to literal initialization";
private static readonly LocalizableString Description = "This field can be initialized with a literal value instead of using an attribute.";
private const string Category = "Usage";

private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
Title,
MessageFormat,
Category,
DiagnosticSeverity.Info,
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(AnalyzeNode, SyntaxKind.FieldDeclaration);
}

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
var fieldDeclaration = (FieldDeclarationSyntax)context.Node;

foreach (var variable in fieldDeclaration.Declaration.Variables)
{
if (variable.Initializer?.Value is LiteralExpressionSyntax literal && literal.Token.ValueText == "default!")
{
var attribute = fieldDeclaration.AttributeLists
.SelectMany(al => al.Attributes)
.FirstOrDefault(attr => IsTargetAttribute(attr.Name.ToString()));

if (attribute != null)
{
var argumentList = attribute.ArgumentList;
if (argumentList != null && argumentList.Arguments.Count > 0)
{
var argument = argumentList.Arguments[0].ToString();
var diagnostic = Diagnostic.Create(Rule, fieldDeclaration.GetLocation(), attribute.Name);
context.ReportDiagnostic(diagnostic);
}
}
}
}
}

private bool IsTargetAttribute(string attributeName)
{
return attributeName == "Hash160" || attributeName == "ByteArray" || attributeName == "UInt256";
}
}

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(InitialValueCodeFixProvider)), Shared]
public class InitialValueCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(InitialValueAnalyzer.DiagnosticId);

public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<FieldDeclarationSyntax>().First();

Check warning on line 87 in src/Neo.SmartContract.Analyzer/InitialValueAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Test

Dereference of a possibly null reference.

Check warning on line 87 in src/Neo.SmartContract.Analyzer/InitialValueAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Test

Dereference of a possibly null reference.

context.RegisterCodeFix(
CodeAction.Create(
title: "Convert to literal initialization",
createChangedDocument: c => ConvertToLiteralInitializationAsync(context.Document, declaration, c),
equivalenceKey: nameof(InitialValueCodeFixProvider)),
diagnostic);
}

private async Task<Document> ConvertToLiteralInitializationAsync(Document document, FieldDeclarationSyntax fieldDeclaration, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

var attribute = fieldDeclaration.AttributeLists
.SelectMany(al => al.Attributes)
.FirstOrDefault(attr => IsTargetAttribute(attr.Name.ToString()));

if (attribute != null && attribute.ArgumentList?.Arguments.Count > 0)
{
var argument = attribute.ArgumentList.Arguments[0].ToString();
var newInitializer = SyntaxFactory.EqualsValueClause(SyntaxFactory.ParseExpression(argument));

var newField = fieldDeclaration

Check warning on line 111 in src/Neo.SmartContract.Analyzer/InitialValueAnalyzer.cs

View workflow job for this annotation

GitHub Actions / Test

Dereference of a possibly null reference.
.RemoveNodes(fieldDeclaration.AttributeLists, SyntaxRemoveOptions.KeepNoTrivia)
.WithDeclaration(fieldDeclaration.Declaration.WithVariables(
SyntaxFactory.SingletonSeparatedList(
fieldDeclaration.Declaration.Variables[0].WithInitializer(newInitializer))));

editor.ReplaceNode(fieldDeclaration, newField);
}

return editor.GetChangedDocument();
}

private bool IsTargetAttribute(string attributeName)
{
return attributeName == "Hash160" || attributeName == "ByteArray" || attributeName == "UInt256";
}
}
}
67 changes: 32 additions & 35 deletions src/Neo.SmartContract.Analyzer/KeywordUsageAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Neo.SmartContract.Analyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class KeywordUsageAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "NC4015";
private static readonly string Title = "Restricted keyword usage";
private static readonly string MessageFormat = "Use of '{0}' is not allowed";
private static readonly string Description = "This keyword is restricted in this project.";
private static readonly LocalizableString Title = "Restricted keyword usage";
private static readonly LocalizableString MessageFormat = "Use of '{0}' is not allowed";
private static readonly LocalizableString Description = "This keyword is restricted in this project.";
private const string Category = "Usage";

private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
Expand All @@ -30,54 +24,57 @@ public class KeywordUsageAnalyzer : DiagnosticAnalyzer
isEnabledByDefault: true,
description: Description);

private static readonly string[] bannedKeywords = new[] { "lock", "fixed", "unsafe", "stackalloc", "await", "dynamic", "unmanaged", "select", "orderby", "nameof", "implicit", "explicit", "yield", "where" };

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

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeNode,
SyntaxKind.UnsafeStatement,
SyntaxKind.LockStatement,
SyntaxKind.FixedStatement,
SyntaxKind.UnsafeStatement,
SyntaxKind.StackAllocArrayCreationExpression,
SyntaxKind.AwaitExpression,
SyntaxKind.QueryExpression,
SyntaxKind.YieldKeyword,
SyntaxKind.InvocationExpression,
SyntaxKind.WhereClause,
SyntaxKind.ConversionOperatorDeclaration);
context.RegisterSyntaxNodeAction(AnalyzeType, SyntaxKind.IdentifierName);
SyntaxKind.YieldReturnStatement,
SyntaxKind.YieldBreakStatement,
SyntaxKind.ConversionOperatorDeclaration,
SyntaxKind.IdentifierName);
}

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
var nodeText = context.Node.ToString();
foreach (var keyword in bannedKeywords)
SyntaxToken keywordToken = context.Node.Kind() switch
{
if (nodeText.Contains(keyword))
{
var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), keyword);
context.ReportDiagnostic(diagnostic);
break;
}
SyntaxKind.UnsafeStatement => ((UnsafeStatementSyntax)context.Node).UnsafeKeyword,
SyntaxKind.LockStatement => ((LockStatementSyntax)context.Node).LockKeyword,
SyntaxKind.FixedStatement => ((FixedStatementSyntax)context.Node).FixedKeyword,
SyntaxKind.StackAllocArrayCreationExpression => ((StackAllocArrayCreationExpressionSyntax)context.Node).StackAllocKeyword,
SyntaxKind.AwaitExpression => ((AwaitExpressionSyntax)context.Node).AwaitKeyword,
SyntaxKind.QueryExpression => ((QueryExpressionSyntax)context.Node).FromClause.FromKeyword,
SyntaxKind.YieldReturnStatement => ((YieldStatementSyntax)context.Node).YieldKeyword,
SyntaxKind.YieldBreakStatement => ((YieldStatementSyntax)context.Node).YieldKeyword,
SyntaxKind.ConversionOperatorDeclaration => ((ConversionOperatorDeclarationSyntax)context.Node).ImplicitOrExplicitKeyword,
SyntaxKind.IdentifierName => ((IdentifierNameSyntax)context.Node).Identifier,
_ => default
};

if (keywordToken != default && IsRestrictedKeyword(keywordToken.ValueText))
{
var diagnostic = Diagnostic.Create(Rule, keywordToken.GetLocation(), keywordToken.Text);
context.ReportDiagnostic(diagnostic);
}
}

private void AnalyzeType(SyntaxNodeAnalysisContext context)
private bool IsRestrictedKeyword(string keyword)
{
if (context.Node is IdentifierNameSyntax identifierName &&
identifierName.Identifier.ValueText == "dynamic")
return keyword switch
{
var typeInfo = context.SemanticModel.GetTypeInfo(identifierName);
if (typeInfo.Type != null && typeInfo.Type.TypeKind == TypeKind.Dynamic)
{
var diagnostic = Diagnostic.Create(Rule, identifierName.GetLocation(), "dynamic");
context.ReportDiagnostic(diagnostic);
}
}
"unsafe" or "lock" or "fixed" or "stackalloc" or "await" or
"yield" or "explicit" or "implicit" or "dynamic" => true,
_ => false
};
}
}
}
Loading

0 comments on commit 9c19cd7

Please sign in to comment.