Skip to content

Commit 536f96b

Browse files
committed
fix: sourcemap renames replacing in invalid contexts
Fixes #1201
1 parent ab61a44 commit 536f96b

File tree

4 files changed

+142
-5
lines changed

4 files changed

+142
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he
44

55
## Nightly only
66

7-
Nothing (yet)
7+
- fix: sourcemap renames replacing in invalid contexts ([#1201](https://github.com/microsoft/vscode-js-debug/issues/1201))
88

99
## v1.65 (March 2022)
1010

src/adapter/evaluator.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { expect } from 'chai';
6+
import Cdp from '../cdp/api';
7+
import { stubbedCdpApi, StubCdpApi } from '../cdp/stubbedApi';
8+
import { Base01Position } from '../common/positions';
9+
import { RenameMapping } from '../common/sourceMaps/renameProvider';
10+
import { Evaluator } from './evaluator';
11+
12+
describe('Evaluator', () => {
13+
let evaluator: Evaluator;
14+
let stubCdp: StubCdpApi;
15+
let renameMapping: RenameMapping;
16+
17+
const result: Cdp.Debugger.EvaluateOnCallFrameResult = {
18+
result: {
19+
type: 'string',
20+
value: 'foo',
21+
},
22+
};
23+
24+
beforeEach(() => {
25+
stubCdp = stubbedCdpApi();
26+
renameMapping = RenameMapping.None;
27+
evaluator = new Evaluator(stubCdp.actual, {
28+
provideForSource: () => renameMapping,
29+
provideOnStackframe: () => renameMapping,
30+
});
31+
});
32+
33+
it('prepares simple expressions', async () => {
34+
const prep = evaluator.prepare('foo', { isInternalScript: false });
35+
expect(prep.canEvaluateDirectly).to.be.true;
36+
stubCdp.Debugger.evaluateOnCallFrame.resolves(result);
37+
expect(await prep.invoke({ callFrameId: '' })).to.equal(result);
38+
expect(stubCdp.Debugger.evaluateOnCallFrame.args[0][0]).to.deep.equal({
39+
callFrameId: '',
40+
expression: 'foo',
41+
});
42+
});
43+
44+
it('appends eval source url to internal', async () => {
45+
const prep = evaluator.prepare('foo');
46+
expect(prep.canEvaluateDirectly).to.be.true;
47+
stubCdp.Debugger.evaluateOnCallFrame.resolves(result);
48+
expect(await prep.invoke({ callFrameId: '' })).to.equal(result);
49+
expect(stubCdp.Debugger.evaluateOnCallFrame.args[0][0].expression).to.match(
50+
/^foo\n\/\/# sourceURL=eval/m,
51+
);
52+
});
53+
54+
it('replaces renamed identifiers', async () => {
55+
const prep = evaluator.prepare('foo', {
56+
isInternalScript: false,
57+
renames: {
58+
mapping: new RenameMapping([
59+
{ original: 'foo', compiled: 'bar', position: new Base01Position(0, 1) },
60+
]),
61+
position: new Base01Position(0, 1),
62+
},
63+
});
64+
expect(prep.canEvaluateDirectly).to.be.true;
65+
stubCdp.Debugger.evaluateOnCallFrame.resolves(result);
66+
expect(await prep.invoke({ callFrameId: '' })).to.equal(result);
67+
expect(stubCdp.Debugger.evaluateOnCallFrame.args[0][0]).to.deep.equal({
68+
callFrameId: '',
69+
expression: 'typeof bar !== "undefined" ? bar : foo;\n',
70+
});
71+
});
72+
73+
it('does not replace identifiers in invalid contexts', async () => {
74+
const prep = evaluator.prepare(
75+
`const baz = foo;
76+
z.find(foo => true)
77+
const { foo } = z;
78+
for (const { foo } of z) {}
79+
try {} catch ({ foo }) {}`,
80+
{
81+
isInternalScript: false,
82+
renames: {
83+
mapping: new RenameMapping([
84+
{ original: 'foo', compiled: 'bar', position: new Base01Position(0, 1) },
85+
]),
86+
position: new Base01Position(0, 1),
87+
},
88+
},
89+
);
90+
expect(prep.canEvaluateDirectly).to.be.true;
91+
stubCdp.Debugger.evaluateOnCallFrame.resolves(result);
92+
expect(await prep.invoke({ callFrameId: '' })).to.equal(result);
93+
expect(stubCdp.Debugger.evaluateOnCallFrame.args[0][0]).to.deep.equal({
94+
callFrameId: '',
95+
expression: [
96+
`const baz = typeof bar !== \"undefined\" ? bar : foo;`,
97+
`z.find(bar => true);`,
98+
`const {bar} = z;`,
99+
`for (const {bar} of z) {}`,
100+
`try {} catch ({bar}) {}`,
101+
'',
102+
].join('\n'),
103+
});
104+
});
105+
});

src/adapter/evaluator.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ICdpApi } from '../cdp/connection';
1313
import { IPosition } from '../common/positions';
1414
import { parseProgram } from '../common/sourceCodeManipulations';
1515
import { IRenameProvider, RenameMapping } from '../common/sourceMaps/renameProvider';
16+
import { isInPatternSlot } from '../common/sourceUtils';
1617
import { StackFrame } from './stackTrace';
1718
import { getSourceSuffix } from './templates';
1819

@@ -274,7 +275,7 @@ export class Evaluator implements IEvaluator {
274275

275276
const parents: Node[] = [];
276277
const transformed = replace(parseProgram(expr), {
277-
enter(node) {
278+
enter(node, parent) {
278279
const asAcorn = node as AcornNode;
279280
if (node.type !== 'Identifier' || expr[asAcorn.start - 1] === '.') {
280281
return;
@@ -285,14 +286,18 @@ export class Evaluator implements IEvaluator {
285286
hoisted.add(node.name);
286287
mutated = true;
287288
this.skip();
288-
return replacement(hoistName, undefinedExpression);
289+
return isInPatternSlot(node, parent)
290+
? { type: 'Identifier', name: hoistName }
291+
: replacement(hoistName, undefinedExpression);
289292
}
290293

291294
const cname = renames?.mapping.getCompiledName(node.name, renames.position);
292295
if (cname) {
293296
mutated = true;
294297
this.skip();
295-
return replacement(cname, node);
298+
return isInPatternSlot(node, parent)
299+
? { type: 'Identifier', name: cname }
300+
: replacement(cname, node);
296301
}
297302
},
298303
leave: () => {

src/common/sourceUtils.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
import { parseExpressionAt, Parser } from 'acorn';
66
import { generate } from 'astring';
77
import { replace, VisitorOption } from 'estraverse';
8-
import { Expression, ExpressionStatement, Identifier, Program, Statement } from 'estree';
8+
import {
9+
Expression,
10+
ExpressionStatement,
11+
Identifier,
12+
Node,
13+
Pattern,
14+
Program,
15+
Statement,
16+
} from 'estree';
917
import { promises as fsPromises } from 'fs';
1018
import { NullablePosition, Position, SourceMapConsumer, SourceMapGenerator } from 'source-map';
1119
import { LineColumn } from '../adapter/breakpoints/breakpointBase';
@@ -321,6 +329,25 @@ export function getOptimalCompiledPosition(
321329
return allLocations[0][0];
322330
}
323331

332+
/**
333+
* Returns if the given node is in a slot reserved for a Pattern where an
334+
* Expression node could not go.
335+
*/
336+
export const isInPatternSlot = (node: Pattern, parent: Node | null | undefined): boolean =>
337+
!!parent &&
338+
(((parent.type === 'FunctionExpression' ||
339+
parent.type === 'FunctionDeclaration' ||
340+
parent.type === 'ArrowFunctionExpression') &&
341+
parent.params.includes(node)) ||
342+
((parent.type === 'ForInStatement' || parent.type === 'ForOfStatement') &&
343+
parent.left === node) ||
344+
(parent.type === 'VariableDeclarator' && parent.id === node) ||
345+
(parent.type === 'AssignmentPattern' && parent.left === node) ||
346+
(parent.type === 'CatchClause' && parent.param === node) ||
347+
('kind' in parent && parent.kind === 'init' && parent.value === node) ||
348+
(parent.type === 'RestElement' && parent.argument === node) ||
349+
(parent.type === 'AssignmentPattern' && parent.left === node));
350+
324351
/**
325352
* Returns the syntax error in the given code, if any.
326353
*/

0 commit comments

Comments
 (0)