analyzer
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class UnityEditorOnlyUsageAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
public const string DiagnosticId = "UNITY_EDITOR_ONLY_USAGE";
|
||||
|
||||
private static readonly LocalizableString Title =
|
||||
"Usage of UNITY_EDITOR-only code";
|
||||
|
||||
private static readonly LocalizableString MessageFormat =
|
||||
"Method/Type '{0}' is only available in UNITY_EDITOR and may cause build errors";
|
||||
|
||||
private static readonly LocalizableString Description =
|
||||
"Warns when code wrapped in #if UNITY_EDITOR is used from code that is not wrapped in the same directive.";
|
||||
|
||||
private const string Category = "Unity";
|
||||
|
||||
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
|
||||
DiagnosticId,
|
||||
Title,
|
||||
MessageFormat,
|
||||
Category,
|
||||
DiagnosticSeverity.Error, // Changed from Warning to Error để hiển thị màu đỏ
|
||||
isEnabledByDefault: true,
|
||||
description: Description);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
|
||||
ImmutableArray.Create(Rule);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
// Kiểm tra method calls
|
||||
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
|
||||
|
||||
// Kiểm tra property/field access
|
||||
context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
|
||||
|
||||
// Kiểm tra identifier (biến đơn giản như m_nCurPanel2)
|
||||
context.RegisterSyntaxNodeAction(AnalyzeIdentifier, SyntaxKind.IdentifierName);
|
||||
|
||||
// Kiểm tra object creation (new ClassName())
|
||||
context.RegisterSyntaxNodeAction(AnalyzeObjectCreation, SyntaxKind.ObjectCreationExpression);
|
||||
}
|
||||
|
||||
private void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var invocation = (InvocationExpressionSyntax)context.Node;
|
||||
|
||||
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
CheckMemberAccess(context, memberAccess, invocation);
|
||||
}
|
||||
else if (invocation.Expression is IdentifierNameSyntax identifier)
|
||||
{
|
||||
CheckIdentifier(context, identifier, invocation);
|
||||
}
|
||||
}
|
||||
|
||||
private void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var memberAccess = (MemberAccessExpressionSyntax)context.Node;
|
||||
CheckMemberAccess(context, memberAccess, memberAccess);
|
||||
}
|
||||
|
||||
private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var identifier = (IdentifierNameSyntax)context.Node;
|
||||
|
||||
// Bỏ qua namespace declarations
|
||||
if (identifier.Parent is QualifiedNameSyntax ||
|
||||
identifier.Parent is UsingDirectiveSyntax ||
|
||||
identifier.Parent is NamespaceDeclarationSyntax ||
|
||||
identifier.Parent is FileScopedNamespaceDeclarationSyntax)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Bỏ qua nếu identifier này là phần của member access (đã xử lý ở AnalyzeMemberAccess)
|
||||
if (identifier.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Name == identifier)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Bỏ qua nếu identifier này là phần của invocation (đã xử lý ở AnalyzeInvocation)
|
||||
if (identifier.Parent is InvocationExpressionSyntax)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Bỏ qua type names trong declarations
|
||||
if (identifier.Parent is VariableDeclarationSyntax ||
|
||||
identifier.Parent is TypeSyntax)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CheckIdentifier(context, identifier, identifier);
|
||||
}
|
||||
|
||||
private void AnalyzeObjectCreation(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var objectCreation = (ObjectCreationExpressionSyntax)context.Node;
|
||||
|
||||
if (objectCreation.Type != null)
|
||||
{
|
||||
var symbol = context.SemanticModel.GetSymbolInfo(objectCreation.Type).Symbol;
|
||||
if (symbol != null && IsEditorOnlySymbol(symbol, context))
|
||||
{
|
||||
if (!IsInEditorOnlyContext(context.Node))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(
|
||||
Rule,
|
||||
objectCreation.Type.GetLocation(),
|
||||
symbol.Name);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckMemberAccess(SyntaxNodeAnalysisContext context, MemberAccessExpressionSyntax memberAccess, SyntaxNode reportNode)
|
||||
{
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess);
|
||||
|
||||
if (symbolInfo.Symbol == null)
|
||||
return;
|
||||
|
||||
// Check if the symbol is defined within #if UNITY_EDITOR
|
||||
if (IsEditorOnlySymbol(symbolInfo.Symbol, context))
|
||||
{
|
||||
// Check if the current usage is NOT within #if UNITY_EDITOR
|
||||
if (!IsInEditorOnlyContext(memberAccess))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(
|
||||
Rule,
|
||||
reportNode.GetLocation(),
|
||||
symbolInfo.Symbol.Name);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckIdentifier(SyntaxNodeAnalysisContext context, IdentifierNameSyntax identifier, SyntaxNode reportNode)
|
||||
{
|
||||
var symbolInfo = context.SemanticModel.GetSymbolInfo(identifier);
|
||||
|
||||
if (symbolInfo.Symbol == null)
|
||||
return;
|
||||
|
||||
if (IsEditorOnlySymbol(symbolInfo.Symbol, context))
|
||||
{
|
||||
if (!IsInEditorOnlyContext(identifier))
|
||||
{
|
||||
var diagnostic = Diagnostic.Create(
|
||||
Rule,
|
||||
reportNode.GetLocation(),
|
||||
symbolInfo.Symbol.Name);
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsEditorOnlySymbol(ISymbol symbol, SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
if (symbol == null)
|
||||
return false;
|
||||
|
||||
// Get the syntax reference
|
||||
var syntaxReferences = symbol.DeclaringSyntaxReferences;
|
||||
if (syntaxReferences.Length == 0)
|
||||
return false;
|
||||
|
||||
foreach (var syntaxRef in syntaxReferences)
|
||||
{
|
||||
var syntax = syntaxRef.GetSyntax(context.CancellationToken);
|
||||
if (IsInEditorOnlyContext(syntax))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsInEditorOnlyContext(SyntaxNode node)
|
||||
{
|
||||
if (node == null)
|
||||
return false;
|
||||
|
||||
var root = node.SyntaxTree.GetRoot();
|
||||
var nodeStart = node.SpanStart;
|
||||
|
||||
// Get all trivia in the file
|
||||
var allTrivia = root.DescendantTrivia();
|
||||
|
||||
int activeIfDirectiveStart = -1;
|
||||
bool inEditorBlock = false;
|
||||
|
||||
foreach (var trivia in allTrivia)
|
||||
{
|
||||
// Stop checking if we've passed the node
|
||||
if (trivia.SpanStart > nodeStart)
|
||||
break;
|
||||
|
||||
if (trivia.IsKind(SyntaxKind.IfDirectiveTrivia))
|
||||
{
|
||||
var directive = trivia.GetStructure() as ConditionalDirectiveTriviaSyntax;
|
||||
if (directive != null && ContainsUnityEditorCondition(directive))
|
||||
{
|
||||
activeIfDirectiveStart = directive.SpanStart;
|
||||
inEditorBlock = true;
|
||||
}
|
||||
}
|
||||
else if (trivia.IsKind(SyntaxKind.EndIfDirectiveTrivia))
|
||||
{
|
||||
// Check if this #endif closes the active UNITY_EDITOR block
|
||||
if (inEditorBlock && activeIfDirectiveStart >= 0)
|
||||
{
|
||||
// This #endif closes the UNITY_EDITOR block
|
||||
// Check if node is before this #endif
|
||||
if (nodeStart < trivia.SpanStart)
|
||||
{
|
||||
// Node is inside the UNITY_EDITOR block
|
||||
return true;
|
||||
}
|
||||
// Node is after this #endif, so not in UNITY_EDITOR block
|
||||
inEditorBlock = false;
|
||||
activeIfDirectiveStart = -1;
|
||||
}
|
||||
}
|
||||
else if (trivia.IsKind(SyntaxKind.ElseDirectiveTrivia))
|
||||
{
|
||||
// #else closes the if block, so if we're in UNITY_EDITOR block, it ends here
|
||||
if (inEditorBlock && activeIfDirectiveStart >= 0)
|
||||
{
|
||||
if (nodeStart < trivia.SpanStart)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
inEditorBlock = false;
|
||||
activeIfDirectiveStart = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're still in an editor block and haven't hit an #endif, check if node is after the #if
|
||||
if (inEditorBlock && activeIfDirectiveStart >= 0)
|
||||
{
|
||||
// Check if there's an #endif after the node
|
||||
foreach (var trivia in allTrivia)
|
||||
{
|
||||
if (trivia.SpanStart <= nodeStart)
|
||||
continue;
|
||||
|
||||
if (trivia.IsKind(SyntaxKind.EndIfDirectiveTrivia))
|
||||
{
|
||||
// Node is between #if UNITY_EDITOR and #endif
|
||||
return true;
|
||||
}
|
||||
else if (trivia.IsKind(SyntaxKind.IfDirectiveTrivia))
|
||||
{
|
||||
// New #if starts, check if it's nested
|
||||
var directive = trivia.GetStructure() as ConditionalDirectiveTriviaSyntax;
|
||||
if (directive != null && ContainsUnityEditorCondition(directive))
|
||||
{
|
||||
// Nested UNITY_EDITOR block, continue checking
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no #endif found after node, node is still in the block
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ContainsUnityEditorCondition(ConditionalDirectiveTriviaSyntax directive)
|
||||
{
|
||||
if (directive == null)
|
||||
return false;
|
||||
|
||||
var condition = directive.Condition?.ToString();
|
||||
if (string.IsNullOrEmpty(condition))
|
||||
return false;
|
||||
|
||||
// Check for UNITY_EDITOR in the condition (exact match or as part of expression)
|
||||
return condition.IndexOf("UNITY_EDITOR", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user