Skip to content

Commit

Permalink
Create cached matcher functions for selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
jviide authored and michaelficarra committed Feb 16, 2023
1 parent 7c3800a commit befa5c8
Showing 1 changed file with 169 additions and 137 deletions.
306 changes: 169 additions & 137 deletions esquery.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,179 +64,211 @@ function inPath(node, ancestor, path) {
}
}

/**
* @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 = []; }

function createMatcher(selector) {
switch(selector.type) {
case 'wildcard':
return true;
return () => true;

case 'identifier':
return selector.value.toLowerCase() === node.type.toLowerCase();
return (node) => selector.value.toLowerCase() === node.type.toLowerCase();

case 'field': {
const path = selector.name.split('.');
const ancestor = ancestry[path.length - 1];
return inPath(node, ancestor, path);

return (node, ancestry) => {
const path = selector.name.split('.');
const ancestor = ancestry[path.length - 1];
return inPath(node, ancestor, path);
};
}
case 'matches':
for (const sel of selector.selectors) {
if (matches(node, sel, ancestry, options)) { return true; }
}
return false;
return (node, ancestry, options) => {
for (const sel of selector.selectors) {
if (matches(node, sel, ancestry, options)) { return true; }
}
return false;
};

case 'compound':
for (const sel of selector.selectors) {
if (!matches(node, sel, ancestry, options)) { return false; }
}
return true;
return (node, ancestry, options) => {
for (const sel of selector.selectors) {
if (!matches(node, sel, ancestry, options)) { return false; }
}
return true;
};

case 'not':
for (const sel of selector.selectors) {
if (matches(node, sel, ancestry, options)) { return false; }
}
return true;
return (node, ancestry, options) => {
for (const sel of selector.selectors) {
if (matches(node, sel, ancestry, options)) { return false; }
}
return true;
};

case 'has': {
const collector = [];
for (const sel of selector.selectors) {
const a = [];
estraverse.traverse(node, {
enter (node, parent) {
if (parent != null) { a.unshift(parent); }
if (matches(node, sel, a, options)) {
collector.push(node);
}
},
leave () { a.shift(); },
keys: options && options.visitorKeys,
fallback: options && options.fallback || 'iteration'
});
}
return collector.length !== 0;

return (node, ancestry, options) => {
const collector = [];
for (const sel of selector.selectors) {
const a = [];
estraverse.traverse(node, {
enter (node, parent) {
if (parent != null) { a.unshift(parent); }
if (matches(node, sel, a, options)) {
collector.push(node);
}
},
leave () { a.shift(); },
keys: options && options.visitorKeys,
fallback: options && options.fallback || 'iteration'
});
}
return collector.length !== 0;
};
}
case 'child':
if (matches(node, selector.right, ancestry, options)) {
return matches(ancestry[0], selector.left, ancestry.slice(1), options);
}
return false;
return (node, ancestry, options) => {
if (matches(node, selector.right, ancestry, options)) {
return matches(ancestry[0], selector.left, ancestry.slice(1), options);
}
return false;
};

case 'descendant':
if (matches(node, selector.right, ancestry, options)) {
for (let i = 0, l = ancestry.length; i < l; ++i) {
if (matches(ancestry[i], selector.left, ancestry.slice(i + 1), options)) {
return true;
return (node, ancestry, options) => {
if (matches(node, selector.right, ancestry, options)) {
for (let i = 0, l = ancestry.length; i < l; ++i) {
if (matches(ancestry[i], selector.left, ancestry.slice(i + 1), options)) {
return true;
}
}
}
}
return false;
return false;
};

case 'attribute': {
const p = getPath(node, selector.name);
switch (selector.operator) {
case void 0:
return p != null;
case '=':
switch (selector.value.type) {
case 'regexp': return typeof p === 'string' && selector.value.value.test(p);
case 'literal': return `${selector.value.value}` === `${p}`;
case 'type': return selector.value.value === typeof p;
}
throw new Error(`Unknown selector value type: ${selector.value.type}`);
case '!=':
switch (selector.value.type) {
case 'regexp': return !selector.value.value.test(p);
case 'literal': return `${selector.value.value}` !== `${p}`;
case 'type': return selector.value.value !== typeof p;
}
throw new Error(`Unknown selector value type: ${selector.value.type}`);
case '<=': return p <= selector.value.value;
case '<': return p < selector.value.value;
case '>': return p > selector.value.value;
case '>=': return p >= selector.value.value;
}
throw new Error(`Unknown operator: ${selector.operator}`);
return (node) => {
const p = getPath(node, selector.name);
switch (selector.operator) {
case void 0:
return p != null;
case '=':
switch (selector.value.type) {
case 'regexp': return typeof p === 'string' && selector.value.value.test(p);
case 'literal': return `${selector.value.value}` === `${p}`;
case 'type': return selector.value.value === typeof p;
}
throw new Error(`Unknown selector value type: ${selector.value.type}`);
case '!=':
switch (selector.value.type) {
case 'regexp': return !selector.value.value.test(p);
case 'literal': return `${selector.value.value}` !== `${p}`;
case 'type': return selector.value.value !== typeof p;
}
throw new Error(`Unknown selector value type: ${selector.value.type}`);
case '<=': return p <= selector.value.value;
case '<': return p < selector.value.value;
case '>': return p > selector.value.value;
case '>=': return p >= selector.value.value;
}
throw new Error(`Unknown operator: ${selector.operator}`);
};
}
case 'sibling':
return matches(node, selector.right, ancestry, options) &&
sibling(node, selector.left, ancestry, LEFT_SIDE, options) ||
selector.left.subject &&
matches(node, selector.left, ancestry, options) &&
sibling(node, selector.right, ancestry, RIGHT_SIDE, options);
return (node, ancestry, options) => {
return matches(node, selector.right, ancestry, options) &&
sibling(node, selector.left, ancestry, LEFT_SIDE, options) ||
selector.left.subject &&
matches(node, selector.left, ancestry, options) &&
sibling(node, selector.right, ancestry, RIGHT_SIDE, options);
};

case 'adjacent':
return matches(node, selector.right, ancestry, options) &&
adjacent(node, selector.left, ancestry, LEFT_SIDE, options) ||
selector.right.subject &&
matches(node, selector.left, ancestry, options) &&
adjacent(node, selector.right, ancestry, RIGHT_SIDE, options);
return (node, ancestry, options) => {
return matches(node, selector.right, ancestry, options) &&
adjacent(node, selector.left, ancestry, LEFT_SIDE, options) ||
selector.right.subject &&
matches(node, selector.left, ancestry, options) &&
adjacent(node, selector.right, ancestry, RIGHT_SIDE, options);
};

case 'nth-child':
return matches(node, selector.right, ancestry, options) &&
nthChild(node, ancestry, function () {
return selector.index.value - 1;
}, options);
return (node, ancestry, options) => {
return matches(node, selector.right, ancestry, options) &&
nthChild(node, ancestry, function () {
return selector.index.value - 1;
}, options);
};

case 'nth-last-child':
return matches(node, selector.right, ancestry, options) &&
nthChild(node, ancestry, function (length) {
return length - selector.index.value;
}, options);
return (node, ancestry, options) => {
return matches(node, selector.right, ancestry, options) &&
nthChild(node, ancestry, function (length) {
return length - selector.index.value;
}, options);
};

case 'class':
switch(selector.name.toLowerCase()){
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}`);
return (node, ancestry) => {
switch(selector.name.toLowerCase()){
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 = []; }

if (!selector._match) {
selector._match = createMatcher(selector);
}
return selector._match(node, ancestry, options);
}

/**
* Get visitor keys of a given node.
* @param {external:AST} node The AST node to get keys.
Expand Down

0 comments on commit befa5c8

Please sign in to comment.