/* vim: set sw=4 sts=4 : */
import estraverse from 'estraverse';
import parser from './parser.js';
/**
* @typedef {"LEFT_SIDE"|"RIGHT_SIDE"} Side
*/
const LEFT_SIDE = 'LEFT_SIDE';
const RIGHT_SIDE = 'RIGHT_SIDE';
/**
* @external AST
* @see https://esprima.readthedocs.io/en/latest/syntax-tree-format.html
*/
/**
* One of the rules of `grammar.pegjs`
* @typedef {PlainObject} SelectorAST
* @see grammar.pegjs
*/
/**
* The `sequence` production of `grammar.pegjs`
* @typedef {PlainObject} SelectorSequenceAST
*/
/**
* Get the value of a property which may be multiple levels down
* in the object.
* @param {?PlainObject} obj
* @param {string[]} keys
* @returns {undefined|boolean|string|number|external:AST}
*/
function getPath(obj, keys) {
for (let i = 0; i < keys.length; ++i) {
if (obj == null) { return obj; }
obj = obj[keys[i]];
}
return obj;
}
/**
* Determine whether `node` can be reached by following `path`,
* starting at `ancestor`.
* @param {?external:AST} node
* @param {?external:AST} ancestor
* @param {string[]} path
* @param {Integer} fromPathIndex
* @returns {boolean}
*/
function inPath(node, ancestor, path, fromPathIndex) {
let current = ancestor;
for (let i = fromPathIndex; i < path.length; ++i) {
if (current == null) {
return false;
}
const field = current[path[i]];
if (Array.isArray(field)) {
for (let k = 0; k < field.length; ++k) {
if (inPath(node, field[k], path, i + 1)) {
return true;
}
}
return false;
}
current = field;
}
return node === current;
}
/**
* A generated matcher function for a selector.
* @typedef {function} SelectorMatcher
*/
/**
* A WeakMap for holding cached matcher functions for selectors.
* @type {WeakMap<SelectorAST, SelectorMatcher>}
*/
const MATCHER_CACHE = typeof WeakMap === 'function' ? new WeakMap : null;
/**
* Look up a matcher function for `selector` in the cache.
* If it does not exist, generate it with `generateMatcher` and add it to the cache.
* In engines without WeakMap, the caching is skipped and matchers are generated with every call.
* @param {?SelectorAST} selector
* @returns {SelectorMatcher}
*/
function getMatcher(selector) {
if (selector == null) {
return () => true;
}
if (MATCHER_CACHE != null) {
let matcher = MATCHER_CACHE.get(selector);
if (matcher != null) {
return matcher;
}
matcher = generateMatcher(selector);
MATCHER_CACHE.set(selector, matcher);
return matcher;
}
return generateMatcher(selector);
}
/**
* Create a matcher function for `selector`,
* @param {?SelectorAST} selector
* @returns {SelectorMatcher}
*/
function generateMatcher(selector) {
switch(selector.type) {
case 'wildcard':
return () => true;
case 'identifier': {
const value = selector.value.toLowerCase();
return (node) => value === node.type.toLowerCase();
}
case 'field': {
const path = selector.name.split('.');
return (node, ancestry) => {
const ancestor = ancestry[path.length - 1];
return inPath(node, ancestor, path, 0);
};
}
case 'matches': {
const matchers = selector.selectors.map(getMatcher);
return (node, ancestry, options) => {
for (let i = 0; i < matchers.length; ++i) {
if (matchers[i](node, ancestry, options)) { return true; }
}
return false;
};
}
case 'compound': {
const matchers = selector.selectors.map(getMatcher);
return (node, ancestry, options) => {
for (let i = 0; i < matchers.length; ++i) {
if (!matchers[i](node, ancestry, options)) { return false; }
}
return true;
};
}
case 'not': {
const matchers = selector.selectors.map(getMatcher);
return (node, ancestry, options) => {
for (let i = 0; i < matchers.length; ++i) {
if (matchers[i](node, ancestry, options)) { return false; }
}
return true;
};
}
case 'has': {
const matchers = selector.selectors.map(getMatcher);
return (node, ancestry, options) => {
let result = false;
const a = [];
estraverse.traverse(node, {
enter (node, parent) {
if (parent != null) { a.unshift(parent); }
for (let i = 0; i < matchers.length; ++i) {
if (matchers[i](node, a, options)) {
result = true;
this.break();
return;
}
}
},
leave () { a.shift(); },
keys: options && options.visitorKeys,
fallback: options && options.fallback || 'iteration'
});
return result;
};
}
case 'child': {
const left = getMatcher(selector.left);
const right = getMatcher(selector.right);
return (node, ancestry, options) => {
if (ancestry.length > 0 && right(node, ancestry, options)) {
return left(ancestry[0], ancestry.slice(1), options);
}
return false;
};
}
case 'descendant': {
const left = getMatcher(selector.left);
const right = getMatcher(selector.right);
return (node, ancestry, options) => {
if (right(node, ancestry, options)) {
for (let i = 0, l = ancestry.length; i < l; ++i) {
if (left(ancestry[i], ancestry.slice(i + 1), options)) {
return true;
}
}
}
return false;
};
}
case 'attribute': {
const path = selector.name.split('.');
switch (selector.operator) {
case void 0:
return (node) => getPath(node, path) != null;
case '=':
switch (selector.value.type) {
case 'regexp':
return (node) => {
const p = getPath(node, path);
return typeof p === 'string' && selector.value.value.test(p);
};
case 'literal': {
const literal = `${selector.value.value}`;
return (node) => literal === `${getPath(node, path)}`;
}
case 'type':
return (node) => selector.value.value === typeof getPath(node, path);
}
throw new Error(`Unknown selector value type: ${selector.value.type}`);
case '!=':
switch (selector.value.type) {
case 'regexp':
return (node) => !selector.value.value.test(getPath(node, path));
case 'literal': {
const literal = `${selector.value.value}`;
return (node) => literal !== `${getPath(node, path)}`;
}
case 'type':
return (node) => selector.value.value !== typeof getPath(node, path);
}
throw new Error(`Unknown selector value type: ${selector.value.type}`);
case '<=':
return (node) => getPath(node, path) <= selector.value.value;
case '<':
return (node) => getPath(node, path) < selector.value.value;
case '>':
return (node) => getPath(node, path) > selector.value.value;
case '>=':
return (node) => getPath(node, path) >= selector.value.value;
}
throw new Error(`Unknown operator: ${selector.operator}`);
}
case 'sibling': {
const left = getMatcher(selector.left);
const right = getMatcher(selector.right);
return (node, ancestry, options) =>
right(node, ancestry, options) &&
sibling(node, left, ancestry, LEFT_SIDE, options) ||
selector.left.subject &&
left(node, ancestry, options) &&
sibling(node, right, ancestry, RIGHT_SIDE, options);
}
case 'adjacent': {
const left = getMatcher(selector.left);
const right = getMatcher(selector.right);
return (node, ancestry, options) =>
right(node, ancestry, options) &&
adjacent(node, left, ancestry, LEFT_SIDE, options) ||
selector.right.subject &&
left(node, ancestry, options) &&
adjacent(node, right, ancestry, RIGHT_SIDE, options);
}
case 'nth-child': {
const nth = selector.index.value;
const right = getMatcher(selector.right);
return (node, ancestry, options) =>
right(node, ancestry, options) &&
nthChild(node, ancestry, nth, options);
}
case 'nth-last-child': {
const nth = -selector.index.value;
const right = getMatcher(selector.right);
return (node, ancestry, options) =>
right(node, ancestry, options) &&
nthChild(node, ancestry, nth, options);
}
case 'class': {
const name = selector.name.toLowerCase();
return (node, ancestry) => {
switch(name){
case 'statement':
if(node.type.slice(-9) === 'Statement') return true;
// fallthrough: interface Declaration <: Statement { }
case 'declaration':
return node.type.slice(-11) === 'Declaration';
case 'pattern':
if(node.type.slice(-7) === 'Pattern') return true;
// fallthrough: interface Expression <: Node, Pattern { }
case 'expression':
return node.type.slice(-10) === 'Expression' ||
node.type.slice(-7) === 'Literal' ||
(
node.type === 'Identifier' &&
(ancestry.length === 0 || ancestry[0].type !== 'MetaProperty')
) ||
node.type === 'MetaProperty';
case 'function':
return node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression';
}
throw new Error(`Unknown class name: ${selector.name}`);
};
}
}
throw new Error(`Unknown selector type: ${selector.type}`);
}
/**
* @callback TraverseOptionFallback
* @param {external:AST} node The given node.
* @returns {string[]} An array of visitor keys for the given node.
*/
/**
* @typedef {object} ESQueryOptions
* @property { { [nodeType: string]: string[] } } [visitorKeys] By passing `visitorKeys` mapping, we can extend the properties of the nodes that traverse the node.
* @property {TraverseOptionFallback} [fallback] By passing `fallback` option, we can control the properties of traversing nodes when encountering unknown nodes.
*/
/**
* Given a `node` and its ancestors, determine if `node` is matched
* by `selector`.
* @param {?external:AST} node
* @param {?SelectorAST} selector
* @param {external:AST[]} [ancestry=[]]
* @param {ESQueryOptions} [options]
* @throws {Error} Unknowns (operator, class name, selector type, or
* selector value type)
* @returns {boolean}
*/
function matches(node, selector, ancestry, options) {
if (!selector) { return true; }
if (!node) { return false; }
if (!ancestry) { ancestry = []; }
return getMatcher(selector)(node, ancestry, options);
}
/**
* Get visitor keys of a given node.
* @param {external:AST} node The AST node to get keys.
* @param {ESQueryOptions|undefined} options
* @returns {string[]} Visitor keys of the node.
*/
function getVisitorKeys(node, options) {
const nodeType = node.type;
if (options && options.visitorKeys && options.visitorKeys[nodeType]) {
return options.visitorKeys[nodeType];
}
if (estraverse.VisitorKeys[nodeType]) {
return estraverse.VisitorKeys[nodeType];
}
if (options && typeof options.fallback === 'function') {
return options.fallback(node);
}
// 'iteration' fallback
return Object.keys(node).filter(function (key) {
return key !== 'type';
});
}
/**
* Check whether the given value is an ASTNode or not.
* @param {any} node The value to check.
* @returns {boolean} `true` if the value is an ASTNode.
*/
function isNode(node) {
return node !== null && typeof node === 'object' && typeof node.type === 'string';
}
/**
* Determines if the given node has a sibling that matches the
* given selector matcher.
* @param {external:AST} node
* @param {SelectorMatcher} matcher
* @param {external:AST[]} ancestry
* @param {Side} side
* @param {ESQueryOptions|undefined} options
* @returns {boolean}
*/
function sibling(node, matcher, ancestry, side, options) {
const [parent] = ancestry;
if (!parent) { return false; }
const keys = getVisitorKeys(parent, options);
for (let i = 0; i < keys.length; ++i) {
const listProp = parent[keys[i]];
if (Array.isArray(listProp)) {
const startIndex = listProp.indexOf(node);
if (startIndex < 0) { continue; }
let lowerBound, upperBound;
if (side === LEFT_SIDE) {
lowerBound = 0;
upperBound = startIndex;
} else {
lowerBound = startIndex + 1;
upperBound = listProp.length;
}
for (let k = lowerBound; k < upperBound; ++k) {
if (isNode(listProp[k]) && matcher(listProp[k], ancestry, options)) {
return true;
}
}
}
}
return false;
}
/**
* Determines if the given node has an adjacent sibling that matches
* the given selector matcher.
* @param {external:AST} node
* @param {SelectorMatcher} matcher
* @param {external:AST[]} ancestry
* @param {Side} side
* @param {ESQueryOptions|undefined} options
* @returns {boolean}
*/
function adjacent(node, matcher, ancestry, side, options) {
const [parent] = ancestry;
if (!parent) { return false; }
const keys = getVisitorKeys(parent, options);
for (let i = 0; i < keys.length; ++i) {
const listProp = parent[keys[i]];
if (Array.isArray(listProp)) {
const idx = listProp.indexOf(node);
if (idx < 0) { continue; }
if (side === LEFT_SIDE && idx > 0 && isNode(listProp[idx - 1]) && matcher(listProp[idx - 1], ancestry, options)) {
return true;
}
if (side === RIGHT_SIDE && idx < listProp.length - 1 && isNode(listProp[idx + 1]) && matcher(listProp[idx + 1], ancestry, options)) {
return true;
}
}
}
return false;
}
/**
* Determines if the given node is the `nth` child.
* If `nth` is negative then the position is counted
* from the end of the list of children.
* @param {external:AST} node
* @param {external:AST[]} ancestry
* @param {Integer} nth
* @param {ESQueryOptions|undefined} options
* @returns {boolean}
*/
function nthChild(node, ancestry, nth, options) {
if (nth === 0) { return false; }
const [parent] = ancestry;
if (!parent) { return false; }
const keys = getVisitorKeys(parent, options);
for (let i = 0; i < keys.length; ++i) {
const listProp = parent[keys[i]];
if (Array.isArray(listProp)){
const idx = nth < 0 ? listProp.length + nth : nth - 1;
if (idx >= 0 && idx < listProp.length && listProp[idx] === node) {
return true;
}
}
}
return false;
}
/**
* For each selector node marked as a subject, find the portion of the
* selector that the subject must match.
* @param {SelectorAST} selector
* @param {SelectorAST} [ancestor] Defaults to `selector`
* @returns {SelectorAST[]}
*/
function subjects(selector, ancestor) {
if (selector == null || typeof selector != 'object') { return []; }
if (ancestor == null) { ancestor = selector; }
const results = selector.subject ? [ancestor] : [];
const keys = Object.keys(selector);
for (let i = 0; i < keys.length; ++i) {
const p = keys[i];
const sel = selector[p];
results.push(...subjects(sel, p === 'left' ? sel : ancestor));
}
return results;
}
/**
* @callback TraverseVisitor
* @param {?external:AST} node
* @param {?external:AST} parent
* @param {external:AST[]} ancestry
*/
/**
* From a JS AST and a selector AST, collect all JS AST nodes that
* match the selector.
* @param {external:AST} ast
* @param {?SelectorAST} selector
* @param {TraverseVisitor} visitor
* @param {ESQueryOptions} [options]
* @returns {external:AST[]}
*/
function traverse(ast, selector, visitor, options) {
if (!selector) { return; }
const ancestry = [];
const matcher = getMatcher(selector);
const altSubjects = subjects(selector).map(getMatcher);
estraverse.traverse(ast, {
enter (node, parent) {
if (parent != null) { ancestry.unshift(parent); }
if (matcher(node, ancestry, options)) {
if (altSubjects.length) {
for (let i = 0, l = altSubjects.length; i < l; ++i) {
if (altSubjects[i](node, ancestry, options)) {
visitor(node, parent, ancestry);
}
for (let k = 0, m = ancestry.length; k < m; ++k) {
const succeedingAncestry = ancestry.slice(k + 1);
if (altSubjects[i](ancestry[k], succeedingAncestry, options)) {
visitor(ancestry[k], parent, succeedingAncestry);
}
}
}
} else {
visitor(node, parent, ancestry);
}
}
},
leave () { ancestry.shift(); },
keys: options && options.visitorKeys,
fallback: options && options.fallback || 'iteration'
});
}
/**
* From a JS AST and a selector AST, collect all JS AST nodes that
* match the selector.
* @param {external:AST} ast
* @param {?SelectorAST} selector
* @param {ESQueryOptions} [options]
* @returns {external:AST[]}
*/
function match(ast, selector, options) {
const results = [];
traverse(ast, selector, function (node) {
results.push(node);
}, options);
return results;
}
/**
* Parse a selector string and return its AST.
* @param {string} selector
* @returns {SelectorAST}
*/
function parse(selector) {
return parser.parse(selector);
}
/**
* Query the code AST using the selector string.
* @param {external:AST} ast
* @param {string} selector
* @param {ESQueryOptions} [options]
* @returns {external:AST[]}
*/
function query(ast, selector, options) {
return match(ast, parse(selector), options);
}
query.parse = parse;
query.match = match;
query.traverse = traverse;
query.matches = matches;
query.query = query;
export default query;
|