diff --git a/cli/src/tests/corpus_test.rs b/cli/src/tests/corpus_test.rs index 70894f25..717fd907 100644 --- a/cli/src/tests/corpus_test.rs +++ b/cli/src/tests/corpus_test.rs @@ -30,71 +30,88 @@ fn test_corpus_for_bash(seed: usize) { "bash - corpus - commands - Quoted Heredocs", "bash - corpus - commands - Heredocs with weird characters", ]), + None, ); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_c(seed: usize) { - test_language_corpus("c", seed, None); + test_language_corpus("c", seed, None, None); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_cpp(seed: usize) { - test_language_corpus("cpp", seed, None); + test_language_corpus("cpp", seed, None, None); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_embedded_template(seed: usize) { - test_language_corpus("embedded-template", seed, None); + test_language_corpus("embedded-template", seed, None, None); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_go(seed: usize) { - test_language_corpus("go", seed, None); + test_language_corpus("go", seed, None, None); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_html(seed: usize) { - test_language_corpus("html", seed, None); + test_language_corpus("html", seed, None, None); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_javascript(seed: usize) { - test_language_corpus("javascript", seed, None); + test_language_corpus("javascript", seed, None, None); +} + +#[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] +fn test_corpus_for_typescript(seed: usize) { + test_language_corpus("typescript", seed, None, Some("typescript")); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_json(seed: usize) { - test_language_corpus("json", seed, None); + test_language_corpus("json", seed, None, None); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_php(seed: usize) { - test_language_corpus("php", seed, None); + test_language_corpus("php", seed, None, Some("php")); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_python(seed: usize) { - test_language_corpus("python", seed, None); + test_language_corpus("python", seed, None, None); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_ruby(seed: usize) { - test_language_corpus("ruby", seed, None); + test_language_corpus("ruby", seed, None, None); } #[test_with_seed(retry=10, seed=*START_SEED, seed_fn=new_seed)] fn test_corpus_for_rust(seed: usize) { - test_language_corpus("rust", seed, None); + test_language_corpus("rust", seed, None, None); } -fn test_language_corpus(language_name: &str, start_seed: usize, skipped: Option<&[&str]>) { +fn test_language_corpus( + language_name: &str, + start_seed: usize, + skipped: Option<&[&str]>, + subdir: Option<&str>, +) { + let subdir = subdir.unwrap_or_default(); + let grammars_dir = fixtures_dir().join("grammars"); let error_corpus_dir = fixtures_dir().join("error_corpus"); let template_corpus_dir = fixtures_dir().join("template_corpus"); - let mut corpus_dir = grammars_dir.join(language_name).join("corpus"); + let mut corpus_dir = grammars_dir.join(language_name).join(subdir).join("corpus"); if !corpus_dir.is_dir() { - corpus_dir = grammars_dir.join(language_name).join("test").join("corpus"); + corpus_dir = grammars_dir + .join(language_name) + .join(subdir) + .join("test") + .join("corpus"); } let error_corpus_file = error_corpus_dir.join(&format!("{}_errors.txt", language_name)); @@ -112,7 +129,12 @@ fn test_language_corpus(language_name: &str, start_seed: usize, skipped: Option< let mut skipped = skipped.map(|x| HashMap::<&str, usize>::from_iter(x.iter().map(|x| (*x, 0)))); - let language = get_language(language_name); + let language_path = if subdir.is_empty() { + language_name.to_string() + } else { + format!("{language_name}/{subdir}") + }; + let language = get_language(&language_path); let mut failure_count = 0; let log_seed = env::var("TREE_SITTER_LOG_SEED").is_ok(); diff --git a/lib/binding_web/binding.js b/lib/binding_web/binding.js index 9d548d32..ca45d459 100644 --- a/lib/binding_web/binding.js +++ b/lib/binding_web/binding.js @@ -842,78 +842,88 @@ 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}"` ); + matchAll = !operator.startsWith('any-'); 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); } + let compare = (n1, n2, positive) => { + return positive ? + n1.text === n2.text : + n1.text !== n2.text; + }; 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; + ? nodes_1.every(n1 => nodes_2.some(n2 => compare(n1, n2, isPositive))) + : nodes_1.some(n1 => nodes_2.some(n2 => compare(n1, n2, isPositive))); }); } 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); } + let test = isPositive ? matches : doesNotMatch; return matchAll - ? nodes.every(n => n.text === stringValue) === isPositive - : nodes.some(n => n.text === stringValue) === isPositive; + ? 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) { + matchAll = !operator.startsWith('any-'); + textPredicates[i].push(function (captures) { const nodes = []; for (const c of captures) { if (c.name === captureName) nodes.push(c.node.text); } + let test = (text, positive) => { + return positive ? + regex.test(text) : + !regex.test(text); + }; if (nodes.length === 0) return !isPositive; return matchAll - ? nodes.every(text => regex.test(text)) === isPositive - : nodes.some(text => regex.test(text)) === isPositive; + ? nodes.every(text => test(text, isPositive)) + : nodes.some(text => test(text, isPositive)) }); break; @@ -957,7 +967,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); diff --git a/lib/binding_web/test/query-test.js b/lib/binding_web/test/query-test.js index 2b2aebe0..34fd5cd1 100644 --- a/lib/binding_web/test/query-test.js +++ b/lib/binding_web/test/query-test.js @@ -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)", () => {