fix: Handle quantified predicates on web-tree-sitter properly

Test cases for all new predicates added in #2532
This commit is contained in:
Andrew Dupont 2023-11-28 13:07:32 -08:00
parent 016d8c2499
commit 24e41d2bb7
2 changed files with 128 additions and 33 deletions

View file

@ -842,78 +842,90 @@ class Language {
const operator = steps[0].value;
let isPositive = true;
let matchAll = true;
let captureName;
switch (operator) {
case 'any-not-eq?':
isPositive = false;
matchAll = false;
case 'any-eq?':
matchAll = false;
case 'not-eq?':
isPositive = false;
case 'any-eq?':
case 'eq?':
if (steps.length !== 3) throw new Error(
`Wrong number of arguments to \`#eq?\` predicate. Expected 2, got ${steps.length - 1}`
`Wrong number of arguments to \`#${operator}\` predicate. Expected 2, got ${steps.length - 1}`
);
if (steps[1].type !== 'capture') throw new Error(
`First argument of \`#eq?\` predicate must be a capture. Got "${steps[1].value}"`
`First argument of \`#${operator}\` predicate must be a capture. Got "${steps[1].value}"`
);
let eqMatchAll = !operator.startsWith('any-');
let eqIsPositive = isPositive;
if (steps[2].type === 'capture') {
const captureName1 = steps[1].name;
const captureName2 = steps[2].name;
textPredicates[i].push(function(captures) {
textPredicates[i].push(function (captures) {
let nodes_1 = [];
let nodes_2 = [];
for (const c of captures) {
if (c.name === captureName1) nodes_1.push(c.node);
if (c.name === captureName2) nodes_2.push(c.node);
}
return matchAll
? nodes_1.every(n1 => nodes_2.some(n2 => n1.text === n2.text)) === isPositive
: nodes_1.some(n1 => nodes_2.some(n2 => n1.text === n2.text)) === isPositive;
let compare = (n1, n2, positive) => {
return positive ?
n1.text === n2.text :
n1.text !== n2.text;
};
return eqMatchAll
? nodes_1.every(n1 => nodes_2.some(n2 => compare(n1, n2, eqIsPositive)))
: nodes_1.some(n1 => nodes_2.some(n2 => compare(n1, n2, eqIsPositive)));
});
} else {
const captureName = steps[1].name;
captureName = steps[1].name;
const stringValue = steps[2].value;
textPredicates[i].push(function(captures) {
let matches = (n) => n.text === stringValue;
let doesNotMatch = (n) => n.text !== stringValue;
textPredicates[i].push(function (captures) {
let nodes = [];
for (const c of captures) {
if (c.name === captureName) nodes.push(c.node);
}
return matchAll
? nodes.every(n => n.text === stringValue) === isPositive
: nodes.some(n => n.text === stringValue) === isPositive;
let test = eqIsPositive ? matches : doesNotMatch;
return eqMatchAll
? nodes.every(test)
: nodes.some(test);
});
}
break;
case 'not-any-match?':
isPositive = false;
matchAll = false;
case 'any-match?':
matchAll = false;
case 'not-match?':
isPositive = false;
case 'match?':
case "any-not-match?":
case "not-match?":
isPositive = false;
case "any-match?":
case "match?":
if (steps.length !== 3) throw new Error(
`Wrong number of arguments to \`#match?\` predicate. Expected 2, got ${steps.length - 1}.`
`Wrong number of arguments to \`#${operator}\` predicate. Expected 2, got ${steps.length - 1}.`
);
if (steps[1].type !== 'capture') throw new Error(
`First argument of \`#match?\` predicate must be a capture. Got "${steps[1].value}".`
`First argument of \`#${operator}\` predicate must be a capture. Got "${steps[1].value}".`
);
if (steps[2].type !== 'string') throw new Error(
`Second argument of \`#match?\` predicate must be a string. Got @${steps[2].value}.`
`Second argument of \`#${operator}\` predicate must be a string. Got @${steps[2].value}.`
);
const captureName = steps[1].name;
captureName = steps[1].name;
const regex = new RegExp(steps[2].value);
textPredicates[i].push(function(captures) {
let matchIsPositive = isPositive;
let matchMatchAll = !operator.startsWith('any-');
textPredicates[i].push(function (captures) {
const nodes = [];
for (const c of captures) {
if (c.name === captureName) nodes.push(c.node.text);
}
if (nodes.length === 0) return !isPositive;
return matchAll
? nodes.every(text => regex.test(text)) === isPositive
: nodes.some(text => regex.test(text)) === isPositive;
let test = (text, positive) => {
return positive ?
regex.test(text) :
!regex.test(text);
};
if (nodes.length === 0) return !matchIsPositive;
return matchMatchAll
? nodes.every(text => test(text, matchIsPositive))
: nodes.some(text => test(text, matchIsPositive))
});
break;
@ -957,7 +969,7 @@ class Language {
}
captureName = steps[1].name;
const values = steps.slice(2).map(s => s.value);
textPredicates[i].push(function(captures) {
textPredicates[i].push(function (captures) {
const nodes = [];
for (const c of captures) {
if (c.name === captureName) nodes.push(c.node.text);

View file

@ -259,6 +259,89 @@ describe("Query", () => {
const captures = query.captures(tree.rootNode, null, null, {matchLimit: 32});
assert.ok(query.didExceedMatchLimit());
});
it("handles quantified captures properly", () => {
let captures;
tree = parser.parse(`
/// foo
/// bar
/// baz
`);
query = JavaScript.query(`
(
(comment)+ @foo
(#any-eq? @foo "/// foo")
)
`);
let expectCount = (tree, queryText, expectedCount) => {
query = JavaScript.query(queryText);
captures = query.captures(tree.rootNode, null, null);
assert.equal(captures.length, expectedCount);
};
expectCount(
tree,
` ( (comment)+ @foo (#any-eq? @foo "/// foo") ) `,
3
);
expectCount(
tree,
` ( (comment)+ @foo (#eq? @foo "/// foo") ) `,
0
);
expectCount(
tree,
` ( (comment)+ @foo (#any-not-eq? @foo "/// foo") ) `,
3
);
expectCount(
tree,
` ( (comment)+ @foo (#not-eq? @foo "/// foo") ) `,
0
);
expectCount(
tree,
` ( (comment)+ @foo (#match? @foo "^/// foo") ) `,
0
);
expectCount(
tree,
` ( (comment)+ @foo (#any-match? @foo "^/// foo") ) `,
3
);
expectCount(
tree,
` ( (comment)+ @foo (#not-match? @foo "^/// foo") ) `,
0
);
expectCount(
tree,
` ( (comment)+ @foo (#not-match? @foo "fsdfsdafdfs") ) `,
3
);
expectCount(
tree,
` ( (comment)+ @foo (#any-not-match? @foo "^///") ) `,
0
);
expectCount(
tree,
` ( (comment)+ @foo (#any-not-match? @foo "^/// foo") ) `,
3
);
})
});
describe(".predicatesForPattern(index)", () => {