Files
test/UnityEditorOnlyAnalyzer/UnityEditorOnlyUsageAnalyzer.cs
T
2026-03-13 12:34:01 +07:00

302 lines
11 KiB
C#

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;
}
}