chore(#4590): add no-rendered-text-length-assert ESLint rule (#4595)

* test(#4590): add no-rendered-text-length-assert ESLint rule

Enforces ADR-456's typed-surface mandate for one specific bug shape: a test
assertion whose pass/fail depends on the length/substring content of a
template literal that interpolates an OS-derived path (os.tmpdir(),
os.homedir(), path.join/resolve/..., or a PATH_RETURNING_FNS resolver).
Because macOS's default tmpdir prefix is longer than Linux's, such an
assertion can pass on one runner and fail on another -- the defect class
behind #4421's incident (git show 4e75b836e9), already fixed there by
pinning to a typed field per ADR-456 Sec(c) before this rule existed to
catch a recurrence.

Two repo-wide sweeps against the real tests/ tree narrowed the rule to a
sound scope: an initial design that traced call arguments (to approximate
the historical incident's cross-file render-function shape) produced false
positives on ordinary fs.readFileSync(path.join(...)) + assert.match
patterns; a second design that matched any bare direct path-returning call
produced 45 false positives on path suffix/prefix/non-emptiness checks. The
shipped rule matches only a path-returning expression interpolated into a
template literal, directly or via one identifier hop -- disclosed in the
rule's own "Known boundaries" as not covering the literal cross-file
incident shape, which would require tracing into a callee's body.

Phase 1 of epic #4589 (CI test-matrix Linux-primary migration) -- Phase 2's
safety argument depends on this class of OS-dependent test assertion being
enforced going forward, not merely fixed once.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(#4590): address code-review findings on no-rendered-text-length-assert

Reletter the "Known boundaries" doc-comment list (a)-(e), fixing a gap left
by an earlier edit pass and every stale cross-reference to it. Collapse
isDirectPathTaint/isTaintedInterpolation's duplicated TemplateLiteral-walk
into one recursive relationship (isTaintedInterpolation now delegates a
nested-template-literal case back to isDirectPathTaint instead of
re-implementing the .some() traversal) -- behavior unchanged, confirmed by
re-running the repo-wide sweep (still zero false positives).

Found by the Standards-axis /code-review pass on this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-09 21:55:51 -04:00
committed by GitHub
parent b2d50ffd83
commit 9770258558
5 changed files with 599 additions and 0 deletions

View File

@@ -39,6 +39,7 @@ import requireRegisteredExit from './eslint-rules/require-registered-exit.cjs';
import noSwallowedPrecondition from './eslint-rules/no-swallowed-precondition.cjs';
import noExactCaseEnvAccess from './eslint-rules/no-exact-case-env-access.cjs';
import noAdhocTimeoutLiteral from './eslint-rules/no-adhoc-timeout-literal.cjs';
import noRenderedTextLengthAssert from './eslint-rules/no-rendered-text-length-assert.cjs';
const adhocTimeoutLiteralAllowlist = require('./eslint-rules/no-adhoc-timeout-literal.allowlist.json');
@@ -72,6 +73,7 @@ const localPlugin = {
'no-swallowed-precondition': noSwallowedPrecondition,
'no-exact-case-env-access': noExactCaseEnvAccess,
'no-adhoc-timeout-literal': noAdhocTimeoutLiteral,
'no-rendered-text-length-assert': noRenderedTextLengthAssert,
},
};
@@ -717,6 +719,13 @@ export default tseslint.config(
// Require a fixed-point termination guard on any dirname() ancestor walk —
// a length/equality-only bound spins forever at a Windows drive root (#4020 / #4220).
'local/no-unbounded-dirname-walk': 'error',
// #4590 (epic #4589 Phase 1): ban length/substring assertions on a
// template literal that interpolates an OS-derived path (tmpdir/homedir
// length differs by OS — the #4421 incident shape). Does NOT flag a bare
// path-returning call probed directly (e.g. `.endsWith('.md')`,
// `.length > 0`) — only path-in-rendered-text embedding. See ADR-456
// §(c) typed-surface mandate.
'local/no-rendered-text-length-assert': 'error',
// Ban unbounded sync child_process spawns in tests (DEFECT.UNBOUNDED-SUBPROCESS).
// No allowlist: the epic (#3064) migrated every site; the rule runs with no
// exemption surface. The only sanctioned escapes are an explicit `timeout` on