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