From b01f1e9a34846264e56645ac86aea7728a1b1844 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 16:22:48 +0900 Subject: [PATCH 1/2] Add Random default alias and generalize dynamic-code alias detection Ports main commit 045f5252 (#1469) to v3-beta. The Object alias already existed via WrapperTemplate's ad-hoc string scan (PR #1234), but Random was missing and the detection approach did not generalize. Replace the "Object"-only text scan with a generic mechanism: SourceShaper now collects every user-defined using-alias name (including global/verbatim/commented forms) into SourceShapeResult.AliasedNames, and WrapperTemplate looks up a small DefaultUsingAlias table (Object, Random) against that set instead of re-parsing using-directive strings. Also fixes a latent gap in global-using detection: the keyword scan between "global" and "using" only skipped whitespace, so a comment in between caused the whole global using directive to be misclassified as a top-level statement. Restores unit test coverage for this behavior (deleted in #1310 because the old tests were execution-based and froze the Editor); these are pure preparer/shaper-level tests with no compile-and-run step, so they carry no freeze risk. --- .../AutoInjectedNamespacesTests.cs | 10 +- .../DynamicCodeSourcePreparerTests.cs | 75 ++++++++++++ .../DynamicCodeTestStringUtility.cs | 26 ++++ .../DynamicCodeTestStringUtility.cs.meta | 11 ++ .../PreUsingResolverTests.cs | 12 +- .../DynamicCodeToolTests/SourceShaperTests.cs | 27 +++++ .../DynamicCodeSourcePreparer.cs | 1 + .../DynamicCompilation/SourceShaper.cs | 114 +++++++++++++++++- .../DynamicCompilation/WrapperTemplate.cs | 80 ++++-------- 9 files changed, 288 insertions(+), 68 deletions(-) create mode 100644 Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTestStringUtility.cs create mode 100644 Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTestStringUtility.cs.meta diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs index 7437031398..f516a81d67 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs @@ -16,7 +16,7 @@ public void Resolve_WhenUnresolvedType_ShouldReportAddedNamespace() { string body = "StringBuilder builder = new StringBuilder();\nreturn builder.ToString();"; string wrappedSource = WrapperTemplate.Build( - new List(), "TestNs", "TestClass", body); + new List(), System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); @@ -28,7 +28,7 @@ public void Resolve_WhenUnresolvedType_ShouldReportAddedAssemblyReference() { string body = "StringBuilder builder = new StringBuilder();\nreturn builder.ToString();"; string wrappedSource = WrapperTemplate.Build( - new List(), "TestNs", "TestClass", body); + new List(), System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); @@ -40,7 +40,7 @@ public void Resolve_WhenNoMissingUsings_ShouldReportEmptyAddedNamespaces() { string body = "int x = 42;\nreturn x;"; string wrappedSource = WrapperTemplate.Build( - new List(), "TestNs", "TestClass", body); + new List(), System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); @@ -52,7 +52,7 @@ public void Resolve_WhenMultipleTypes_ShouldReportAllAddedNamespaces() { string body = "StringBuilder sb = new StringBuilder();\nRegex r = new Regex(\"x\");\nreturn sb.ToString();"; string wrappedSource = WrapperTemplate.Build( - new List(), "TestNs", "TestClass", body); + new List(), System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); @@ -65,7 +65,7 @@ public void Resolve_WhenAlreadyHasUsing_ShouldNotReportIt() { List usings = new() { "using System.Text;" }; string body = "StringBuilder builder = new StringBuilder();\nreturn builder.ToString();"; - string wrappedSource = WrapperTemplate.Build(usings, "TestNs", "TestClass", body); + string wrappedSource = WrapperTemplate.Build(usings, System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs index 2dcd4128d4..ba066c3466 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs @@ -44,6 +44,81 @@ public void Prepare_WhenStringLiteralExists_ShouldEmitPreambleBeforeUserCodeMark prepared.PreparedSource.IndexOf(WrapperTemplate.UserCodeStartMarker, System.StringComparison.Ordinal)); } + [Test] + public void Prepare_WhenScriptUsesBareUnityObject_ShouldAddObjectAlias() + { + PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( + "GameObject go = new GameObject(\"source\");\nObject.Instantiate(go);\nreturn null;", + DynamicCodeConstants.DEFAULT_NAMESPACE, + DynamicCodeConstants.DEFAULT_CLASS_NAME); + + Assert.IsNotNull(prepared.PreparedSource); + StringAssert.Contains("using Object = UnityEngine.Object;", prepared.PreparedSource); + } + + [Test] + public void Prepare_WhenObjectAliasAlreadyExists_ShouldNotAddDuplicateAlias() + { + PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( + "using Object = UnityEngine.Object;\nObject.Instantiate(new GameObject(\"source\"));\nreturn null;", + DynamicCodeConstants.DEFAULT_NAMESPACE, + DynamicCodeConstants.DEFAULT_CLASS_NAME); + + Assert.IsNotNull(prepared.PreparedSource); + Assert.AreEqual( + 1, + DynamicCodeTestStringUtility.CountSubstring(prepared.PreparedSource, "using Object = UnityEngine.Object;")); + } + + [Test] + public void Prepare_WhenCustomObjectAliasAlreadyExists_ShouldRespectUserAlias() + { + PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( + "using Object = System.Object;\nreturn new Object();", + DynamicCodeConstants.DEFAULT_NAMESPACE, + DynamicCodeConstants.DEFAULT_CLASS_NAME); + + Assert.IsNotNull(prepared.PreparedSource); + Assert.AreEqual( + 1, + DynamicCodeTestStringUtility.CountSubstring(prepared.PreparedSource, "using Object = ")); + } + + [Test] + public void Prepare_WhenVerbatimObjectAliasAlreadyExists_ShouldRespectUserAlias() + { + PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( + "using @Object = System.Object;\nreturn new @Object();", + DynamicCodeConstants.DEFAULT_NAMESPACE, + DynamicCodeConstants.DEFAULT_CLASS_NAME); + + Assert.IsNotNull(prepared.PreparedSource); + StringAssert.Contains("using @Object = System.Object;", prepared.PreparedSource); + Assert.AreEqual( + 0, + DynamicCodeTestStringUtility.CountSubstring(prepared.PreparedSource, "using Object = UnityEngine.Object;")); + } + + [Test] + public void Prepare_WhenScriptUsesBareUnityRandom_ShouldAddRandomAlias() + { + PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( + "int value = Random.Range(0, 10);\nreturn value;", + DynamicCodeConstants.DEFAULT_NAMESPACE, + DynamicCodeConstants.DEFAULT_CLASS_NAME); + + Assert.IsNotNull(prepared.PreparedSource); + StringAssert.Contains("using Random = UnityEngine.Random;", prepared.PreparedSource); + } + + [Test] + public void CountSubstring_WhenTargetIsEmpty_ShouldReturnZero() + { + int count = DynamicCodeTestStringUtility.CountSubstring("source", ""); + + Assert.AreEqual(0, count); + } + [Test] public void Prepare_WhenInterpolatedStringExists_ShouldSkipLiteralHoisting() { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTestStringUtility.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTestStringUtility.cs new file mode 100644 index 0000000000..6490300475 --- /dev/null +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTestStringUtility.cs @@ -0,0 +1,26 @@ +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests +{ + /// + /// Shared string-counting helper for dynamic code source assertions. + /// + internal static class DynamicCodeTestStringUtility + { + public static int CountSubstring(string source, string target) + { + if (string.IsNullOrEmpty(target)) + { + return 0; + } + + int count = 0; + int index = 0; + while ((index = source.IndexOf(target, index, System.StringComparison.Ordinal)) >= 0) + { + count++; + index += target.Length; + } + + return count; + } + } +} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTestStringUtility.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTestStringUtility.cs.meta new file mode 100644 index 0000000000..e2b6b2de71 --- /dev/null +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeTestStringUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4e47f270d1343898e09dad9cd8d2402 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs index 6e025853e7..016593934e 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs @@ -171,7 +171,7 @@ public void Resolve_WhenUnresolvedType_ShouldInjectUsing() { string body = "StringBuilder builder = new StringBuilder();\nreturn builder.ToString();"; string wrappedSource = WrapperTemplate.Build( - new List(), "TestNs", "TestClass", body); + new List(), System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); @@ -184,7 +184,7 @@ public void Resolve_WhenUnresolvedType_ShouldReportAssemblyReference() { string body = "StringBuilder builder = new StringBuilder();\nreturn builder.ToString();"; string wrappedSource = WrapperTemplate.Build( - new List(), "TestNs", "TestClass", body); + new List(), System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); @@ -196,7 +196,7 @@ public void Resolve_WhenAlreadyHasUsing_ShouldNotAddDuplicate() { List usings = new() { "using System.Text;" }; string body = "StringBuilder builder = new StringBuilder();\nreturn builder.ToString();"; - string wrappedSource = WrapperTemplate.Build(usings, "TestNs", "TestClass", body); + string wrappedSource = WrapperTemplate.Build(usings, System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); @@ -209,7 +209,7 @@ public void Resolve_WhenNoUserTypes_ShouldNotAddSystemText() { string body = "int x = 42;\nreturn x;"; string wrappedSource = WrapperTemplate.Build( - new List(), "TestNs", "TestClass", body); + new List(), System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); @@ -233,7 +233,7 @@ public void Resolve_WhenMultipleTypes_ShouldInjectAll() { string body = "StringBuilder sb = new StringBuilder();\nRegex r = new Regex(\"x\");\nreturn sb.ToString();"; string wrappedSource = WrapperTemplate.Build( - new List(), "TestNs", "TestClass", body); + new List(), System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); @@ -246,7 +246,7 @@ public void Resolve_WhenFullyQualifiedTypeIsUsed_ShouldReportAssemblyReference() { string body = "System.Text.StringBuilder builder = new System.Text.StringBuilder();\nreturn builder.ToString();"; string wrappedSource = WrapperTemplate.Build( - new List(), "TestNs", "TestClass", body); + new List(), System.Array.Empty(), "TestNs", "TestClass", body); PreUsingResult result = PreUsingResolver.Resolve(wrappedSource, AssemblyTypeIndex.Instance); diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs index 431ba911cc..7658a2f882 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs @@ -59,5 +59,32 @@ public void Analyze_WhenAttributedTypeHasAccessModifier_ShouldDetectTypeDeclarat Assert.IsTrue(result.HasTypeDeclaration); Assert.IsFalse(result.HasTopLevelStatements); } + + [Test] + public void Analyze_WhenVerbatimUsingAlias_ShouldRecordNormalizedAliasName() + { + SourceShapeResult result = SourceShaper.Analyze( + "using @Object = System.Object;\nreturn new @Object();"); + + Assert.That(result.AliasedNames, Does.Contain("Object")); + } + + [Test] + public void Analyze_WhenGlobalUsingAliasHasComments_ShouldRecordAliasName() + { + SourceShapeResult result = SourceShaper.Analyze( + "global /* comment */ using /* comment */ Random /* comment */ = UnityEngine.Random;\nreturn Random.Range(0, 1);"); + + Assert.That(result.AliasedNames, Does.Contain("Random")); + } + + [Test] + public void Analyze_WhenUsingAliasHasComments_ShouldRecordAliasName() + { + SourceShapeResult result = SourceShaper.Analyze( + "using /* comment */ Object /* comment */ = UnityEngine.Object;\nreturn null;"); + + Assert.That(result.AliasedNames, Does.Contain("Object")); + } } } diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeSourcePreparer.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeSourcePreparer.cs index 112fbcfe74..99dce39efa 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeSourcePreparer.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeSourcePreparer.cs @@ -60,6 +60,7 @@ private static PreparedDynamicCode Prepare( : DynamicCodeLiteralHoister.Rewrite(body); string preparedSource = WrapperTemplate.Build( shape.UsingDirectives, + shape.AliasedNames, namespaceName, className, hoistedResult.RewrittenSource, diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs index 50665886a8..809bcb1238 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs @@ -121,7 +121,7 @@ private static SourceTopLevelStep AnalyzeTopLevelSourceStep( int braceDepth, SourceShapeResult result) { - int usingPos = SkipWhitespace(source, pos + 6); + int usingPos = SkipWhitespaceAndComments(source, pos + 6); if (!StartsWithKeyword(source, usingPos, "using")) { return null; @@ -137,10 +137,117 @@ private static SourceTopLevelStep AddUsingDirectiveStep( SourceShapeResult result) { int semiEnd = FindSemicolon(source, segmentStart); - result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd()); + RegisterUsingDirective(result, source, segmentStart, semiEnd); return new SourceTopLevelStep(semiEnd + 1, braceDepth); } + private static void RegisterUsingDirective( + SourceShapeResult result, + string source, + int segmentStart, + int semiEnd) + { + result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd()); + + string aliasName = ExtractUsingAliasName(source, segmentStart, semiEnd); + if (!string.IsNullOrEmpty(aliasName)) + { + result.AliasedNames.Add(aliasName); + } + } + + // Recognizes "using Name = ...", "using @Name = ...", and their "global using" variants + // so WrapperTemplate can skip injecting a default alias the user's code already defines. + private static string ExtractUsingAliasName(string source, int segmentStart, int semiEnd) + { + int position = segmentStart; + if (StartsWithKeyword(source, position, "global")) + { + position = SkipWhitespaceAndComments(source, position + "global".Length); + } + + if (!StartsWithKeyword(source, position, "using")) + { + return null; + } + + position = SkipWhitespaceAndComments(source, position + "using".Length); + if (StartsWithKeyword(source, position, "static")) + { + return null; + } + + (string Name, int EndPosition) aliasName = ReadAliasName(source, position, semiEnd); + if (aliasName.Name == null) + { + return null; + } + + int equalsPosition = SkipWhitespaceAndComments(source, aliasName.EndPosition); + if (equalsPosition > semiEnd || source[equalsPosition] != '=') + { + return null; + } + + return aliasName.Name; + } + + private static (string Name, int EndPosition) ReadAliasName(string source, int position, int semiEnd) + { + int currentPosition = position; + if (currentPosition <= semiEnd && source[currentPosition] == '@') + { + currentPosition++; + } + + if (currentPosition > semiEnd || !IsIdentifierStart(source[currentPosition])) + { + return (null, position); + } + + int nameStart = currentPosition; + currentPosition++; + while (currentPosition <= semiEnd && IsIdentifierPart(source[currentPosition])) + { + currentPosition++; + } + + return (source.Substring(nameStart, currentPosition - nameStart), currentPosition); + } + + private static bool IsIdentifierStart(char value) + { + return char.IsLetter(value) || value == '_'; + } + + private static bool IsIdentifierPart(char value) + { + return char.IsLetterOrDigit(value) || value == '_'; + } + + private static int SkipWhitespaceAndComments(string source, int pos) + { + int current = SkipWhitespace(source, pos); + while (true) + { + if (TryMatchLineComment(source, current, out int afterLine)) + { + current = SkipWhitespace(source, afterLine); + continue; + } + + if (TryMatchBlockComment(source, current, out int afterBlock)) + { + current = SkipWhitespace(source, afterBlock); + continue; + } + + break; + } + + return current; + } + private static SourceTopLevelStep? TryAnalyzeTopLevelDeclaration( string source, int pos, @@ -254,7 +361,7 @@ public static string WrapIfNeeded(string source, string namespaceName, string cl : body + "\nreturn null;"; } - return WrapperTemplate.Build(shape.UsingDirectives, namespaceName, className, body); + return WrapperTemplate.Build(shape.UsingDirectives, shape.AliasedNames, namespaceName, className, body); } internal static int SkipWhitespace(string s, int pos) @@ -711,6 +818,7 @@ public SourceTopLevelStep(int position, int braceDepth) internal sealed class SourceShapeResult { public List UsingDirectives { get; } = new List(); + public HashSet AliasedNames { get; } = new HashSet(System.StringComparer.Ordinal); public bool HasNamespaceDeclaration { get; set; } public bool HasTypeDeclaration { get; set; } public bool HasTopLevelStatements { get; set; } diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/WrapperTemplate.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/WrapperTemplate.cs index 97bab39095..c1454bc9ed 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/WrapperTemplate.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/WrapperTemplate.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Text; namespace io.github.hatayama.UnityCliLoop.FirstPartyTools @@ -12,13 +14,23 @@ internal static class WrapperTemplate internal const string UserCodeStartMarker = "#line 1 \"user-snippet.cs\""; internal const string UserCodeEndMarker = "#line default"; + private static readonly DefaultUsingAlias[] DefaultUsingAliases = + { + new("Object", "UnityEngine.Object"), + new("Random", "UnityEngine.Random"), + }; + public static string Build( IReadOnlyList usingDirectives, + IReadOnlyCollection aliasedNames, string namespaceName, string className, string body, IReadOnlyList preambleLines = null) { + Debug.Assert(usingDirectives != null, "usingDirectives must not be null"); + Debug.Assert(aliasedNames != null, "aliasedNames must not be null"); + StringBuilder sb = new(); sb.AppendLine("#pragma warning disable CS0162"); @@ -30,10 +42,7 @@ public static string Build( sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using UnityEngine;"); sb.AppendLine("using UnityEditor;"); - if (!HasObjectAlias(usingDirectives)) - { - sb.AppendLine("using Object = UnityEngine.Object;"); - } + AppendDefaultUsingAliases(sb, aliasedNames); foreach (string directive in usingDirectives) { @@ -80,70 +89,33 @@ public static string Build( return sb.ToString(); } - private static bool HasObjectAlias(IReadOnlyList usingDirectives) + private static void AppendDefaultUsingAliases( + StringBuilder sb, + IReadOnlyCollection aliasedNames) { - if (usingDirectives == null) + for (int index = 0; index < DefaultUsingAliases.Length; index++) { - return false; - } - - for (int index = 0; index < usingDirectives.Count; index++) - { - string directive = usingDirectives[index]?.TrimStart(); - if (string.IsNullOrEmpty(directive)) + DefaultUsingAlias alias = DefaultUsingAliases[index]; + if (aliasedNames.Contains(alias.Name)) { continue; } - if (IsObjectAliasDirective(directive)) - { - return true; - } + sb.AppendLine($"using {alias.Name} = {alias.TargetTypeName};"); } - - return false; } - private static bool IsObjectAliasDirective(string directive) + private readonly struct DefaultUsingAlias { - int usingPosition = 0; - if (SourceShaper.StartsWithKeyword(directive, usingPosition, "global")) - { - usingPosition = SkipWhitespaceAndComments(directive, usingPosition + "global".Length); - } + public string Name { get; } - if (!SourceShaper.StartsWithKeyword(directive, usingPosition, "using")) - { - return false; - } - - int aliasStart = SkipWhitespaceAndComments(directive, usingPosition + "using".Length); - if (!SourceShaper.StartsWithKeyword(directive, aliasStart, "Object")) - { - return false; - } - - int equalsPosition = SkipWhitespaceAndComments(directive, aliasStart + "Object".Length); - return equalsPosition < directive.Length && directive[equalsPosition] == '='; - } + public string TargetTypeName { get; } - private static int SkipWhitespaceAndComments(string source, int position) - { - int currentPosition = SourceShaper.SkipWhitespace(source, position); - while (IsCommentStart(source, currentPosition)) + public DefaultUsingAlias(string name, string targetTypeName) { - int nextPosition = SourceShaper.AdvanceOneTokenPublic(source, currentPosition); - currentPosition = SourceShaper.SkipWhitespace(source, nextPosition); + Name = name; + TargetTypeName = targetTypeName; } - - return currentPosition; - } - - private static bool IsCommentStart(string source, int position) - { - return position + 1 < source.Length && - source[position] == '/' && - (source[position + 1] == '/' || source[position + 1] == '*'); } } } From acd95ad359b607b924d4bdd882993b30ebfd7040 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 9 Jul 2026 16:38:15 +0900 Subject: [PATCH 2/2] Address Fable 5 review: fix plain-using comment skip and add test comments The plain "using" path in TryAnalyzeTopLevelUsing still used SkipWhitespace instead of SkipWhitespaceAndComments, so a comment between "using" and "var"/"(" caused a using-statement to be misclassified as a using-directive and dropped from the user code. Fixed to match the already-corrected global-using path, and added a regression test for the comment-before-var case. Also added the required WHAT comment to each new test method per this repo's test convention. --- .../DynamicCodeSourcePreparerTests.cs | 6 ++++++ .../DynamicCodeToolTests/SourceShaperTests.cs | 15 +++++++++++++++ .../DynamicCompilation/SourceShaper.cs | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs index ba066c3466..d8c131b6f5 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSourcePreparerTests.cs @@ -47,6 +47,7 @@ public void Prepare_WhenStringLiteralExists_ShouldEmitPreambleBeforeUserCodeMark [Test] public void Prepare_WhenScriptUsesBareUnityObject_ShouldAddObjectAlias() { + // Verifies a bare "Object" call resolves to UnityEngine.Object via an injected alias. PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( "GameObject go = new GameObject(\"source\");\nObject.Instantiate(go);\nreturn null;", DynamicCodeConstants.DEFAULT_NAMESPACE, @@ -59,6 +60,7 @@ public void Prepare_WhenScriptUsesBareUnityObject_ShouldAddObjectAlias() [Test] public void Prepare_WhenObjectAliasAlreadyExists_ShouldNotAddDuplicateAlias() { + // Verifies the default Object alias is not injected a second time when the user already declared it. PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( "using Object = UnityEngine.Object;\nObject.Instantiate(new GameObject(\"source\"));\nreturn null;", DynamicCodeConstants.DEFAULT_NAMESPACE, @@ -73,6 +75,7 @@ public void Prepare_WhenObjectAliasAlreadyExists_ShouldNotAddDuplicateAlias() [Test] public void Prepare_WhenCustomObjectAliasAlreadyExists_ShouldRespectUserAlias() { + // Verifies a user-defined "Object" alias to a different type is preserved, not overridden by the default. PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( "using Object = System.Object;\nreturn new Object();", DynamicCodeConstants.DEFAULT_NAMESPACE, @@ -87,6 +90,7 @@ public void Prepare_WhenCustomObjectAliasAlreadyExists_ShouldRespectUserAlias() [Test] public void Prepare_WhenVerbatimObjectAliasAlreadyExists_ShouldRespectUserAlias() { + // Verifies a verbatim "@Object" alias is detected so the default Object alias is skipped. PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( "using @Object = System.Object;\nreturn new @Object();", DynamicCodeConstants.DEFAULT_NAMESPACE, @@ -102,6 +106,7 @@ public void Prepare_WhenVerbatimObjectAliasAlreadyExists_ShouldRespectUserAlias( [Test] public void Prepare_WhenScriptUsesBareUnityRandom_ShouldAddRandomAlias() { + // Verifies a bare "Random" call resolves to UnityEngine.Random via an injected alias. PreparedDynamicCode prepared = DynamicCodeSourcePreparer.Prepare( "int value = Random.Range(0, 10);\nreturn value;", DynamicCodeConstants.DEFAULT_NAMESPACE, @@ -114,6 +119,7 @@ public void Prepare_WhenScriptUsesBareUnityRandom_ShouldAddRandomAlias() [Test] public void CountSubstring_WhenTargetIsEmpty_ShouldReturnZero() { + // Verifies the shared string-counting test helper treats an empty target as zero matches. int count = DynamicCodeTestStringUtility.CountSubstring("source", ""); Assert.AreEqual(0, count); diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs index 7658a2f882..5fe081a910 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/SourceShaperTests.cs @@ -63,6 +63,7 @@ public void Analyze_WhenAttributedTypeHasAccessModifier_ShouldDetectTypeDeclarat [Test] public void Analyze_WhenVerbatimUsingAlias_ShouldRecordNormalizedAliasName() { + // Verifies "using @Object = ..." records the alias name without the leading '@'. SourceShapeResult result = SourceShaper.Analyze( "using @Object = System.Object;\nreturn new @Object();"); @@ -72,6 +73,7 @@ public void Analyze_WhenVerbatimUsingAlias_ShouldRecordNormalizedAliasName() [Test] public void Analyze_WhenGlobalUsingAliasHasComments_ShouldRecordAliasName() { + // Verifies comments between "global", "using", and the alias name do not block alias detection. SourceShapeResult result = SourceShaper.Analyze( "global /* comment */ using /* comment */ Random /* comment */ = UnityEngine.Random;\nreturn Random.Range(0, 1);"); @@ -81,10 +83,23 @@ public void Analyze_WhenGlobalUsingAliasHasComments_ShouldRecordAliasName() [Test] public void Analyze_WhenUsingAliasHasComments_ShouldRecordAliasName() { + // Verifies comments between "using" and the alias name do not block alias detection. SourceShapeResult result = SourceShaper.Analyze( "using /* comment */ Object /* comment */ = UnityEngine.Object;\nreturn null;"); Assert.That(result.AliasedNames, Does.Contain("Object")); } + + [Test] + public void Analyze_WhenUsingVarHasCommentBeforeVar_ShouldTreatAsTopLevelStatement() + { + // Verifies a comment between "using" and "var" still resolves to a using-statement, + // not a using-directive (regression guard for the SkipWhitespaceAndComments fix). + SourceShapeResult result = SourceShaper.Analyze( + "using /* comment */ var scope = new System.IO.MemoryStream();\nreturn null;"); + + Assert.IsTrue(result.HasTopLevelStatements); + Assert.AreEqual(0, result.UsingDirectives.Count); + } } } diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs index 809bcb1238..60de0388ea 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/SourceShaper.cs @@ -100,7 +100,7 @@ private static SourceTopLevelStep AnalyzeTopLevelSourceStep( } int segmentStart = pos; - int afterUsing = SkipWhitespace(source, pos + 5); + int afterUsing = SkipWhitespaceAndComments(source, pos + 5); if (StartsWithKeyword(source, afterUsing, "static")) { return AddUsingDirectiveStep(source, segmentStart, braceDepth, result);