| // Copyright 2022 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 Common from '../../core/common/common.js'; |
| import * as Root from '../../core/root/root.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import * as Protocol from '../../generated/protocol.js'; |
| import * as Bindings from '../bindings/bindings.js'; |
| import * as Formatter from '../formatter/formatter.js'; |
| import * as TextUtils from '../text_utils/text_utils.js'; |
| |
| interface CachedScopeMap { |
| sourceMap: SDK.SourceMap.SourceMap|undefined; |
| mappingPromise: Promise<{variableMapping: Map<string, string>, thisMapping: string|null}>; |
| } |
| |
| const scopeToCachedIdentifiersMap = new WeakMap<Formatter.FormatterWorkerPool.ScopeTreeNode, CachedScopeMap>(); |
| const cachedMapByCallFrame = new WeakMap<SDK.DebuggerModel.CallFrame, Map<string, string|null>>(); |
| |
| export async function getTextFor(contentProvider: TextUtils.ContentProvider.ContentProvider): |
| Promise<TextUtils.Text.Text|null> { |
| const contentData = await contentProvider.requestContentData(); |
| if (TextUtils.ContentData.ContentData.isError(contentData) || !contentData.isTextContent) { |
| return null; |
| } |
| return contentData.textObj; |
| } |
| |
| export class IdentifierPositions { |
| name: string; |
| positions: Array<{lineNumber: number, columnNumber: number}>; |
| |
| constructor(name: string, positions: Array<{lineNumber: number, columnNumber: number}> = []) { |
| this.name = name; |
| this.positions = positions; |
| } |
| |
| addPosition(lineNumber: number, columnNumber: number): void { |
| this.positions.push({lineNumber, columnNumber}); |
| } |
| } |
| |
| const computeScopeTree = async function(script: SDK.Script.Script): Promise<{ |
| scopeTree: |
| Formatter.FormatterWorkerPool.ScopeTreeNode, text: TextUtils.Text.Text, |
| }|null> { |
| if (!script.sourceMapURL) { |
| return null; |
| } |
| |
| return await SDK.ScopeTreeCache.scopeTreeForScript(script); |
| }; |
| |
| /** |
| * @returns the scope chain from outer-most to inner-most scope where the inner-most |
| * scope either contains or matches the "needle". |
| */ |
| const findScopeChain = function( |
| scopeTree: Formatter.FormatterWorkerPool.ScopeTreeNode, |
| scopeNeedle: {start: number, end: number}): Formatter.FormatterWorkerPool.ScopeTreeNode[] { |
| if (!contains(scopeTree, scopeNeedle)) { |
| return []; |
| } |
| |
| // Find the corresponding scope in the scope tree. |
| let containingScope = scopeTree; |
| const scopeChain = [scopeTree]; |
| while (true) { |
| let childFound = false; |
| for (const child of containingScope.children) { |
| if (contains(child, scopeNeedle)) { |
| // We found a nested containing scope, continue with search there. |
| scopeChain.push(child); |
| containingScope = child; |
| childFound = true; |
| break; |
| } |
| // Sanity check: |scope| should not straddle any of the scopes in the tree. That is: |
| // Either |scope| is disjoint from |child| or |child| must be inside |scope|. |
| // (Or the |scope| is inside |child|, but that case is covered above.) |
| if (!disjoint(scopeNeedle, child) && !contains(scopeNeedle, child)) { |
| console.error('Wrong nesting of scopes'); |
| return []; |
| } |
| } |
| if (!childFound) { |
| // We found the deepest scope in the tree that contains our scope chain entry. |
| break; |
| } |
| } |
| |
| return scopeChain; |
| |
| function contains(scope: {start: number, end: number}, candidate: {start: number, end: number}): boolean { |
| return (scope.start <= candidate.start) && (scope.end >= candidate.end); |
| } |
| function disjoint(scope: {start: number, end: number}, other: {start: number, end: number}): boolean { |
| return (scope.end <= other.start) || (other.end <= scope.start); |
| } |
| }; |
| |
| export async function findScopeChainForDebuggerScope(scope: SDK.DebuggerModel.ScopeChainEntry): |
| Promise<Formatter.FormatterWorkerPool.ScopeTreeNode[]> { |
| const startLocation = scope.range()?.start; |
| const endLocation = scope.range()?.end; |
| if (!startLocation || !endLocation) { |
| return []; |
| } |
| |
| const script = startLocation.script(); |
| if (!script) { |
| return []; |
| } |
| |
| const scopeTreeAndText = await computeScopeTree(script); |
| if (!scopeTreeAndText) { |
| return []; |
| } |
| const {scopeTree, text} = scopeTreeAndText; |
| |
| // Compute the offset within the scope tree coordinate space. |
| const scopeOffsets = { |
| start: text.offsetFromPosition(startLocation.lineNumber, startLocation.columnNumber), |
| end: text.offsetFromPosition(endLocation.lineNumber, endLocation.columnNumber), |
| }; |
| |
| return findScopeChain(scopeTree, scopeOffsets); |
| } |
| |
| export const scopeIdentifiers = async function( |
| script: SDK.Script.Script, scope: Formatter.FormatterWorkerPool.ScopeTreeNode, |
| ancestorScopes: Formatter.FormatterWorkerPool.ScopeTreeNode[]): Promise<{ |
| freeVariables: |
| IdentifierPositions[], boundVariables: IdentifierPositions[], |
| }|null> { |
| const text = await getTextFor(script); |
| if (!text) { |
| return null; |
| } |
| |
| // Now we have containing scope. Collect all the scope variables. |
| const boundVariables = []; |
| const cursor = new TextUtils.TextCursor.TextCursor(text.lineEndings()); |
| for (const variable of scope.variables) { |
| // Skip the fixed-kind variable (i.e., 'this' or 'arguments') if we only found their "definition" |
| // without any uses. |
| if (variable.kind === Formatter.FormatterWorkerPool.DefinitionKind.FIXED && variable.offsets.length <= 1) { |
| continue; |
| } |
| |
| const identifier = new IdentifierPositions(variable.name); |
| for (const offset of variable.offsets) { |
| cursor.resetTo(offset); |
| identifier.addPosition(cursor.lineNumber(), cursor.columnNumber()); |
| } |
| boundVariables.push(identifier); |
| } |
| |
| // Compute free variables by collecting all the ancestor variables that are used in |containingScope|. |
| const freeVariables = []; |
| for (const ancestor of ancestorScopes) { |
| for (const ancestorVariable of ancestor.variables) { |
| let identifier = null; |
| for (const offset of ancestorVariable.offsets) { |
| if (offset >= scope.start && offset < scope.end) { |
| if (!identifier) { |
| identifier = new IdentifierPositions(ancestorVariable.name); |
| } |
| cursor.resetTo(offset); |
| identifier.addPosition(cursor.lineNumber(), cursor.columnNumber()); |
| } |
| } |
| if (identifier) { |
| freeVariables.push(identifier); |
| } |
| } |
| } |
| return {boundVariables, freeVariables}; |
| }; |
| |
| const identifierAndPunctuationRegExp = /^\s*([A-Za-z_$][A-Za-z_$0-9]*)\s*([.;,=]?)\s*$/; |
| |
| const enum Punctuation { |
| NONE = 'none', |
| COMMA = 'comma', |
| DOT = 'dot', |
| SEMICOLON = 'semicolon', |
| EQUALS = 'equals', |
| } |
| |
| const resolveDebuggerScope = async(scope: SDK.DebuggerModel.ScopeChainEntry): |
| Promise<{variableMapping: Map<string, string>, thisMapping: string | null}> => { |
| if (!Common.Settings.Settings.instance().moduleSetting('js-source-maps-enabled').get()) { |
| return {variableMapping: new Map(), thisMapping: null}; |
| } |
| const script = scope.callFrame().script; |
| const scopeChain = await findScopeChainForDebuggerScope(scope); |
| return await resolveScope(script, scopeChain); |
| }; |
| |
| const resolveScope = async(script: SDK.Script.Script, scopeChain: Formatter.FormatterWorkerPool.ScopeTreeNode[]): |
| Promise<{variableMapping: Map<string, string>, thisMapping: string | null}> => { |
| const parsedScope = scopeChain[scopeChain.length - 1]; |
| if (!parsedScope) { |
| return {variableMapping: new Map<string, string>(), thisMapping: null}; |
| } |
| let cachedScopeMap = scopeToCachedIdentifiersMap.get(parsedScope); |
| const sourceMap = script.sourceMap(); |
| |
| if (!cachedScopeMap || cachedScopeMap.sourceMap !== sourceMap) { |
| const identifiersPromise = |
| (async () => { |
| const variableMapping = new Map<string, string>(); |
| let thisMapping = null; |
| |
| if (!sourceMap) { |
| return {variableMapping, thisMapping}; |
| } |
| // Extract as much as possible from SourceMap and resolve |
| // missing identifier names from SourceMap ranges. |
| const promises: Array<Promise<void>> = []; |
| |
| const resolveEntry = (id: IdentifierPositions, handler: (sourceName: string) => void): void => { |
| // First see if we have a source map entry with a name for the identifier. |
| for (const position of id.positions) { |
| const entry = sourceMap.findEntry(position.lineNumber, position.columnNumber); |
| if (entry?.name) { |
| handler(entry.name); |
| return; |
| } |
| } |
| /** If there is no entry with the name field, try to infer the name from the source positions. **/ |
| async function resolvePosition(): Promise<void> { |
| if (!sourceMap) { |
| return; |
| } |
| // Let us find the first non-empty mapping of |id| and return that. Ideally, we would |
| // try to compute all the mappings and only use the mapping if all the non-empty |
| // mappings agree. However, that can be expensive for identifiers with many uses, |
| // so we iterate sequentially, stopping at the first non-empty mapping. |
| for (const position of id.positions) { |
| const sourceName = await resolveSourceName(script, sourceMap, id.name, position); |
| if (sourceName) { |
| handler(sourceName); |
| return; |
| } |
| } |
| } |
| promises.push(resolvePosition()); |
| }; |
| |
| const parsedVariables = await scopeIdentifiers(script, parsedScope, scopeChain.slice(0, -1)); |
| if (!parsedVariables) { |
| return {variableMapping, thisMapping}; |
| } |
| for (const id of parsedVariables.boundVariables) { |
| resolveEntry(id, sourceName => { |
| // Let use ignore 'this' mappings - those are handled separately. |
| if (sourceName !== 'this') { |
| variableMapping.set(id.name, sourceName); |
| } |
| }); |
| } |
| for (const id of parsedVariables.freeVariables) { |
| resolveEntry(id, sourceName => { |
| if (sourceName === 'this') { |
| thisMapping = id.name; |
| } |
| }); |
| } |
| await Promise.all(promises).then(getScopeResolvedForTest()); |
| return {variableMapping, thisMapping}; |
| })(); |
| cachedScopeMap = {sourceMap, mappingPromise: identifiersPromise}; |
| scopeToCachedIdentifiersMap.set(parsedScope, {sourceMap, mappingPromise: identifiersPromise}); |
| } |
| return await cachedScopeMap.mappingPromise; |
| |
| async function resolveSourceName( |
| script: SDK.Script.Script, sourceMap: SDK.SourceMap.SourceMap, name: string, |
| position: {lineNumber: number, columnNumber: number}): Promise<string|null> { |
| const ranges = sourceMap.findEntryRanges(position.lineNumber, position.columnNumber); |
| if (!ranges) { |
| return null; |
| } |
| // Extract the underlying text from the compiled code's range and make sure that |
| // it starts with the identifier |name|. |
| const uiSourceCode = |
| Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().uiSourceCodeForSourceMapSourceURL( |
| script.debuggerModel, ranges.sourceURL, script.isContentScript()); |
| if (!uiSourceCode) { |
| return null; |
| } |
| const compiledText = await getTextFor(script); |
| if (!compiledText) { |
| return null; |
| } |
| const compiledToken = compiledText.extract(ranges.range); |
| const parsedCompiledToken = extractIdentifier(compiledToken); |
| if (!parsedCompiledToken) { |
| return null; |
| } |
| const {name: compiledName, punctuation: compiledPunctuation} = parsedCompiledToken; |
| if (compiledName !== name) { |
| return null; |
| } |
| |
| // Extract the mapped name from the source code range and ensure that the punctuation |
| // matches the one from the compiled code. |
| const sourceText = await getTextFor(uiSourceCode); |
| if (!sourceText) { |
| return null; |
| } |
| const sourceToken = sourceText.extract(ranges.sourceRange); |
| const parsedSourceToken = extractIdentifier(sourceToken); |
| if (!parsedSourceToken) { |
| return null; |
| } |
| const {name: sourceName, punctuation: sourcePunctuation} = parsedSourceToken; |
| // Accept the source name if it is followed by the same punctuation. |
| if (compiledPunctuation === sourcePunctuation) { |
| return sourceName; |
| } |
| // Let us also allow semicolons into commas since that it is a common transformation. |
| if (compiledPunctuation === Punctuation.COMMA && sourcePunctuation === Punctuation.SEMICOLON) { |
| return sourceName; |
| } |
| |
| return null; |
| |
| function extractIdentifier(token: string): {name: string, punctuation: Punctuation}|null { |
| const match = token.match(identifierAndPunctuationRegExp); |
| if (!match) { |
| return null; |
| } |
| |
| const name = match[1]; |
| let punctuation: Punctuation|null = null; |
| switch (match[2]) { |
| case '.': |
| punctuation = Punctuation.DOT; |
| break; |
| case ',': |
| punctuation = Punctuation.COMMA; |
| break; |
| case ';': |
| punctuation = Punctuation.SEMICOLON; |
| break; |
| case '=': |
| punctuation = Punctuation.EQUALS; |
| break; |
| case '': |
| punctuation = Punctuation.NONE; |
| break; |
| default: |
| console.error(`Name token parsing error: unexpected token "${match[2]}"`); |
| return null; |
| } |
| |
| return {name, punctuation}; |
| } |
| } |
| }; |
| |
| export const resolveScopeChain = |
| async function(callFrame: SDK.DebuggerModel.CallFrame): Promise<SDK.DebuggerModel.ScopeChainEntry[]> { |
| const {pluginManager} = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(); |
| let scopeChain: SDK.DebuggerModel.ScopeChainEntry[]|null|undefined = await pluginManager.resolveScopeChain(callFrame); |
| if (scopeChain) { |
| return scopeChain; |
| } |
| |
| scopeChain = Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.USE_SOURCE_MAP_SCOPES) ? |
| callFrame.script.sourceMap()?.resolveScopeChain(callFrame) : |
| null; |
| if (scopeChain) { |
| return scopeChain; |
| } |
| |
| if (callFrame.script.isWasm()) { |
| return callFrame.scopeChain(); |
| } |
| const thisObject = await resolveThisObject(callFrame); |
| return callFrame.scopeChain().map(scope => new ScopeWithSourceMappedVariables(scope, thisObject)); |
| }; |
| |
| /** |
| * @returns A mapping from original name -> compiled name. If the orignal name is unavailable (e.g. because the compiled name was |
| * shadowed) we set it to `null`. |
| */ |
| export const allVariablesInCallFrame = |
| async(callFrame: SDK.DebuggerModel.CallFrame): Promise<Map<string, string|null>> => { |
| if (!Common.Settings.Settings.instance().moduleSetting('js-source-maps-enabled').get()) { |
| return new Map<string, string|null>(); |
| } |
| const cachedMap = cachedMapByCallFrame.get(callFrame); |
| if (cachedMap) { |
| return cachedMap; |
| } |
| |
| const scopeChain = callFrame.scopeChain(); |
| const nameMappings = await Promise.all(scopeChain.map(resolveDebuggerScope)); |
| const reverseMapping = new Map<string, string|null>(); |
| const compiledNames = new Set<string>(); |
| for (const {variableMapping} of nameMappings) { |
| for (const [compiledName, originalName] of variableMapping) { |
| if (!originalName) { |
| continue; |
| } |
| if (!reverseMapping.has(originalName)) { |
| // An inner scope might have shadowed {compiledName}. Mark it as "unavailable" in that case. |
| const compiledNameOrNull = compiledNames.has(compiledName) ? null : compiledName; |
| reverseMapping.set(originalName, compiledNameOrNull); |
| } |
| compiledNames.add(compiledName); |
| } |
| } |
| cachedMapByCallFrame.set(callFrame, reverseMapping); |
| return reverseMapping; |
| }; |
| |
| /** |
| * @returns A mapping from original name -> compiled name. If the orignal name is unavailable (e.g. because the compiled name was |
| * shadowed) we set it to `null`. |
| */ |
| export const allVariablesAtPosition = |
| async(location: SDK.DebuggerModel.Location): Promise<Map<string, string|null>> => { |
| const reverseMapping = new Map<string, string|null>(); |
| if (!Common.Settings.Settings.instance().moduleSetting('js-source-maps-enabled').get()) { |
| return reverseMapping; |
| } |
| const script = location.script(); |
| if (!script) { |
| return reverseMapping; |
| } |
| |
| const scopeTreeAndText = await computeScopeTree(script); |
| if (!scopeTreeAndText) { |
| return reverseMapping; |
| } |
| |
| const {scopeTree, text} = scopeTreeAndText; |
| const locationOffset = text.offsetFromPosition(location.lineNumber, location.columnNumber); |
| const scopeChain = findScopeChain(scopeTree, {start: locationOffset, end: locationOffset}); |
| const compiledNames = new Set<string>(); |
| |
| while (scopeChain.length > 0) { |
| const {variableMapping} = await resolveScope(script, scopeChain); |
| for (const [compiledName, originalName] of variableMapping) { |
| if (!originalName) { |
| continue; |
| } |
| if (!reverseMapping.has(originalName)) { |
| // An inner scope might have shadowed {compiledName}. Mark it as "unavailable" in that case. |
| const compiledNameOrNull = compiledNames.has(compiledName) ? null : compiledName; |
| reverseMapping.set(originalName, compiledNameOrNull); |
| } |
| compiledNames.add(compiledName); |
| } |
| scopeChain.pop(); |
| } |
| return reverseMapping; |
| }; |
| |
| export const resolveThisObject = |
| async(callFrame: SDK.DebuggerModel.CallFrame): Promise<SDK.RemoteObject.RemoteObject|null> => { |
| const scopeChain = callFrame.scopeChain(); |
| if (scopeChain.length === 0) { |
| return callFrame.thisObject(); |
| } |
| |
| const {thisMapping} = await resolveDebuggerScope(scopeChain[0]); |
| if (!thisMapping) { |
| return callFrame.thisObject(); |
| } |
| |
| const result = await callFrame.evaluate(({ |
| expression: thisMapping, |
| objectGroup: 'backtrace', |
| includeCommandLineAPI: false, |
| silent: true, |
| returnByValue: false, |
| generatePreview: true, |
| })); |
| if ('exceptionDetails' in result) { |
| return !result.exceptionDetails && result.object ? result.object : callFrame.thisObject(); |
| } |
| return null; |
| }; |
| |
| export const resolveScopeInObject = function(scope: SDK.DebuggerModel.ScopeChainEntry): SDK.RemoteObject.RemoteObject { |
| const endLocation = scope.range()?.end; |
| const startLocationScript = scope.range()?.start.script() ?? null; |
| |
| if (scope.type() === Protocol.Debugger.ScopeType.Global || !startLocationScript || !endLocation || |
| !startLocationScript.sourceMapURL) { |
| return scope.object(); |
| } |
| |
| return new RemoteObject(scope); |
| }; |
| |
| /** |
| * Wraps a debugger `Scope` but returns a scope object where variable names are |
| * mapped to their authored name. |
| * |
| * This implementation does not utilize source map "Scopes" information but obtains |
| * original variable names via parsing + mappings + names. |
| */ |
| class ScopeWithSourceMappedVariables implements SDK.DebuggerModel.ScopeChainEntry { |
| readonly #debuggerScope: SDK.DebuggerModel.ScopeChainEntry; |
| /** The resolved `this` of the current call frame */ |
| readonly #thisObject: SDK.RemoteObject.RemoteObject|null; |
| |
| constructor(scope: SDK.DebuggerModel.ScopeChainEntry, thisObject: SDK.RemoteObject.RemoteObject|null) { |
| this.#debuggerScope = scope; |
| this.#thisObject = thisObject; |
| } |
| |
| callFrame(): SDK.DebuggerModel.CallFrame { |
| return this.#debuggerScope.callFrame(); |
| } |
| |
| type(): string { |
| return this.#debuggerScope.type(); |
| } |
| |
| typeName(): string { |
| return this.#debuggerScope.typeName(); |
| } |
| |
| name(): string|undefined { |
| return this.#debuggerScope.name(); |
| } |
| |
| range(): SDK.DebuggerModel.LocationRange|null { |
| return this.#debuggerScope.range(); |
| } |
| |
| object(): SDK.RemoteObject.RemoteObject { |
| return resolveScopeInObject(this.#debuggerScope); |
| } |
| |
| description(): string { |
| return this.#debuggerScope.description(); |
| } |
| |
| icon(): string|undefined { |
| return this.#debuggerScope.icon(); |
| } |
| |
| extraProperties(): SDK.RemoteObject.RemoteObjectProperty[] { |
| const extraProperties = this.#debuggerScope.extraProperties(); |
| if (this.#thisObject && this.type() === Protocol.Debugger.ScopeType.Local) { |
| extraProperties.unshift(new SDK.RemoteObject.RemoteObjectProperty( |
| 'this', this.#thisObject, undefined, undefined, undefined, undefined, undefined, /* synthetic */ true)); |
| } |
| return extraProperties; |
| } |
| } |
| |
| export class RemoteObject extends SDK.RemoteObject.RemoteObject { |
| private readonly scope: SDK.DebuggerModel.ScopeChainEntry; |
| private readonly object: SDK.RemoteObject.RemoteObject; |
| constructor(scope: SDK.DebuggerModel.ScopeChainEntry) { |
| super(); |
| this.scope = scope; |
| this.object = scope.object(); |
| } |
| |
| override customPreview(): Protocol.Runtime.CustomPreview|null { |
| return this.object.customPreview(); |
| } |
| |
| override get objectId(): Protocol.Runtime.RemoteObjectId|undefined { |
| return this.object.objectId; |
| } |
| |
| override get type(): string { |
| return this.object.type; |
| } |
| |
| override get subtype(): string|undefined { |
| return this.object.subtype; |
| } |
| |
| override get value(): typeof this.object.value { |
| return this.object.value; |
| } |
| |
| override get description(): string|undefined { |
| return this.object.description; |
| } |
| |
| override get hasChildren(): boolean { |
| return this.object.hasChildren; |
| } |
| |
| override get preview(): Protocol.Runtime.ObjectPreview|undefined { |
| return this.object.preview; |
| } |
| |
| override arrayLength(): number { |
| return this.object.arrayLength(); |
| } |
| |
| override getOwnProperties(generatePreview: boolean): Promise<SDK.RemoteObject.GetPropertiesResult> { |
| return this.object.getOwnProperties(generatePreview); |
| } |
| |
| override async getAllProperties(accessorPropertiesOnly: boolean, generatePreview: boolean): |
| Promise<SDK.RemoteObject.GetPropertiesResult> { |
| const allProperties = await this.object.getAllProperties(accessorPropertiesOnly, generatePreview); |
| const {variableMapping} = await resolveDebuggerScope(this.scope); |
| |
| const properties = allProperties.properties; |
| const internalProperties = allProperties.internalProperties; |
| const newProperties = properties?.map(property => { |
| const name = variableMapping.get(property.name); |
| return name !== undefined ? property.cloneWithNewName(name) : property; |
| }); |
| return {properties: newProperties ?? [], internalProperties}; |
| } |
| |
| override async setPropertyValue(argumentName: string|Protocol.Runtime.CallArgument, value: string): |
| Promise<string|undefined> { |
| const {variableMapping} = await resolveDebuggerScope(this.scope); |
| |
| let name; |
| if (typeof argumentName === 'string') { |
| name = argumentName; |
| } else { |
| name = (argumentName.value as string); |
| } |
| |
| let actualName: string = name; |
| for (const compiledName of variableMapping.keys()) { |
| if (variableMapping.get(compiledName) === name) { |
| actualName = compiledName; |
| break; |
| } |
| } |
| return await this.object.setPropertyValue(actualName, value); |
| } |
| |
| override async deleteProperty(name: Protocol.Runtime.CallArgument): Promise<string|undefined> { |
| return await this.object.deleteProperty(name); |
| } |
| |
| override callFunction<T, U>( |
| functionDeclaration: (this: U, ...args: any[]) => T, |
| args?: Protocol.Runtime.CallArgument[]): Promise<SDK.RemoteObject.CallFunctionResult> { |
| return this.object.callFunction(functionDeclaration, args); |
| } |
| |
| override callFunctionJSON<T, U>( |
| functionDeclaration: (this: U, ...args: any[]) => T, args?: Protocol.Runtime.CallArgument[]): Promise<T|null> { |
| return this.object.callFunctionJSON(functionDeclaration, args); |
| } |
| |
| override release(): void { |
| this.object.release(); |
| } |
| |
| override debuggerModel(): SDK.DebuggerModel.DebuggerModel { |
| return this.object.debuggerModel(); |
| } |
| |
| override runtimeModel(): SDK.RuntimeModel.RuntimeModel { |
| return this.object.runtimeModel(); |
| } |
| |
| override isNode(): boolean { |
| return this.object.isNode(); |
| } |
| } |
| |
| /** |
| * Resolve the frame's function name using the name associated with the opening |
| * paren that starts the scope. If there is no name associated with the scope |
| * start or if the function scope does not start with a left paren (e.g., arrow |
| * function with one parameter), the resolution returns null. |
| **/ |
| async function getFunctionNameFromScopeStart( |
| script: SDK.Script.Script, lineNumber: number, columnNumber: number): Promise<string|null> { |
| // To reduce the overhead of resolving function names, |
| // we check for source maps first and immediately leave |
| // this function if the script doesn't have a sourcemap. |
| const sourceMap = script.sourceMap(); |
| if (!sourceMap) { |
| return null; |
| } |
| |
| const scopeName = sourceMap.findOriginalFunctionName({line: lineNumber, column: columnNumber}); |
| if (scopeName !== null) { |
| return scopeName; |
| } |
| |
| const mappingEntry = sourceMap.findEntry(lineNumber, columnNumber); |
| if (!mappingEntry?.sourceURL) { |
| return null; |
| } |
| |
| const name = mappingEntry.name; |
| if (!name) { |
| return null; |
| } |
| |
| const text = await getTextFor(script); |
| if (!text) { |
| return null; |
| } |
| |
| const openRange = new TextUtils.TextRange.TextRange(lineNumber, columnNumber, lineNumber, columnNumber + 1); |
| |
| if (text.extract(openRange) !== '(') { |
| return null; |
| } |
| |
| return name; |
| } |
| |
| export async function resolveDebuggerFrameFunctionName(frame: SDK.DebuggerModel.CallFrame): Promise<string|null> { |
| const startLocation = frame.localScope()?.range()?.start; |
| if (!startLocation) { |
| return null; |
| } |
| return await getFunctionNameFromScopeStart(frame.script, startLocation.lineNumber, startLocation.columnNumber); |
| } |
| |
| export async function resolveProfileFrameFunctionName( |
| {scriptId, lineNumber, columnNumber}: Partial<Protocol.Runtime.CallFrame>, |
| target: SDK.Target.Target|null): Promise<string|null> { |
| if (!target || lineNumber === undefined || columnNumber === undefined || scriptId === undefined) { |
| return null; |
| } |
| const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); |
| const script = debuggerModel?.scriptForId(String(scriptId)); |
| |
| if (!debuggerModel || !script) { |
| return null; |
| } |
| |
| const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(); |
| const location = new SDK.DebuggerModel.Location(debuggerModel, scriptId, lineNumber, columnNumber); |
| const functionInfoFromPlugin = await debuggerWorkspaceBinding.pluginManager.getFunctionInfo(script, location); |
| if (functionInfoFromPlugin && 'frames' in functionInfoFromPlugin) { |
| const last = functionInfoFromPlugin.frames.at(-1); |
| if (last?.name) { |
| return last.name; |
| } |
| } |
| return await getFunctionNameFromScopeStart(script, lineNumber, columnNumber); |
| } |
| |
| let scopeResolvedForTest: (...arg0: unknown[]) => void = function(): void {}; |
| |
| export const getScopeResolvedForTest = (): (...arg0: unknown[]) => void => { |
| return scopeResolvedForTest; |
| }; |
| |
| export const setScopeResolvedForTest = (scope: (...arg0: unknown[]) => void): void => { |
| scopeResolvedForTest = scope; |
| }; |