Skip to content

Commit afd4351

Browse files
committed
test(coverage): cover reported uncovered lines across cli/lsp/planner/query
Genuine missing tests: - planner/check_expr_parser: OR/AND second-operand unparseable paths - planner/check_self_contradiction: OR-conjunct skip in group_predicates_by_column - planner/schema: nullable primary-key column rejection - lsp/position: has_windows_drive_prefix (call site is cfg!(windows)-gated) - lsp/{drift::compute,references::search,symbols}: path_to_uri leading-slash branch Eliminate defensive/unreachable branches (mirrors symbols.rs while-let pattern): - check_expr_parser: drop unreachable parse_predicate guard + dead bump() - unwrap_yaml else (types/inlay_hints/classify_yaml): fused while-let - locator: fuse pair_key_matches let-else; symbols: drop dead let _ = stripped Restructure LLVM attribution-flaky regions to single coverage regions: - cli/erd/dot.rs: escape arm -> escaped.extend - query/add_constraint/foreign_key: closure chains -> explicit for loops
1 parent c9fbce9 commit afd4351

15 files changed

Lines changed: 168 additions & 71 deletions

File tree

crates/vespertide-cli/src/commands/erd/dot.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,11 @@ fn escape_record_field(value: &str) -> String {
7676

7777
for ch in value.chars() {
7878
match ch {
79-
'\\' | '{' | '}' | '|' | '<' | '>' | '"' => {
80-
escaped.push('\\');
81-
escaped.push(ch);
82-
}
79+
// Single-statement push of the escape byte + the char so the
80+
// taken-arm body maps to one coverage region (two sequential
81+
// `push` calls split into adjacent regions that LLVM coverage
82+
// attributes inconsistently).
83+
'\\' | '{' | '}' | '|' | '<' | '>' | '"' => escaped.extend(['\\', ch]),
8384
_ => escaped.push(ch),
8485
}
8586
}

crates/vespertide-lsp/src/diagnostics/locator.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -416,10 +416,11 @@ fn is_pair(node: tree_sitter::Node<'_>) -> bool {
416416
}
417417

418418
fn pair_key_matches(node: tree_sitter::Node<'_>, source: &[u8], expected: &str) -> bool {
419-
let Some(key) = node.named_child(0) else {
420-
return false;
421-
};
422-
node_text(key, source).is_some_and(|text| strip_quotes(text) == expected)
419+
// Fused chain so a key-less pair (no `named_child(0)`) folds into the
420+
// same `false` result without a separate defensive `return` line.
421+
node.named_child(0)
422+
.and_then(|key| node_text(key, source))
423+
.is_some_and(|text| strip_quotes(text) == expected)
423424
}
424425

425426
fn node_text<'a>(node: tree_sitter::Node<'_>, source: &'a [u8]) -> Option<&'a str> {

crates/vespertide-lsp/src/diagnostics/validation/types.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,17 @@ pub(super) struct EnumValueDescriptor {
3636

3737
/// Peel YAML's `flow_node` / `block_node` wrappers (no-op on JSON).
3838
pub(super) fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
39+
// Fused while-let so the empty-wrapper case (no usable `named_child(0)`)
40+
// and the kind-mismatch case share the same loop exit — no defensive
41+
// `return` line that only an (unobservable) empty tree-sitter wrapper
42+
// could reach.
3943
let mut current = node;
40-
while matches!(current.kind(), "flow_node" | "block_node") {
41-
if let Some(inner) = current
44+
while matches!(current.kind(), "flow_node" | "block_node")
45+
&& let Some(inner) = current
4246
.named_child(0)
4347
.filter(|inner| inner.id() != current.id())
44-
{
45-
current = inner;
46-
} else {
47-
return current;
48-
}
48+
{
49+
current = inner;
4950
}
5051
current
5152
}

crates/vespertide-lsp/src/drift/compute.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,4 +365,22 @@ mod tests {
365365
assert!(guess_uri(tmp.path(), "user").is_some());
366366
assert!(guess_uri(tmp.path(), "missing").is_none());
367367
}
368+
369+
#[test]
370+
fn path_to_uri_prepends_leading_slash_for_relative_path() {
371+
// A relative path never starts with `/` on any platform, so this
372+
// exercises the leading-slash normalization branch deterministically
373+
// (absolute POSIX paths on CI would otherwise skip it).
374+
let uri = path_to_uri(std::path::Path::new("models/user.json")).expect("uri");
375+
assert!(
376+
uri.as_str().starts_with("file:///"),
377+
"got: {}",
378+
uri.as_str()
379+
);
380+
assert!(
381+
uri.as_str().ends_with("models/user.json"),
382+
"got: {}",
383+
uri.as_str()
384+
);
385+
}
368386
}

crates/vespertide-lsp/src/inlay_hints.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -372,16 +372,17 @@ fn find_pair_with_key<'tree>(
372372
}
373373

374374
fn unwrap_yaml_node(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
375+
// Fused while-let so the empty-wrapper case (no usable `named_child(0)`)
376+
// and the kind-mismatch case share the same loop exit — no defensive
377+
// `return` line that only an (unobservable) empty tree-sitter wrapper
378+
// could reach.
375379
let mut current = node;
376-
while matches!(current.kind(), "flow_node" | "block_node") {
377-
if let Some(inner) = current
380+
while matches!(current.kind(), "flow_node" | "block_node")
381+
&& let Some(inner) = current
378382
.named_child(0)
379383
.filter(|inner| inner.id() != current.id())
380-
{
381-
current = inner;
382-
} else {
383-
return current;
384-
}
384+
{
385+
current = inner;
385386
}
386387
current
387388
}

crates/vespertide-lsp/src/position.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,15 @@ mod tests {
297297

298298
assert_eq!(byte_to_lsp_position(&doc, byte), pos);
299299
}
300+
301+
#[test]
302+
fn has_windows_drive_prefix_detects_and_rejects() {
303+
// Tested directly: the only call site sits behind `if cfg!(windows)`,
304+
// so on non-Windows CI the function is otherwise never invoked.
305+
assert!(has_windows_drive_prefix("C:/Users/x"));
306+
assert!(has_windows_drive_prefix("d:/data"));
307+
assert!(!has_windows_drive_prefix("/usr/local"));
308+
assert!(!has_windows_drive_prefix("ab"));
309+
assert!(!has_windows_drive_prefix(""));
310+
}
300311
}

crates/vespertide-lsp/src/references/search.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,4 +735,16 @@ mod tests {
735735
"declaration and CHECK expression should both be found: {out:?}"
736736
);
737737
}
738+
739+
#[test]
740+
fn path_to_uri_prepends_leading_slash_for_relative_path() {
741+
// A relative path never starts with `/` on any platform, so this
742+
// exercises the leading-slash normalization branch deterministically.
743+
let uri = path_to_uri(std::path::Path::new("models/user.json"));
744+
assert!(
745+
uri.as_str().starts_with("file:///"),
746+
"got: {}",
747+
uri.as_str()
748+
);
749+
}
738750
}

crates/vespertide-lsp/src/semantic_tokens/classify_yaml.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -298,16 +298,17 @@ fn inner_scalar_range(node: tree_sitter::Node<'_>) -> std::ops::Range<usize> {
298298
}
299299

300300
fn unwrap_yaml(node: tree_sitter::Node<'_>) -> tree_sitter::Node<'_> {
301+
// Fused while-let so the empty-wrapper case (no usable `named_child(0)`)
302+
// and the kind-mismatch case share the same loop exit — no defensive
303+
// `return` line that only an (unobservable) empty tree-sitter wrapper
304+
// could reach.
301305
let mut current = node;
302-
while matches!(current.kind(), "flow_node" | "block_node") {
303-
if let Some(inner) = current
306+
while matches!(current.kind(), "flow_node" | "block_node")
307+
&& let Some(inner) = current
304308
.named_child(0)
305309
.filter(|inner| inner.id() != current.id())
306-
{
307-
current = inner;
308-
} else {
309-
return current;
310-
}
310+
{
311+
current = inner;
311312
}
312313
current
313314
}

crates/vespertide-lsp/src/symbols.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,6 @@ fn direct_pair_value<'a>(
286286
let value = unwrap_yaml_node(pair.named_child(1)?);
287287
let raw = source.get(value.byte_range())?;
288288
let text = std::str::from_utf8(raw).ok()?;
289-
let stripped = strip_quotes(text);
290289
// Adjust byte range to skip quotes when the value is a quoted string.
291290
let range = match value.kind() {
292291
"string" => value.named_child(0).map_or_else(
@@ -296,9 +295,6 @@ fn direct_pair_value<'a>(
296295
"double_quote_scalar" | "single_quote_scalar" => trim_one_byte(&value.byte_range()),
297296
_ => value.byte_range(),
298297
};
299-
// Defensive: if stripping changed the byte length unexpectedly, fall
300-
// back to the raw range so we never panic on slice indexing.
301-
let _ = stripped;
302298
Some((strip_quotes(text), range))
303299
}
304300

@@ -1005,4 +1001,16 @@ mod tests {
10051001

10061002
assert!(compute("xyz_nothing_matches", &docs, None).is_empty());
10071003
}
1004+
1005+
#[test]
1006+
fn path_to_uri_prepends_leading_slash_for_relative_path() {
1007+
// A relative path never starts with `/` on any platform, so this
1008+
// exercises the leading-slash normalization branch deterministically.
1009+
let uri = path_to_uri(std::path::Path::new("models/user.json")).expect("uri");
1010+
assert!(
1011+
uri.as_str().starts_with("file:///"),
1012+
"got: {}",
1013+
uri.as_str()
1014+
);
1015+
}
10081016
}

crates/vespertide-planner/src/validate/check_expr_parser.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -489,14 +489,6 @@ impl Parser {
489489
self.tokens.get(self.pos)
490490
}
491491

492-
fn bump(&mut self) -> Option<Token> {
493-
let t = self.tokens.get(self.pos).cloned();
494-
if t.is_some() {
495-
self.pos += 1;
496-
}
497-
t
498-
}
499-
500492
fn eat_keyword(&mut self, kw: Keyword) -> bool {
501493
if matches!(self.peek(), Some(Token::Keyword(k)) if *k == kw) {
502494
self.pos += 1;
@@ -577,15 +569,20 @@ impl Parser {
577569
self.pos += 1;
578570
inner
579571
}
580-
Some(Token::Ident(_)) => self.parse_predicate(),
572+
Some(Token::Ident(column)) => {
573+
// `peek` already confirmed the next token is an identifier;
574+
// clone the column name and advance past it so `parse_predicate`
575+
// receives a guaranteed-present column without a redundant
576+
// (and otherwise-unreachable) re-check.
577+
let column = column.clone();
578+
self.pos += 1;
579+
self.parse_predicate(column)
580+
}
581581
_ => CheckExpr::Unparseable,
582582
}
583583
}
584584

585-
fn parse_predicate(&mut self) -> CheckExpr {
586-
let Some(Token::Ident(column)) = self.bump() else {
587-
return CheckExpr::Unparseable;
588-
};
585+
fn parse_predicate(&mut self, column: String) -> CheckExpr {
589586
// Look at next token to decide the predicate shape.
590587
let negated_for_in_or_between = self.eat_keyword(Keyword::Not);
591588
match self.peek().cloned() {

0 commit comments

Comments
 (0)