diff options
Diffstat (limited to 'devtools/client/debugger/src/workers/parser/utils')
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" }, + ] +); |