use weavatrix_parse::{Language, Token, TokenKind, tokenize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeclarationRange {
pub start: usize,
pub end: usize,
pub end_proven: bool,
}
#[must_use]
pub fn body_lines(source: &str, path: &str, name: &str, line: u32) -> (u32, u32) {
let Some(range) = locate(source, path, name, line).filter(|range| range.end_proven) else {
return (line, line);
};
let line_of = |offset: usize| {
u32::try_from(source[..offset.min(source.len())].matches('\n').count() + 1)
.unwrap_or(u32::MAX)
};
let start = line_of(range.start);
(start, line_of(range.end).max(start))
}
#[must_use]
pub fn locate(source: &str, path: &str, name: &str, line: u32) -> Option<DeclarationRange> {
let facts = weavatrix_parse::extract_path(path, source)?;
let declaration = facts
.declarations
.iter()
.find(|candidate| candidate.name == name && candidate.span.line == line)
.or_else(|| {
facts
.declarations
.iter()
.find(|candidate| candidate.name == name)
})?;
let start = declaration.span.start;
let language = Language::from_extension(path.rsplit_once('.')?.1)?;
Some(declaration_end(source, language, start).map_or(
DeclarationRange {
start,
end: declaration.span.end,
end_proven: false,
},
|end| DeclarationRange {
start,
end,
end_proven: true,
},
))
}
fn declaration_end(source: &str, language: Language, start: usize) -> Option<usize> {
let tokens = tokenize(source, language);
let mut depth = 0_u32;
let mut opened = false;
for token in tokens.iter().skip_while(|token| token.end <= start) {
if !is_code(token) {
continue;
}
let text = source.get(token.start..token.end)?;
for (offset, character) in text.char_indices() {
match character {
'{' => {
depth += 1;
opened = true;
}
'}' if depth > 0 => {
depth -= 1;
if depth == 0 {
return Some(token.start + offset + character.len_utf8());
}
}
';' if !opened => return Some(token.start + offset + character.len_utf8()),
_ => {}
}
}
}
None
}
fn is_code(token: &Token) -> bool {
!matches!(
token.kind,
TokenKind::String
| TokenKind::LineComment
| TokenKind::BlockComment
| TokenKind::Regex
| TokenKind::Unterminated
)
}
#[cfg(test)]
mod tests {
use super::locate;
#[test]
fn a_function_range_covers_modifiers_through_the_closing_brace() {
let source = "pub fn one() -> u32 {\n 1\n}\n\npub fn two() {}\n";
let range = locate(source, "src/lib.rs", "one", 1).expect("declaration");
assert!(range.end_proven);
assert_eq!(
&source[range.start..range.end],
"pub fn one() -> u32 {\n 1\n}"
);
}
#[test]
fn a_brace_inside_a_string_does_not_close_the_body() {
let source = "pub fn one() -> &'static str {\n \"}\"\n}\n";
let range = locate(source, "src/lib.rs", "one", 1).expect("declaration");
assert!(
source[range.start..range.end].ends_with("}\"\n}"),
"the string's brace must not end the declaration, got {:?}",
&source[range.start..range.end]
);
}
#[test]
fn a_brace_inside_a_comment_does_not_close_the_body() {
let source = "pub fn one() -> u32 {\n // }\n 1\n}\n";
let range = locate(source, "src/lib.rs", "one", 1).expect("declaration");
assert_eq!(
&source[range.start..range.end],
"pub fn one() -> u32 {\n // }\n 1\n}"
);
}
#[test]
fn nested_braces_are_matched_to_the_outermost() {
let source = "pub fn one() -> u32 {\n if true { 1 } else { 2 }\n}\n";
let range = locate(source, "src/lib.rs", "one", 1).expect("declaration");
assert!(source[range.start..range.end].contains("else { 2 }"));
assert!(source[range.start..range.end].ends_with("\n}"));
}
#[test]
fn the_line_picks_between_same_named_declarations() {
let source = "mod a {\n pub fn one() -> u32 { 1 }\n}\nmod b {\n pub fn one() -> u32 { 2 }\n}\n";
let first = locate(source, "src/lib.rs", "one", 2);
let second = locate(source, "src/lib.rs", "one", 5);
if let (Some(first), Some(second)) = (first, second) {
assert_ne!(
first.start, second.start,
"the line must select the declaration, not the first match"
);
}
}
#[test]
fn a_declaration_without_a_body_ends_at_its_terminator() {
let source = "pub const ONE: u32 = 1;\n";
if let Some(range) = locate(source, "src/lib.rs", "ONE", 1) {
assert!(range.end_proven);
assert_eq!(&source[range.start..range.end], "pub const ONE: u32 = 1;");
}
}
#[test]
fn an_unknown_name_locates_nothing() {
let source = "pub fn one() {}\n";
assert!(locate(source, "src/lib.rs", "absent", 1).is_none());
}
}