mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-07 12:33:47 +08:00
The patch makes the resolver usable by the console. There should be no functional changes. Bug: chromium:1297047 Change-Id: I020375c4c5587d1053a801dbf1b9c6a850e367aa Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3461555 Reviewed-by: Kim-Anh Tran <kimanh@chromium.org> Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Reviewed-by: Tim Van der Lippe <tvanderlippe@chromium.org> Commit-Queue: Jaroslav Sevcik <jarin@chromium.org>
412 lines
15 KiB
TypeScript
412 lines
15 KiB
TypeScript
// Copyright 2022 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import * as Platform from '../../core/platform/platform.js';
|
|
import * as SDK from '../../core/sdk/sdk.js';
|
|
import * as Bindings from '../bindings/bindings.js';
|
|
import * as Formatter from '../formatter/formatter.js';
|
|
import * as TextUtils from '../text_utils/text_utils.js';
|
|
import type * as Workspace from '../workspace/workspace.js';
|
|
import * as Protocol from '../../generated/protocol.js';
|
|
|
|
interface CachedScopeMap {
|
|
sourceMap: SDK.SourceMap.SourceMap|null;
|
|
identifiersPromise: Promise<Map<string, string>>;
|
|
}
|
|
|
|
const scopeToCachedIdentifiersMap = new WeakMap<SDK.DebuggerModel.ScopeChainEntry, CachedScopeMap>();
|
|
const cachedMapByCallFrame = new WeakMap<SDK.DebuggerModel.CallFrame, Map<string, string>>();
|
|
|
|
export class Identifier {
|
|
name: string;
|
|
lineNumber: number;
|
|
columnNumber: number;
|
|
constructor(name: string, lineNumber: number, columnNumber: number) {
|
|
this.name = name;
|
|
this.lineNumber = lineNumber;
|
|
this.columnNumber = columnNumber;
|
|
}
|
|
}
|
|
|
|
export const scopeIdentifiers = async function(scope: SDK.DebuggerModel.ScopeChainEntry): Promise<Identifier[]> {
|
|
if (scope.type() === Protocol.Debugger.ScopeType.Global) {
|
|
return [];
|
|
}
|
|
const startLocation = scope.startLocation();
|
|
const endLocation = scope.endLocation();
|
|
if (!startLocation || !endLocation) {
|
|
return [];
|
|
}
|
|
const script = startLocation.script();
|
|
if (!script || !script.sourceMapURL || script !== endLocation.script()) {
|
|
return [];
|
|
}
|
|
const {content} = await script.requestContent();
|
|
if (!content) {
|
|
return [];
|
|
}
|
|
|
|
const text = new TextUtils.Text.Text(content);
|
|
const scopeRange = new TextUtils.TextRange.TextRange(
|
|
startLocation.lineNumber, startLocation.columnNumber, endLocation.lineNumber, endLocation.columnNumber);
|
|
const scopeText = text.extract(scopeRange);
|
|
const scopeStart = text.toSourceRange(scopeRange).offset;
|
|
const prefix = 'function fui';
|
|
const identifiers =
|
|
await Formatter.FormatterWorkerPool.formatterWorkerPool().javaScriptIdentifiers(prefix + scopeText);
|
|
const result = [];
|
|
const cursor = new TextUtils.TextCursor.TextCursor(text.lineEndings());
|
|
for (const id of identifiers) {
|
|
if (id.offset < prefix.length) {
|
|
continue;
|
|
}
|
|
const start = scopeStart + id.offset - prefix.length;
|
|
cursor.resetTo(start);
|
|
result.push(new Identifier(id.name, cursor.lineNumber(), cursor.columnNumber()));
|
|
}
|
|
return result;
|
|
};
|
|
|
|
export const resolveScopeChain =
|
|
async function(callFrame: SDK.DebuggerModel.CallFrame|null): Promise<SDK.DebuggerModel.ScopeChainEntry[]|null> {
|
|
if (!callFrame) {
|
|
return null;
|
|
}
|
|
const {pluginManager} = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
|
|
if (pluginManager) {
|
|
const scopeChain = await pluginManager.resolveScopeChain(callFrame);
|
|
if (scopeChain) {
|
|
return scopeChain;
|
|
}
|
|
}
|
|
return callFrame.scopeChain();
|
|
};
|
|
|
|
export const resolveScope = async(scope: SDK.DebuggerModel.ScopeChainEntry): Promise<Map<string, string>> => {
|
|
let cachedScopeMap = scopeToCachedIdentifiersMap.get(scope);
|
|
const script = scope.callFrame().script;
|
|
const sourceMap = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().sourceMapForScript(script);
|
|
|
|
if (!cachedScopeMap || cachedScopeMap.sourceMap !== sourceMap) {
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const identifiersPromise = (async(): Promise<Map<any, any>> => {
|
|
const namesMapping = new Map<string, string>();
|
|
if (sourceMap) {
|
|
const textCache = new Map<string, TextUtils.Text.Text>();
|
|
// Extract as much as possible from SourceMap and resolve
|
|
// missing identifier names from SourceMap ranges.
|
|
const promises = [];
|
|
for (const id of await scopeIdentifiers(scope)) {
|
|
const entry = sourceMap.findEntry(id.lineNumber, id.columnNumber);
|
|
if (entry && entry.name) {
|
|
namesMapping.set(id.name, entry.name);
|
|
} else {
|
|
promises.push(resolveSourceName(script, sourceMap, id, textCache).then(sourceName => {
|
|
if (sourceName) {
|
|
namesMapping.set(id.name, sourceName);
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
await Promise.all(promises).then(getScopeResolvedForTest());
|
|
}
|
|
return namesMapping;
|
|
})();
|
|
cachedScopeMap = {sourceMap, identifiersPromise};
|
|
scopeToCachedIdentifiersMap.set(scope, {sourceMap, identifiersPromise});
|
|
}
|
|
return await cachedScopeMap.identifiersPromise;
|
|
|
|
async function resolveSourceName(
|
|
script: SDK.Script.Script, sourceMap: SDK.SourceMap.SourceMap, id: Identifier,
|
|
textCache: Map<string, TextUtils.Text.Text>): Promise<string|null> {
|
|
const startEntry = sourceMap.findEntry(id.lineNumber, id.columnNumber);
|
|
const endEntry = sourceMap.findEntry(id.lineNumber, id.columnNumber + id.name.length);
|
|
if (!startEntry || !endEntry || !startEntry.sourceURL || startEntry.sourceURL !== endEntry.sourceURL ||
|
|
!startEntry.sourceLineNumber || !startEntry.sourceColumnNumber || !endEntry.sourceLineNumber ||
|
|
!endEntry.sourceColumnNumber) {
|
|
return null;
|
|
}
|
|
const sourceTextRange = new TextUtils.TextRange.TextRange(
|
|
startEntry.sourceLineNumber, startEntry.sourceColumnNumber, endEntry.sourceLineNumber,
|
|
endEntry.sourceColumnNumber);
|
|
const uiSourceCode =
|
|
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().uiSourceCodeForSourceMapSourceURL(
|
|
script.debuggerModel, startEntry.sourceURL, script.isContentScript());
|
|
if (!uiSourceCode) {
|
|
return null;
|
|
}
|
|
const {content} = await uiSourceCode.requestContent();
|
|
if (!content) {
|
|
return null;
|
|
}
|
|
let text = textCache.get(content);
|
|
if (!text) {
|
|
text = new TextUtils.Text.Text(content);
|
|
textCache.set(content, text);
|
|
}
|
|
const originalIdentifier = text.extract(sourceTextRange).trim();
|
|
return /[a-zA-Z0-9_$]+/.test(originalIdentifier) ? originalIdentifier : null;
|
|
}
|
|
};
|
|
|
|
export const allVariablesInCallFrame = async(callFrame: SDK.DebuggerModel.CallFrame): Promise<Map<string, string>> => {
|
|
const cachedMap = cachedMapByCallFrame.get(callFrame);
|
|
if (cachedMap) {
|
|
return cachedMap;
|
|
}
|
|
|
|
const scopeChain = callFrame.scopeChain();
|
|
const nameMappings = await Promise.all(scopeChain.map(resolveScope));
|
|
const reverseMapping = new Map<string, string>();
|
|
for (const map of nameMappings) {
|
|
for (const [compiledName, originalName] of map) {
|
|
if (originalName && !reverseMapping.has(originalName)) {
|
|
reverseMapping.set(originalName, compiledName);
|
|
}
|
|
}
|
|
}
|
|
cachedMapByCallFrame.set(callFrame, reverseMapping);
|
|
return reverseMapping;
|
|
};
|
|
|
|
export const resolveExpression = async(
|
|
callFrame: SDK.DebuggerModel.CallFrame, originalText: string, uiSourceCode: Workspace.UISourceCode.UISourceCode,
|
|
lineNumber: number, startColumnNumber: number, endColumnNumber: number): Promise<string> => {
|
|
if (uiSourceCode.mimeType() === 'application/wasm') {
|
|
// For WebAssembly disassembly, lookup the different possiblities.
|
|
return `memories["${originalText}"] ?? locals["${originalText}"] ?? tables["${originalText}"] ?? functions["${
|
|
originalText}"] ?? globals["${originalText}"]`;
|
|
}
|
|
if (!uiSourceCode.contentType().isFromSourceMap()) {
|
|
return '';
|
|
}
|
|
const reverseMapping = await allVariablesInCallFrame(callFrame);
|
|
if (reverseMapping.has(originalText)) {
|
|
return reverseMapping.get(originalText) as string;
|
|
}
|
|
const rawLocations =
|
|
await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().uiLocationToRawLocations(
|
|
uiSourceCode, lineNumber, startColumnNumber);
|
|
const rawLocation = rawLocations.find(location => location.debuggerModel === callFrame.debuggerModel);
|
|
if (!rawLocation) {
|
|
return '';
|
|
}
|
|
const script = rawLocation.script();
|
|
if (!script) {
|
|
return '';
|
|
}
|
|
const sourceMap =
|
|
(Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().sourceMapForScript(script) as
|
|
SDK.SourceMap.TextSourceMap);
|
|
if (!sourceMap) {
|
|
return '';
|
|
}
|
|
const {content} = await script.requestContent();
|
|
if (!content) {
|
|
return '';
|
|
}
|
|
const text = new TextUtils.Text.Text(content);
|
|
const textRange = sourceMap.reverseMapTextRange(
|
|
uiSourceCode.url(),
|
|
new TextUtils.TextRange.TextRange(lineNumber, startColumnNumber, lineNumber, endColumnNumber));
|
|
if (!textRange) {
|
|
return '';
|
|
}
|
|
const subjectText = text.extract(textRange);
|
|
if (!subjectText) {
|
|
return '';
|
|
}
|
|
return await Formatter.FormatterWorkerPool.formatterWorkerPool().evaluatableJavaScriptSubstring(subjectText);
|
|
};
|
|
|
|
export const resolveThisObject =
|
|
async(callFrame: SDK.DebuggerModel.CallFrame|null): Promise<SDK.RemoteObject.RemoteObject|null> => {
|
|
if (!callFrame) {
|
|
return null;
|
|
}
|
|
const scopeChain = callFrame.scopeChain();
|
|
if (scopeChain.length === 0) {
|
|
return callFrame.thisObject();
|
|
}
|
|
|
|
const namesMapping = await resolveScope(scopeChain[0]);
|
|
const thisMappings = Platform.MapUtilities.inverse(namesMapping).get('this');
|
|
if (!thisMappings || thisMappings.size !== 1) {
|
|
return callFrame.thisObject();
|
|
}
|
|
|
|
const [expression] = thisMappings.values();
|
|
const result = await callFrame.evaluate(({
|
|
expression,
|
|
objectGroup: 'backtrace',
|
|
includeCommandLineAPI: false,
|
|
silent: true,
|
|
returnByValue: false,
|
|
generatePreview: true,
|
|
} as SDK.RuntimeModel.EvaluationOptions));
|
|
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 startLocation = scope.startLocation();
|
|
const endLocation = scope.endLocation();
|
|
const startLocationScript = startLocation ? startLocation.script() : null;
|
|
|
|
if (scope.type() === Protocol.Debugger.ScopeType.Global || !startLocationScript || !endLocation ||
|
|
!startLocationScript.sourceMapURL || startLocationScript !== endLocation.script()) {
|
|
return scope.object();
|
|
}
|
|
|
|
return new RemoteObject(scope);
|
|
};
|
|
|
|
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();
|
|
}
|
|
|
|
customPreview(): Protocol.Runtime.CustomPreview|null {
|
|
return this.object.customPreview();
|
|
}
|
|
|
|
get objectId(): Protocol.Runtime.RemoteObjectId|undefined {
|
|
return this.object.objectId;
|
|
}
|
|
|
|
get type(): string {
|
|
return this.object.type;
|
|
}
|
|
|
|
get subtype(): string|undefined {
|
|
return this.object.subtype;
|
|
}
|
|
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
get value(): any {
|
|
return this.object.value;
|
|
}
|
|
|
|
get description(): string|undefined {
|
|
return this.object.description;
|
|
}
|
|
|
|
get hasChildren(): boolean {
|
|
return this.object.hasChildren;
|
|
}
|
|
|
|
get preview(): Protocol.Runtime.ObjectPreview|undefined {
|
|
return this.object.preview;
|
|
}
|
|
|
|
arrayLength(): number {
|
|
return this.object.arrayLength();
|
|
}
|
|
|
|
getOwnProperties(generatePreview: boolean): Promise<SDK.RemoteObject.GetPropertiesResult> {
|
|
return this.object.getOwnProperties(generatePreview);
|
|
}
|
|
|
|
async getAllProperties(accessorPropertiesOnly: boolean, generatePreview: boolean):
|
|
Promise<SDK.RemoteObject.GetPropertiesResult> {
|
|
const allProperties = await this.object.getAllProperties(accessorPropertiesOnly, generatePreview);
|
|
const namesMapping = await resolveScope(this.scope);
|
|
|
|
const properties = allProperties.properties;
|
|
const internalProperties = allProperties.internalProperties;
|
|
const newProperties = [];
|
|
if (properties) {
|
|
for (let i = 0; i < properties.length; ++i) {
|
|
const property = properties[i];
|
|
const name = namesMapping.get(property.name) || properties[i].name;
|
|
if (!property.value) {
|
|
continue;
|
|
}
|
|
newProperties.push(new SDK.RemoteObject.RemoteObjectProperty(
|
|
name, property.value, property.enumerable, property.writable, property.isOwn, property.wasThrown,
|
|
property.symbol, property.synthetic));
|
|
}
|
|
}
|
|
return {properties: newProperties, internalProperties: internalProperties};
|
|
}
|
|
|
|
async setPropertyValue(argumentName: string|Protocol.Runtime.CallArgument, value: string): Promise<string|undefined> {
|
|
const namesMapping = await resolveScope(this.scope);
|
|
|
|
let name;
|
|
if (typeof argumentName === 'string') {
|
|
name = argumentName;
|
|
} else {
|
|
name = (argumentName.value as string);
|
|
}
|
|
|
|
let actualName: string = name;
|
|
for (const compiledName of namesMapping.keys()) {
|
|
if (namesMapping.get(compiledName) === name) {
|
|
actualName = compiledName;
|
|
break;
|
|
}
|
|
}
|
|
return this.object.setPropertyValue(actualName, value);
|
|
}
|
|
|
|
async deleteProperty(name: Protocol.Runtime.CallArgument): Promise<string|undefined> {
|
|
return this.object.deleteProperty(name);
|
|
}
|
|
|
|
callFunction<T>(functionDeclaration: (this: Object, ...arg1: unknown[]) => T, args?: Protocol.Runtime.CallArgument[]):
|
|
Promise<SDK.RemoteObject.CallFunctionResult> {
|
|
return this.object.callFunction(functionDeclaration, args);
|
|
}
|
|
|
|
callFunctionJSON<T>(
|
|
functionDeclaration: (this: Object, ...arg1: unknown[]) => T,
|
|
args?: Protocol.Runtime.CallArgument[]): Promise<T> {
|
|
return this.object.callFunctionJSON(functionDeclaration, args);
|
|
}
|
|
|
|
release(): void {
|
|
this.object.release();
|
|
}
|
|
|
|
debuggerModel(): SDK.DebuggerModel.DebuggerModel {
|
|
return this.object.debuggerModel();
|
|
}
|
|
|
|
runtimeModel(): SDK.RuntimeModel.RuntimeModel {
|
|
return this.object.runtimeModel();
|
|
}
|
|
|
|
isNode(): boolean {
|
|
return this.object.isNode();
|
|
}
|
|
}
|
|
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any
|
|
let _scopeResolvedForTest: (...arg0: any[]) => void = function(): void {};
|
|
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any
|
|
export const getScopeResolvedForTest = (): (...arg0: any[]) => void => {
|
|
return _scopeResolvedForTest;
|
|
};
|
|
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any
|
|
export const setScopeResolvedForTest = (scope: (...arg0: any[]) => void): void => {
|
|
_scopeResolvedForTest = scope;
|
|
};
|