From f96381481bdce2327757a85952dba94889599ce4 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 09:38:30 +0000 Subject: [PATCH] A developer site reached through concatenation is still a developer site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `throw new Error(`head ${n} tail` + detail)` puts a BinaryExpression between the literal and the NewExpression, so the parent-only check saw the `+` and not the throw, and reported a thrown message as a reader-visible leak. Found while measuring the .js scan on legend-tree.js:392, and my first report of it was wrong in a way worth recording: I told dx-shepherd the developer-site exemption "is not firing on the .js path". It fires fine. The gap is concatenation, and a .vue file concatenating inside a throw hits it identically — the .js scan only surfaced a shape that is rarer in templates. Three .vue findings cleared when this landed, which is the proof it was never .js-specific: 92 -> 89. Only `+` chains are walked. A string built with + is still the same argument to the same call; any other expression type between the two is a different construct and should be judged on its own site. Two-sided: a thrown message built by concatenation goes quiet, and a concatenated sentence that is NOT at a developer site still counts. The .js scan itself is NOT in this commit. It needs the data-map exemption first, or it hands two owners ~60 findings that are lookup keys they have already classified. --- internal/check-hardcoded-english.mjs | 36 +++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/check-hardcoded-english.mjs b/internal/check-hardcoded-english.mjs index a111e7ec3..35b5f5528 100644 --- a/internal/check-hardcoded-english.mjs +++ b/internal/check-hardcoded-english.mjs @@ -189,6 +189,26 @@ function isDomQuerySite(parent) { } // A literal that is an argument to one of these never reaches a reader, whatever it says. +// ★ CONCATENATION HIDES A DEVELOPER SITE FROM ITS OWN GUARD. `throw new Error(\`head\` + rest)` +// puts a BinaryExpression between the literal and the NewExpression, so the parent check below sees +// the `+` and not the `new Error`. Measured on legend-tree.js:392, a thrown message reported as a +// leak. It is NOT a .js-path bug — a .vue file concatenating inside a throw hits it identically; the +// .js scan just surfaced a shape that is rarer in templates. +// +// Only `+` chains are walked: a string built with + is still the same argument to the same call, +// whereas any other expression type between the two is a different construct and should be judged +// on its own site. +function developerSiteAncestor(parent, stack) { + if (isDeveloperSite(parent)) return true + if (!parent || parent.type !== 'BinaryExpression' || parent.operator !== '+') return false + for (let i = stack.length - 1; i >= 0; i--) { + const n = stack[i] + if (n.type === 'BinaryExpression' && n.operator === '+') continue + return isDeveloperSite(n) + } + return false +} + function isDeveloperSite(parent) { if (!parent) return false if (parent.type === 'CallExpression' && parent.callee?.object?.name === 'console') return true @@ -513,7 +533,7 @@ function scanScript(code, file, out, lineBase = 0) { // ★ DEVELOPER STRINGS ARE EXEMPT BY CALL SITE, NOT BY CONTENT (upg100). `throw new Error('…')` // and console.* never reach a reader, and judging them by their text means inventing a rule for // every sentence a developer might write. The SITE is the fact; the wording is not. - if (isDeveloperSite(parent) || isDomQuerySite(parent)) return + if (developerSiteAncestor(parent, stack) || isDomQuerySite(parent)) return if (parent && /Import|Export/.test(parent.type)) return if (parent && parent.type === 'TaggedTemplateExpression') return if (guardedFallback(stack, names)) return @@ -944,6 +964,20 @@ const a = greeting + 'Welcome to the prayer map' 'an HTML attribute seam on dir/lang is allowed') ok(!allowedBy('Direction: left to right'), 'prose ABOUT direction is NOT — the shape needs an attribute name followed by =') + // ★ A DEVELOPER SITE REACHED THROUGH CONCATENATION. `throw new Error(\`head\` + rest)` puts a + // BinaryExpression between the literal and the NewExpression, so the parent-only check missed it + // and reported a thrown message as a leak (legend-tree.js:392). Not a .js-path bug — a .vue file + // concatenating inside a throw hits it identically. + const DEV = ` +` + const dev = scanSfc(DEV, 'dev.vue').map((f) => f.text) + ok(!dev.some((t) => /leaf nodes violate/.test(t)), + 'QUIET on a thrown developer message built by CONCATENATION') + ok(dev.some((t) => /A sentence a reader actually sees/.test(t)), + 'COUNTS a concatenated sentence that is NOT at a developer site') // the catalogue-side rule, tested directly: the two facts that make a fallback safe ok(fallbackFullyResolved('dashboard.map_loading'), 'a key present and really translated in all 11 locales counts as resolved')