Skip to content

Commit a195660

Browse files
authored
Allow for custom node type keys (#139)
1 parent d00dac8 commit a195660

File tree

4 files changed

+116
-9
lines changed

4 files changed

+116
-9
lines changed

esquery.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ function generateMatcher(selector) {
117117

118118
case 'identifier': {
119119
const value = selector.value.toLowerCase();
120-
return (node) => value === node.type.toLowerCase();
120+
return (node, ancestry, options) => {
121+
const nodeTypeKey = (options && options.nodeTypeKey) || 'type';
122+
return value === node[nodeTypeKey].toLowerCase();
123+
};
121124
}
122125

123126
case 'field': {
@@ -294,8 +297,12 @@ function generateMatcher(selector) {
294297
}
295298

296299
case 'class': {
300+
297301
const name = selector.name.toLowerCase();
298-
return (node, ancestry) => {
302+
return (node, ancestry, options) => {
303+
304+
if (options && options.nodeTypeKey) return false;
305+
299306
switch(name){
300307
case 'statement':
301308
if(node.type.slice(-9) === 'Statement') return true;
@@ -333,6 +340,7 @@ function generateMatcher(selector) {
333340
*/
334341
/**
335342
* @typedef {object} ESQueryOptions
343+
* @property {string} [nodeTypeKey="type"] By passing `nodeTypeKey`, we can allow other ASTs to use ESQuery.
336344
* @property { { [nodeType: string]: string[] } } [visitorKeys] By passing `visitorKeys` mapping, we can extend the properties of the nodes that traverse the node.
337345
* @property {TraverseOptionFallback} [fallback] By passing `fallback` option, we can control the properties of traversing nodes when encountering unknown nodes.
338346
*/
@@ -363,7 +371,9 @@ function matches(node, selector, ancestry, options) {
363371
* @returns {string[]} Visitor keys of the node.
364372
*/
365373
function getVisitorKeys(node, options) {
366-
const nodeType = node.type;
374+
const nodeTypeKey = (options && options.nodeTypeKey) || 'type';
375+
376+
const nodeType = node[nodeTypeKey];
367377
if (options && options.visitorKeys && options.visitorKeys[nodeType]) {
368378
return options.visitorKeys[nodeType];
369379
}
@@ -375,18 +385,20 @@ function getVisitorKeys(node, options) {
375385
}
376386
// 'iteration' fallback
377387
return Object.keys(node).filter(function (key) {
378-
return key !== 'type';
388+
return key !== nodeTypeKey;
379389
});
380390
}
381391

382392

383393
/**
384394
* Check whether the given value is an ASTNode or not.
385395
* @param {any} node The value to check.
396+
* @param {ESQueryOptions|undefined} options The options to use.
386397
* @returns {boolean} `true` if the value is an ASTNode.
387398
*/
388-
function isNode(node) {
389-
return node !== null && typeof node === 'object' && typeof node.type === 'string';
399+
function isNode(node, options) {
400+
const nodeTypeKey = (options && options.nodeTypeKey) || 'type';
401+
return node !== null && typeof node === 'object' && typeof node[nodeTypeKey] === 'string';
390402
}
391403

392404
/**
@@ -417,7 +429,7 @@ function sibling(node, matcher, ancestry, side, options) {
417429
upperBound = listProp.length;
418430
}
419431
for (let k = lowerBound; k < upperBound; ++k) {
420-
if (isNode(listProp[k]) && matcher(listProp[k], ancestry, options)) {
432+
if (isNode(listProp[k], options) && matcher(listProp[k], ancestry, options)) {
421433
return true;
422434
}
423435
}
@@ -445,10 +457,10 @@ function adjacent(node, matcher, ancestry, side, options) {
445457
if (Array.isArray(listProp)) {
446458
const idx = listProp.indexOf(node);
447459
if (idx < 0) { continue; }
448-
if (side === LEFT_SIDE && idx > 0 && isNode(listProp[idx - 1]) && matcher(listProp[idx - 1], ancestry, options)) {
460+
if (side === LEFT_SIDE && idx > 0 && isNode(listProp[idx - 1], options) && matcher(listProp[idx - 1], ancestry, options)) {
449461
return true;
450462
}
451-
if (side === RIGHT_SIDE && idx < listProp.length - 1 && isNode(listProp[idx + 1]) && matcher(listProp[idx + 1], ancestry, options)) {
463+
if (side === RIGHT_SIDE && idx < listProp.length - 1 && isNode(listProp[idx + 1], options) && matcher(listProp[idx + 1], ancestry, options)) {
452464
return true;
453465
}
454466
}

tests/fixtures/customNodesWithKind.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export default {
2+
kind: 'CustomRoot',
3+
list: [
4+
{
5+
kind: 'CustomChild',
6+
name: 'one',
7+
sublist: [{ kind: 'CustomGrandChild' }],
8+
},
9+
{
10+
kind: 'CustomChild',
11+
name: 'two',
12+
sublist: [],
13+
},
14+
{
15+
kind: 'CustomChild',
16+
name: 'three',
17+
sublist: [
18+
{ kind: 'CustomGrandChild' },
19+
{ kind: 'CustomGrandChild' },
20+
],
21+
},
22+
{
23+
kind: 'CustomChild',
24+
name: 'four',
25+
sublist: [
26+
{ kind: 'CustomGrandChild' },
27+
{ kind: 'CustomGrandChild' },
28+
{ kind: 'CustomGrandChild' },
29+
],
30+
},
31+
{
32+
kind: 'CustomExpression'
33+
},
34+
{
35+
kind: 'CustomStatement'
36+
}
37+
],
38+
};

tests/matches.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import forLoop from './fixtures/forLoop.js';
33
import simpleProgram from './fixtures/simpleProgram.js';
44
import conditional from './fixtures/conditional.js';
55
import customNodes from './fixtures/customNodes.js';
6+
import customNodesWithKind from './fixtures/customNodesWithKind.js';
67

78
describe('matches', function () {
89
it('falsey node', function () {
@@ -167,6 +168,41 @@ describe('matches with custom AST and custom visitor keys', function () {
167168
});
168169
});
169170

171+
describe('matches with custom AST and nodeTypeKey and custom visitor keys', function () {
172+
it('adjacent/sibling', function () {
173+
const options = {
174+
visitorKeys: {
175+
CustomRoot: ['list'],
176+
CustomChild: ['sublist'],
177+
CustomGrandChild: [],
178+
CustomStatement: [],
179+
CustomExpression: []
180+
},
181+
nodeTypeKey: 'kind'
182+
};
183+
184+
let selector = esquery.parse('CustomChild + CustomChild');
185+
assert.doesNotThrow(() => {
186+
esquery.matches(
187+
customNodesWithKind.list[1],
188+
selector,
189+
[customNodesWithKind],
190+
options
191+
);
192+
});
193+
194+
selector = esquery.parse('CustomChild ~ CustomChild');
195+
assert.doesNotThrow(() => {
196+
esquery.matches(
197+
customNodesWithKind.list[1],
198+
selector,
199+
[customNodesWithKind],
200+
options
201+
);
202+
});
203+
});
204+
});
205+
170206
describe('matches with custom AST and fallback option', function () {
171207
it('adjacent/sibling', function () {
172208
const options = {

tests/queryClass.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import esquery from '../esquery.js';
22
import ast from './fixtures/allClasses.js';
3+
import customNodesWithKind from './fixtures/customNodesWithKind.js';
34

45
describe('Class query', function () {
56

@@ -66,4 +67,24 @@ describe('Class query', function () {
6667
assert.equal(10, matches.length);
6768
});
6869

70+
it('custom nodes with :expression, :statement', function () {
71+
const options = {
72+
visitorKeys: {
73+
CustomRoot: ['list'],
74+
CustomChild: ['sublist'],
75+
CustomGrandChild: [],
76+
CustomStatement: [],
77+
CustomExpression: []
78+
},
79+
nodeTypeKey: 'kind'
80+
};
81+
82+
const matches1 = esquery(customNodesWithKind, ':expression', options);
83+
assert.equal(0, matches1.length);
84+
85+
const matches2 = esquery(customNodesWithKind, ':statement', options);
86+
assert.equal(0, matches2.length);
87+
});
88+
89+
6990
});

0 commit comments

Comments
 (0)