Skip to content

Commit 5eb0a30

Browse files
authored
Merge pull request tree-sitter#1547 from the-mikedavis/md-test-tags
test tags queries in 'tree-sitter test'
2 parents 3bd6fae + e7f0cc4 commit 5eb0a30

File tree

7 files changed

+255
-1
lines changed

7 files changed

+255
-1
lines changed

cli/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod query_testing;
88
pub mod tags;
99
pub mod test;
1010
pub mod test_highlight;
11+
pub mod test_tags;
1112
pub mod util;
1213
pub mod wasm;
1314

cli/src/main.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use glob::glob;
44
use std::path::Path;
55
use std::{env, fs, u64};
66
use tree_sitter_cli::{
7-
generate, highlight, logger, parse, playground, query, tags, test, test_highlight, util, wasm,
7+
generate, highlight, logger, parse, playground, query, tags, test, test_highlight, test_tags,
8+
util, wasm,
89
};
910
use tree_sitter_config::Config;
1011
use tree_sitter_loader as loader;
@@ -338,6 +339,11 @@ fn run() -> Result<()> {
338339
if test_highlight_dir.is_dir() {
339340
test_highlight::test_highlights(&loader, &test_highlight_dir)?;
340341
}
342+
343+
let test_tag_dir = test_dir.join("tags");
344+
if test_tag_dir.is_dir() {
345+
test_tags::test_tags(&loader, &test_tag_dir)?;
346+
}
341347
}
342348

343349
("parse", Some(matches)) => {

cli/src/test_tags.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use crate::query_testing::{parse_position_comments, Assertion};
2+
use ansi_term::Colour;
3+
use anyhow::{anyhow, Result};
4+
use std::fs;
5+
use std::path::Path;
6+
use tree_sitter::Point;
7+
use tree_sitter_loader::Loader;
8+
use tree_sitter_tags::{TagsConfiguration, TagsContext};
9+
10+
#[derive(Debug)]
11+
pub struct Failure {
12+
row: usize,
13+
column: usize,
14+
expected_tag: String,
15+
actual_tags: Vec<String>,
16+
}
17+
18+
impl std::error::Error for Failure {}
19+
20+
impl std::fmt::Display for Failure {
21+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
22+
write!(
23+
f,
24+
"Failure - row: {}, column: {}, expected tag: '{}', actual tag: ",
25+
self.row, self.column, self.expected_tag
26+
)?;
27+
if self.actual_tags.is_empty() {
28+
write!(f, "none.")?;
29+
} else {
30+
for (i, actual_tag) in self.actual_tags.iter().enumerate() {
31+
if i > 0 {
32+
write!(f, ", ")?;
33+
}
34+
write!(f, "'{}'", actual_tag)?;
35+
}
36+
}
37+
Ok(())
38+
}
39+
}
40+
41+
pub fn test_tags(loader: &Loader, directory: &Path) -> Result<()> {
42+
let mut failed = false;
43+
let mut tags_context = TagsContext::new();
44+
45+
println!("tags:");
46+
for tag_test_file in fs::read_dir(directory)? {
47+
let tag_test_file = tag_test_file?;
48+
let test_file_path = tag_test_file.path();
49+
let test_file_name = tag_test_file.file_name();
50+
let (language, language_config) = loader
51+
.language_configuration_for_file_name(&test_file_path)?
52+
.ok_or_else(|| anyhow!("No language found for path {:?}", test_file_path))?;
53+
let tags_config = language_config
54+
.tags_config(language)?
55+
.ok_or_else(|| anyhow!("No tags config found for {:?}", test_file_path))?;
56+
match test_tag(
57+
&mut tags_context,
58+
tags_config,
59+
fs::read(&test_file_path)?.as_slice(),
60+
) {
61+
Ok(assertion_count) => {
62+
println!(
63+
" ✓ {} ({} assertions)",
64+
Colour::Green.paint(test_file_name.to_string_lossy().as_ref()),
65+
assertion_count
66+
);
67+
}
68+
Err(e) => {
69+
println!(
70+
" ✗ {}",
71+
Colour::Red.paint(test_file_name.to_string_lossy().as_ref())
72+
);
73+
println!(" {}", e);
74+
failed = true;
75+
}
76+
}
77+
}
78+
79+
if failed {
80+
Err(anyhow!(""))
81+
} else {
82+
Ok(())
83+
}
84+
}
85+
86+
pub fn test_tag(
87+
tags_context: &mut TagsContext,
88+
tags_config: &TagsConfiguration,
89+
source: &[u8],
90+
) -> Result<usize> {
91+
let tags = get_tag_positions(tags_context, tags_config, source)?;
92+
let assertions = parse_position_comments(tags_context.parser(), tags_config.language, source)?;
93+
94+
// Iterate through all of the assertions, checking against the actual tags.
95+
let mut i = 0;
96+
let mut actual_tags = Vec::<&String>::new();
97+
for Assertion {
98+
position,
99+
expected_capture_name: expected_tag,
100+
} in &assertions
101+
{
102+
let mut passed = false;
103+
104+
'tag_loop: loop {
105+
if let Some(tag) = tags.get(i) {
106+
if tag.1 <= *position {
107+
i += 1;
108+
continue;
109+
}
110+
111+
// Iterate through all of the tags that start at or before this assertion's
112+
// position, looking for one that matches the assertion
113+
let mut j = i;
114+
while let (false, Some(tag)) = (passed, tags.get(j)) {
115+
if tag.0 > *position {
116+
break 'tag_loop;
117+
}
118+
119+
let tag_name = &tag.2;
120+
if *tag_name == *expected_tag {
121+
passed = true;
122+
break 'tag_loop;
123+
} else {
124+
actual_tags.push(tag_name);
125+
}
126+
127+
j += 1;
128+
}
129+
} else {
130+
break;
131+
}
132+
}
133+
134+
if !passed {
135+
return Err(Failure {
136+
row: position.row,
137+
column: position.column,
138+
expected_tag: expected_tag.clone(),
139+
actual_tags: actual_tags.into_iter().cloned().collect(),
140+
}
141+
.into());
142+
}
143+
}
144+
145+
Ok(assertions.len())
146+
}
147+
148+
pub fn get_tag_positions(
149+
tags_context: &mut TagsContext,
150+
tags_config: &TagsConfiguration,
151+
source: &[u8],
152+
) -> Result<Vec<(Point, Point, String)>> {
153+
let (tags_iter, _has_error) = tags_context.generate_tags(&tags_config, &source, None)?;
154+
let tag_positions = tags_iter
155+
.filter_map(|t| t.ok())
156+
.map(|tag| {
157+
let tag_postfix = tags_config.syntax_type_name(tag.syntax_type_id).to_string();
158+
let tag_name = if tag.is_definition {
159+
format!("definition.{}", tag_postfix)
160+
} else {
161+
format!("reference.{}", tag_postfix)
162+
};
163+
(tag.span.start, tag.span.end, tag_name)
164+
})
165+
.collect();
166+
Ok(tag_positions)
167+
}

cli/src/tests/helpers/fixtures.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
44
use tree_sitter::Language;
55
use tree_sitter_highlight::HighlightConfiguration;
66
use tree_sitter_loader::Loader;
7+
use tree_sitter_tags::TagsConfiguration;
78

89
include!("./dirs.rs");
910

@@ -54,6 +55,14 @@ pub fn get_highlight_config(
5455
result
5556
}
5657

58+
pub fn get_tags_config(language_name: &str) -> TagsConfiguration {
59+
let language = get_language(language_name);
60+
let queries_path = get_language_queries_path(language_name);
61+
let tags_query = fs::read_to_string(queries_path.join("tags.scm")).unwrap();
62+
let locals_query = fs::read_to_string(queries_path.join("locals.scm")).unwrap_or(String::new());
63+
TagsConfiguration::new(language, &tags_query, &locals_query).unwrap()
64+
}
65+
5766
pub fn get_test_language(name: &str, parser_code: &str, path: Option<&Path>) -> Language {
5867
let parser_c_path = SCRATCH_DIR.join(&format!("{}-parser.c", name));
5968
if !fs::read_to_string(&parser_c_path)

cli/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ mod pathological_test;
77
mod query_test;
88
mod tags_test;
99
mod test_highlight_test;
10+
mod test_tags_test;
1011
mod tree_test;

cli/src/tests/test_tags_test.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use super::helpers::fixtures::{get_language, get_tags_config};
2+
use crate::query_testing::{parse_position_comments, Assertion};
3+
use crate::test_tags::get_tag_positions;
4+
use tree_sitter::{Parser, Point};
5+
use tree_sitter_tags::TagsContext;
6+
7+
#[test]
8+
fn test_tags_test_with_basic_test() {
9+
let language = get_language("python");
10+
let config = get_tags_config("python");
11+
let source = [
12+
"# hi",
13+
"def abc(d):",
14+
" # <- definition.function",
15+
" e = fgh(d)",
16+
" # ^ reference.call",
17+
" return d(e)",
18+
" # ^ reference.call",
19+
"",
20+
]
21+
.join("\n");
22+
23+
let assertions =
24+
parse_position_comments(&mut Parser::new(), language, source.as_bytes()).unwrap();
25+
26+
assert_eq!(
27+
assertions,
28+
&[
29+
Assertion {
30+
position: Point::new(1, 4),
31+
expected_capture_name: "definition.function".to_string(),
32+
},
33+
Assertion {
34+
position: Point::new(3, 9),
35+
expected_capture_name: "reference.call".to_string(),
36+
},
37+
Assertion {
38+
position: Point::new(5, 11),
39+
expected_capture_name: "reference.call".to_string(),
40+
},
41+
]
42+
);
43+
44+
let mut tags_context = TagsContext::new();
45+
let tag_positions = get_tag_positions(&mut tags_context, &config, source.as_bytes()).unwrap();
46+
assert_eq!(
47+
tag_positions,
48+
&[
49+
(
50+
Point::new(1, 4),
51+
Point::new(1, 7),
52+
"definition.function".to_string()
53+
),
54+
(
55+
Point::new(3, 8),
56+
Point::new(3, 11),
57+
"reference.call".to_string()
58+
),
59+
(
60+
Point::new(5, 11),
61+
Point::new(5, 12),
62+
"reference.call".to_string()
63+
),
64+
]
65+
)
66+
}

tags/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ impl TagsContext {
252252
}
253253
}
254254

255+
pub fn parser(&mut self) -> &mut Parser {
256+
&mut self.parser
257+
}
258+
255259
pub fn generate_tags<'a>(
256260
&'a mut self,
257261
config: &'a TagsConfiguration,

0 commit comments

Comments
 (0)