Skip to content

v1.108.273 — A pattern that names two extensions and matches neither

Choose a tag to compare

@jgravelle jgravelle released this 12 Aug 15:34
· 42 commits to main since this release

Two things, both about the same mistake: a pattern that names two extensions and matches neither.

The regression (#445)

v1.108.271 fixed #435 for the nuxt and nestjs profiles by rewriting their entry_point_patterns as brace alternation ({ts,js}). The consumer is fnmatch, which expands no braces — fnmatch.translate("src/main.{ts,js}") yields (?s:src/main\.\{ts,js\})\Z, requiring a filename that literally contains {ts,js}.

The change did not add JavaScript coverage. It removed the TypeScript coverage that was working.

profile  file                      v1.108.270  v1.108.271
nestjs   src/main.ts               True        False
nestjs   src/app.module.ts         True        False
nuxt     plugins/a/b.ts            True        False
nuxt     middleware/x/g.ts         True        False

nestjs is the worst case: its entire entry set was those two patterns, so a stock NestJS project went from two entry points to zero — and NestJS is TypeScript-first by convention, so this was the common path. The failure direction is the one that matters here: a lost reachability seed does not error, it silently reports a genuine framework root as unreachable.

Why the guard passed, which is the more useful half

The #435 sweep checks which extensions appear in a pattern string, and skipped brace patterns outright (if "{" in glob: continue) as already-covered. A pattern naming both ts and js satisfied a spelling check while matching no file on disk.

A test that inspects the shape of a fix cannot tell it from a plausible-looking non-fix. The new tests run patterns against realistic scaffold paths through _matches_any_pattern — the real matcher both dead-code tools share — so they measure effect. One of them asserts a non-entry file is still not an entry point, because every other test here would also pass if the patterns had been replaced with *.

A second fnmatch surprise, pre-existing

**/ translates to (?>.*?/) and therefore requires a slash: plugins/**/*.ts matches plugins/a/b.ts but not plugins/auth.ts. Every profile using **/ was missing files sitting directly in the named directory — the commonest layout there is. Fixing braces alone would have left the patterns still missing the majority case.

Both constraints now live in _entry_globs / _entry_named, which emit the flat and nested form per extension, so a profile lists where its entry points live and never how to spell a glob.

Explicit alternatives were chosen over teaching the matcher braces: that leaves the semantics of a user-facing parameter unchanged under the 1.x no-removal contract. Whether entry_point_patterns should also accept braces is filed separately as #446, along with the finding that a no-match today is silent in find_dead_code and only conditionally reported in get_dead_code_v2.

#435 closed

The next profile gained its JS/JSX counterparts — Next.js's own extension sets, js/jsx/ts/tsx for pages and layouts and js/ts for route handlers and middleware, across both the root and src/ layouts.

It had been deferred behind PR #433 so as not to force a conflict onto a contributor's rebase. That merged 2026-08-11. It landed here rather than on its own because doing it first would have propagated the brace defect to a third profile.

The ratchet added in v1.108.271 is what forced the fix and the retirement of its exemption to land together, and it worked exactly as designed.

Verification

tests/test_v1_108_273.py (11), 6 fail against the shipped v1.108.272 profiles; the 5 passing on both sides pin the fnmatch premises and the new helpers.

Full suite 7720 passed / 8 skipped / 0 failed, and 7714 / 14 / 0 under a clean 3.13 environment (7728 collected both ways); ruff check src/ clean; all 9 CI jobs green.