302 lines
11 KiB
C#
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;
|
|
}
|
|
}
|