Skip to content

test: document webpack source map behaviour for column=0 lookups#299

Open
szegedi wants to merge 4 commits intomainfrom
szegedi/webpack-sourcemap-regression-test
Open

test: document webpack source map behaviour for column=0 lookups#299
szegedi wants to merge 4 commits intomainfrom
szegedi/webpack-sourcemap-regression-test

Conversation

@szegedi
Copy link

@szegedi szegedi commented Mar 13, 2026

Background

PR #81 changed originalPositionFor to always try LEAST_UPPER_BOUND first (then fall back to GREATEST_LOWER_BOUND). It was reverted in #106 because it broke webpack source maps: for real non-zero columns, LEAST_UPPER_BOUND finds the next mapping (≥ column) rather than the mapping at that column, returning wrong function names.

PR #248 fixed that regression by only using LEAST_UPPER_BOUND when column === 0, and GREATEST_LOWER_BOUND otherwise. This is correct for Node.js ≥ 25, where V8's LineTick struct carries real column numbers.

There was still a concern that #248 will be incorrect for Webpack, so I decided we should investigate this, and either fix it, or at least understand the behavior, as I'd finally like to add #248 to a release. This is what we ended up doing, understanding the behavior, and codifying it in tests so we can detect future regressions.

Investigation findings

Is the original webpack failure reproduced by #248? — Partially.

Situation Pre-#248 behaviour #248 behaviour
Real column (Node.js ≥ 25) GREATEST_LOWER_BOUND → correct ✓ GREATEST_LOWER_BOUND → correct ✓
column=0, no real info (Node.js < 25) GREATEST_LOWER_BOUND → null → unmapped LEAST_UPPER_BOUND → first mapping → mapped to wrong function

On Node.js ≥ 25 everything is correct. #248 is a genuine fix for that platform.

Residual limitation on Node.js < 25: The LineTick struct has no column field on those versions, so the C++ layer always emits column=0 for every sample. With column=0, LEAST_UPPER_BOUND finds the first mapping on the line. In a webpack bundle (all output minified onto one line) every function gets attributed to whichever source function appears first in the map.

This is not a regression vs. the pre-#248 state: before #248 those functions were simply unmapped (falling back to generated names). Both outcomes are imperfect; #248 trades "unmapped" for "mapped to first function". On the balance, this is likely an improvement — a wrong-but-close mapping is more useful in a profile than no mapping at all.

What this PR does

Adds two unit tests to test-sourcemapper.ts using a synthetic webpack-style source map (three functions at columns 10, 30, 50 on a single line, from three different source files):

  1. Node.js ≥ 25 path (column > 0): verifies each function resolves to its correct source.
  2. Node.js < 25 path (column = 0): documents the known limitation — all three functions resolve to the first mapping on the line. The test assertions encode the current behaviour explicitly so any future change is immediately visible.

No behaviour changes; tests only.

🤖 Generated with Claude Code

Add tests for SourceMapper.mappingInfo with a synthetic webpack-style
single-line bundle to document the known limitation introduced by #248.

Background
----------
PR #81 changed originalPositionFor to always try LEAST_UPPER_BOUND
first (then fall back to GREATEST_LOWER_BOUND). This was reverted in
#106 because it broke webpack source maps: for real non-zero columns
LEAST_UPPER_BOUND finds the *next* mapping (≥ column) rather than the
one at the column, returning wrong function names.

PR #248 fixed that regression by using LEAST_UPPER_BOUND only when
column === 0, and GREATEST_LOWER_BOUND otherwise. This correctly
handles Node.js ≥ 25 where V8's LineTick struct carries real column
numbers.

Residual limitation (Node.js < 25)
-----------------------------------
On Node.js < 25, the LineTick struct has no column field. The C++ layer
therefore always emits column=0 for every LineTick sample. With
column=0, the sourcemapper uses LEAST_UPPER_BOUND, which finds the
*first* mapping on the line. In a webpack bundle (all output on one
line) every function maps to the same first source function in the map.

This is not a regression vs. the pre-#248 state: before #248, those
functions were simply unmapped (column=0 + GREATEST_LOWER_BOUND →
nothing ≤ 0 → null → falls back to generated name/file). Both outcomes
are imperfect; #248 trades "unmapped" for "mapped to first function",
which may or may not be preferable depending on the use case.

The two new tests pin both behaviours explicitly so any future change
to this logic is immediately visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Mar 13, 2026

Overall package size

Self size: 2.04 MB
Deduped: 2.4 MB
No deduping: 2.4 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | source-map | 0.7.6 | 185.63 kB | 185.63 kB | | pprof-format | 2.2.1 | 163.06 kB | 163.06 kB | | node-gyp-build | 3.9.0 | 8.81 kB | 8.81 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@szegedi szegedi added the semver-patch Bug or security fixes, mainly label Mar 13, 2026
@szegedi szegedi changed the title test: document webpack source map behaviour for column=0 lookups (#248) test: document webpack source map behaviour for column=0 lookups Mar 13, 2026
@szegedi szegedi marked this pull request as ready for review March 13, 2026 12:17
@pr-commenter
Copy link

pr-commenter bot commented Mar 13, 2026

Benchmarks

Benchmark execution time: 2026-03-13 12:45:12

Comparing candidate commit d6f79f2 in PR branch szegedi/webpack-sourcemap-regression-test with baseline commit 6ab3e39 in branch main.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 91 metrics, 29 unstable metrics.

Qard and others added 2 commits March 13, 2026 13:32
When lineNumbers is enabled, the column is always zero. If only one
occurence of a call occurs on one line then that is correctly selected.
However, in cases where the same function is called multiple times in
the same line it will be unable to differentiate them and would use
the unmapped value. This now makes it select the first call in the line
as a best-guess for the match, and in Node.js v25 will use the new
column field in LineTick to select the correct column where possible.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver-patch Bug or security fixes, mainly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants