weavatrix_rust_refactor/
declaration.rs1use weavatrix_parse::{Language, Token, TokenKind, tokenize};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct DeclarationRange {
17 pub start: usize,
19 pub end: usize,
21 pub end_proven: bool,
26}
27
28#[must_use]
36pub fn body_lines(source: &str, path: &str, name: &str, line: u32) -> (u32, u32) {
37 let Some(range) = locate(source, path, name, line).filter(|range| range.end_proven) else {
38 return (line, line);
39 };
40 let line_of = |offset: usize| {
41 u32::try_from(source[..offset.min(source.len())].matches('\n').count() + 1)
42 .unwrap_or(u32::MAX)
43 };
44 let start = line_of(range.start);
45 (start, line_of(range.end).max(start))
46}
47
48#[must_use]
53pub fn locate(source: &str, path: &str, name: &str, line: u32) -> Option<DeclarationRange> {
54 let facts = weavatrix_parse::extract_path(path, source)?;
55 let declaration = facts
56 .declarations
57 .iter()
58 .find(|candidate| candidate.name == name && candidate.span.line == line)
59 .or_else(|| {
60 facts
61 .declarations
62 .iter()
63 .find(|candidate| candidate.name == name)
64 })?;
65 let start = declaration.span.start;
66 let language = Language::from_extension(path.rsplit_once('.')?.1)?;
67 Some(declaration_end(source, language, start).map_or(
68 DeclarationRange {
69 start,
70 end: declaration.span.end,
71 end_proven: false,
72 },
73 |end| DeclarationRange {
74 start,
75 end,
76 end_proven: true,
77 },
78 ))
79}
80
81fn declaration_end(source: &str, language: Language, start: usize) -> Option<usize> {
88 let tokens = tokenize(source, language);
89 let mut depth = 0_u32;
90 let mut opened = false;
91 for token in tokens.iter().skip_while(|token| token.end <= start) {
92 if !is_code(token) {
93 continue;
94 }
95 let text = source.get(token.start..token.end)?;
96 for (offset, character) in text.char_indices() {
97 match character {
98 '{' => {
99 depth += 1;
100 opened = true;
101 }
102 '}' if depth > 0 => {
103 depth -= 1;
104 if depth == 0 {
105 return Some(token.start + offset + character.len_utf8());
106 }
107 }
108 ';' if !opened => return Some(token.start + offset + character.len_utf8()),
110 _ => {}
111 }
112 }
113 }
114 None
115}
116
117fn is_code(token: &Token) -> bool {
119 !matches!(
120 token.kind,
121 TokenKind::String
122 | TokenKind::LineComment
123 | TokenKind::BlockComment
124 | TokenKind::Regex
125 | TokenKind::Unterminated
126 )
127}
128
129#[cfg(test)]
130mod tests {
131 use super::locate;
132
133 #[test]
134 fn a_function_range_covers_modifiers_through_the_closing_brace() {
135 let source = "pub fn one() -> u32 {\n 1\n}\n\npub fn two() {}\n";
136 let range = locate(source, "src/lib.rs", "one", 1).expect("declaration");
137 assert!(range.end_proven);
138 assert_eq!(
139 &source[range.start..range.end],
140 "pub fn one() -> u32 {\n 1\n}"
141 );
142 }
143
144 #[test]
145 fn a_brace_inside_a_string_does_not_close_the_body() {
146 let source = "pub fn one() -> &'static str {\n \"}\"\n}\n";
147 let range = locate(source, "src/lib.rs", "one", 1).expect("declaration");
148 assert!(
149 source[range.start..range.end].ends_with("}\"\n}"),
150 "the string's brace must not end the declaration, got {:?}",
151 &source[range.start..range.end]
152 );
153 }
154
155 #[test]
156 fn a_brace_inside_a_comment_does_not_close_the_body() {
157 let source = "pub fn one() -> u32 {\n // }\n 1\n}\n";
158 let range = locate(source, "src/lib.rs", "one", 1).expect("declaration");
159 assert_eq!(
160 &source[range.start..range.end],
161 "pub fn one() -> u32 {\n // }\n 1\n}"
162 );
163 }
164
165 #[test]
166 fn nested_braces_are_matched_to_the_outermost() {
167 let source = "pub fn one() -> u32 {\n if true { 1 } else { 2 }\n}\n";
168 let range = locate(source, "src/lib.rs", "one", 1).expect("declaration");
169 assert!(source[range.start..range.end].contains("else { 2 }"));
170 assert!(source[range.start..range.end].ends_with("\n}"));
171 }
172
173 #[test]
174 fn the_line_picks_between_same_named_declarations() {
175 let source = "mod a {\n pub fn one() -> u32 { 1 }\n}\nmod b {\n pub fn one() -> u32 { 2 }\n}\n";
176 let first = locate(source, "src/lib.rs", "one", 2);
177 let second = locate(source, "src/lib.rs", "one", 5);
178 if let (Some(first), Some(second)) = (first, second) {
179 assert_ne!(
180 first.start, second.start,
181 "the line must select the declaration, not the first match"
182 );
183 }
184 }
185
186 #[test]
187 fn a_declaration_without_a_body_ends_at_its_terminator() {
188 let source = "pub const ONE: u32 = 1;\n";
189 if let Some(range) = locate(source, "src/lib.rs", "ONE", 1) {
190 assert!(range.end_proven);
191 assert_eq!(&source[range.start..range.end], "pub const ONE: u32 = 1;");
192 }
193 }
194
195 #[test]
196 fn an_unknown_name_locates_nothing() {
197 let source = "pub fn one() {}\n";
198 assert!(locate(source, "src/lib.rs", "absent", 1).is_none());
199 }
200}