UNPKG

jsinspect-plus

Version:

Detect copy-pasted and structurally similar code. Supports ES2020 standard (and most proposed features), TS and TSX files. Using Babel 8's parser.

262 lines (230 loc) 7.2 kB
/** * Cache for getChildren, holding the keys to traverse for a given Node type. * Some JSX node types are hardcoded to ensure correct property traversal order. * For example, this ensures that a JSXElement's children are traversed prior * to the closing element. */ const childKeys = { JSXElement: ['openingElement', 'extra', 'children', 'closingElement'], JSXOpeningElement: ['name', 'attributes'], JSXAttribute: ['name', 'value'], }; class NodeUtils { /** * Walks a root node's subtrees using DFS, invoking the passed callback with * three args: node, parent, and ancestors. The root node, presumably Program, * is ignored. * * @param {Node} root The root node of the AST to traverse * @param {function} fn Callback to invoke */ static walkSubtrees(root, fn) { const visit = (node, parent, ancestors) => { fn(node, parent, ancestors); ancestors = ancestors.concat(node); NodeUtils.getChildren(node).forEach((child) => { visit(child, node, ancestors); }); }; NodeUtils.getChildren(root).forEach((child) => { visit(child, null, []); }); } /** * Returns an array of nodes in the passed AST, traversed using DFS. Accepts * an optional maximum number, n, of nodes to return. The returned array * always begins with the root node. * * @param {Node} node The root node of the AST to traverse * @param {int} [n] Optional max number of nodes to return * @returns {Node[]} */ static getDFSTraversal(node, n) { const res = []; const dfs = (node) => { if (n && res.length >= n) return; res.push(node); NodeUtils.getChildren(node).forEach(dfs); }; dfs(node); return res.slice(0, n); } /** * Returns an array of nodes in the passed AST, traversed using BFS. Accepts * an optional maximum number, n, of nodes to return. The returned array * always begins with the root node. * * @param {Node} node The root node of the AST to traverse * @param {int} [n] Optional max number of nodes to return * @returns {Node[]} */ static getBFSTraversal(node, n) { const queue = [node]; const res = [node]; while (queue.length) { node = queue.shift(); if (n && res.length >= n) { return res.slice(0, n); } const children = NodeUtils.getChildren(node) || []; for (let i = 0; i < children.length; i++) { queue.push(children[i]); res.push(children[i]); } } return res.slice(0, n); } /** * Returns a given node's children as an array of nodes. Designed for use * with ESTree/Babel/parser spec ASTs. * * @param {Node} node The node for which to retrieve its children * @returns {Node[]} An array of child nodes */ static getChildren(node) { let res = []; if (!childKeys[node.type]) { childKeys[node.type] = Object.keys(node).filter((key) => { return key !== 'loc' && typeof node[key] === 'object'; }); } // Ignore null values, as well as JSText nodes incorrectly generated // by babel/parser that contain only newlines and spaces const filterIgnored = (nodes) => nodes.filter(node => { return node && (node.type !== 'JSXText' || node.value.trim()); }); childKeys[node.type].forEach((key) => { var val = node[key]; if (val && val.type) { res.push(val); } else if (val instanceof Array) { res = res.concat(filterIgnored(val)); } }); return res; } /** * Returns whether the first node appears before the second, by * comparing both their starting lines and columns. * * @param {object} a * @param {object} b * @returns {boolean} */ static isBefore(a, b) { a = a.loc.start; b = b.loc.start; return a.line < b.line || (a.line === b.line && a.column < b.column); } /** * Returns whether the nodes are part of an ES6 module import. * * @param {Node[]} nodes * @returns {boolean} */ static isES6ModuleImport(nodes) { return nodes[0] && nodes[0].type === 'ImportDeclaration'; } /** * Returns whether or not the nodes belong to class boilerplate. * * @param {Node[]} nodes * @returns {boolean} */ static isES6ClassBoilerplate(nodes) { const last = nodes[nodes.length - 1]; return last.type === 'ClassDeclaration' || last.type === 'ClassBody'; } /** * Returns whether the nodes are part of an AMD require or define * expression. * * @param {Node[]} nodes * @returns {boolean} */ static isAMD(nodes) { const hasAMDName = function (node) { if (!node || !node.name) return; return (node.name === 'define' || node.name === 'require'); } // Iterate from last node for (let i = nodes.length - 1; i >= nodes.length - 5; i--) { if (!nodes[i]) { return false; } else if (nodes[i].type !== 'ExpressionStatement' || nodes[i].expression.type !== 'CallExpression') { continue; } // Handle basic cases where define/require are a property const callee = nodes[i].expression.callee; if (hasAMDName(callee)) { return true; } else if (callee.type === 'MemberExpression' && hasAMDName(callee.property)) { return true; } } return false; } /** * Returns whether the nodes are part of a CommonJS require statement. * * @param {Node[]} nodes * @returns {boolean} */ static isCommonJS(nodes) { if (!nodes[0]) { return false; } else if (nodes[0].type === 'ExpressionStatement' && nodes[0].expression.type === 'CallExpression' && nodes[0].expression.callee.name === 'require') { return true; } else if (nodes[0].type === 'VariableDeclaration' && nodes[0].declarations) { for (let j = 0; j < nodes[0].declarations.length; j++) { const declaration = nodes[0].declarations[j]; if (declaration.type === 'VariableDeclarator' && declaration.init && declaration.init.type === 'CallExpression' && declaration.init.callee.name === 'require') { return true; } } } return false; } /** * Returns whether all nodes are of the same type. * * @param {Node[]} nodes * @returns {boolean} */ static typesMatch(nodes) { return nodes.every(node => node && node.type === nodes[0].type); } /** * Returns whether all nodes have the same identifier. * * @param {Node[]} nodes * @returns {boolean} */ static identifiersMatch(nodes) { return nodes[0] && nodes.every(node => { return node && node.name === nodes[0].name; }); } /** * Returns whether all nodes have the same literal value. * * @param {Node[]} nodes * @returns {boolean} */ static literalsMatch(nodes) { const isLiteral = (node) => { return node.type.includes('Literal') || node.type === 'JSXText'; } return nodes[0] && nodes.every(node => { return node && (!isLiteral(node) || node.value === nodes[0].value); }); } } module.exports = NodeUtils;