summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/workers/parser/utils
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/workers/parser/utils')
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/ast.js230
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/contains.js29
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/formatSymbols.js65
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/getFunctionName.js96
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/helpers.js232
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/inferClassName.js93
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/parse-script-tags/customParse.js132
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/parse-script-tags/index.js60
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/parse-script-tags/parseScriptFragment.js155
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/simple-path.js104
-rw-r--r--devtools/client/debugger/src/workers/parser/utils/tests/ast.spec.js41
11 files changed, 1237 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..445645c12f
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/ast.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 { parseScriptTags } from "./parse-script-tags";
+import * as babelParser from "@babel/parser";
+import * as t from "@babel/types";
+import { getSource } from "../sources";
+
+const 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",
+ "regexpUnicodeSets",
+ ],
+ },
+ 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",
+ "regexpUnicodeSets",
+ ],
+ },
+};
+
+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",
+ "regexpUnicodeSets",
+ ],
+ ...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(sourceIds) {
+ for (const sourceId of sourceIds) {
+ ASTs.delete(sourceId);
+ }
+}
+
+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..0045b54136
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/helpers.js
@@ -0,0 +1,232 @@
+/* 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 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,
+ },
+ ];
+}
+
+/**
+ * Add the identifiers for a given object pattern.
+ *
+ * @param {Array.<Object>} identifiers
+ * the current list of identifiers where to push the new identifiers
+ * related to this path.
+ * @param {Set<String>} identifiersKeys
+ * List of currently registered identifier location key.
+ * @param {Object} pattern
+ */
+export function addPatternIdentifiers(identifiers, identifiersKeys, pattern) {
+ let items;
+ if (t.isObjectPattern(pattern)) {
+ items = pattern.properties.map(({ value }) => value);
+ }
+
+ if (t.isArrayPattern(pattern)) {
+ items = pattern.elements;
+ }
+
+ if (items) {
+ addIdentifiers(identifiers, identifiersKeys, items);
+ }
+}
+
+function addIdentifiers(identifiers, identifiersKeys, items) {
+ for (const item of items) {
+ if (t.isObjectPattern(item) || t.isArrayPattern(item)) {
+ addPatternIdentifiers(identifiers, identifiersKeys, item);
+ } else if (t.isIdentifier(item)) {
+ if (!identifiersKeys.has(nodeLocationKey(item.loc))) {
+ identifiers.push({
+ name: item.name,
+ expression: item.name,
+ location: item.loc,
+ });
+ }
+ }
+ }
+}
+
+// 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({ start, end }) {
+ 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/parse-script-tags/customParse.js b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/customParse.js
new file mode 100644
index 0000000000..5c7fed480d
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/customParse.js
@@ -0,0 +1,132 @@
+/**
+ * https://github.com/ryanjduffy/parse-script-tags
+ *
+ * Copyright (c) by Ryan Duff
+ * Licensed under the MIT License.
+ */
+
+import * as types from "@babel/types";
+
+import parseFragment from "./parseScriptFragment.js";
+
+const startScript = /<script[^>]*>/im;
+const endScript = /<\/script\s*>/im;
+// https://stackoverflow.com/questions/5034781/js-regex-to-split-by-line#comment5633979_5035005
+const newLines = /\r\n|[\n\v\f\r\x85\u2028\u2029]/;
+
+function getType(tag) {
+ const fragment = parseFragment(tag);
+
+ if (fragment) {
+ const type = fragment.attributes.type;
+
+ return type ? type.toLowerCase() : null;
+ }
+
+ return null;
+}
+
+function getCandidateScriptLocations(source, index) {
+ const i = index || 0;
+ const str = source.substring(i);
+
+ const startMatch = startScript.exec(str);
+ if (startMatch) {
+ const startsAt = startMatch.index + startMatch[0].length;
+ const afterStart = str.substring(startsAt);
+ const endMatch = endScript.exec(afterStart);
+ if (endMatch) {
+ const locLength = endMatch.index;
+ const locIndex = i + startsAt;
+ const endIndex = locIndex + locLength + endMatch[0].length;
+
+ // extract the complete tag (incl start and end tags and content). if the
+ // type is invalid (= not JS), skip this tag and continue
+ const tag = source.substring(i + startMatch.index, endIndex);
+ const type = getType(tag);
+ if (
+ type &&
+ type !== "javascript" &&
+ type !== "text/javascript" &&
+ type !== "module"
+ ) {
+ return getCandidateScriptLocations(source, endIndex);
+ }
+
+ return [
+ adjustForLineAndColumn(source, {
+ index: locIndex,
+ length: locLength,
+ source: source.substring(locIndex, locIndex + locLength),
+ }),
+ ...getCandidateScriptLocations(source, endIndex),
+ ];
+ }
+ }
+
+ return [];
+}
+
+function parseScripts(locations, parser) {
+ return locations.map(parser);
+}
+
+function calcLineAndColumn(source, index) {
+ const lines = source.substring(0, index).split(newLines);
+ const line = lines.length;
+ const column = lines.pop().length + 1;
+
+ return {
+ column,
+ line,
+ };
+}
+
+function adjustForLineAndColumn(fullSource, location) {
+ const { column, line } = calcLineAndColumn(fullSource, location.index);
+ return Object.assign({}, location, {
+ line,
+ column,
+ // prepend whitespace for scripts that do not start on the first column
+ // NOTE: `column` is 1-based
+ source: " ".repeat(column - 1) + location.source,
+ });
+}
+
+function parseScriptTags(source, parser) {
+ const scripts = parseScripts(getCandidateScriptLocations(source), parser)
+ .filter(types.isFile)
+ .reduce(
+ (main, script) => {
+ return {
+ statements: main.statements.concat(script.program.body),
+ comments: main.comments.concat(script.comments),
+ tokens: main.tokens.concat(script.tokens),
+ };
+ },
+ {
+ statements: [],
+ comments: [],
+ tokens: [],
+ }
+ );
+
+ const program = types.program(scripts.statements);
+ const file = types.file(program, scripts.comments, scripts.tokens);
+
+ const end = calcLineAndColumn(source, source.length);
+ file.start = program.start = 0;
+ file.end = program.end = source.length;
+ file.loc = program.loc = {
+ start: {
+ line: 1,
+ column: 0,
+ },
+ end,
+ };
+
+ return file;
+}
+
+export default parseScriptTags;
+export { getCandidateScriptLocations, parseScripts, parseScriptTags };
diff --git a/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/index.js b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/index.js
new file mode 100644
index 0000000000..e85d0cdc53
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/index.js
@@ -0,0 +1,60 @@
+/**
+ * https://github.com/ryanjduffy/parse-script-tags
+ *
+ * Copyright (c) by Ryan Duff
+ * Licensed under the MIT License.
+ */
+
+import * as types from "@babel/types";
+import * as babelParser from "@babel/parser";
+
+import {
+ getCandidateScriptLocations,
+ parseScripts as customParseScripts,
+ parseScriptTags as customParseScriptTags,
+} from "./customParse.js";
+
+function parseScript({ source, line }) {
+ // remove empty or only whitespace scripts
+ if (source.length === 0 || /^\s+$/.test(source)) {
+ return null;
+ }
+
+ try {
+ return babelParser.parse(source, {
+ sourceType: "script",
+ startLine: line,
+ });
+ } catch (e) {
+ return null;
+ }
+}
+
+function parseScripts(locations, parser = parseScript) {
+ return customParseScripts(locations, parser);
+}
+
+function extractScriptTags(source) {
+ return parseScripts(getCandidateScriptLocations(source), loc => {
+ const ast = parseScript(loc);
+
+ if (ast) {
+ return loc;
+ }
+
+ return null;
+ }).filter(types.isFile);
+}
+
+function parseScriptTags(source, parser = parseScript) {
+ return customParseScriptTags(source, parser);
+}
+
+export default parseScriptTags;
+export {
+ extractScriptTags,
+ getCandidateScriptLocations,
+ parseScript,
+ parseScripts,
+ parseScriptTags,
+};
diff --git a/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/parseScriptFragment.js b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/parseScriptFragment.js
new file mode 100644
index 0000000000..11dfe4dc1b
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/parse-script-tags/parseScriptFragment.js
@@ -0,0 +1,155 @@
+/**
+ * https://github.com/ryanjduffy/parse-script-tags
+ *
+ * Copyright (c) by Ryan Duff
+ * Licensed under the MIT License.
+ */
+
+const alphanum = /[a-z0-9\-]/i;
+
+function parseToken(str, start) {
+ let i = start;
+ while (i < str.length && alphanum.test(str.charAt(i++))) {
+ continue;
+ }
+
+ if (i !== start) {
+ return {
+ token: str.substring(start, i - 1),
+ index: i,
+ };
+ }
+
+ return null;
+}
+
+function parseAttributes(str, start) {
+ let i = start;
+ const attributes = {};
+ let attribute = null;
+
+ while (i < str.length) {
+ const c = str.charAt(i);
+
+ if (attribute === null && c == ">") {
+ break;
+ } else if (attribute === null && alphanum.test(c)) {
+ attribute = {
+ name: null,
+ value: true,
+ bool: true,
+ terminator: null,
+ };
+
+ const attributeNameNode = parseToken(str, i);
+ if (attributeNameNode) {
+ attribute.name = attributeNameNode.token;
+ i = attributeNameNode.index - 2;
+ }
+ } else if (attribute !== null) {
+ if (c === "=") {
+ // once we've started an attribute, look for = to indicate
+ // it's a non-boolean attribute
+ attribute.bool = false;
+ if (attribute.value === true) {
+ attribute.value = "";
+ }
+ } else if (
+ !attribute.bool &&
+ attribute.terminator === null &&
+ (c === '"' || c === "'")
+ ) {
+ // once we've determined it's non-boolean, look for a
+ // value terminator (", ')
+ attribute.terminator = c;
+ } else if (attribute.terminator) {
+ if (c === attribute.terminator) {
+ // if we had a terminator and found another, we've
+ // reach the end of the attribute
+ attributes[attribute.name] = attribute.value;
+ attribute = null;
+ } else {
+ // otherwise, append the character to the attribute value
+ attribute.value += c;
+
+ // check for an escaped terminator and push it as well
+ // to avoid terminating prematurely
+ if (c === "\\") {
+ const next = str.charAt(i + 1);
+ if (next === attribute.terminator) {
+ attribute.value += next;
+ i += 1;
+ }
+ }
+ }
+ } else if (!/\s/.test(c)) {
+ // if we've hit a non-space character and aren't processing a value,
+ // we're starting a new attribute so push the attribute and clear the
+ // local variable
+ attributes[attribute.name] = attribute.value;
+ attribute = null;
+
+ // move the cursor back to re-find the start of the attribute
+ i -= 1;
+ }
+ }
+
+ i++;
+ }
+
+ if (i !== start) {
+ return {
+ attributes,
+ index: i,
+ };
+ }
+
+ return null;
+}
+
+function parseFragment(str, start = 0) {
+ let tag = null;
+ let open = false;
+ let attributes = {};
+
+ let i = start;
+ while (i < str.length) {
+ const c = str.charAt(i++);
+
+ if (!open && !tag && c === "<") {
+ // Open Start Tag
+ open = true;
+
+ const tagNode = parseToken(str, i);
+ if (!tagNode) {
+ return null;
+ }
+
+ i = tagNode.index - 1;
+ tag = tagNode.token;
+ } else if (open && c === ">") {
+ // Close Start Tag
+ break;
+ } else if (open) {
+ // Attributes
+ const attributeNode = parseAttributes(str, i - 1);
+
+ if (attributeNode) {
+ i = attributeNode.index;
+ attributes = attributeNode.attributes || attributes;
+ }
+ }
+ }
+
+ if (tag) {
+ return {
+ tag,
+ attributes,
+ };
+ }
+
+ return null;
+}
+
+export default parseFragment;
+export { parseFragment };
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..b3958dcf42
--- /dev/null
+++ b/devtools/client/debugger/src/workers/parser/utils/simple-path.js
@@ -0,0 +1,104 @@
+/* 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;
+ }
+
+ 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;
+ }
+
+ 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);
+ }
+}
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" },
+ ]
+);