From 0a8c5aeeb33ad2f70a6184614d59a266a20d62bb Mon Sep 17 00:00:00 2001 From: vincent861223 Date: Wed, 15 Apr 2026 16:16:38 -0600 Subject: [PATCH] fix: resolve function names from preceding source map token TypeScript's source map generator attaches the original function name to the `function` keyword token rather than the identifier that follows. When the token at the identifier position has no name, check the immediately preceding token and use its name if it maps to the same original source position. Co-authored-by: bdingman-daedalus Co-authored-by: zobell --- Cargo.lock | 2 +- src/name_resolver.rs | 28 +++++++++-- tests/fixtures/ts-function-name/README.md | 16 +++++++ tests/fixtures/ts-function-name/minified.js | 1 + .../fixtures/ts-function-name/minified.js.map | 1 + tests/fixtures/ts-function-name/original.ts | 3 ++ .../fixtures/ts-function-name/sentry-repro.js | 5 ++ .../ts-function-name/sentry-repro.js.map | 1 + tests/integration.rs | 48 +++++++++++++++++++ 9 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/ts-function-name/README.md create mode 100644 tests/fixtures/ts-function-name/minified.js create mode 100644 tests/fixtures/ts-function-name/minified.js.map create mode 100644 tests/fixtures/ts-function-name/original.ts create mode 100644 tests/fixtures/ts-function-name/sentry-repro.js create mode 100644 tests/fixtures/ts-function-name/sentry-repro.js.map diff --git a/Cargo.lock b/Cargo.lock index 46bdb4d..d0c86f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,7 +383,7 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-source-scopes" -version = "0.6.0" +version = "0.7.1" dependencies = [ "indexmap", "sourcemap", diff --git a/src/name_resolver.rs b/src/name_resolver.rs index 9b5d528..39ac539 100644 --- a/src/name_resolver.rs +++ b/src/name_resolver.rs @@ -37,9 +37,31 @@ impl<'a, T: AsRef> NameResolver<'a, T> { && token.get_dst_col() >= source_position.column.saturating_sub(1); if is_exactish_match { - token.get_name() - } else { - None + if let Some(name) = token.get_name() { + return Some(name); + } + + // If the token at the identifier position has no name, check the + // immediately preceding token. Some source map generators (e.g. + // TypeScript) attach the original function name to the `function` + // keyword token rather than the identifier that follows it. + // We only use the preceding token's name if it maps to the same + // original source position, indicating it's part of the same mapping. + if token.get_dst_col() > 0 { + if let Some(prev_token) = self + .sourcemap + .lookup_token(token.get_dst_line(), token.get_dst_col() - 1) + { + if prev_token.get_src_id() == token.get_src_id() + && prev_token.get_src_line() == token.get_src_line() + && prev_token.get_src_col() == token.get_src_col() + { + return prev_token.get_name(); + } + } + } } + + None } } diff --git a/tests/fixtures/ts-function-name/README.md b/tests/fixtures/ts-function-name/README.md new file mode 100644 index 0000000..0a64e4d --- /dev/null +++ b/tests/fixtures/ts-function-name/README.md @@ -0,0 +1,16 @@ +This fixture reproduces a TypeScript source map pattern where the original function +name is attached to the `function` keyword token rather than to the identifier. + +The source map has these segments around the function declaration: + + col 7 (within `function` keyword) → original col 9, name = "initServer" + col 8 (space) → original col 9, name = (none) + [no segment at col 9 where `ab` starts] + +When looking up col 9 (the `ab` identifier), the nearest token is at col 8 +(no name). The fix checks col 7 (one before), finds it maps to the same +original source position, and uses its name "initServer". + +The source map was generated programmatically to mimic TypeScript compiler output. +The real-world case this tests: TypeScript compiling `function initServer()` to +`function Uc1bk()`, where Sentry's symbolicator was unable to resolve the name. diff --git a/tests/fixtures/ts-function-name/minified.js b/tests/fixtures/ts-function-name/minified.js new file mode 100644 index 0000000..b05ae8a --- /dev/null +++ b/tests/fixtures/ts-function-name/minified.js @@ -0,0 +1 @@ +function ab(){console.log("hello")} diff --git a/tests/fixtures/ts-function-name/minified.js.map b/tests/fixtures/ts-function-name/minified.js.map new file mode 100644 index 0000000..8bf8dd4 --- /dev/null +++ b/tests/fixtures/ts-function-name/minified.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["original.ts"],"names":["initServer","console"],"mappings":"AAAA,OAASA,CAAA,GAAY,EAAC,CAClBC,qBACJ","file":"minified.js","sourcesContent":["function initServer() {\n console.log(\"hello\");\n}"]} \ No newline at end of file diff --git a/tests/fixtures/ts-function-name/original.ts b/tests/fixtures/ts-function-name/original.ts new file mode 100644 index 0000000..3c36236 --- /dev/null +++ b/tests/fixtures/ts-function-name/original.ts @@ -0,0 +1,3 @@ +function initServer() { + console.log("hello"); +} diff --git a/tests/fixtures/ts-function-name/sentry-repro.js b/tests/fixtures/ts-function-name/sentry-repro.js new file mode 100644 index 0000000..19b8dab --- /dev/null +++ b/tests/fixtures/ts-function-name/sentry-repro.js @@ -0,0 +1,5 @@ +'use strict'; +!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="a1b2c3d4-e5f6-7890-abcd-ef1234567890")}catch(e){}}(); +function a(){console.log("hello")}a(); + +//# debugId=a1b2c3d4-e5f6-7890-abcd-ef1234567890 diff --git a/tests/fixtures/ts-function-name/sentry-repro.js.map b/tests/fixtures/ts-function-name/sentry-repro.js.map new file mode 100644 index 0000000..9b2af6b --- /dev/null +++ b/tests/fixtures/ts-function-name/sentry-repro.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["original.js"],"names":["initServer"],"mappings":";;AAASA,QAAA,CAAA,CAAU,CAAC,CAAE,CAClB,OAAO,IAAI,CAAC,OAAO,CACvB,CACAA,CAAU,CAAC","file":"minified.js","sourcesContent":["function initServer() {\n console.log(\"hello\");\n}\ninitServer();\n"],"debug_id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"} \ No newline at end of file diff --git a/tests/integration.rs b/tests/integration.rs index 121f080..79e39e3 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -232,3 +232,51 @@ fn should_resolve_exactish() { assert_eq!(resolved_scopes[3].2, Some("invoke".into())); assert_eq!(resolved_scopes[4].2, Some("test".into())); } + +#[test] +fn should_resolve_name_from_function_keyword_token() { + // TypeScript attaches the original name to the `function` keyword token + // rather than to the identifier that follows. The source map has: + // col 7 → name "initServer" (within `function` keyword) + // col 8 → no name (space) + // [no segment at col 9 where `ab` starts] + // lookup_token(0, 9) returns the col-8 token (no name). + // The fix should look back to col 7 and use "initServer". + let minified = fixture("ts-function-name/minified.js"); + let map = fixture("ts-function-name/minified.js.map"); + + let scopes = extract_scope_names(&minified).unwrap(); + + let resolved_scopes = resolve_original_scopes(&minified, &map, scopes); + + // The function scope should resolve from "ab" to "initServer" + assert_eq!(resolved_scopes[0].1, Some("ab".into())); + assert_eq!(resolved_scopes[0].2, Some("initServer".into())); +} + +#[test] +fn should_resolve_name_from_function_keyword_token_three_segments() { + // Variant where the source map has THREE segments around the function: + // col 0 → name "initServer" (start of `function` keyword) + // col 8 → no name (space) + // col 9 → no name (identifier `a`) + // The name-bearing segment is 2 tokens back from the identifier, so the + // single-step lookback does not reach it. This documents a limitation: + // when there's an extra no-name segment between the named token and the + // identifier, the name is not resolved. + let minified = fixture("ts-function-name/sentry-repro.js"); + let map = fixture("ts-function-name/sentry-repro.js.map"); + + let scopes = extract_scope_names(&minified).unwrap(); + + let resolved_scopes = resolve_original_scopes(&minified, &map, scopes); + + let func_scope = resolved_scopes + .iter() + .find(|(_, m, _)| m.as_deref() == Some("a")) + .expect("should find function 'a'"); + + // Currently NOT resolved — the name is too many tokens back. + // If the fix is improved to walk further back, update this to "initServer". + assert_eq!(func_scope.2, Some("a".into())); +}