UNPKG

chrome-devtools-frontend

Version:
202 lines (163 loc) • 6.39 kB
// Copyright 2024 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import {PAGE_EXPOSED_FUNCTIONS} from './injected.js'; export function formatError(message: string): string { return `Error: ${message}`; } export class SideEffectError extends Error {} export interface GetErrorStackOutput { message: string; stack?: string; } /* istanbul ignore next */ export function getErrorStackOnThePage(this: Error): GetErrorStackOutput { return {stack: this.stack, message: this.message}; } /* istanbul ignore next */ export function stringifyObjectOnThePage(this: unknown): string { const seenBefore = new WeakMap(); return JSON.stringify(this, function replacer(this: unknown, key: string, value: unknown) { if (typeof value === 'object' && value !== null) { if (seenBefore.has(value)) { return '(cycle)'; } seenBefore.set(value, true); } if (value instanceof HTMLElement) { const idAttribute = value.id ? ` id="${value.id}"` : ''; const classAttribute = value.classList.value ? ` class="${value.classList.value}"` : ''; return `<${value.nodeName.toLowerCase()}${idAttribute}${classAttribute}>${value.hasChildNodes() ? '...' : ''}</${ value.nodeName.toLowerCase()}>`; } if (this instanceof CSSStyleDeclaration) { // Do not add number keys to the output. if (!isNaN(Number(key))) { return undefined; } } return value; }); } export async function stringifyRemoteObject( object: SDK.RemoteObject.RemoteObject, functionDeclaration: string): Promise<string> { switch (object.type) { case Protocol.Runtime.RemoteObjectType.String: return `'${object.value}'`; case Protocol.Runtime.RemoteObjectType.Bigint: return `${object.value}n`; case Protocol.Runtime.RemoteObjectType.Boolean: case Protocol.Runtime.RemoteObjectType.Number: return `${object.value}`; case Protocol.Runtime.RemoteObjectType.Undefined: return 'undefined'; case Protocol.Runtime.RemoteObjectType.Symbol: case Protocol.Runtime.RemoteObjectType.Function: return `${object.description}`; case Protocol.Runtime.RemoteObjectType.Object: { if (object.subtype === 'error') { const res = await object.callFunctionJSON(getErrorStackOnThePage, []); if (!res) { throw new Error('Could not stringify the object' + object); } return EvaluateAction.stringifyError(res, functionDeclaration); } const res = await object.callFunction(stringifyObjectOnThePage); if (!res.object || res.object.type !== Protocol.Runtime.RemoteObjectType.String) { throw new Error('Could not stringify the object' + object); } return res.object.value; } default: throw new Error('Unknown type to stringify ' + object.type); } } export interface Options { throwOnSideEffect: boolean; } export class EvaluateAction { static async execute( functionDeclaration: string, args: SDK.RemoteObject.RemoteObject[], executionContext: SDK.RuntimeModel.ExecutionContext, {throwOnSideEffect}: Options): Promise<string> { if (executionContext.debuggerModel.selectedCallFrame()) { return formatError('Cannot evaluate JavaScript because the execution is paused on a breakpoint.'); } const response = await executionContext.callFunctionOn({ functionDeclaration, returnByValue: false, allowUnsafeEvalBlockedByCSP: false, throwOnSideEffect, userGesture: true, awaitPromise: true, arguments: args.map(remoteObject => { return {objectId: remoteObject.objectId}; }), }); try { if (!response) { throw new Error('Response is not found'); } if ('error' in response) { return formatError(response.error); } if (response.exceptionDetails) { const exceptionDescription = response.exceptionDetails.exception?.description; if (SDK.RuntimeModel.RuntimeModel.isSideEffectFailure(response)) { throw new SideEffectError(exceptionDescription); } return formatError(exceptionDescription ?? 'JS exception'); } return await stringifyRemoteObject(response.object, functionDeclaration); } finally { executionContext.runtimeModel.releaseEvaluationResult(response); } } static getExecutedLineFromStack(stack: string, pageExposedFunctions: string[]): number|null { const lines = stack.split('\n'); const stackLines = lines.map(curr => curr.trim()).filter(trimmedLine => { return trimmedLine.startsWith('at'); }); const selectedStack = stackLines.find(stackLine => { const splittedStackLine = stackLine.split(' '); if (splittedStackLine.length < 2) { return false; } const signature = splittedStackLine[1] === 'async' ? splittedStackLine[2] : // if the stack line contains async the function name is the next element splittedStackLine[1]; const lastDotIndex = signature.lastIndexOf('.'); const functionName = lastDotIndex !== -1 ? signature.substring(lastDotIndex + 1) : signature; return !pageExposedFunctions.includes(functionName); }); if (!selectedStack) { return null; } const frameLocationRegex = /:(\d+)(?::\d+)?\)?$/; const match = selectedStack.match(frameLocationRegex); if (!match?.[1]) { return null; } const lineNum = parseInt(match[1], 10); if (isNaN(lineNum)) { return null; } return lineNum - 1; } static stringifyError(result: GetErrorStackOutput, functionDeclaration: string): string { if (!result.stack) { return `Error: ${result.message}`; } const lineNum = EvaluateAction.getExecutedLineFromStack(result.stack, PAGE_EXPOSED_FUNCTIONS); if (!lineNum) { return `Error: ${result.message}`; } const functionLines = functionDeclaration.split('\n'); const errorLine = functionLines[lineNum]; if (!errorLine) { return `Error: ${result.message}`; } return `Error: executing the line "${errorLine.trim()}" failed with the following error:\n${result.message}`; } }