Skip to content
This repository was archived by the owner on Apr 3, 2024. It is now read-only.

Commit d5abfac

Browse files
Louis-YeJustinBeckwithmctavish
authored
fix: surface correct error message for ambiguous sourcemap matches (#971)
* fix: surface correct error message for ambiguous sourcemap matches Fixes #961 * Update test cases to be more natural statements Co-authored-by: Justin Beckwith <[email protected]> Co-authored-by: James McTavish <[email protected]>
1 parent 87b9569 commit d5abfac

File tree

5 files changed

+123
-50
lines changed

5 files changed

+123
-50
lines changed

src/agent/io/sourcemapper.ts

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,25 @@ const readFilep = promisify(fs.readFile);
2626

2727
/** @define {string} */ const MAP_EXT = '.map';
2828

29+
/** Represents one source map file. */
2930
export interface MapInfoInput {
31+
// The path of generated output file in the source map. For example, if
32+
// "src/index1.ts" and "src/index2.ts" are generated into "dist/index.js" and
33+
// "dist/index.js.map", then this field is "dist/index.js (relative to the
34+
// process's working directory).
3035
outputFile: string;
36+
37+
// The source map's path (relative to the process's working directory). For
38+
// the same example above, this field is "dist/index.js.map".
3139
mapFile: string;
40+
41+
// The SourceMapConsumer object after parsing the content in the mapFile.
3242
mapConsumer: sourceMap.SourceMapConsumer;
33-
// the sources are in ascending order from
34-
// shortest to longest
43+
44+
// The original sources in the source map. Each value is relative to the
45+
// source map file. For the same example above, this field is
46+
// ['../src/index1.ts', '../src/index2.ts']. the sources are in ascending
47+
// order from shortest to longest.
3548
sources: string[];
3649
}
3750

@@ -41,6 +54,12 @@ export interface MapInfoOutput {
4154
column?: number;
4255
}
4356

57+
export class MultiFileMatchError implements Error {
58+
readonly name = 'MultiFileMatchError';
59+
readonly message = 'Error: matching multiple files';
60+
constructor(readonly files: string[]) {}
61+
}
62+
4463
/**
4564
* @param {!Map} infoMap The map that maps input source files to
4665
* SourceMapConsumer objects that are used to calculate mapping information
@@ -167,13 +186,22 @@ export class SourceMapper {
167186
* source file provided there isn't any ambiguity with associating the input
168187
* path to exactly one output transpiled file.
169188
*
170-
* @param inputPath The (possibly relative) path to the original source file.
189+
* If there are more than one matches, throw the error to include all the
190+
* matched candidates.
191+
*
192+
* If there is no such mapping, it could be because the input file is not
193+
* the input to a transpilation process or it is the input to a transpilation
194+
* process but its corresponding .map file was not given to the constructor of
195+
* this mapper.
196+
*
197+
* @param inputPath The path to an input file that could possibly be the input
198+
* to a transpilation process.
199+
* The path can be relative to the process's current working directory.
171200
* @return The `MapInfoInput` object that describes the transpiled file
172-
* associated with the specified input path. `null` is returned if either
173-
* zero files are associated with the input path or if more than one file
174-
* could possibly be associated with the given input path.
201+
* associated with the specified input path. `null` is returned if there is
202+
* no files that are associated with the input path.
175203
*/
176-
private getMappingInfo(inputPath: string): MapInfoInput | null {
204+
getMapInfoInput(inputPath: string): MapInfoInput | null {
177205
if (this.infoMap.has(path.normalize(inputPath))) {
178206
return this.infoMap.get(inputPath) as MapInfoInput;
179207
}
@@ -182,30 +210,17 @@ export class SourceMapper {
182210
inputPath,
183211
Array.from(this.infoMap.keys())
184212
);
213+
185214
if (matches.length === 1) {
186215
return this.infoMap.get(matches[0]) as MapInfoInput;
187216
}
217+
if (matches.length > 1) {
218+
throw new MultiFileMatchError(matches);
219+
}
188220

189221
return null;
190222
}
191223

192-
/**
193-
* Used to determine if the source file specified by the given path has
194-
* a .map file and an output file associated with it.
195-
*
196-
* If there is no such mapping, it could be because the input file is not
197-
* the input to a transpilation process or it is the input to a transpilation
198-
* process but its corresponding .map file was not given to the constructor
199-
* of this mapper.
200-
*
201-
* @param {string} inputPath The path to an input file that could
202-
* possibly be the input to a transpilation process. The path should be
203-
* relative to the process's current working directory.
204-
*/
205-
hasMappingInfo(inputPath: string): boolean {
206-
return this.getMappingInfo(inputPath) !== null;
207-
}
208-
209224
/**
210225
* @param {string} inputPath The path to an input file that could possibly
211226
* be the input to a transpilation process. The path should be relative to
@@ -225,20 +240,18 @@ export class SourceMapper {
225240
* If the given input file does not have mapping information associated
226241
* with it then null is returned.
227242
*/
228-
mappingInfo(
243+
getMapInfoOutput(
229244
inputPath: string,
230245
lineNumber: number,
231-
colNumber: number
246+
colNumber: number,
247+
entry: MapInfoInput
232248
): MapInfoOutput | null {
233249
inputPath = path.normalize(inputPath);
234-
const entry = this.getMappingInfo(inputPath);
235-
if (entry === null) {
236-
return null;
237-
}
238250

239251
const relPath = path
240252
.relative(path.dirname(entry.mapFile), inputPath)
241253
.replace(/\\/g, '/');
254+
242255
/**
243256
* Note: Since `entry.sources` is in ascending order from shortest
244257
* to longest, the first source path that ends with the

src/agent/v8/inspector-debugapi.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import consoleLogLevel = require('console-log-level');
2323
import * as stackdriver from '../../types/stackdriver';
2424
import {ResolvedDebugAgentConfig} from '../config';
2525
import {FileStats, ScanStats} from '../io/scanner';
26-
import {MapInfoOutput, SourceMapper} from '../io/sourcemapper';
26+
import {
27+
MapInfoOutput,
28+
SourceMapper,
29+
MultiFileMatchError,
30+
} from '../io/sourcemapper';
2731
import * as state from '../state/inspector-state';
2832
import * as utils from '../util/utils';
2933

@@ -159,7 +163,26 @@ export class InspectorDebugApi implements debugapi.DebugApi {
159163
);
160164
}
161165
const baseScriptPath = path.normalize(breakpoint.location.path);
162-
if (!this.sourcemapper.hasMappingInfo(baseScriptPath)) {
166+
let mapInfoInput = null;
167+
try {
168+
mapInfoInput = this.sourcemapper.getMapInfoInput(baseScriptPath);
169+
} catch (error) {
170+
if (error instanceof MultiFileMatchError) {
171+
this.logger.warn(
172+
`Unable to unambiguously find ${baseScriptPath}. Multiple matches: ${error.files}`
173+
);
174+
return utils.setErrorStatusAndCallback(
175+
cb,
176+
breakpoint,
177+
StatusMessage.BREAKPOINT_SOURCE_LOCATION,
178+
utils.messages.SOURCE_FILE_AMBIGUOUS
179+
);
180+
} else {
181+
throw error;
182+
}
183+
}
184+
185+
if (mapInfoInput === null) {
163186
const extension = path.extname(baseScriptPath);
164187
if (!this.config.javascriptFileExtensions.includes(extension)) {
165188
return utils.setErrorStatusAndCallback(
@@ -174,10 +197,11 @@ export class InspectorDebugApi implements debugapi.DebugApi {
174197
} else {
175198
const line = breakpoint.location.line;
176199
const column = 0;
177-
const mapInfo = this.sourcemapper.mappingInfo(
200+
const mapInfo = this.sourcemapper.getMapInfoOutput(
178201
baseScriptPath,
179202
line,
180-
column
203+
column,
204+
mapInfoInput
181205
);
182206

183207
const compile = utils.getBreakpointCompiler(breakpoint);

src/agent/v8/legacy-debugapi.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ export class V8DebugApi implements debugapi.DebugApi {
129129
);
130130
}
131131
const baseScriptPath = path.normalize(breakpoint.location.path);
132-
if (!this.sourcemapper.hasMappingInfo(baseScriptPath)) {
132+
const mapInfoInput = this.sourcemapper.getMapInfoInput(baseScriptPath);
133+
if (mapInfoInput === null) {
133134
const extension = path.extname(baseScriptPath);
134135
if (!this.config.javascriptFileExtensions.includes(extension)) {
135136
return utils.setErrorStatusAndCallback(
@@ -143,10 +144,11 @@ export class V8DebugApi implements debugapi.DebugApi {
143144
} else {
144145
const line = breakpoint.location.line;
145146
const column = 0;
146-
const mapInfo = this.sourcemapper.mappingInfo(
147+
const mapInfo = this.sourcemapper.getMapInfoOutput(
147148
baseScriptPath,
148149
line,
149-
column
150+
column,
151+
mapInfoInput
150152
);
151153
const compile = utils.getBreakpointCompiler(breakpoint);
152154
if (breakpoint.condition && compile) {
@@ -168,6 +170,7 @@ export class V8DebugApi implements debugapi.DebugApi {
168170
this.setInternal(breakpoint, mapInfo, compile, cb);
169171
}
170172
}
173+
171174
clear(
172175
breakpoint: stackdriver.Breakpoint,
173176
cb: (err: Error | null) => void

test/test-sourcemapper.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ function testTool(
6868
tool +
6969
' it states that it has mapping info for files it knows about',
7070
done => {
71-
assert.strictEqual(
72-
sourcemapper.hasMappingInfo(inputFilePath),
73-
true,
71+
assert.notStrictEqual(
72+
sourcemapper.getMapInfoInput(inputFilePath),
73+
null,
7474
`The sourcemapper should have information about '${inputFilePath}'`
7575
);
7676
done();
@@ -83,17 +83,17 @@ function testTool(
8383
' it states that it has mapping info for files with a path' +
8484
' similar to a path it knows about',
8585
done => {
86-
assert.strictEqual(
87-
sourcemapper.hasMappingInfo(relativeInputFilePath),
88-
true
86+
assert.notStrictEqual(
87+
sourcemapper.getMapInfoInput(inputFilePath),
88+
null
8989
);
9090
const movedPath = path.join(
9191
'/some/other/base/dir/',
9292
relativeInputFilePath
9393
);
94-
assert.strictEqual(
95-
sourcemapper.hasMappingInfo(movedPath),
96-
true,
94+
assert.notStrictEqual(
95+
sourcemapper.getMapInfoInput(inputFilePath),
96+
null,
9797
`The sourcemapper should have information about paths similar to '${movedPath}'`
9898
);
9999
done();
@@ -108,16 +108,23 @@ function testTool(
108108
done => {
109109
const invalidPath = inputFilePath + '_INVALID';
110110
assert.strictEqual(
111-
sourcemapper.hasMappingInfo(invalidPath),
112-
false,
111+
sourcemapper.getMapInfoInput(invalidPath),
112+
null,
113113
`The source mapper should not have information the path '${invalidPath}' it doesn't recognize`
114114
);
115115
done();
116116
}
117117
);
118118

119119
const testLineMapping = (inputLine: number, expectedOutputLine: number) => {
120-
const info = sourcemapper.mappingInfo(inputFilePath, inputLine, 0);
120+
const mapInfoInput = sourcemapper.getMapInfoInput(inputFilePath);
121+
assert.notEqual(mapInfoInput, null);
122+
const info = sourcemapper.getMapInfoOutput(
123+
inputFilePath,
124+
inputLine,
125+
0,
126+
mapInfoInput!
127+
);
121128
assert.notStrictEqual(
122129
info,
123130
null,

test/test-v8debugapi.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ describe('v8debugapi', () => {
412412
});
413413
});
414414

415-
it('should reject breakpoint when filename is ambiguous', done => {
415+
it('should reject breakpoint when javascript file is ambiguous', done => {
416416
require('./fixtures/a/hello.js');
417417
require('./fixtures/b/hello.js');
418418
// TODO(dominickramer): Have this actually implement Breakpoint
@@ -458,6 +458,32 @@ describe('v8debugapi', () => {
458458
});
459459
});
460460

461+
it('should reject breakpoint when source mapping is ambiguous', done => {
462+
const bp: stackdriver.Breakpoint = {
463+
id: 'ambiguous',
464+
location: {line: 1, path: 'in.ts'},
465+
} as {} as stackdriver.Breakpoint;
466+
api.set(bp, err => {
467+
assert.ok(err);
468+
assert.ok(bp.status);
469+
assert.ok(bp.status instanceof StatusMessage);
470+
assert.ok(bp.status!.isError);
471+
assert(
472+
bp.status!.description.format === utils.messages.SOURCE_FILE_AMBIGUOUS
473+
);
474+
475+
// Verify that a warning log message is emitted.
476+
assert.strictEqual(
477+
logger.warns.length,
478+
1,
479+
`Expected 1 warning log message, got ${logger.allCalls.length}`
480+
);
481+
const message = logger.warns[0].args[0];
482+
assert.notStrictEqual(message.indexOf('Multiple matches:'), -1);
483+
done();
484+
});
485+
});
486+
461487
it('should reject breakpoint on non-existent line', done => {
462488
require('./fixtures/foo.js');
463489
// TODO(dominickramer): Have this actually implement Breakpoint

0 commit comments

Comments
 (0)