Files
vscode-debug-visualizer/data-extraction/src/js/api/DataExtractorApiImpl.ts
2023-11-19 12:48:20 +01:00

295 lines
7.3 KiB
TypeScript

import {
DataExtractorInfo,
VisualizationData,
} from "../../DataExtractionResult";
import * as helpers from "../helpers";
import {
CallFrameInfo,
CallFrameRequest,
CallFramesSnapshot,
DataExtraction,
DataExtractor,
DataExtractorApi,
DataExtractorContext,
DataResult,
JSONString,
SkippedCallFrames,
} from "./DataExtractorApi";
import { LoadDataExtractorsFn } from "./LoadDataExtractorsFn";
import { registerDefaultExtractors } from "./default-extractors";
/**
* @internal
*/
export class DataExtractorApiImpl implements DataExtractorApi {
public static lastContext: DataExtractorContext | undefined = undefined;
private readonly extractors = new Map<string, DataExtractor>();
private readonly extractorSources = new Map<string, LoadDataExtractorsFn>();
private toJson<TData>(data: TData): JSONString<TData> {
return JSON.stringify(data) as any;
}
public registerExtractor(extractor: DataExtractor): void {
this.extractors.set(extractor.id, extractor);
}
public registerExtractors(extractors: DataExtractor[]): void {
for (const e of extractors) {
this.registerExtractor(e);
}
}
public getData(
valueFn: () => unknown,
evalFn: <T>(expression: string) => T,
preferredDataExtractorId: string | undefined,
variablesInScope: Record<string, () => unknown>,
callFramesSnapshot: CallFramesSnapshot | null
): JSONString<DataResult> {
const callFrameRequests: CallFrameRequest[] = [];
const rootContext = new ContextImpl(
variablesInScope,
removeEnd(removeStart(valueFn.toString(), "() => ("), ")").trim(),
evalFn,
this,
undefined,
callFramesSnapshot?.frames ?? [],
callFrameRequests
);
DataExtractorApiImpl.lastContext = rootContext;
const value = valueFn();
const extractions = this.getExtractions(value, rootContext);
DataExtractorApiImpl.lastContext = undefined;
const requestId =
callFrameRequests.length === 0
? ""
: "" + cyrb53(JSON.stringify(callFrameRequests));
if ((callFramesSnapshot?.requestId ?? "") !== requestId) {
return this.toJson({
kind: "OutdatedCallFrameSnapshot",
callFramesRequest: {
requestId,
requestedCallFrames: callFrameRequests,
},
});
}
let usedExtraction = extractions[0];
if (!usedExtraction) {
return this.toJson({ kind: "NoExtractors" } as DataResult);
}
if (preferredDataExtractorId) {
const preferred = extractions.find(
(e) => e.id === preferredDataExtractorId
);
if (preferred) {
usedExtraction = preferred;
}
}
try {
const data = usedExtraction.extractData();
return this.toJson({
kind: "Data",
extractionResult: {
data,
usedExtractor: mapExtractor(usedExtraction),
availableExtractors: extractions.map(mapExtractor),
},
} as DataResult);
} catch (e) {
return this.toJson({
kind: "Data",
extractionResult: {
data: visualizeError(e),
usedExtractor: mapExtractor(usedExtraction),
availableExtractors: extractions.map(mapExtractor),
},
} as DataResult);
}
}
public getExtractions(
value: unknown,
context: DataExtractorContext
): DataExtraction[] {
const extractions = new Array<DataExtraction>();
const extractors = new Array<DataExtractor>();
for (const fn of this.extractorSources.values()) {
try {
fn((extractor) => {
extractors.push(extractor);
}, helpers);
} catch (e) {
console.error('Error in data extractor source', fn, e);
}
}
for (const extractor of [...this.extractors.values(), ...extractors]) {
if (extractor.dataCtor !== undefined) {
if (
typeof value !== "object" ||
value === null ||
value.constructor.name !== extractor.dataCtor
) {
continue;
}
}
try {
extractor.getExtractions(
value,
{
addExtraction(extraction) {
if (extraction.id === undefined) {
extraction.id = extractor.id;
}
if (extraction.name === undefined) {
extraction.name = extractor.id;
}
extractions.push(extraction);
},
},
context
);
} catch (e) {
extractions.push({
id: extractor.id,
name: extractor.id,
priority: 0,
extractData() {
return visualizeError(e);
},
});
}
}
extractions.sort((a, b) => b.priority - a.priority);
return extractions;
}
public registerDefaultExtractors(preferExisting: boolean = false): void {
// TODO consider preferExisting
registerDefaultExtractors(this);
}
public setDataExtractorFn(
id: string,
fn: LoadDataExtractorsFn | undefined
): void {
if (fn) {
this.extractorSources.set(id, fn);
} else {
this.extractorSources.delete(id);
}
}
}
function visualizeError(e: unknown | Error): VisualizationData {
return helpers.asData({
kind: { text: true },
fileName: 'error.md',
text: `# Error while running data extractor\n\n${formatErrorStr(e)}`,
});
}
function formatErrorStr(e: unknown | Error): string {
if (e instanceof Error) {
return `${e.message}\n\nStack:\n${e.stack}`;
}
return "" + e;
}
function mapExtractor(e: DataExtraction): DataExtractorInfo {
return {
id: e.id! as any,
name: e.name!,
priority: e.priority,
};
}
class ContextImpl implements DataExtractorContext {
constructor(
public readonly variablesInScope: Record<string, () => unknown>,
public readonly expression: string | undefined,
public readonly evalFn: <T>(expression: string) => T,
private readonly _api: DataExtractorApiImpl,
private readonly _parent: ContextImpl | undefined,
public readonly callFrameInfos: readonly (
| CallFrameInfo
| SkippedCallFrames
)[],
private readonly _callFrameRequests: CallFrameRequest[]
) { }
addCallFrameRequest(request: CallFrameRequest): void {
this._callFrameRequests.push(request);
}
get _level(): number {
return this._parent ? this._parent._level + 1 : 0;
}
extract(value: any): VisualizationData | undefined {
if (this._level > 10) {
throw new Error("extract() called too many times recursively");
}
const extractions = this._api.getExtractions(
value,
new ContextImpl(
this.variablesInScope,
undefined,
this.evalFn,
this._api,
this,
this.callFrameInfos,
this._callFrameRequests
)
);
if (extractions.length === 0) {
return undefined;
}
return extractions[0].extractData();
}
}
function removeStart(str: string, start: string): string {
if (str.startsWith(start)) {
return str.substr(start.length);
}
return str;
}
function removeEnd(str: string, end: string): string {
if (str.endsWith(end)) {
return str.substr(0, str.length - end.length);
}
return str;
}
// From https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
function cyrb53(str: string, seed = 0) {
let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}