1#![deny(unsafe_code)]
8#![warn(rust_2018_idioms)]
9#![warn(missing_docs)]
10#![warn(clippy::all)]
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum LoadTiming {
15 CompileTime,
17 Runtime,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ImportBehavior {
24 CallsImport,
26 NoImport,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct DispatchSemantics {
33 pub load_timing: LoadTiming,
35 pub import_behavior: ImportBehavior,
37}
38
39impl DispatchSemantics {
40 #[must_use]
42 pub fn hover_description(&self) -> &'static str {
43 match (self.load_timing, self.import_behavior) {
44 (LoadTiming::CompileTime, ImportBehavior::CallsImport) => {
45 "compile-time load; calls import()"
46 }
47 (LoadTiming::Runtime, ImportBehavior::NoImport) => "runtime load; no import() call",
48 (LoadTiming::CompileTime, ImportBehavior::NoImport) => {
49 "compile-time load; no import() call"
50 }
51 (LoadTiming::Runtime, ImportBehavior::CallsImport) => "runtime load; calls import()",
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum RequireForm {
59 ModuleName,
61 FilePath,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ModuleImportKind {
68 Use,
70 Require,
72 UseParent,
74 UseBase,
76}
77
78impl ModuleImportKind {
79 #[must_use]
81 pub fn dispatch_semantics(self) -> DispatchSemantics {
82 match self {
83 ModuleImportKind::Use | ModuleImportKind::UseParent | ModuleImportKind::UseBase => {
84 DispatchSemantics {
85 load_timing: LoadTiming::CompileTime,
86 import_behavior: ImportBehavior::CallsImport,
87 }
88 }
89 ModuleImportKind::Require => DispatchSemantics {
90 load_timing: LoadTiming::Runtime,
91 import_behavior: ImportBehavior::NoImport,
92 },
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct ModuleImportHead<'a> {
100 pub kind: ModuleImportKind,
102 pub token: &'a str,
104 pub token_start: usize,
106 pub token_end: usize,
108 require_form: Option<RequireForm>,
111}
112
113impl<'a> ModuleImportHead<'a> {
114 #[must_use]
116 pub fn require_form(&self) -> Option<RequireForm> {
117 self.require_form
118 }
119}
120
121#[must_use]
143pub fn parse_module_import_head(line: &str) -> Option<ModuleImportHead<'_>> {
144 if let Some((token, token_start, token_end)) = parse_statement_head(line, "use") {
145 let kind = match token {
146 "parent" => ModuleImportKind::UseParent,
147 "base" => ModuleImportKind::UseBase,
148 _ => ModuleImportKind::Use,
149 };
150 return Some(ModuleImportHead { kind, token, token_start, token_end, require_form: None });
151 }
152
153 if let Some(result) = parse_require_head(line) {
154 return Some(result);
155 }
156
157 None
158}
159
160fn parse_require_head(line: &str) -> Option<ModuleImportHead<'_>> {
162 let trimmed = line.trim_start();
163 let leading = line.len().saturating_sub(trimmed.len());
164
165 let rest = trimmed.strip_prefix("require")?;
166 if !rest.chars().next().is_some_and(char::is_whitespace) {
167 return None;
168 }
169
170 let after_keyword = leading + "require".len();
171
172 let rest_trimmed = rest.trim_start();
174 let quote_offset = rest.len() - rest_trimmed.len();
175
176 if let Some(inner) = rest_trimmed
177 .strip_prefix('"')
178 .and_then(|s| s.strip_suffix('"').or_else(|| s.split('"').next()))
179 .or_else(|| {
180 rest_trimmed
181 .strip_prefix('\'')
182 .and_then(|s| s.strip_suffix('\'').or_else(|| s.split('\'').next()))
183 })
184 {
185 let quote_char_len = 1usize; let token_start = after_keyword + quote_offset + quote_char_len;
188 let token_end = token_start + inner.len();
189 return Some(ModuleImportHead {
190 kind: ModuleImportKind::Require,
191 token: inner,
192 token_start,
193 token_end,
194 require_form: Some(RequireForm::FilePath),
195 });
196 }
197
198 let (token, token_rel_start, token_rel_end) = first_token_with_range(rest)?;
200 let token_start = after_keyword + token_rel_start;
201 let token_end = after_keyword + token_rel_end;
202
203 Some(ModuleImportHead {
204 kind: ModuleImportKind::Require,
205 token,
206 token_start,
207 token_end,
208 require_form: Some(RequireForm::ModuleName),
209 })
210}
211
212fn parse_statement_head<'a>(line: &'a str, keyword: &str) -> Option<(&'a str, usize, usize)> {
213 let trimmed = line.trim_start();
214 let leading = line.len().saturating_sub(trimmed.len());
215
216 let rest = trimmed.strip_prefix(keyword)?;
217 if !rest.chars().next().is_some_and(char::is_whitespace) {
218 return None;
219 }
220
221 let (token, token_rel_start, token_rel_end) = first_token_with_range(rest)?;
222 let token_start = leading + keyword.len() + token_rel_start;
223 let token_end = leading + keyword.len() + token_rel_end;
224
225 Some((token, token_start, token_end))
226}
227
228fn first_token_with_range(input: &str) -> Option<(&str, usize, usize)> {
229 let mut token_start = None;
230
231 for (idx, ch) in input.char_indices() {
232 match token_start {
233 None => {
234 if is_token_delimiter(ch) {
235 continue;
236 }
237 token_start = Some(idx);
238 }
239 Some(start) => {
240 if is_token_delimiter(ch) {
241 if start == idx {
242 return None;
243 }
244 return Some((&input[start..idx], start, idx));
245 }
246 }
247 }
248 }
249
250 if let Some(start) = token_start {
251 if start < input.len() { Some((&input[start..], start, input.len())) } else { None }
252 } else {
253 None
254 }
255}
256
257fn is_token_delimiter(ch: char) -> bool {
258 ch.is_whitespace() || matches!(ch, ';' | '(' | ')')
259}
260
261#[cfg(test)]
262mod tests {
263 use super::{ModuleImportKind, parse_module_import_head};
264
265 #[test]
266 fn parses_use_statement_head() {
267 let parsed = parse_module_import_head("use Foo::Bar;");
268
269 assert!(parsed.is_some());
270 if let Some(head) = parsed {
271 assert_eq!(head.kind, ModuleImportKind::Use);
272 assert_eq!(head.token, "Foo::Bar");
273 assert_eq!(head.token_start, 4);
274 assert_eq!(head.token_end, 12);
275 }
276 }
277
278 #[test]
279 fn parses_require_statement_head() {
280 let parsed = parse_module_import_head(" require Foo::Bar;");
281
282 assert!(parsed.is_some());
283 if let Some(head) = parsed {
284 assert_eq!(head.kind, ModuleImportKind::Require);
285 assert_eq!(head.token, "Foo::Bar");
286 assert_eq!(head.token_start, 10);
287 assert_eq!(head.token_end, 18);
288 }
289 }
290
291 #[test]
292 fn classifies_parent_and_base_specializations() {
293 let parent = parse_module_import_head("use parent qw(Foo::Bar);");
294 let base = parse_module_import_head("use base 'Foo::Bar';");
295
296 assert!(parent.is_some());
297 if let Some(head) = parent {
298 assert_eq!(head.kind, ModuleImportKind::UseParent);
299 assert_eq!(head.token, "parent");
300 }
301
302 assert!(base.is_some());
303 if let Some(head) = base {
304 assert_eq!(head.kind, ModuleImportKind::UseBase);
305 assert_eq!(head.token, "base");
306 }
307 }
308
309 #[test]
310 fn rejects_non_keyword_boundaries() {
311 assert!(parse_module_import_head("user Foo::Bar;").is_none());
312 assert!(parse_module_import_head("required Foo::Bar;").is_none());
313 }
314
315 #[test]
316 fn rejects_missing_tokens() {
317 assert!(parse_module_import_head("use ;").is_none());
318 assert!(parse_module_import_head("require").is_none());
319 }
320}