diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/debugger/src/workers/parser/utils | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/debugger/src/workers/parser/utils')
8 files changed, 926 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/workers/parser/utils/ast.js b/devtools/client/debugger/src/workers/parser/utils/ast.js new file mode 100644 index 0000000000..2adbcd9c7c --- /dev/null +++ b/devtools/client/debugger/src/workers/parser/utils/ast.js @@ -0,0 +1,225 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import parseScriptTags from "parse-script-tags"; +import * as babelParser from "@babel/parser"; +import * as t from "@babel/types"; +import { getSource } from "../sources"; + +let ASTs = new Map(); + +function _parse(code, opts) { + return babelParser.parse(code, { + ...opts, + tokens: true, + }); +} + +const sourceOptions = { + generated: { + sourceType: "unambiguous", + tokens: true, + plugins: [ + "classStaticBlock", + "classPrivateProperties", + "classPrivateMethods", + "classProperties", + "objectRestSpread", + "optionalChaining", + "privateIn", + "nullishCoalescingOperator", + ], + }, + original: { + sourceType: "unambiguous", + tokens: true, + plugins: [ + "jsx", + "flow", + "doExpressions", + "optionalChaining", + "nullishCoalescingOperator", + "decorators-legacy", + "objectRestSpread", + "classStaticBlock", + "classPrivateProperties", + "classPrivateMethods", + "classProperties", + "exportDefaultFrom", + "exportNamespaceFrom", + "asyncGenerators", + "functionBind", + "functionSent", + "dynamicImport", + "react-jsx", + ], + }, +}; + +export function parse(text, opts) { + let ast = {}; + if (!text) { + return ast; + } + + try { + ast = _parse(text, opts); + } catch (error) { + console.error(error); + } + + return ast; +} + +// Custom parser for parse-script-tags that adapts its input structure to +// our parser's signature +function htmlParser({ source, line }) { + return parse(source, { startLine: line, ...sourceOptions.generated }); +} + +const VUE_COMPONENT_START = /^\s*</; +function vueParser({ source, line }) { + return parse(source, { + startLine: line, + ...sourceOptions.original, + }); +} +function parseVueScript(code) { + if (typeof code !== "string") { + return {}; + } + + let ast; + + // .vue files go through several passes, so while there is a + // single-file-component Vue template, there are also generally .vue files + // that are still just JS as well. + if (code.match(VUE_COMPONENT_START)) { + ast = parseScriptTags(code, vueParser); + if (t.isFile(ast)) { + // parseScriptTags is currently hard-coded to return scripts, but Vue + // always expects ESM syntax, so we just hard-code it. + ast.program.sourceType = "module"; + } + } else { + ast = parse(code, sourceOptions.original); + } + return ast; +} + +export function parseConsoleScript(text, opts) { + try { + return _parse(text, { + plugins: [ + "classStaticBlock", + "classPrivateProperties", + "classPrivateMethods", + "objectRestSpread", + "dynamicImport", + "nullishCoalescingOperator", + "optionalChaining", + ], + ...opts, + allowAwaitOutsideFunction: true, + }); + } catch (e) { + return null; + } +} + +export function parseScript(text, opts) { + return _parse(text, opts); +} + +export function getAst(sourceId) { + if (ASTs.has(sourceId)) { + return ASTs.get(sourceId); + } + + const source = getSource(sourceId); + + if (source.isWasm) { + return null; + } + + let ast = {}; + const { contentType } = source; + if (contentType == "text/html") { + ast = parseScriptTags(source.text, htmlParser) || {}; + } else if (contentType && contentType === "text/vue") { + ast = parseVueScript(source.text) || {}; + } else if ( + contentType && + contentType.match(/(javascript|jsx)/) && + !contentType.match(/typescript-jsx/) + ) { + const type = source.id.includes("original") ? "original" : "generated"; + const options = sourceOptions[type]; + ast = parse(source.text, options); + } else if (contentType && contentType.match(/typescript/)) { + const options = { + ...sourceOptions.original, + plugins: [ + ...sourceOptions.original.plugins.filter( + p => + p !== "flow" && + p !== "decorators" && + p !== "decorators2" && + (p !== "jsx" || contentType.match(/typescript-jsx/)) + ), + "decorators-legacy", + "typescript", + ], + }; + ast = parse(source.text, options); + } + + ASTs.set(source.id, ast); + return ast; +} + +export function clearASTs() { + ASTs = new Map(); +} + +export function traverseAst(sourceId, visitor, state) { + const ast = getAst(sourceId); + if (!ast || !Object.keys(ast).length) { + return null; + } + + t.traverse(ast, visitor, state); + return ast; +} + +export function hasNode(rootNode, predicate) { + try { + t.traverse(rootNode, { + enter: (node, ancestors) => { + if (predicate(node, ancestors)) { + throw new Error("MATCH"); + } + }, + }); + } catch (e) { + if (e.message === "MATCH") { + return true; + } + } + return false; +} + +export function replaceNode(ancestors, node) { + const parent = ancestors[ancestors.length - 1]; + + if (typeof parent.index === "number") { + if (Array.isArray(node)) { + parent.node[parent.key].splice(parent.index, 1, ...node); + } else { + parent.node[parent.key][parent.index] = node; + } + } else { + parent.node[parent.key] = node; + } +} diff --git a/devtools/client/debugger/src/workers/parser/utils/contains.js b/devtools/client/debugger/src/workers/parser/utils/contains.js new file mode 100644 index 0000000000..ed4cb31c1d --- /dev/null +++ b/devtools/client/debugger/src/workers/parser/utils/contains.js @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +function startsBefore(a, b) { + let before = a.start.line < b.line; + if (a.start.line === b.line) { + before = + a.start.column >= 0 && b.column >= 0 ? a.start.column <= b.column : true; + } + return before; +} + +function endsAfter(a, b) { + let after = a.end.line > b.line; + if (a.end.line === b.line) { + after = + a.end.column >= 0 && b.column >= 0 ? a.end.column >= b.column : true; + } + return after; +} + +export function containsPosition(a, b) { + return startsBefore(a, b) && endsAfter(a, b); +} + +export function containsLocation(a, b) { + return containsPosition(a, b.start) && containsPosition(a, b.end); +} diff --git a/devtools/client/debugger/src/workers/parser/utils/formatSymbols.js b/devtools/client/debugger/src/workers/parser/utils/formatSymbols.js new file mode 100644 index 0000000000..3bcf37e7c4 --- /dev/null +++ b/devtools/client/debugger/src/workers/parser/utils/formatSymbols.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { getSymbols } from "../getSymbols"; + +function formatLocation(loc) { + if (!loc) { + return ""; + } + + const { start, end } = loc; + const startLoc = `(${start.line}, ${start.column})`; + const endLoc = `(${end.line}, ${end.column})`; + + return `[${startLoc}, ${endLoc}]`; +} + +function summarize(symbol) { + if (typeof symbol == "boolean") { + return symbol ? "true" : "false"; + } + + const loc = formatLocation(symbol.location); + const params = symbol.parameterNames + ? `(${symbol.parameterNames.join(", ")})` + : ""; + const expression = symbol.expression || ""; + const klass = symbol.klass || ""; + const name = symbol.name == undefined ? "" : symbol.name; + const names = symbol.specifiers ? symbol.specifiers.join(", ") : ""; + const values = symbol.values ? symbol.values.join(", ") : ""; + const index = symbol.index ? symbol.index : ""; + + return `${loc} ${expression} ${name}${params} ${klass} ${names} ${values} ${index}`.trim(); // eslint-disable-line max-len +} +const bools = ["hasJsx", "hasTypes"]; +const strings = ["framework"]; +function formatBool(name, symbols) { + return `${name}: ${symbols[name] ? "true" : "false"}`; +} + +function formatString(name, symbols) { + return `${name}: ${symbols[name]}`; +} + +function formatKey(name, symbols) { + if (bools.includes(name)) { + return formatBool(name, symbols); + } + + if (strings.includes(name)) { + return formatString(name, symbols); + } + + return `${name}:\n${symbols[name].map(summarize).join("\n")}`; +} + +export function formatSymbols(sourceId) { + const symbols = getSymbols(sourceId); + + return Object.keys(symbols) + .map(name => formatKey(name, symbols)) + .join("\n\n"); +} diff --git a/devtools/client/debugger/src/workers/parser/utils/getFunctionName.js b/devtools/client/debugger/src/workers/parser/utils/getFunctionName.js new file mode 100644 index 0000000000..1fe85a5c69 --- /dev/null +++ b/devtools/client/debugger/src/workers/parser/utils/getFunctionName.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import * as t from "@babel/types"; + +// Perform ES6's anonymous function name inference for all +// locations where static analysis is possible. +// eslint-disable-next-line complexity +export default function getFunctionName(node, parent) { + if (t.isIdentifier(node.id)) { + return node.id.name; + } + + if ( + t.isObjectMethod(node, { computed: false }) || + t.isClassMethod(node, { computed: false }) || + t.isClassPrivateMethod(node) + ) { + const { key } = node; + + if (t.isIdentifier(key)) { + return key.name; + } + if (t.isStringLiteral(key)) { + return key.value; + } + if (t.isNumericLiteral(key)) { + return `${key.value}`; + } + + if (t.isPrivateName(key)) { + return `#${key.id.name}`; + } + } + + if ( + t.isObjectProperty(parent, { computed: false, value: node }) || + // TODO: Babylon 6 doesn't support computed class props. It is included + // here so that it is most flexible. Once Babylon 7 is used, this + // can change to use computed: false like ObjectProperty. + (t.isClassProperty(parent, { value: node }) && !parent.computed) || + (t.isClassPrivateProperty(parent, { value: node }) && !parent.computed) + ) { + const { key } = parent; + + if (t.isIdentifier(key)) { + return key.name; + } + if (t.isStringLiteral(key)) { + return key.value; + } + if (t.isNumericLiteral(key)) { + return `${key.value}`; + } + + if (t.isPrivateName(key)) { + return `#${key.id.name}`; + } + } + + if (t.isAssignmentExpression(parent, { operator: "=", right: node })) { + if (t.isIdentifier(parent.left)) { + return parent.left.name; + } + + // This case is not supported in standard ES6 name inference, but it + // is included here since it is still a helpful case during debugging. + if (t.isMemberExpression(parent.left, { computed: false })) { + return parent.left.property.name; + } + } + + if ( + t.isAssignmentPattern(parent, { right: node }) && + t.isIdentifier(parent.left) + ) { + return parent.left.name; + } + + if ( + t.isVariableDeclarator(parent, { init: node }) && + t.isIdentifier(parent.id) + ) { + return parent.id.name; + } + + if ( + t.isExportDefaultDeclaration(parent, { declaration: node }) && + t.isFunctionDeclaration(node) + ) { + return "default"; + } + + return "anonymous"; +} diff --git a/devtools/client/debugger/src/workers/parser/utils/helpers.js b/devtools/client/debugger/src/workers/parser/utils/helpers.js new file mode 100644 index 0000000000..0850ea678c --- /dev/null +++ b/devtools/client/debugger/src/workers/parser/utils/helpers.js @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import * as t from "@babel/types"; +import generate from "@babel/generator"; + +export function isFunction(node) { + return ( + t.isFunction(node) || + t.isArrowFunctionExpression(node) || + t.isObjectMethod(node) || + t.isClassMethod(node) + ); +} + +export function isAwaitExpression(path) { + const { node, parent } = path; + return ( + t.isAwaitExpression(node) || + t.isAwaitExpression(parent.init) || + t.isAwaitExpression(parent) + ); +} + +export function isYieldExpression(path) { + const { node, parent } = path; + return ( + t.isYieldExpression(node) || + t.isYieldExpression(parent.init) || + t.isYieldExpression(parent) + ); +} + +export function isObjectShorthand(parent) { + if (!t.isObjectProperty(parent)) { + return false; + } + + if (parent.value && parent.value.left) { + return ( + parent.value.type === "AssignmentPattern" && + parent.value.left.type === "Identifier" + ); + } + + return ( + parent.value && + parent.key.start == parent.value.start && + parent.key.loc.identifierName === parent.value.loc.identifierName + ); +} + +export function getObjectExpressionValue(node) { + const { value } = node; + + if (t.isIdentifier(value)) { + return value.name; + } + + if (t.isCallExpression(value) || t.isFunctionExpression(value)) { + return ""; + } + const code = generate(value).code; + + const shouldWrap = t.isObjectExpression(value); + return shouldWrap ? `(${code})` : code; +} + +export function getCode(node) { + return generate(node).code; +} + +export function getComments(ast) { + if (!ast || !ast.comments) { + return []; + } + return ast.comments.map(comment => ({ + name: comment.location, + location: comment.loc, + })); +} + +export function getSpecifiers(specifiers) { + if (!specifiers) { + return []; + } + + return specifiers.map(specifier => specifier.local?.name); +} + +export function isComputedExpression(expression) { + return /^\[/m.test(expression); +} + +export function getMemberExpression(root) { + function _getMemberExpression(node, expr) { + if (t.isMemberExpression(node)) { + expr = [node.property.name].concat(expr); + return _getMemberExpression(node.object, expr); + } + + if (t.isCallExpression(node)) { + return []; + } + + if (t.isThisExpression(node)) { + return ["this"].concat(expr); + } + + return [node.name].concat(expr); + } + + const expr = _getMemberExpression(root, []); + return expr.join("."); +} + +export function getVariables(dec) { + if (!dec.id) { + return []; + } + + if (t.isArrayPattern(dec.id)) { + if (!dec.id.elements) { + return []; + } + + // NOTE: it's possible that an element is empty or has several variables + // e.g. const [, a] = arr + // e.g. const [{a, b }] = 2 + return dec.id.elements + .filter(Boolean) + .map(element => ({ + name: t.isAssignmentPattern(element) + ? element.left.name + : element.name || element.argument?.name, + location: element.loc, + })) + .filter(({ name }) => name); + } + + return [ + { + name: dec.id.name, + location: dec.loc, + }, + ]; +} + +export function getPatternIdentifiers(pattern) { + let items = []; + if (t.isObjectPattern(pattern)) { + items = pattern.properties.map(({ value }) => value); + } + + if (t.isArrayPattern(pattern)) { + items = pattern.elements; + } + + return getIdentifiers(items); +} + +function getIdentifiers(items) { + let ids = []; + items.forEach(function (item) { + if (t.isObjectPattern(item) || t.isArrayPattern(item)) { + ids = ids.concat(getPatternIdentifiers(item)); + } else if (t.isIdentifier(item)) { + const { start, end } = item.loc; + ids.push({ + name: item.name, + expression: item.name, + location: { start, end }, + }); + } + }); + return ids; +} + +// Top Level checks the number of "body" nodes in the ancestor chain +// if the node is top-level, then it shoul only have one body. +export function isTopLevel(ancestors) { + return ancestors.filter(ancestor => ancestor.key == "body").length == 1; +} + +export function nodeLocationKey(a) { + const { start, end } = a.location; + return `${start.line}:${start.column}:${end.line}:${end.column}`; +} + +export function getFunctionParameterNames(path) { + if (path.node.params != null) { + return path.node.params.map(param => { + if (param.type !== "AssignmentPattern") { + return param.name; + } + + // Parameter with default value + if ( + param.left.type === "Identifier" && + param.right.type === "Identifier" + ) { + return `${param.left.name} = ${param.right.name}`; + } else if ( + param.left.type === "Identifier" && + param.right.type === "StringLiteral" + ) { + return `${param.left.name} = ${param.right.value}`; + } else if ( + param.left.type === "Identifier" && + param.right.type === "ObjectExpression" + ) { + return `${param.left.name} = {}`; + } else if ( + param.left.type === "Identifier" && + param.right.type === "ArrayExpression" + ) { + return `${param.left.name} = []`; + } else if ( + param.left.type === "Identifier" && + param.right.type === "NullLiteral" + ) { + return `${param.left.name} = null`; + } + + return null; + }); + } + return []; +} diff --git a/devtools/client/debugger/src/workers/parser/utils/inferClassName.js b/devtools/client/debugger/src/workers/parser/utils/inferClassName.js new file mode 100644 index 0000000000..09d25f275d --- /dev/null +++ b/devtools/client/debugger/src/workers/parser/utils/inferClassName.js @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import * as t from "@babel/types"; + +// the function class is inferred from a call like +// createClass or extend +function fromCallExpression(callExpression) { + const allowlist = ["extend", "createClass"]; + const { callee } = callExpression.node; + if (!callee) { + return null; + } + + const name = t.isMemberExpression(callee) + ? callee.property.name + : callee.name; + + if (!allowlist.includes(name)) { + return null; + } + + const variable = callExpression.findParent(p => + t.isVariableDeclarator(p.node) + ); + if (variable) { + return variable.node.id.name; + } + + const assignment = callExpression.findParent(p => + t.isAssignmentExpression(p.node) + ); + + if (!assignment) { + return null; + } + + const { left } = assignment.node; + + if (left.name) { + return name; + } + + if (t.isMemberExpression(left)) { + return left.property.name; + } + + return null; +} + +// the function class is inferred from a prototype assignment +// e.g. TodoClass.prototype.render = function() {} +function fromPrototype(assignment) { + const { left } = assignment.node; + if (!left) { + return null; + } + + if ( + t.isMemberExpression(left) && + left.object && + t.isMemberExpression(left.object) && + left.object.property.identifier === "prototype" + ) { + return left.object.object.name; + } + + return null; +} + +// infer class finds an appropriate class for functions +// that are defined inside of a class like thing. +// e.g. `class Foo`, `TodoClass.prototype.foo`, +// `Todo = createClass({ foo: () => {}})` +export function inferClassName(path) { + const classDeclaration = path.findParent(p => t.isClassDeclaration(p.node)); + if (classDeclaration) { + return classDeclaration.node.id.name; + } + + const callExpression = path.findParent(p => t.isCallExpression(p.node)); + if (callExpression) { + return fromCallExpression(callExpression); + } + + const assignment = path.findParent(p => t.isAssignmentExpression(p.node)); + if (assignment) { + return fromPrototype(assignment); + } + + return null; +} diff --git a/devtools/client/debugger/src/workers/parser/utils/simple-path.js b/devtools/client/debugger/src/workers/parser/utils/simple-path.js new file mode 100644 index 0000000000..c167103566 --- /dev/null +++ b/devtools/client/debugger/src/workers/parser/utils/simple-path.js @@ -0,0 +1,147 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +export default function createSimplePath(ancestors) { + if (ancestors.length === 0) { + return null; + } + + // Slice the array because babel-types traverse may continue mutating + // the ancestors array in later traversal logic. + return new SimplePath(ancestors.slice()); +} + +/** + * Mimics @babel/traverse's NodePath API in a simpler fashion that isn't as + * heavy, but still allows the ease of passing paths around to process nested + * AST structures. + */ +class SimplePath { + _index; + _ancestors; + _ancestor; + + _parentPath; + + constructor(ancestors, index = ancestors.length - 1) { + if (index < 0 || index >= ancestors.length) { + console.error(ancestors); + throw new Error("Created invalid path"); + } + + this._ancestors = ancestors; + this._ancestor = ancestors[index]; + this._index = index; + } + + get parentPath() { + let path = this._parentPath; + if (path === undefined) { + if (this._index === 0) { + path = null; + } else { + path = new SimplePath(this._ancestors, this._index - 1); + } + this._parentPath = path; + } + + return path; + } + + get parent() { + return this._ancestor.node; + } + + get node() { + const { node, key, index } = this._ancestor; + + if (typeof index === "number") { + return node[key][index]; + } + + return node[key]; + } + + get key() { + return this._ancestor.key; + } + + set node(replacement) { + if (this.type !== "Identifier") { + throw new Error( + "Replacing anything other than leaf nodes is undefined behavior " + + "in t.traverse()" + ); + } + + const { node, key, index } = this._ancestor; + if (typeof index === "number") { + node[key][index] = replacement; + } else { + node[key] = replacement; + } + } + + get type() { + return this.node.type; + } + + get inList() { + return typeof this._ancestor.index === "number"; + } + + get containerIndex() { + const { index } = this._ancestor; + + if (typeof index !== "number") { + throw new Error("Cannot get index of non-array node"); + } + + return index; + } + + get depth() { + return this._index; + } + + replace(node) { + this.node = node; + } + + find(predicate) { + for (let path = this; path; path = path.parentPath) { + if (predicate(path)) { + return path; + } + } + return null; + } + + findParent(predicate) { + if (!this.parentPath) { + throw new Error("Cannot use findParent on root path"); + } + + return this.parentPath.find(predicate); + } + + getSibling(offset) { + const { node, key, index } = this._ancestor; + + if (typeof index !== "number") { + throw new Error("Non-array nodes do not have siblings"); + } + + const container = node[key]; + + const siblingIndex = index + offset; + if (siblingIndex < 0 || siblingIndex >= container.length) { + return null; + } + + return new SimplePath( + this._ancestors.slice(0, -1).concat([{ node, key, index: siblingIndex }]) + ); + } +} diff --git a/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js b/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js new file mode 100644 index 0000000000..e8d2964205 --- /dev/null +++ b/devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { getAst } from "../ast"; +import { setSource } from "../../sources"; +import cases from "jest-in-case"; + +import { makeMockSourceAndContent } from "../../../../utils/test-mockup"; + +const astKeys = [ + "type", + "start", + "end", + "loc", + "errors", + "program", + "comments", + "tokens", +]; + +cases( + "ast.getAst", + ({ name }) => { + const source = makeMockSourceAndContent(undefined, "foo", name, "2"); + setSource({ + id: source.id, + text: source.content.value || "", + contentType: source.content.contentType, + isWasm: false, + }); + const ast = getAst("foo"); + expect(ast && Object.keys(ast)).toEqual(astKeys); + }, + [ + { name: "text/javascript" }, + { name: "application/javascript" }, + { name: "application/x-javascript" }, + { name: "text/jsx" }, + ] +); |