diff options
Diffstat (limited to 'packages/bun-debug-adapter-protocol/debugger')
6 files changed, 299 insertions, 74 deletions
diff --git a/packages/bun-debug-adapter-protocol/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/debugger/adapter.ts index 6af7d9ea6..ed40c86e4 100644 --- a/packages/bun-debug-adapter-protocol/debugger/adapter.ts +++ b/packages/bun-debug-adapter-protocol/debugger/adapter.ts @@ -5,7 +5,7 @@ import { WebSocketInspector } from "../../bun-inspector-protocol"; import type { ChildProcess } from "node:child_process"; import { spawn, spawnSync } from "node:child_process"; import capabilities from "./capabilities"; -import { SourceMap } from "./sourcemap"; +import { Location, SourceMap } from "./sourcemap"; import { compare, parse } from "semver"; type InitializeRequest = DAP.InitializeRequest & { @@ -509,31 +509,37 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener { #generatedLocation(source: Source, line?: number, column?: number): JSC.Debugger.Location { const { sourceMap, scriptId, path } = source; - const { line: line0, column: column0 } = sourceMap.generatedLocation( - this.#lineTo0BasedLine(line), - this.#columnTo0BasedColumn(column), - path, - ); + const { line: gline, column: gcolumn } = sourceMap.generatedLocation({ + line: this.#lineTo0BasedLine(line), + column: this.#columnTo0BasedColumn(column), + url: path, + }); return { scriptId, - lineNumber: line0, - columnNumber: column0, + lineNumber: gline, + columnNumber: gcolumn, }; } #lineTo0BasedLine(line?: number): number { + if (!numberIsValid(line)) { + return 0; + } if (this.#initialized?.linesStartAt1) { - return line ? line - 1 : 0; + return line - 1; } - return line ?? 0; + return line; } #columnTo0BasedColumn(column?: number): number { + if (!numberIsValid(column)) { + return 0; + } if (this.#initialized?.columnsStartAt1) { - return column ? column - 1 : 0; + return column - 1; } - return column ?? 0; + return column; } #originalLocation( @@ -548,26 +554,26 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener { } const { sourceMap } = source; - const { line: line0, column: column0 } = sourceMap.originalLocation(line, column); + const { line: oline, column: ocolumn } = sourceMap.originalLocation({ line, column }); return { - line: this.#lineFrom0BasedLine(line0), - column: this.#columnFrom0BasedColumn(column0), + line: this.#lineFrom0BasedLine(oline), + column: this.#columnFrom0BasedColumn(ocolumn), }; } #lineFrom0BasedLine(line?: number): number { if (this.#initialized?.linesStartAt1) { - return line ? line + 1 : 1; + return numberIsValid(line) ? line + 1 : 1; } - return line ?? 0; + return numberIsValid(line) ? line : 0; } #columnFrom0BasedColumn(column?: number): number { if (this.#initialized?.columnsStartAt1) { - return column ? column + 1 : 1; + return numberIsValid(column) ? column + 1 : 1; } - return column ?? 0; + return numberIsValid(column) ? column : 0; } async setBreakpoints(request: DAP.SetBreakpointsRequest): Promise<DAP.SetBreakpointsResponse> { @@ -944,6 +950,15 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener { ["Debugger.paused"](event: JSC.Debugger.PausedEvent): void { const { reason, callFrames, asyncStackTrace, data } = event; + if (reason === "PauseOnNextStatement") { + for (const { functionName } of callFrames) { + if (functionName === "module code") { + this.#send("Debugger.resume"); + return; + } + } + } + this.#stackFrames.length = 0; this.#stopped ||= stoppedReason(reason); for (const callFrame of callFrames) { @@ -1143,21 +1158,26 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener { const { scriptId } = location; const source = this.#getSourceIfPresent(scriptId); - let { lineNumber, columnNumber } = location; + let originalLocation: Location; if (source) { - const { line, column } = this.#originalLocation(source, location); - lineNumber = line; - columnNumber = column; + originalLocation = this.#originalLocation(source, location); + } else { + const { lineNumber, columnNumber } = location; + originalLocation = { + line: this.#lineFrom0BasedLine(lineNumber), + column: this.#columnFrom0BasedColumn(columnNumber), + }; } + const { line, column } = originalLocation; const scopes: Scope[] = []; const stackFrame: StackFrame = { callFrameId, scriptId, id: this.#stackFrames.length, name: functionName || "<anonymous>", - line: lineNumber, - column: columnNumber || 0, + line, + column, presentationHint: stackFramePresentationHint(source?.path), source, scopes, @@ -1166,30 +1186,39 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener { for (const scope of scopeChain) { const { name, type, location, object, empty } = scope; - if (empty || !location) { + if (empty) { continue; } - const { scriptId } = location; - const source = this.#getSourceIfPresent(scriptId); + const { variablesReference } = this.#addVariable(object); const presentationHint = scopePresentationHint(type); const title = presentationHint ? titleize(presentationHint) : "Unknown"; const displayName = name ? `${title}: ${name}` : title; - let { lineNumber, columnNumber } = location; - if (source) { - const { line, column } = this.#originalLocation(source, location); - lineNumber = line; - columnNumber = column; + let originalLocation: Location | undefined; + if (location) { + const { scriptId } = location; + const source = this.#getSourceIfPresent(scriptId); + + if (source) { + originalLocation = this.#originalLocation(source, location); + } else { + const { lineNumber, columnNumber } = location; + originalLocation = { + line: this.#lineFrom0BasedLine(lineNumber), + column: this.#columnFrom0BasedColumn(columnNumber), + }; + } } + const { line, column } = originalLocation ?? {}; scopes.push({ name: displayName, presentationHint, expensive: presentationHint === "globals", variablesReference, - line: lineNumber, - column: columnNumber, + line, + column, source, }); } @@ -1214,20 +1243,25 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener { const callFrameId = callFrameToId(callFrame); const source = this.#getSourceIfPresent(scriptId); - let { lineNumber, columnNumber } = callFrame; + let originalLocation: Location; if (source) { - const { line, column } = this.#originalLocation(source, callFrame); - lineNumber = line; - columnNumber = column; + originalLocation = this.#originalLocation(source, callFrame); + } else { + const { lineNumber, columnNumber } = callFrame; + originalLocation = { + line: this.#lineFrom0BasedLine(lineNumber), + column: this.#columnFrom0BasedColumn(columnNumber), + }; } + const { line, column } = originalLocation; const stackFrame: StackFrame = { callFrameId, scriptId, id: this.#stackFrames.length, name: functionName || "<anonymous>", - line: lineNumber, - column: columnNumber, + line, + column, source, presentationHint: stackFramePresentationHint(source?.path), canRestart: false, @@ -1731,3 +1765,7 @@ function consoleLevelToAnsiColor(level: JSC.Console.ConsoleMessage["level"]): st } return undefined; } + +function numberIsValid(number?: number): number is number { + return typeof number === "number" && isFinite(number) && number >= 0; +} diff --git a/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.js b/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.js new file mode 100644 index 000000000..6c16a1202 --- /dev/null +++ b/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.js @@ -0,0 +1,36 @@ +"use strict"; +export default { + fetch(request) { + const animal = getAnimal(request.url); + const voice = animal.talk(); + return new Response(voice); + }, +}; +function getAnimal(query) { + switch (query.split("/").pop()) { + case "dog": + return new Dog(); + case "cat": + return new Cat(); + } + return new Bird(); +} +class Dog { + name = "dog"; + talk() { + return "woof"; + } +} +class Cat { + name = "cat"; + talk() { + return "meow"; + } +} +class Bird { + name = "bird"; + talk() { + return "chirp"; + } +} +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsicGFja2FnZXMvYnVuLWRlYnVnLWFkYXB0ZXItcHJvdG9jb2wvZGVidWdnZXIvZml4dHVyZXMvd2l0aC1zb3VyY2VtYXAudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImV4cG9ydCBkZWZhdWx0IHtcbiAgZmV0Y2gocmVxdWVzdDogUmVxdWVzdCk6IFJlc3BvbnNlIHtcbiAgICBjb25zdCBhbmltYWwgPSBnZXRBbmltYWwocmVxdWVzdC51cmwpO1xuICAgIGNvbnN0IHZvaWNlID0gYW5pbWFsLnRhbGsoKTtcbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKHZvaWNlKTtcbiAgfSxcbn07XG5cbmZ1bmN0aW9uIGdldEFuaW1hbChxdWVyeTogc3RyaW5nKTogQW5pbWFsIHtcbiAgc3dpdGNoIChxdWVyeS5zcGxpdChcIi9cIikucG9wKCkpIHtcbiAgICBjYXNlIFwiZG9nXCI6XG4gICAgICByZXR1cm4gbmV3IERvZygpO1xuICAgIGNhc2UgXCJjYXRcIjpcbiAgICAgIHJldHVybiBuZXcgQ2F0KCk7XG4gIH1cbiAgcmV0dXJuIG5ldyBCaXJkKCk7XG59XG5cbmludGVyZmFjZSBBbmltYWwge1xuICByZWFkb25seSBuYW1lOiBzdHJpbmc7XG4gIHRhbGsoKTogc3RyaW5nO1xufVxuXG5jbGFzcyBEb2cgaW1wbGVtZW50cyBBbmltYWwge1xuICBuYW1lID0gXCJkb2dcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwid29vZlwiO1xuICB9XG59XG5cbmNsYXNzIENhdCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImNhdFwiO1xuXG4gIHRhbGsoKTogc3RyaW5nIHtcbiAgICByZXR1cm4gXCJtZW93XCI7XG4gIH1cbn1cblxuY2xhc3MgQmlyZCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImJpcmRcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwiY2hpcnBcIjtcbiAgfVxufVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFBLGVBQWU7QUFBQSxFQUNiLE1BQU0sU0FBNEI7QUFDaEMsVUFBTSxTQUFTLFVBQVUsUUFBUSxHQUFHO0FBQ3BDLFVBQU0sUUFBUSxPQUFPLEtBQUs7QUFDMUIsV0FBTyxJQUFJLFNBQVMsS0FBSztBQUFBLEVBQzNCO0FBQ0Y7QUFFQSxTQUFTLFVBQVUsT0FBdUI7QUFDeEMsVUFBUSxNQUFNLE1BQU0sR0FBRyxFQUFFLElBQUksR0FBRztBQUFBLElBQzlCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLElBQ2pCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLEVBQ25CO0FBQ0EsU0FBTyxJQUFJLEtBQUs7QUFDbEI7QUFPQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLEtBQXVCO0FBQUEsRUFDM0IsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7IiwKICAibmFtZXMiOiBbXQp9Cg== diff --git a/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.ts b/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.ts new file mode 100644 index 000000000..f245ebf76 --- /dev/null +++ b/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.ts @@ -0,0 +1,46 @@ +export default { + fetch(request: Request): Response { + const animal = getAnimal(request.url); + const voice = animal.talk(); + return new Response(voice); + }, +}; + +function getAnimal(query: string): Animal { + switch (query.split("/").pop()) { + case "dog": + return new Dog(); + case "cat": + return new Cat(); + } + return new Bird(); +} + +interface Animal { + readonly name: string; + talk(): string; +} + +class Dog implements Animal { + name = "dog"; + + talk(): string { + return "woof"; + } +} + +class Cat implements Animal { + name = "cat"; + + talk(): string { + return "meow"; + } +} + +class Bird implements Animal { + name = "bird"; + + talk(): string { + return "chirp"; + } +} diff --git a/packages/bun-debug-adapter-protocol/debugger/fixtures/without-sourcemap.js b/packages/bun-debug-adapter-protocol/debugger/fixtures/without-sourcemap.js new file mode 100644 index 000000000..6a5d9a948 --- /dev/null +++ b/packages/bun-debug-adapter-protocol/debugger/fixtures/without-sourcemap.js @@ -0,0 +1,20 @@ +export default { + fetch(request) { + return new Response(a()); + }, +}; + +function a() { + return b(); +} + +function b() { + return c(); +} + +function c() { + function d() { + return "hello"; + } + return d(); +} diff --git a/packages/bun-debug-adapter-protocol/debugger/sourcemap.test.ts b/packages/bun-debug-adapter-protocol/debugger/sourcemap.test.ts new file mode 100644 index 000000000..44d9ca362 --- /dev/null +++ b/packages/bun-debug-adapter-protocol/debugger/sourcemap.test.ts @@ -0,0 +1,31 @@ +import { test, expect } from "bun:test"; +import { readFileSync } from "node:fs"; +import { SourceMap } from "./sourcemap"; + +test("works without source map", () => { + const sourceMap = getSourceMap("without-sourcemap.js"); + expect(sourceMap.generatedLocation({ line: 7 })).toEqual({ line: 7, column: 0, verified: true }); + expect(sourceMap.generatedLocation({ line: 7, column: 2 })).toEqual({ line: 7, column: 2, verified: true }); + expect(sourceMap.originalLocation({ line: 11 })).toEqual({ line: 11, column: 0, verified: true }); + expect(sourceMap.originalLocation({ line: 11, column: 2 })).toEqual({ line: 11, column: 2, verified: true }); +}); + +test("works with source map", () => { + const sourceMap = getSourceMap("with-sourcemap.js"); + // FIXME: Columns don't appear to be accurate for `generatedLocation` + expect(sourceMap.generatedLocation({ line: 3 })).toMatchObject({ line: 4, verified: true }); + expect(sourceMap.generatedLocation({ line: 27 })).toMatchObject({ line: 20, verified: true }); + expect(sourceMap.originalLocation({ line: 32 })).toEqual({ line: 43, column: 4, verified: true }); + expect(sourceMap.originalLocation({ line: 13 })).toEqual({ line: 13, column: 6, verified: true }); +}); + +function getSourceMap(filename: string): SourceMap { + const { pathname } = new URL(`./fixtures/${filename}`, import.meta.url); + const source = readFileSync(pathname, "utf-8"); + const match = source.match(/\/\/# sourceMappingURL=(.*)$/m); + if (match) { + const [, url] = match; + return SourceMap(url); + } + return SourceMap(); +} diff --git a/packages/bun-debug-adapter-protocol/debugger/sourcemap.ts b/packages/bun-debug-adapter-protocol/debugger/sourcemap.ts index eeceb520f..adb6dc57d 100644 --- a/packages/bun-debug-adapter-protocol/debugger/sourcemap.ts +++ b/packages/bun-debug-adapter-protocol/debugger/sourcemap.ts @@ -1,13 +1,28 @@ +import type { LineRange, MappedPosition } from "source-map-js"; import { SourceMapConsumer } from "source-map-js"; -export type Location = { - line: number; - column: number; +export type LocationRequest = { + line?: number; + column?: number; + url?: string; }; +export type Location = { + line: number; // 0-based + column: number; // 0-based +} & ( + | { + verified: true; + } + | { + verified?: false; + message?: string; + } +); + export interface SourceMap { - generatedLocation(line?: number, column?: number, url?: string): Location; - originalLocation(line?: number, column?: number): Location; + generatedLocation(request: LocationRequest): Location; + originalLocation(request: LocationRequest): Location; } class ActualSourceMap implements SourceMap { @@ -21,11 +36,11 @@ class ActualSourceMap implements SourceMap { #getSource(url?: string): string { const sources = this.#sources; - if (sources.length === 1) { - return sources[0]; + if (!sources.length) { + return ""; } - if (!url) { - return sources[0] ?? ""; + if (sources.length === 1 || !url) { + return sources[0]; } for (const source of sources) { if (url.endsWith(source)) { @@ -35,61 +50,87 @@ class ActualSourceMap implements SourceMap { return ""; } - generatedLocation(line?: number, column?: number, url?: string): Location { + generatedLocation(request: LocationRequest): Location { + const { line, column, url } = request; + let lineRange: LineRange; try { const source = this.#getSource(url); - const { line: gline, column: gcolumn } = this.#sourceMap.generatedPositionFor({ + lineRange = this.#sourceMap.generatedPositionFor({ line: lineTo1BasedLine(line), column: columnToColumn(column), source, }); - console.log(`[sourcemap] -->`, { source, url, line, column }, { gline, gcolumn }); + } catch (error) { return { - line: lineTo0BasedLine(gline), - column: columnToColumn(gcolumn), + line: lineToLine(line), + column: columnToColumn(column), + verified: false, + message: unknownToError(error), }; - } catch (error) { - console.warn(error); + } + if (!locationIsValid(lineRange)) { return { line: lineToLine(line), column: columnToColumn(column), + verified: false, }; } + const { line: gline, column: gcolumn } = lineRange; + return { + line: lineToLine(gline), + column: columnToColumn(gcolumn), + verified: true, + }; } - originalLocation(line?: number, column?: number): Location { + originalLocation(request: LocationRequest): Location { + const { line, column } = request; + let mappedPosition: MappedPosition; try { - const { line: oline, column: ocolumn } = this.#sourceMap.originalPositionFor({ + mappedPosition = this.#sourceMap.originalPositionFor({ line: lineTo1BasedLine(line), column: columnToColumn(column), }); - console.log(`[sourcemap] <--`, { line, column }, { oline, ocolumn }); + } catch (error) { return { - line: lineTo0BasedLine(oline), - column: columnToColumn(ocolumn), + line: lineToLine(line), + column: columnToColumn(column), + verified: false, + message: unknownToError(error), }; - } catch (error) { - console.warn(error); + } + if (!locationIsValid(mappedPosition)) { return { line: lineToLine(line), column: columnToColumn(column), + verified: false, }; } + const { line: oline, column: ocolumn } = mappedPosition; + return { + line: lineTo0BasedLine(oline), + column: columnToColumn(ocolumn), + verified: true, + }; } } class NoopSourceMap implements SourceMap { - generatedLocation(line?: number, column?: number, url?: string): Location { + generatedLocation(request: LocationRequest): Location { + const { line, column } = request; return { line: lineToLine(line), column: columnToColumn(column), + verified: true, }; } - originalLocation(line?: number, column?: number): Location { + originalLocation(request: LocationRequest): Location { + const { line, column } = request; return { line: lineToLine(line), column: columnToColumn(column), + verified: true, }; } } @@ -104,10 +145,6 @@ export function SourceMap(url?: string): SourceMap { const [_, base64] = url.split(",", 2); const decoded = Buffer.from(base64, "base64url").toString("utf8"); const schema = JSON.parse(decoded); - // HACK: Bun is sometimes sending invalid mappings - try { - schema.mappings = schema.mappings.replace(/[^a-z,;]/gi, "").slice(1); - } catch {} const sourceMap = new SourceMapConsumer(schema); return new ActualSourceMap(sourceMap); } catch (error) { @@ -117,17 +154,34 @@ export function SourceMap(url?: string): SourceMap { } function lineTo1BasedLine(line?: number): number { - return line ? line + 1 : 1; + return numberIsValid(line) ? line + 1 : 1; } function lineTo0BasedLine(line?: number): number { - return line ? line - 1 : 0; + return numberIsValid(line) ? line - 1 : 0; } function lineToLine(line?: number): number { - return line ?? 0; + return numberIsValid(line) ? line : 0; } function columnToColumn(column?: number): number { - return column ?? 0; + return numberIsValid(column) ? column : 0; +} + +function locationIsValid(location: Location): location is Location { + const { line, column } = location; + return numberIsValid(line) && numberIsValid(column); +} + +function numberIsValid(number?: number): number is number { + return typeof number === "number" && isFinite(number) && number >= 0; +} + +function unknownToError(error: unknown): string { + if (error instanceof Error) { + const { message } = error; + return message; + } + return String(error); } |