Return correct path and line in query errors from the CLI

This commit is contained in:
Max Brunsfeld 2020-09-24 13:47:27 -07:00
parent 297e2bcb28
commit 518916f221
5 changed files with 372 additions and 234 deletions

View file

@ -1,7 +1,7 @@
use super::test_highlight;
use std::fmt::Write;
use std::io;
use tree_sitter::QueryError;
use tree_sitter::{QueryError, QueryErrorKind};
#[derive(Debug)]
pub struct Error(pub Vec<String>);
@ -51,31 +51,19 @@ impl Error {
}
}
impl<'a> From<QueryError> for Error {
fn from(error: QueryError) -> Self {
match error {
QueryError::Capture(row, c) => Error::new(format!(
"Query error on line {}: Invalid capture name {}",
row, c
)),
QueryError::Field(row, f) => Error::new(format!(
"Query error on line {}: Invalid field name {}",
row, f
)),
QueryError::NodeType(row, t) => Error::new(format!(
"Query error on line {}. Invalid node type {}",
row, t
)),
QueryError::Syntax(row, l) => Error::new(format!(
"Query error on line {}. Invalid syntax:\n{}",
row, l
)),
QueryError::Structure(row, l) => Error::new(format!(
"Query error on line {}. Impossible pattern:\n{}",
row, l
)),
QueryError::Predicate(p) => Error::new(format!("Query error: {}", p)),
impl<'a> From<(&str, QueryError)> for Error {
fn from((path, error): (&str, QueryError)) -> Self {
let mut msg = format!("Query error at {}:{}. ", path, error.row + 1);
match error.kind {
QueryErrorKind::Capture => write!(&mut msg, "Invalid capture name {}", error.message),
QueryErrorKind::Field => write!(&mut msg, "Invalid field name {}", error.message),
QueryErrorKind::NodeType => write!(&mut msg, "Invalid node type {}", error.message),
QueryErrorKind::Syntax => write!(&mut msg, "Invalid syntax:\n{}", error.message),
QueryErrorKind::Structure => write!(&mut msg, "Impossible pattern:\n{}", error.message),
QueryErrorKind::Predicate => write!(&mut msg, "Invalid predicate: {}", error.message),
}
.unwrap();
Self::new(msg)
}
}

View file

@ -5,12 +5,13 @@ use regex::{Regex, RegexBuilder};
use serde_derive::Deserialize;
use std::collections::HashMap;
use std::io::BufReader;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Mutex;
use std::time::SystemTime;
use std::{fs, mem};
use tree_sitter::Language;
use tree_sitter::{Language, QueryError};
use tree_sitter_highlight::HighlightConfiguration;
use tree_sitter_tags::TagsConfiguration;
@ -543,13 +544,32 @@ impl Loader {
impl<'a> LanguageConfiguration<'a> {
pub fn highlight_config(&self, language: Language) -> Result<Option<&HighlightConfiguration>> {
fn include_path_in_error<'a>(
mut error: QueryError,
ranges: &'a Vec<(String, Range<usize>)>,
source: &str,
start_offset: usize,
) -> (&'a str, QueryError) {
let offset = error.offset - start_offset;
let (path, range) = ranges
.iter()
.find(|(_, range)| range.contains(&offset))
.unwrap();
error.row = source[range.start..offset]
.chars()
.filter(|c| *c == '\n')
.count();
(path.as_ref(), error)
}
self.highlight_config
.get_or_try_init(|| {
let highlights_query =
let (highlights_query, highlight_ranges) =
self.read_queries(&self.highlights_filenames, "highlights.scm")?;
let injections_query =
let (injections_query, injection_ranges) =
self.read_queries(&self.injections_filenames, "injections.scm")?;
let locals_query = self.read_queries(&self.locals_filenames, "locals.scm")?;
let (locals_query, locals_ranges) =
self.read_queries(&self.locals_filenames, "locals.scm")?;
if highlights_query.is_empty() {
Ok(None)
@ -560,9 +580,25 @@ impl<'a> LanguageConfiguration<'a> {
&injections_query,
&locals_query,
)
.map_err(Error::wrap(|| {
format!("Failed to load queries in {:?}", self.root_path)
}))?;
.map_err(|error| {
if error.offset < injections_query.len() {
include_path_in_error(error, &injection_ranges, &injections_query, 0)
} else if error.offset < injections_query.len() + locals_query.len() {
include_path_in_error(
error,
&locals_ranges,
&locals_query,
injections_query.len(),
)
} else {
include_path_in_error(
error,
&highlight_ranges,
&highlights_query,
injections_query.len() + locals_query.len(),
)
}
})?;
let mut all_highlight_names = self.highlight_names.lock().unwrap();
if self.use_all_highlight_names {
for capture_name in result.query.capture_names() {
@ -581,8 +617,8 @@ impl<'a> LanguageConfiguration<'a> {
pub fn tags_config(&self, language: Language) -> Result<Option<&TagsConfiguration>> {
self.tags_config
.get_or_try_init(|| {
let tags_query = self.read_queries(&self.tags_filenames, "tags.scm")?;
let locals_query = self.read_queries(&self.locals_filenames, "locals.scm")?;
let (tags_query, _) = self.read_queries(&self.tags_filenames, "tags.scm")?;
let (locals_query, _) = self.read_queries(&self.locals_filenames, "locals.scm")?;
if tags_query.is_empty() {
Ok(None)
} else {
@ -596,27 +632,34 @@ impl<'a> LanguageConfiguration<'a> {
.map(Option::as_ref)
}
fn read_queries(&self, paths: &Option<Vec<String>>, default_path: &str) -> Result<String> {
fn read_queries(
&self,
paths: &Option<Vec<String>>,
default_path: &str,
) -> Result<(String, Vec<(String, Range<usize>)>)> {
let mut query = String::new();
let mut path_ranges = Vec::new();
if let Some(paths) = paths.as_ref() {
let mut query = String::new();
for path in paths {
let path = self.root_path.join(path);
query += &fs::read_to_string(&path).map_err(Error::wrap(|| {
let abs_path = self.root_path.join(path);
let prev_query_len = query.len();
query += &fs::read_to_string(&abs_path).map_err(Error::wrap(|| {
format!("Failed to read query file {:?}", path)
}))?;
path_ranges.push((path.clone(), prev_query_len..query.len()));
}
Ok(query)
} else {
let queries_path = self.root_path.join("queries");
let path = queries_path.join(default_path);
if path.exists() {
fs::read_to_string(&path).map_err(Error::wrap(|| {
query = fs::read_to_string(&path).map_err(Error::wrap(|| {
format!("Failed to read query file {:?}", path)
}))
} else {
Ok(String::new())
}))?;
path_ranges.push((default_path.to_string(), 0..query.len()));
}
}
Ok((query, path_ranges))
}
}

View file

@ -102,14 +102,14 @@ pub fn check_queries_at_path(language: Language, path: &Path) -> Result<()> {
if path.exists() {
for entry in fs::read_dir(path)? {
let entry = entry?;
let hidden = entry.file_name().to_str().unwrap_or("").starts_with(".");
let filepath = entry.file_name();
let filepath = filepath.to_str().unwrap_or("");
let hidden = filepath.starts_with(".");
if !hidden {
let content = fs::read_to_string(entry.path()).map_err(Error::wrap(|| {
format!("Error reading query file {:?}", entry.file_name())
}))?;
Query::new(language, &content).map_err(Error::wrap(|| {
format!("Error in query file {:?}", entry.file_name())
}))?;
Query::new(language, &content).map_err(|e| (filepath, e))?;
}
}
}

View file

@ -4,8 +4,8 @@ use lazy_static::lazy_static;
use std::env;
use std::fmt::Write;
use tree_sitter::{
Language, Node, Parser, Query, QueryCapture, QueryCursor, QueryError, QueryMatch,
QueryPredicate, QueryPredicateArg, QueryProperty,
Language, Node, Parser, Query, QueryCapture, QueryCursor, QueryError, QueryErrorKind,
QueryMatch, QueryPredicate, QueryPredicateArg, QueryProperty,
};
lazy_static! {
@ -26,109 +26,98 @@ fn test_query_errors_on_invalid_syntax() {
// Mismatched parens
assert_eq!(
Query::new(language, "(if_statement"),
Err(QueryError::Syntax(
1,
[
"(if_statement", //
" ^",
]
.join("\n")
))
Query::new(language, "(if_statement").unwrap_err().message,
[
"(if_statement", //
" ^",
]
.join("\n")
);
assert_eq!(
Query::new(language, "; comment 1\n; comment 2\n (if_statement))"),
Err(QueryError::Syntax(
3,
[
" (if_statement))", //
" ^",
]
.join("\n")
))
Query::new(language, "; comment 1\n; comment 2\n (if_statement))")
.unwrap_err()
.message,
[
" (if_statement))", //
" ^",
]
.join("\n")
);
// Return an error at the *beginning* of a bare identifier not followed a colon.
// If there's a colon but no pattern, return an error at the end of the colon.
assert_eq!(
Query::new(language, "(if_statement identifier)"),
Err(QueryError::Syntax(
1,
[
"(if_statement identifier)", //
" ^",
]
.join("\n")
))
Query::new(language, "(if_statement identifier)")
.unwrap_err()
.message,
[
"(if_statement identifier)", //
" ^",
]
.join("\n")
);
assert_eq!(
Query::new(language, "(if_statement condition:)"),
Err(QueryError::Syntax(
1,
[
"(if_statement condition:)", //
" ^",
]
.join("\n")
))
Query::new(language, "(if_statement condition:)")
.unwrap_err()
.message,
[
"(if_statement condition:)", //
" ^",
]
.join("\n")
);
// Return an error at the beginning of an unterminated string.
assert_eq!(
Query::new(language, r#"(identifier) "h "#),
Err(QueryError::Syntax(
1,
[
r#"(identifier) "h "#, //
r#" ^"#,
]
.join("\n")
))
Query::new(language, r#"(identifier) "h "#)
.unwrap_err()
.message,
[
r#"(identifier) "h "#, //
r#" ^"#,
]
.join("\n")
);
assert_eq!(
Query::new(language, r#"((identifier) ()"#),
Err(QueryError::Syntax(
1,
[
"((identifier) ()", //
" ^",
]
.join("\n")
))
Query::new(language, r#"((identifier) ()"#)
.unwrap_err()
.message,
[
"((identifier) ()", //
" ^",
]
.join("\n")
);
assert_eq!(
Query::new(language, r#"((identifier) [])"#),
Err(QueryError::Syntax(
1,
[
"((identifier) [])", //
" ^",
]
.join("\n")
))
Query::new(language, r#"((identifier) [])"#)
.unwrap_err()
.message,
[
"((identifier) [])", //
" ^",
]
.join("\n")
);
assert_eq!(
Query::new(language, r#"((identifier) (#a)"#),
Err(QueryError::Syntax(
1,
[
"((identifier) (#a)", //
" ^",
]
.join("\n")
))
Query::new(language, r#"((identifier) (#a)"#)
.unwrap_err()
.message,
[
"((identifier) (#a)", //
" ^",
]
.join("\n")
);
assert_eq!(
Query::new(language, r#"((identifier) @x (#eq? @x a"#),
Err(QueryError::Syntax(
1,
[
r#"((identifier) @x (#eq? @x a"#,
r#" ^"#,
]
.join("\n")
))
Query::new(language, r#"((identifier) @x (#eq? @x a"#)
.unwrap_err()
.message,
[
r#"((identifier) @x (#eq? @x a"#,
r#" ^"#,
]
.join("\n")
);
});
}
@ -139,53 +128,97 @@ fn test_query_errors_on_invalid_symbols() {
let language = get_language("javascript");
assert_eq!(
Query::new(language, "(clas)"),
Err(QueryError::NodeType(1, "clas".to_string()))
Query::new(language, "(clas)").unwrap_err(),
QueryError {
row: 1,
offset: 1,
column: 1,
kind: QueryErrorKind::NodeType,
message: "clas".to_string()
}
);
assert_eq!(
Query::new(language, "(if_statement (arrayyyyy))"),
Err(QueryError::NodeType(1, "arrayyyyy".to_string()))
Query::new(language, "(if_statement (arrayyyyy))").unwrap_err(),
QueryError {
row: 1,
offset: 15,
column: 15,
kind: QueryErrorKind::NodeType,
message: "arrayyyyy".to_string()
},
);
assert_eq!(
Query::new(language, "(if_statement condition: (non_existent3))"),
Err(QueryError::NodeType(1, "non_existent3".to_string()))
Query::new(language, "(if_statement condition: (non_existent3))").unwrap_err(),
QueryError {
row: 1,
offset: 26,
column: 26,
kind: QueryErrorKind::NodeType,
message: "non_existent3".to_string()
},
);
assert_eq!(
Query::new(language, "(if_statement condit: (identifier))"),
Err(QueryError::Field(1, "condit".to_string()))
Query::new(language, "(if_statement condit: (identifier))").unwrap_err(),
QueryError {
row: 1,
offset: 14,
column: 14,
kind: QueryErrorKind::Field,
message: "condit".to_string()
},
);
assert_eq!(
Query::new(language, "(if_statement conditioning: (identifier))"),
Err(QueryError::Field(1, "conditioning".to_string()))
Query::new(language, "(if_statement conditioning: (identifier))").unwrap_err(),
QueryError {
row: 1,
offset: 14,
column: 14,
kind: QueryErrorKind::Field,
message: "conditioning".to_string()
}
);
});
}
#[test]
fn test_query_errors_on_invalid_conditions() {
fn test_query_errors_on_invalid_predicates() {
allocations::record(|| {
let language = get_language("javascript");
assert_eq!(
Query::new(language, "((identifier) @id (@id))"),
Err(QueryError::Syntax(
1,
[
Query::new(language, "((identifier) @id (@id))").unwrap_err(),
QueryError {
kind: QueryErrorKind::Syntax,
row: 1,
column: 19,
offset: 19,
message: [
"((identifier) @id (@id))", //
" ^"
]
.join("\n")
))
}
);
assert_eq!(
Query::new(language, "((identifier) @id (#eq? @id))"),
Err(QueryError::Predicate(
"Wrong number of arguments to #eq? predicate. Expected 2, got 1.".to_string()
))
Query::new(language, "((identifier) @id (#eq? @id))").unwrap_err(),
QueryError {
kind: QueryErrorKind::Predicate,
row: 0,
column: 0,
offset: 0,
message: "Wrong number of arguments to #eq? predicate. Expected 2, got 1."
.to_string()
}
);
assert_eq!(
Query::new(language, "((identifier) @id (#eq? @id @ok))"),
Err(QueryError::Capture(1, "ok".to_string()))
Query::new(language, "((identifier) @id (#eq? @id @ok))").unwrap_err(),
QueryError {
kind: QueryErrorKind::Capture,
row: 1,
column: 29,
offset: 29,
message: "ok".to_string(),
}
);
});
}
@ -201,14 +234,17 @@ fn test_query_errors_on_impossible_patterns() {
js_lang,
"(binary_expression left: (identifier) left: (identifier))"
),
Err(QueryError::Structure(
1,
[
Err(QueryError {
kind: QueryErrorKind::Structure,
row: 1,
offset: 38,
column: 38,
message: [
"(binary_expression left: (identifier) left: (identifier))",
" ^"
]
.join("\n"),
))
})
);
Query::new(
@ -218,27 +254,33 @@ fn test_query_errors_on_impossible_patterns() {
.unwrap();
assert_eq!(
Query::new(js_lang, "(function_declaration name: (statement_block))"),
Err(QueryError::Structure(
1,
[
Err(QueryError {
kind: QueryErrorKind::Structure,
row: 1,
offset: 22,
column: 22,
message: [
"(function_declaration name: (statement_block))",
" ^",
]
.join("\n")
))
})
);
Query::new(rb_lang, "(call receiver:(call))").unwrap();
assert_eq!(
Query::new(rb_lang, "(call receiver:(binary))"),
Err(QueryError::Structure(
1,
[
Err(QueryError {
kind: QueryErrorKind::Structure,
row: 1,
offset: 6,
column: 6,
message: [
"(call receiver:(binary))", //
" ^",
]
.join("\n")
))
})
);
Query::new(
@ -259,37 +301,46 @@ fn test_query_errors_on_impossible_patterns() {
(generator_function_declaration (identifier))
]",
),
Err(QueryError::Structure(
3,
[
Err(QueryError {
kind: QueryErrorKind::Structure,
row: 3,
offset: 88,
column: 42,
message: [
" (function_declaration (object))", //
" ^",
]
.join("\n")
))
})
);
assert_eq!(
Query::new(js_lang, "(identifier (identifier))",),
Err(QueryError::Structure(
1,
[
Err(QueryError {
kind: QueryErrorKind::Structure,
row: 1,
offset: 12,
column: 12,
message: [
"(identifier (identifier))", //
" ^",
]
.join("\n")
))
})
);
assert_eq!(
Query::new(js_lang, "(true (true))",),
Err(QueryError::Structure(
1,
[
Err(QueryError {
kind: QueryErrorKind::Structure,
row: 1,
offset: 6,
column: 6,
message: [
"(true (true))", //
" ^",
]
.join("\n")
))
})
);
Query::new(
@ -298,16 +349,20 @@ fn test_query_errors_on_impossible_patterns() {
condition: (parenthesized_expression (_expression) @cond))",
)
.unwrap();
assert_eq!(
Query::new(js_lang, "(if_statement condition: (_expression))",),
Err(QueryError::Structure(
1,
[
Err(QueryError {
kind: QueryErrorKind::Structure,
row: 1,
offset: 14,
column: 14,
message: [
"(if_statement condition: (_expression))", //
" ^",
]
.join("\n")
))
})
);
});
}