Skip to content

Escape the names the package's inspectors print - #192

Merged
wallstop merged 8 commits into
masterfrom
session-089-inspector-escaped-names
Sep 29, 2026
Merged

wallstop merged 8 commits into
masterfrom
session-089-inspector-escaped-names

Conversation

@wallstop

@wallstop wallstop commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

DISCLOSURE: LLM-GENERATED TEXT

Why:

A theme, font, or command name carrying a bidirectional override reached the package's own inspectors unescaped, so a name read one way in a tooltip or dropdown and another way in the value it selected (#190).

What:

  • Escape theme, font, command, and asset-path names in all three custom inspectors.
  • Show a popup an escaped copy beside the raw name its index selects; SetTheme, SetFont, and _disabledCommands are unchanged.
  • Refill the command dropdown's display copy in place, so escaping adds no per-frame allocation.
  • Pin the tooltips with a source gate; no CI lane compiles the Editor assembly.
  • State in the theme guide what a font can draw, naming the two families that carry CJK or Hangul and the packs they ship in.

How we know:

The gate names all five unescaped tooltips on 2dc1e31 and passes on this head; six mutation probes and six scanner tests prove it can still fail.
The EditMode sanitizer suite runs here under NUnitLite: 11 of 15 methods pass, and the four that need a live UnityEngine.Object are untouched by this change.


Note

Low Risk
Display-only escaping in editors with raw values preserved for actions; collateral count-hoisting refactors are mechanical and documented, with low user-visible risk outside inspector labels.

Overview
Inspector display text now matches the console’s escaping rules: theme, font, command, and asset-path names in TerminalUIEditor, TerminalFontPackEditor, and TerminalThemePackEditor go through LogTextSanitizer for tooltips and EditorGUILayout.Popup labels, while indices and stored values still use the raw names (SetTheme, SetFont, disabled commands unchanged).

LogTextSanitizer.SanitizeInto fills a separate label array in place so per-frame inspector redraws do not allocate a new string array every time. A Node contract suite (tooling~/scripts/tests/display-text.test.mjs) enforces sanitized holes in GUIContent interpolations and that popup-heavy editor files reference the sanitizer, since Editor assemblies are not compiled in CI.

User-facing notes: CHANGELOG.md records the inspector fix; Documentation~/theming.md documents which shipped fonts cover CJK/Hangul and that emoji usually show as missing-glyph boxes.

Collateral (same PR): LLM guidance in .llm/context.md and hot-path-allocations tightens loop-bound rules (hoist only when count does not change; one count read per method). A repo-wide pass applies that pattern in runtime, editor, and generator code (including hoisting CollapseCaseInsensitiveDuplicates’ outer loop). DirectoryHelper drops a duplicated path branch.

Reviewed by Cursor Bugbot for commit 774cbb1. Bugbot is set up for automated code reviews on this repo. Configure here.

Why: a theme, font, or command name carrying a bidi override reached the
custom inspectors unescaped, so a name read one way in a tooltip and
another way in the value the index selected.

What changed:
- Escape theme, font, command, and asset-path names in all three editors.
- Popups show the escaped copy; the index still selects the raw name.
- Pin the tooltips with a source gate; no CI lane compiles the editors.

How we know: the gate names all five unescaped tooltips on the old
sources and passes on the new.
Why: most shipped fonts are Latin-only, and the guide did not say a
character the selected font lacks shows a missing-glyph box.

What changed:
- One paragraph in the theme guide, with the two exceptions measured.
…free

Why: the docs claimed no shipped font carries emoji and named two fonts
by a name the product never shows; the popup backstop was satisfied by a
mention in a comment; the same rule was stated six times.

What changed:
- Name the asset names and the packs, and bound the emoji claim.
- Read the popup backstop from code, so a TODO does not satisfy it.
- State the rule once, and say what the gate does not cover.
- Cover the null and empty starting copies the callers use.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit dc524d2. Configure here.

Comment thread Tests/Editor/LogTextSanitizerTests.cs Outdated
Comment thread tooling~/scripts/tests/display-text.test.mjs Outdated
Comment thread tooling~/scripts/tests/display-text.test.mjs
…ound

Why: two of the sanitizer test's assertions could not have run - one
would not compile without a using, one compared the wrong array - and the
gate's comment removal walked its spans out of order, so a TODO still
satisfied the popup backstop.

What changed:
- Add the using, and assert the literals the null copy must hold.
- Order the removed spans, and interleave the control that tests it.
- State the rule once, and list what the gate does not see.
- Name the shipped fonts as list-fonts prints them, with their packs.
Comment thread Runtime/Helper/LogTextSanitizer.cs Outdated
*/
public static void SanitizeInto(string[] names, ref string[] copy)
{
if (copy == null || copy.Length != names.Length)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we SSOT length, here and anywhere else?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DISCLOSURE: LLM-GENERATED TEXT

Done, and swept rather than just patched.

SanitizeInto reads names.Length once now; it sizes the allocation and the loop. The loop writes copy, never names, which is the condition that makes a single read correct here.

I swept every method in Runtime/, Editor/, Tests/, Samples~, and Generator~ for the same shape - one collection's Length/Count read three or more times. Twenty-two methods matched. Nine were genuinely single-source and are now hoisted: BuildUsageHint, Next, Previous, TryEatArgument, KebabCase, RefreshLogs, TryTokenComplete, Ensure, and CatalogEmitter.Emit.

Six I deliberately left, because a single read there would be a bug, not a cleanup:

  • CyclicBuffer.Resize and the Fix Invalid Fonts loop read the count of the collection they are removing from. The inline re-read is what terminates them.
  • KebabCase's trailing while shrinks builder.Length in its own body.
  • TerminalThemeStyleSheetHelper.GetAvailableThemes reassigns ussContent between its reads, so the three reads are three different values.
  • TextFieldPaste.TryApply's second read is the post-trim value by design.
  • CollapseCaseInsensitiveDuplicates already documents the outer-loop case.

The test sites are also left alone, and one of them is the reason I would not sweep tests mechanically: CountReflectsNumberOfEntries reads history.Count after every Push, and that repetition is the assertion.

One extra find: AbsoluteToUnityRelativePath had a second StartsWith block with the identical condition directly after the first, which always returns. It was unreachable, and it was also wrong - it prefixed Assets/ onto a slice that already begins with Assets/. Deleted; rule 5.

The rule and all three traps are now written down in the hot-path-allocations skill, with the sweep script's logic as the worked example of which sites to leave alone.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DISCLOSURE: LLM-GENERATED TEXT

Follow-up, because the first reply was not good enough.

I answered "here and anywhere else" with one sweep, quoted the tally, and stopped. A second, independent pass of the same rule found eleven more hoistable sites, one of them 65 lines below a site I had already hoisted in the same file, and two of my classifications were simply wrong:

  • CollapseCaseInsensitiveDuplicates I excused because an existing comment said its outer Count read must stay inline - the loop "rewrites entries". An element overwrite cannot change a count. The comment was wrong, I had cited it as a correct example in the skill, and I believed it rather than checking. Hoisted, and the comment now states the real rule.
  • GetAvailableThemes I classified as "reassigned between the reads". All three reads are after the last assignment. Hoistable all along.

So the rule I wrote into the skill was too broad in one direction (it forbade a legal hoist for any element write) and my evidence for completeness was a number I could not defend. Both are fixed: rule 18 and the skill now say the body must change that collection's count, and the skill no longer quotes a tally - a number in a skill file is a claim about the codebase that nobody can audit from the file, and mine did not survive one independent pass.

The guidance now carries the lesson rather than just the rule: treat an inherited justification as a claim to verify, not a fact.

Verification for the second pass: the completion collapse is byte-identical old vs new across twelve corpora built to hit the dedupe (case-variant runs, adjacent duplicates, single entries, non-letters), and the same differential across the other six files is identical. Generator~ was untouched, so the payload still matches.

Why: the sanitizer read names.Length three times, and a $@ tooltip hole was
never reported because the check read two characters back from the @.

What changed:
- Read each count once, in the eight production methods where one value
  answers every read; leave the six where the loop mutates the collection
  or the variable is reassigned, and say so at each.
- Drop the unreachable second StartsWith block in AbsoluteToUnityRelativePath.
- Fix the $@ detection and add a control that fails without the fix.
@wallstop

Copy link
Copy Markdown
Owner Author

DISCLOSURE: LLM-GENERATED TEXT

All three reviewed; two were fixed in 212225b before this landed and the third is now fixed in cbc58b9.

LogTextSanitizerTests.cs:227 - stale arrays. Fixed in 212225b. The assertion compares the literals a null copy must hold, not a variable the previous block refilled. Confirmed by mutation on the host: the old form fails, the new one passes.

display-text.test.mjs:158 - verbatim interpolation never matches. Correct, and the one that mattered: it is a false negative in the rule that carries the weight. start indexes the @, so the deciding character is text[start - 1]; reading two back never matches, so every $@"...{name}" hole went unreported. Fixed. The control that had been passing for the wrong reason - it passed because the string was never read as interpolated at all - is replaced by three cases: $@"Will set {name}" must report, $@"literal {{name}}" must not, and reverting the detection fails the first.

display-text.test.mjs:314 - spans walked out of order. Correct, fixed in 212225b by ordering the merged span list. The control was wrong in the same way: its comment preceded every literal, so it passed with or without the ordering. It now interleaves a comment between two literals, and reverting the sort fails it.

Why: the count rule lived only as "never hoist a mutated collection's
length", so the safe direction had no guidance and the drift half of it
was invisible.

What changed:
- State one read per method as a drift rule, not a speed one, in
  hot-path-allocations and in context.md rule 18.
- Name the four reasons a second read is a different number, each with
  the in-repo site that proves it.
- Record that the test sites are excluded, and which one breaks if hoisted.
Why: CatalogEmitter gained the hoisted count, so the committed DLL no
longer matched its sources and the two-checkout gate failed.

How we know: verify-analyzer-payload.ps1 passes for both payloads.
Why: the first sweep hoisted 9 of 22 sites and called the rest traps. A
second pass found 11 more hoistable, one 65 lines below a site already
hoisted, and one excused by an inherited comment that was itself false.

What changed:
- Hoist the 11: RunTyped, RouteInvocation, AddArgument, the completion
  dedupe, the bake loops, RefreshRows, the palette caret, the property
  path walk, GetAvailableThemes, and the collapse's outer bound.
- An element overwrite is not a change of count, so the collapse's
  "must stay inline" comment was wrong; the loop is now hoisted.
- Rule 18 and the skill say "changes that collection's count", and the
  skill no longer quotes a tally it cannot defend.
@wallstop
wallstop merged commit 1b1e2b9 into master Sep 29, 2026
12 of 13 checks passed
@wallstop
wallstop deleted the session-089-inspector-escaped-names branch September 29, 2026 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant