Skip to content

Commit 852dff8

Browse files
committed
fix(weidu-d): keep formatter idempotent when a ~string~ abuts a keyword
A transition trigger written with no space before THEN (~..NOTVALID)~THEN, valid WeiDU) formatted to ~..~THEN on the first pass but ~..~ THEN once the broken line was reparsed, so the formatter was not idempotent and the external format sweep failed on BG1NPC's x#faint.d. normalizeTransitionText now inserts a separating space at a ~/"-delimited string <-> word-character boundary (either direction), so the trigger and the following keyword are always spaced. %-delimited strings are deliberately excluded: %var%-interpolated names (%tutu_var%KELDDA, TAZOK%eet_var%) abut literal text on purpose and must not be split. Unparks x#faint.d (previously excluded for this bug). Full IE format sweep is green with it back in; added a CLI idempotence regression test.
1 parent ce23663 commit 852dff8

3 files changed

Lines changed: 36 additions & 12 deletions

File tree

external/infinity-engine-exclude.txt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,3 @@ BG1NPC/bg1npc/phase2/tpa/bg1npc_xzarqst.tpa
210210
# BG1NPC malformed D file (weidu rejects it; unresolvable %macro% used as standalone CHAIN content)
211211
BG1NPC/bg1npc/phase2/dlg/x#kivantazok_ee.d
212212

213-
# BG1NPC valid dialog parked for a pre-existing formatter idempotency bug (not malformed):
214-
# ~string~ abutting THEN with no source space normalizes inconsistently. Tracked separately; unpark after fixing normalizeTransitionText.
215-
BG1NPC/bg1npc/phase2/dlg/x#faint.d

format/src/weidu-d/core.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,22 +87,31 @@ function isNextFeature(node: SyntaxNode): boolean {
8787
);
8888
}
8989

90-
// Normalize transition text: collapse whitespace in code, preserve strings and block comments exactly
90+
// True when a `~`/`"` string delimiter directly abuts a word character (in either order). That is the
91+
// spot that renders as `~trigger~THEN` from a no-space source and reformats to `~trigger~ THEN`,
92+
// breaking idempotency. `%` is deliberately excluded: %var%-interpolated names (%tutu_var%KELDDA,
93+
// TAZOK%eet_var%) abut literal text on purpose and must never be split by a space.
94+
function needsBoundarySpace(prev: string, next: string): boolean {
95+
const isTildeOrQuote = (c: string): boolean => c === "~" || c === '"';
96+
const isWord = (c: string): boolean => /\w/.test(c);
97+
return (isTildeOrQuote(prev) && isWord(next)) || (isWord(prev) && isTildeOrQuote(next));
98+
}
99+
100+
// Normalize transition text: collapse whitespace in code, preserve strings and block comments exactly,
101+
// and insert a separating space where a ~/"-string abuts a word with none (so `~..~THEN` is stable).
91102
function normalizeTransitionText(text: string): string {
92103
const tokens = tokenizeWeidu(text);
93-
const parts: string[] = [];
104+
let result = "";
94105

95106
for (const token of tokens) {
96-
if (token.type === WeiduTokenType.Code) {
97-
// Collapse whitespace in code parts
98-
parts.push(token.text.replaceAll(/\s+/g, " "));
99-
} else {
100-
// Preserve strings and comments exactly as-is
101-
parts.push(token.text);
107+
const part = token.type === WeiduTokenType.Code ? token.text.replaceAll(/\s+/g, " ") : token.text;
108+
if (part.length > 0 && result.length > 0 && needsBoundarySpace(result[result.length - 1]!, part[0]!)) {
109+
result += " ";
102110
}
111+
result += part;
103112
}
104113

105-
return parts.join("").trim();
114+
return result.trim();
106115
}
107116

108117
// Get line length excluding comment

format/test/format-cli.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,22 @@ describe("format CLI integration", () => {
353353
expect(code).toBe(0);
354354
});
355355
});
356+
357+
describe("weidu-d transition idempotence", () => {
358+
// Regression: a ~trigger~ string abutting THEN with no source space (valid WeiDU) rendered as
359+
// ~..~THEN on the first pass but ~..~ THEN once the broken transition line was reparsed, so the
360+
// formatter was not idempotent. A separating space is now inserted at a ~/"-string<->word
361+
// boundary (never a %var% one, so interpolated names stay abutted).
362+
it("adds a stable space between a ~trigger~ and an abutting THEN across a line break", () => {
363+
const input =
364+
"EXTEND_BOTTOM ~%tutu_var%CORSON~ 6\n" +
365+
'IF ~InParty("faldorn") InMyArea("faldorn") !StateCheck("faldorn",CD_STATE_NOTVALID)~THEN EXTERN ~%FALDORN_JOINED%~ FaldornCorsone\n' +
366+
"END\n";
367+
const file = path.join(tmpDir, "trans.d");
368+
fs.writeFileSync(file, input);
369+
const { code, stderr } = run(file, "--save-and-check");
370+
expect(stderr).not.toContain("Formatter not idempotent");
371+
expect(code).toBe(0);
372+
});
373+
});
356374
});

0 commit comments

Comments
 (0)