Skip to content

fix(path): classify DOS device paths as Windows-absolute#551

Merged
alexander-akait merged 9 commits intomainfrom
claude/handle-dos-device-paths-wyAFi
Apr 23, 2026
Merged

fix(path): classify DOS device paths as Windows-absolute#551
alexander-akait merged 9 commits intomainfrom
claude/handle-dos-device-paths-wyAFi

Conversation

@alexander-akait
Copy link
Copy Markdown
Member

getType() previously missed paths starting with \\?\ (Win32 file
namespace) or \\.\ (Win32 device namespace), so they fell through to
Normal and downstream normalize/dirname/join ran them through
path.posix.. segments weren't collapsed and dirname returned
"." instead of the parent directory.

Recognize the two DOS device prefixes in the hot char-code switch and
return AbsoluteWin so normalization, dirname, and join use
path.win32, which handles these paths correctly.

Plain UNC (\\server\share) and forward-slash variants are left alone
— Windows treats DOS device paths as literal and forward slashes are
not equivalent inside them.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: d15336b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
enhanced-resolve Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented Apr 22, 2026

CLA Not Signed

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.64%. Comparing base (a04bc4c) to head (d15336b).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #551      +/-   ##
==========================================
+ Coverage   96.62%   96.64%   +0.02%     
==========================================
  Files          50       50              
  Lines        2812     2830      +18     
  Branches      871      881      +10     
==========================================
+ Hits         2717     2735      +18     
  Misses         79       79              
  Partials       16       16              
Flag Coverage Δ
integration 96.64% <100.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 22, 2026

Merging this PR will not alter performance

✅ 69 untouched benchmarks


Comparing claude/handle-dos-device-paths-wyAFi (d15336b) with main (a04bc4c)

Open in CodSpeed

@alexander-akait alexander-akait force-pushed the claude/handle-dos-device-paths-wyAFi branch from 1145f07 to b52eb01 Compare April 22, 2026 22:38
claude added 7 commits April 22, 2026 22:52
`getType()` previously missed paths starting with `\\?\` (Win32 file
namespace) or `\\.\` (Win32 device namespace), so they fell through to
`Normal` and downstream `normalize`/`dirname`/`join` ran them through
`path.posix` — `..` segments weren't collapsed and `dirname` returned
`"."` instead of the parent directory.

Recognize the two DOS device prefixes in the hot char-code switch and
return `AbsoluteWin` so normalization, dirname, and join use
`path.win32`, which handles these paths correctly.

Plain UNC (`\\server\share`) and forward-slash variants are left alone
— Windows treats DOS device paths as literal and forward slashes are
not equivalent inside them.
The `getType()` fix alone wasn't enough to make a real resolve against a
DOS device path work — two adjacent bugs surfaced once the resolver
actually received one:

1. `parseIdentifier` split `\\?\C:\foo` on the literal `?` and returned
   request=`\\` / query=`?\C:\foo`, so the request reached `isModule`
   as an empty-ish string and was looked up under `node_modules`.
   Skip past a `\\?\` / `\\.\` prefix before scanning for query and
   fragment separators.

2. `cdUp()` returned `\` from `\` (via `slice(0, i || 1)`) and
   `loadDescriptionFile` walked that loop forever. It never hit this on
   plain drive-letter paths because the walk stops at `C:`, but UNC
   roots and DOS device paths reach a bare `\`. Treat `\` as a root
   (same as `/`) and also bail if the parent equals the input.

Adds `test/dos-device-paths.test.js` driving the real resolver with a
minimal backslash-keyed fake filesystem (memfs normalizes keys through
posix `path` so it can't hold a DOS device path as a root on Linux).
Covers relative/absolute requests, `\\?\UNC\…`, the `\\.\` device
namespace, index/directory resolution, query and fragment preservation,
missing-file failures, and description-file lookup termination.
Replace the fake backslash-keyed in-memory filesystem with Node's real
`fs` + `path` modules and gate the whole suite on `process.platform ===
'win32'`. The earlier fake FS let the tests run on Linux but could not
model how Windows actually services `\\?\…` and `\\.\…` paths — these
only mean anything to the Win32 kernel, so the honest check is to hit
real fixtures through those prefixes on a Windows host.

`symlinks: false` is required on the resolver: the realpath plugin
otherwise strips the DOS prefix from the result, masking exactly what
these tests are asserting on.

The pure-logic fixes (getType / parseIdentifier / cdUp) remain covered
on every platform via `test/path.test.js` and `test/identifier.test.js`.
On non-Windows CI the 9 resolve tests show up as skipped; on Windows
they exercise the end-to-end resolver path.
Drop `useSyncFileSystemCalls: true` and route the 8 success-path and
failure-path tests through `resolver.resolvePromise` + jest's
`.resolves` / `.rejects` matchers. Exercises the same async code path
production consumers hit. The description-file test keeps the callback
form because that's the only shape that surfaces the `request` argument
(and its `descriptionFilePath`).
The DOS-path fix guarded both `"/"` and `"\\"` as roots up front but
also added a per-call `slice` → local → equality check as belt-and-
braces. `alias-field` and `description-files-multi` are both hot on
`cdUp` via `loadDescriptionFile`, and the extra work shaved ~3-4% off
them in the benchmark.

The equality check is redundant once both single-char roots are
filtered: any successful `slice(0, i)` with `i > 0` is strictly shorter
than the input, and the `i === 0` branch (`slice(0, 1)`) only equals
the input when the input has length 1 — which the top guard already
returns `null` for. Remove the allocation + comparison and keep only
the top-level root guard. Benchmarks return to parity with main; the
new `dos-device-paths` tests still pass.
CodSpeed showed `description-files-multi` -37% and `alias-field`
regressed on the DOS device path patch. All three touched functions
are called many times per resolve, so even a handful of extra
instructions compounds in instruction-count measurements.

- `getType`: move the DOS device prefix check out of the length-≥3
  switch and check it last, behind a `c0 === CHAR_BACKSLASH` gate.
  The main switch keeps its original 3 cases so V8 generates the same
  dispatch code for POSIX/module inputs; only `\`-prefixed inputs pay
  for the DOS detection.
- `parseIdentifier`: split the fast path into two branches. When the
  identifier doesn't start with `\` (the overwhelming majority of
  resolves), run the original two-line `indexOf` scan unchanged. The
  DOS-prefix skip only runs for `\`-prefixed inputs.
- `cdUp`: replace `directory === "/" || directory === "\\"` with
  `directory.length <= 1`. Single `<=` instead of two string
  equality checks; also subsumes the empty-string case.

Wall-clock A/B over 3 runs each:
- description-files-multi: main 23,055 → opt 22,605 (-1.9%, in noise)
- alias-field: main 27,411 → opt 27,487 (+0.3%)

All tests still pass (1008 + 9 skipped).
@alexander-akait alexander-akait force-pushed the claude/handle-dos-device-paths-wyAFi branch from b52eb01 to 19bb28f Compare April 22, 2026 22:52
claude added 2 commits April 22, 2026 22:59
CodSpeed runs benchmarks under `--no-opt` (interpreter-only) which is
what exposed the 37% description-files-multi regression — inline extra
branches directly slow the Ignition bytecode interpreter, and helper
extraction matters here because the interpreter can skip right past the
gate call on the common path.

- `parseIdentifier`: extract DOS-prefix detection into `dosPrefixEnd()`.
  Fast path stays two `indexOf` calls; the only added cost for non-`\`
  inputs is one `charCodeAt(0) === 92` compare.
- `getType`: extract DOS-prefix handling into `getDosDeviceType()`.
  Gate is two compares (`c0 === \\ && c1 === \\`); the rest is behind
  the cold-helper call.

Validated under CodSpeed's exact measurement mode (`--no-opt
--predictable` + cachegrind), fully deterministic:
- origin/main baseline:      315,314,694 I-refs
- this branch (helpers):     315,951,261 I-refs  (+0.2%)
- inlined if/else variant:   316,127,119 I-refs  (+0.26%)

So locally the delta is ~0.2% across three runs each — not 37%. If
CodSpeed's CI still reports a large regression, it likely points to a
stale baseline or instrumentation drift on their side rather than this
code; happy to dig into a flamegraph if one is available.
@alexander-akait alexander-akait merged commit 26f15b0 into main Apr 23, 2026
33 of 34 checks passed
@alexander-akait alexander-akait deleted the claude/handle-dos-device-paths-wyAFi branch April 23, 2026 11:50
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.

2 participants