using System; using System.Collections.Generic; using System.Linq; using UnityPipeline.Microsoft.CodeAnalysis; using UnityPipeline.Microsoft.CodeAnalysis.CSharp; using UnityPipeline.Microsoft.CodeAnalysis.CSharp.Syntax; using UnityEngine; namespace Unity.Pipeline.CodeReload { /// /// Accessibility gate for [CodeReload] method bodies on the compiled (Assembly.Load) backend. /// /// The compiled backend cannot use non-public members of the target type: even if the override /// compiled with Roslyn's access checks relaxed, Unity's Mono enforces accessibility when /// JIT-ing the loaded override IL and does not honor [assembly: IgnoresAccessChecksTo] /// (verified on Mono 6.13) — the access would throw (e.g. FieldAccessException) on first /// dispatch and silently fall back to the original body. Compiled-backend overrides therefore /// compile with standard access checks (see /// RoslynCompilationService.AllowNonPublicMemberAccess, interpreter-only), and this /// validator runs up front — only when the compiled backend will execute the override — to turn /// the eventual raw CS0122 into a clear reload error. The interpreter backend reaches /// non-public members via reflection and must not be gated. /// static class AccessibilityValidator { /// /// Check that every [CodeReload] method body only accesses public instance members of its /// declaring type. Runs over an already-built semantic model + class declaration, so the /// in-place reload path shares one compilation between validation and transformation /// instead of building two. /// /// The semantic model built over the source containing the declaring type. /// The declaring type's class declaration within . /// Map of method name to body source, for the methods being validated. /// Name of the declaring type to validate against. /// The validation result, including any violations found. public static AccessibilityValidationResult ValidatePublicAccess( SemanticModel model, ClassDeclarationSyntax classDecl, Dictionary methodBodies, string originalTypeName) { var result = new AccessibilityValidationResult { IsValid = true, Violations = new List() }; try { var classSymbol = model.GetDeclaredSymbol(classDecl); foreach (var method in classDecl.Members.OfType()) { var methodName = method.Identifier.ValueText; if (!methodBodies.ContainsKey(methodName) || method.Body == null) continue; var seen = new HashSet(); foreach (var name in method.Body.DescendantNodes().OfType()) { if (IsMemberName(name)) continue; var symbol = model.GetSymbolInfo(name).Symbol; if (!IsInstanceMemberOf(symbol, classSymbol)) continue; if (symbol.DeclaredAccessibility != Accessibility.Public && seen.Add(symbol.Name)) { result.Violations.Add(new AccessibilityViolation { MemberName = symbol.Name, MethodName = methodName, AccessLevel = symbol.DeclaredAccessibility, ViolationType = AccessibilityViolationType.PrivateAccess, ErrorMessage = $"Cannot access non-public member '{symbol.Name}' " + $"({symbol.DeclaredAccessibility}) in [CodeReload] method '{methodName}'", Suggestion = $"Make '{symbol.Name}' public in {originalTypeName}, or reload " + "through the interpreter backend (reload_file_editor_interpreter), which " + "reaches non-public members. The compiled backend loads the override " + "as a separate assembly and Mono enforces accessibility at dispatch." }); } } } result.IsValid = result.Violations.Count == 0; return result; } catch (Exception ex) { Debug.LogError($"CodeReload: Accessibility validation error: {ex.Message}"); return new AccessibilityValidationResult { IsValid = false, ValidationError = ex.Message, Violations = new List() }; } } /// True if the name is the right-hand member name of an access (foo.Bar -> Bar). private static bool IsMemberName(SimpleNameSyntax node) { if (node.Parent is MemberAccessExpressionSyntax ma && ma.Name == node) return true; if (node.Parent is QualifiedNameSyntax) return true; if (node.Parent is MemberBindingExpressionSyntax) return true; return false; } /// True if the symbol is an instance field/property/method/event of the type or a base. private static bool IsInstanceMemberOf(ISymbol symbol, INamedTypeSymbol type) { if (symbol == null || symbol.IsStatic) return false; switch (symbol.Kind) { case SymbolKind.Field: case SymbolKind.Property: case SymbolKind.Method: case SymbolKind.Event: break; default: return false; } for (var t = type; t != null; t = t.BaseType) { if (SymbolEqualityComparer.Default.Equals(t, symbol.ContainingType)) return true; } return false; } } /// /// Result of accessibility validation for code reload methods. /// class AccessibilityValidationResult { /// True when no accessibility violations were found. public bool IsValid { get; set; } /// Violations found, if any. public List Violations { get; set; } = new List(); /// Set when validation itself could not run (e.g. a parse failure), rather than a violation being found. public string ValidationError { get; set; } /// Human-readable summary of or all . public string GetFormattedErrorMessage() { if (!string.IsNullOrEmpty(ValidationError)) return $"CodeReload Validation Error: {ValidationError}"; if (Violations.Count == 0) return "All member access is valid for code reload."; var errorMessage = $"CodeReload Validation Failed: {Violations.Count} accessibility violation(s) found\n\n"; for (int i = 0; i < Violations.Count; i++) { var violation = Violations[i]; errorMessage += $"{i + 1}. Method '{violation.MethodName}': {violation.ErrorMessage}\n"; errorMessage += $" → Suggestion: {violation.Suggestion}\n"; if (i < Violations.Count - 1) errorMessage += "\n"; } errorMessage += "\nFix these accessibility issues and run reload_file again."; return errorMessage; } } /// /// Information about a specific accessibility violation in code reload code. /// class AccessibilityViolation { /// Name of the non-public member that was accessed. public string MemberName { get; set; } /// Name of the [CodeReload] method containing the access. public string MethodName { get; set; } /// The member's actual accessibility. public Accessibility AccessLevel { get; set; } /// Category of violation. public AccessibilityViolationType ViolationType { get; set; } /// Human-readable description of the violation. public string ErrorMessage { get; set; } /// Suggested fix. public string Suggestion { get; set; } } /// /// Types of accessibility violations that can occur in code reload methods. /// enum AccessibilityViolationType { /// A private member was accessed. PrivateAccess, /// An internal member was accessed. InternalAccess, /// A protected member was accessed. ProtectedAccess, /// The source could not be parsed to determine accessibility. ParseError } }