Skip to content

Commit 4f23b14

Browse files
devversionJonathan Ginsburg
authored and
Jonathan Ginsburg
committed
fix(reporter): warning if stack trace contains generated code invocation
For some projects, a preprocessor like TypeScript may run to downlevel certain features to a lower ECMAScript target. e.g. a project may consume Angular for example, which ships ES2020. If TypeScript, ESBuild, Babel etc. is used to do this, they may inject generated code which does not map to any original source. If any of these helpers (the generated code) is then part of a stack trace, Karma will incorrectly report an error for an unresolved source map position. Generated code is valid within source maps and can be denoted as mappings with a 1-variable-length mapping. See the source map spec: https://sourcemaps.info/spec.html. The warning for generated code is especially bad when the majority of file paths, and the actually relevant-portions in the stack are resolved properly. e.g. Errors initially look like this without the source mapping processing of Karma: (See PR description as commit lint does not allow for long stack traces..) A helper function shows up in the stacktrace but has no original mapping as it is purely generated by TypeScript/ESbuild etc. The following warning is printed and pollutes the test output while the remaining stack trace paths (as said before), have been remapped properly: ``` SourceMap position not found for trace: http://localhost:9877/base/angular_material/ src/material/select/testing/unit_tests_bundle_spec.js:26:26 ``` The resolved stacktrace looks like this after the transformation: (see PR description as commit lint does not allow for long stack traces here..) More details on the scenario here: https://gist.github.com/devversion/549d25915c2dc98a8896ba4408a1e27c.
1 parent 4c6f681 commit 4f23b14

File tree

2 files changed

+86
-2
lines changed

2 files changed

+86
-2
lines changed

lib/reporter.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function createErrorFormatter (config, emitter, SourceMapConsumer) {
4848
input = JSON.stringify(input, null, indentation)
4949
}
5050

51-
let msg = input.replace(URL_REGEXP, function (_, prefix, path, __, ___, line, ____, column) {
51+
let msg = input.replace(URL_REGEXP, function (stackTracePath, prefix, path, __, ___, line, ____, column) {
5252
const normalizedPath = prefix === 'base/' ? `${basePath}/${path}` : path
5353
const file = lastServedFiles.find((file) => file.path === normalizedPath)
5454

@@ -64,12 +64,21 @@ function createErrorFormatter (config, emitter, SourceMapConsumer) {
6464
const zeroBasedColumn = Math.max(0, (column || 1) - 1)
6565
const original = getSourceMapConsumer(file.sourceMap).originalPositionFor({ line, column: zeroBasedColumn, bias })
6666

67+
// If there is no original position/source for the current stack trace path, then
68+
// we return early with the formatted generated position. This handles the case of
69+
// generated code which does not map to anything, see Case 1 of the source-map spec.
70+
// https://sourcemaps.info/spec.html.
71+
if (original.source === null) {
72+
return PathUtils.formatPathMapping(path, line, column)
73+
}
74+
6775
// Source maps often only have a local file name, resolve to turn into a full path if
6876
// the path is not absolute yet.
6977
const oneBasedOriginalColumn = original.column == null ? original.column : original.column + 1
7078
return `${PathUtils.formatPathMapping(resolve(path, original.source), original.line, oneBasedOriginalColumn)} <- ${PathUtils.formatPathMapping(path, line, column)}`
7179
} catch (e) {
72-
log.warn(`SourceMap position not found for trace: ${input}`)
80+
log.warn(`An unexpected error occurred while resolving the original position for: ${stackTracePath}`)
81+
log.warn(e)
7382
}
7483
}
7584

test/unit/reporter.spec.js

+75
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const loadFile = require('mocks').loadFile
55
const path = require('path')
66
const _ = require('lodash')
77
const sinon = require('sinon')
8+
const logger = require('../../lib/logger')
89

910
const File = require('../../lib/file')
1011

@@ -127,6 +128,8 @@ describe('reporter', () => {
127128
describe('source maps', () => {
128129
let originalPositionForCallCount = 0
129130
let sourceMappingPath = null
131+
let log
132+
let logWarnStub
130133

131134
class MockSourceMapConsumer {
132135
constructor (sourceMap) {
@@ -147,9 +150,36 @@ describe('reporter', () => {
147150
}
148151
}
149152

153+
class MockSourceMapConsumerWithParseError {
154+
constructor () {
155+
throw new Error('Fake parse error from source map consumer')
156+
}
157+
}
158+
159+
class MockSourceMapConsumerWithGeneratedCode {
160+
constructor (sourceMap) {
161+
this.source = sourceMap.content.replace('SOURCE MAP ', sourceMappingPath)
162+
}
163+
164+
originalPositionFor () {
165+
return {
166+
source: null,
167+
line: null,
168+
column: null
169+
}
170+
}
171+
}
172+
150173
beforeEach(() => {
151174
originalPositionForCallCount = 0
152175
sourceMappingPath = '/original/'
176+
177+
log = logger.create('reporter')
178+
logWarnStub = sinon.spy(log, 'warn')
179+
})
180+
181+
afterEach(() => {
182+
logWarnStub.restore()
153183
})
154184

155185
MockSourceMapConsumer.GREATEST_LOWER_BOUND = 1
@@ -170,6 +200,51 @@ describe('reporter', () => {
170200
})
171201
})
172202

203+
// Regression test for cases like: https://github.com/karma-runner/karma/pull/1098.
204+
// Note that the scenario outlined in the PR should no longer surface due to a check
205+
// ensuring that the line always is non-zero, but there could be other parsing errors.
206+
it('should handle source map errors gracefully', (done) => {
207+
formatError = m.createErrorFormatter({ basePath: '', hostname: 'localhost', port: 123 }, emitter,
208+
MockSourceMapConsumerWithParseError)
209+
210+
const servedFiles = [new File('/a.js'), new File('/b.js')]
211+
servedFiles[0].sourceMap = { content: 'SOURCE MAP a.js' }
212+
servedFiles[1].sourceMap = { content: 'SOURCE MAP b.js' }
213+
214+
emitter.emit('file_list_modified', { served: servedFiles })
215+
216+
_.defer(() => {
217+
const ERROR = 'at http://localhost:123/base/b.js:2:6'
218+
expect(formatError(ERROR)).to.equal('at b.js:2:6\n')
219+
expect(logWarnStub.callCount).to.equal(2)
220+
expect(logWarnStub).to.have.been.calledWith('An unexpected error occurred while resolving the original position for: http://localhost:123/base/b.js:2:6')
221+
expect(logWarnStub).to.have.been.calledWith(sinon.match({ message: 'Fake parse error from source map consumer' }))
222+
done()
223+
})
224+
})
225+
226+
// Generated code can be added by e.g. TypeScript or Babel when it transforms
227+
// native async/await to generators. Calls would then be wrapped with a helper
228+
// that is generated and does not map to anything, so-called generated code that
229+
// is allowed as case #1 in the source map spec.
230+
it('should not warn for trace file portion for generated code', (done) => {
231+
formatError = m.createErrorFormatter({ basePath: '', hostname: 'localhost', port: 123 }, emitter,
232+
MockSourceMapConsumerWithGeneratedCode)
233+
234+
const servedFiles = [new File('/a.js'), new File('/b.js')]
235+
servedFiles[0].sourceMap = { content: 'SOURCE MAP a.js' }
236+
servedFiles[1].sourceMap = { content: 'SOURCE MAP b.js' }
237+
238+
emitter.emit('file_list_modified', { served: servedFiles })
239+
240+
_.defer(() => {
241+
const ERROR = 'at http://localhost:123/base/b.js:2:6'
242+
expect(formatError(ERROR)).to.equal('at b.js:2:6\n')
243+
expect(logWarnStub.callCount).to.equal(0)
244+
done()
245+
})
246+
})
247+
173248
it('should rewrite stack traces (when basePath is empty)', (done) => {
174249
formatError = m.createErrorFormatter({ basePath: '', hostname: 'localhost', port: 123 }, emitter, MockSourceMapConsumer)
175250
const servedFiles = [new File('/a.js'), new File('/b.js')]

0 commit comments

Comments
 (0)