Skip to content

Commit 3d2612b

Browse files
authored
Add analyzer enforcement for NEP interface compliance (#1413)
* Add analyzer enforcement for NEP standard interfaces * Fix nullable guards in NEP standard codefix
1 parent f9596cf commit 3d2612b

File tree

8 files changed

+1044
-6
lines changed

8 files changed

+1044
-6
lines changed

src/Neo.SmartContract.Analyzer/AnalyzerReleases.Unshipped.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ NC4024 | Usage | Error | MultipleCatchBlockAnalyzer
2727
NC4025 | Method | Error | EnumMethodsUsageAnalyzer
2828
NC4027 | Usage | Warning | CatchOnlySystemExceptionAnalyzer
2929
NC4028 | Namespace| Error | SystemThreadingUsageAnalyzer
30+
NC4029 | Usage | Warning | NepStandardImplementationAnalyzer
31+
NC4030 | Usage | Warning | NepStandardImplementationAnalyzer
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
// Copyright (C) 2015-2025 The Neo Project.
2+
//
3+
// NepStandardImplementationAnalyzer.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 System;
13+
using System.Collections.Generic;
14+
using System.Collections.Immutable;
15+
using System.Linq;
16+
using Microsoft.CodeAnalysis;
17+
using Microsoft.CodeAnalysis.CSharp.Syntax;
18+
using Microsoft.CodeAnalysis.Diagnostics;
19+
20+
namespace Neo.SmartContract.Analyzer;
21+
22+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
23+
public sealed class NepStandardImplementationAnalyzer : DiagnosticAnalyzer
24+
{
25+
public const string DiagnosticId = "NC4029";
26+
public const string InterfaceDiagnosticId = "NC4030";
27+
28+
private const string StandardPropertyName = "Standard";
29+
private const string MissingMembersPropertyName = "MissingMembers";
30+
private const string InterfacePropertyName = "Interface";
31+
32+
private static readonly DiagnosticDescriptor Rule = new(
33+
DiagnosticId,
34+
"Supported standard requires mandatory API surface",
35+
"Contract declares support for {0} but is missing required members or interfaces: {1}",
36+
"Usage",
37+
DiagnosticSeverity.Warning,
38+
isEnabledByDefault: true,
39+
description: "When a contract declares support for NEP standards it must expose the required members or implement the canonical interface.");
40+
41+
private static readonly DiagnosticDescriptor InterfaceRule = new(
42+
InterfaceDiagnosticId,
43+
"Supported standard requires interface implementation",
44+
"Contract declares support for {0} but does not implement required interface {1}",
45+
"Usage",
46+
DiagnosticSeverity.Warning,
47+
isEnabledByDefault: true,
48+
description: "Certain NEP standards mandate that contracts implement a specific interface.");
49+
50+
private static readonly ImmutableArray<string> Nep17RequiredMembers = ImmutableArray.Create(
51+
"Symbol",
52+
"Decimals",
53+
"TotalSupply",
54+
"BalanceOf",
55+
"Transfer");
56+
57+
private static readonly ImmutableArray<string> Nep11AdditionalMembers = ImmutableArray.Create(
58+
"OwnerOf",
59+
"Properties",
60+
"Tokens",
61+
"TokensOf");
62+
63+
private static readonly ImmutableDictionary<NepStandardKind, ImmutableArray<string>> RequiredMembersByStandard =
64+
new Dictionary<NepStandardKind, ImmutableArray<string>>
65+
{
66+
{ NepStandardKind.Nep17, Nep17RequiredMembers },
67+
{ NepStandardKind.Nep11, Nep17RequiredMembers.AddRange(Nep11AdditionalMembers) }
68+
}.ToImmutableDictionary();
69+
70+
private static readonly ImmutableDictionary<NepStandardKind, string> RequiredInterfaceByStandard =
71+
new Dictionary<NepStandardKind, string>
72+
{
73+
{ NepStandardKind.Nep24, "Neo.SmartContract.Framework.Interfaces.INep24" },
74+
{ NepStandardKind.Nep26, "Neo.SmartContract.Framework.Interfaces.INEP26" },
75+
{ NepStandardKind.Nep27, "Neo.SmartContract.Framework.Interfaces.INEP27" },
76+
{ NepStandardKind.Nep29, "Neo.SmartContract.Framework.Interfaces.INEP29" },
77+
{ NepStandardKind.Nep30, "Neo.SmartContract.Framework.Interfaces.INEP30" }
78+
}.ToImmutableDictionary();
79+
80+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule, InterfaceRule);
81+
82+
public override void Initialize(AnalysisContext context)
83+
{
84+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
85+
context.EnableConcurrentExecution();
86+
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
87+
}
88+
89+
private static void AnalyzeSymbol(SymbolAnalysisContext context)
90+
{
91+
if (context.Symbol is not INamedTypeSymbol namedType ||
92+
namedType.TypeKind != TypeKind.Class ||
93+
namedType.IsAbstract)
94+
{
95+
return;
96+
}
97+
98+
var supportedAttribute = namedType.GetAttributes()
99+
.FirstOrDefault(attr =>
100+
attr.AttributeClass?.ToDisplayString() ==
101+
"Neo.SmartContract.Framework.Attributes.SupportedStandardsAttribute");
102+
103+
if (supportedAttribute is null)
104+
return;
105+
106+
var supportedStandards = ResolveStandards(supportedAttribute);
107+
if (supportedStandards.Count == 0)
108+
return;
109+
110+
foreach (var standard in supportedStandards)
111+
{
112+
if (RequiredInterfaceByStandard.TryGetValue(standard, out var interfaceName))
113+
{
114+
if (!ImplementsInterface(namedType, interfaceName))
115+
{
116+
ReportInterfaceDiagnostic(context, supportedAttribute, namedType, standard, interfaceName);
117+
}
118+
}
119+
120+
if (!RequiredMembersByStandard.TryGetValue(standard, out var requiredMembers) || requiredMembers.IsDefaultOrEmpty)
121+
continue;
122+
123+
var missingMembers = FindMissingMembers(namedType, requiredMembers, standard);
124+
if (missingMembers.Count == 0)
125+
continue;
126+
127+
ReportMemberDiagnostic(context, supportedAttribute, namedType, standard, missingMembers);
128+
}
129+
}
130+
131+
private static List<string> FindMissingMembers(INamedTypeSymbol typeSymbol, ImmutableArray<string> requiredMembers, NepStandardKind standard)
132+
{
133+
var missing = new List<string>();
134+
135+
foreach (var memberName in requiredMembers)
136+
{
137+
if (HasMember(typeSymbol, memberName, standard))
138+
continue;
139+
140+
missing.Add(memberName);
141+
}
142+
143+
return missing;
144+
}
145+
146+
private static bool HasMember(INamedTypeSymbol typeSymbol, string memberName, NepStandardKind standard)
147+
{
148+
bool Predicate(IMethodSymbol method) =>
149+
!method.IsImplicitlyDeclared &&
150+
method.Parameters.Length switch
151+
{
152+
0 when memberName is "Symbol" or "Decimals" or "TotalSupply" or "Tokens" => true,
153+
1 when memberName is "BalanceOf" or "OwnerOf" or "TokensOf" or "Properties" => true,
154+
3 when memberName == "Transfer" && standard == NepStandardKind.Nep11 => true,
155+
4 when memberName == "Transfer" && standard == NepStandardKind.Nep17 => true,
156+
_ => false
157+
};
158+
159+
bool PropertyPredicate(IPropertySymbol property) =>
160+
!property.IsImplicitlyDeclared &&
161+
memberName is "Symbol" or "Decimals" or "TotalSupply";
162+
163+
for (var current = typeSymbol; current is not null; current = current.BaseType)
164+
{
165+
foreach (var member in current.GetMembers(memberName))
166+
{
167+
if (member is IMethodSymbol method)
168+
{
169+
if (Predicate(method))
170+
return true;
171+
}
172+
else if (member is IPropertySymbol property)
173+
{
174+
if (PropertyPredicate(property))
175+
return true;
176+
}
177+
}
178+
}
179+
180+
return false;
181+
}
182+
183+
private static void ReportMemberDiagnostic(SymbolAnalysisContext context, AttributeData supportedAttribute, INamedTypeSymbol namedType, NepStandardKind standard, List<string> missingMembers)
184+
{
185+
var properties = ImmutableDictionary<string, string?>.Empty
186+
.Add(StandardPropertyName, FormatStandardName(standard))
187+
.Add(MissingMembersPropertyName, string.Join(",", missingMembers));
188+
189+
var location = supportedAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken) switch
190+
{
191+
AttributeSyntax syntax => syntax.GetLocation(),
192+
_ => namedType.Locations.FirstOrDefault()
193+
} ?? namedType.Locations.FirstOrDefault();
194+
195+
if (location is null)
196+
return;
197+
198+
var diagnostic = Diagnostic.Create(
199+
Rule,
200+
location,
201+
properties,
202+
FormatStandardName(standard),
203+
string.Join(", ", missingMembers));
204+
205+
context.ReportDiagnostic(diagnostic);
206+
}
207+
208+
private static void ReportInterfaceDiagnostic(SymbolAnalysisContext context, AttributeData supportedAttribute, INamedTypeSymbol namedType, NepStandardKind standard, string interfaceName)
209+
{
210+
var location = supportedAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken) switch
211+
{
212+
AttributeSyntax syntax => syntax.GetLocation(),
213+
_ => namedType.Locations.FirstOrDefault()
214+
} ?? namedType.Locations.FirstOrDefault();
215+
216+
if (location is null)
217+
return;
218+
219+
var diagnostic = Diagnostic.Create(
220+
InterfaceRule,
221+
location,
222+
ImmutableDictionary<string, string?>.Empty
223+
.Add(StandardPropertyName, FormatStandardName(standard))
224+
.Add(InterfacePropertyName, interfaceName),
225+
FormatStandardName(standard),
226+
interfaceName);
227+
228+
context.ReportDiagnostic(diagnostic);
229+
}
230+
231+
private static bool ImplementsInterface(INamedTypeSymbol typeSymbol, string interfaceMetadataName) =>
232+
typeSymbol.AllInterfaces.Any(i => i.ToDisplayString() == interfaceMetadataName);
233+
234+
private static HashSet<NepStandardKind> ResolveStandards(AttributeData attributeData)
235+
{
236+
var result = new HashSet<NepStandardKind>();
237+
238+
foreach (var argument in attributeData.ConstructorArguments)
239+
{
240+
switch (argument.Kind)
241+
{
242+
case TypedConstantKind.Array:
243+
foreach (var item in argument.Values)
244+
TryAddStandard(item, result);
245+
break;
246+
default:
247+
TryAddStandard(argument, result);
248+
break;
249+
}
250+
}
251+
252+
return result;
253+
}
254+
255+
private static void TryAddStandard(TypedConstant constant, ISet<NepStandardKind> standards)
256+
{
257+
if (constant.IsNull)
258+
return;
259+
260+
if (constant.Type is null)
261+
return;
262+
263+
if (constant.Type.TypeKind == TypeKind.Enum &&
264+
constant.Type.ToDisplayString() == "Neo.SmartContract.Framework.NepStandard")
265+
{
266+
var enumValue = constant.Value?.ToString();
267+
if (Enum.TryParse<NepStandardKind>(enumValue, ignoreCase: true, out var standard))
268+
standards.Add(standard);
269+
return;
270+
}
271+
272+
if (constant.Type.SpecialType == SpecialType.System_String &&
273+
constant.Value is string stringValue)
274+
{
275+
var normalized = new string(stringValue
276+
.Where(char.IsLetterOrDigit)
277+
.Select(char.ToUpperInvariant)
278+
.ToArray());
279+
280+
switch (normalized)
281+
{
282+
case "NEP11":
283+
standards.Add(NepStandardKind.Nep11);
284+
break;
285+
case "NEP17":
286+
standards.Add(NepStandardKind.Nep17);
287+
break;
288+
case "NEP24":
289+
standards.Add(NepStandardKind.Nep24);
290+
break;
291+
case "NEP26":
292+
standards.Add(NepStandardKind.Nep26);
293+
break;
294+
case "NEP27":
295+
standards.Add(NepStandardKind.Nep27);
296+
break;
297+
case "NEP29":
298+
standards.Add(NepStandardKind.Nep29);
299+
break;
300+
case "NEP30":
301+
standards.Add(NepStandardKind.Nep30);
302+
break;
303+
}
304+
}
305+
}
306+
307+
private static string FormatStandardName(NepStandardKind standard) =>
308+
standard switch
309+
{
310+
NepStandardKind.Nep11 => "NEP-11",
311+
NepStandardKind.Nep17 => "NEP-17",
312+
NepStandardKind.Nep24 => "NEP-24",
313+
NepStandardKind.Nep26 => "NEP-26",
314+
NepStandardKind.Nep27 => "NEP-27",
315+
NepStandardKind.Nep29 => "NEP-29",
316+
NepStandardKind.Nep30 => "NEP-30",
317+
_ => standard.ToString()
318+
};
319+
320+
private enum NepStandardKind
321+
{
322+
Unknown,
323+
Nep11,
324+
Nep17,
325+
Nep24,
326+
Nep26,
327+
Nep27,
328+
Nep29,
329+
Nep30
330+
}
331+
}

0 commit comments

Comments
 (0)