1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum LoadTiming {
9 CompileTime,
11 Runtime,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ImportBehavior {
18 CallsImport,
20 NoImport,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct DispatchSemantics {
27 pub load_timing: LoadTiming,
29 pub import_behavior: ImportBehavior,
31}
32
33impl DispatchSemantics {
34 #[must_use]
36 pub fn hover_description(&self) -> &'static str {
37 match (self.load_timing, self.import_behavior) {
38 (LoadTiming::CompileTime, ImportBehavior::CallsImport) => {
39 "compile-time load; calls import()"
40 }
41 (LoadTiming::Runtime, ImportBehavior::NoImport) => "runtime load; no import() call",
42 (LoadTiming::CompileTime, ImportBehavior::NoImport) => {
43 "compile-time load; no import() call"
44 }
45 (LoadTiming::Runtime, ImportBehavior::CallsImport) => "runtime load; calls import()",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ImportListForm {
53 Default,
55 Empty,
57 Explicit,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum RequireForm {
64 ModuleName,
66 FilePath,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum ModuleImportKind {
73 Use,
75 Require,
77 UseParent,
79 UseBase,
81}
82
83impl ModuleImportKind {
84 #[must_use]
86 pub fn dispatch_semantics(self) -> DispatchSemantics {
87 match self {
88 ModuleImportKind::Use | ModuleImportKind::UseParent | ModuleImportKind::UseBase => {
89 DispatchSemantics {
90 load_timing: LoadTiming::CompileTime,
91 import_behavior: ImportBehavior::CallsImport,
92 }
93 }
94 ModuleImportKind::Require => DispatchSemantics {
95 load_timing: LoadTiming::Runtime,
96 import_behavior: ImportBehavior::NoImport,
97 },
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct ModuleImportHead<'a> {
105 pub kind: ModuleImportKind,
107 pub token: &'a str,
109 pub token_start: usize,
111 pub token_end: usize,
113 require_form: Option<RequireForm>,
116 pub import_list: Option<ImportListForm>,
118}
119
120#[must_use]
125pub fn resolve_known_export_tag(module: &str, tag: &str) -> Option<&'static [&'static str]> {
126 let normalized_tag = tag.strip_prefix(':').unwrap_or(tag);
127 match (module, normalized_tag) {
128 ("POSIX", "sys_wait_h") => Some(&["WIFEXITED", "WEXITSTATUS", "WIFSIGNALED", "WTERMSIG"]),
129 ("POSIX", "fcntl_h") => Some(&["F_GETFL", "F_SETFL", "F_SETFD", "F_GETFD"]),
130 ("POSIX", "termios_h") => Some(&["TCSANOW", "TCSADRAIN", "TCSAFLUSH", "B9600"]),
131 ("File::Find", "find") => Some(&["find", "finddepth"]),
132 ("Fcntl", "seek") => Some(&["SEEK_SET", "SEEK_CUR", "SEEK_END"]),
133 ("Fcntl", "lock") => Some(&["LOCK_SH", "LOCK_EX", "LOCK_NB", "LOCK_UN"]),
134 ("Encode", "fallback") => Some(&["FB_DEFAULT", "FB_CROAK", "FB_QUIET", "FB_WARN"]),
135 _ => None,
136 }
137}
138
139impl<'a> ModuleImportHead<'a> {
140 #[must_use]
142 pub fn require_form(&self) -> Option<RequireForm> {
143 self.require_form
144 }
145}
146
147#[must_use]
152pub fn parse_module_import_head(line: &str) -> Option<ModuleImportHead<'_>> {
153 if let Some((token, token_start, token_end)) = parse_statement_head(line, "use") {
154 let kind = match token {
155 "parent" => ModuleImportKind::UseParent,
156 "base" => ModuleImportKind::UseBase,
157 _ => ModuleImportKind::Use,
158 };
159
160 let import_list = match kind {
161 ModuleImportKind::Use => Some(classify_use_import_list(&line[token_end..])),
162 ModuleImportKind::UseParent | ModuleImportKind::UseBase => None,
163 ModuleImportKind::Require => None,
164 };
165
166 return Some(ModuleImportHead {
167 kind,
168 token,
169 token_start,
170 token_end,
171 require_form: None,
172 import_list,
173 });
174 }
175
176 if let Some(result) = parse_require_head(line) {
177 return Some(result);
178 }
179
180 None
181}
182
183fn parse_require_head(line: &str) -> Option<ModuleImportHead<'_>> {
185 let trimmed = line.trim_start();
186 let leading = line.len().saturating_sub(trimmed.len());
187
188 let rest = trimmed.strip_prefix("require")?;
189 if !rest.chars().next().is_some_and(char::is_whitespace) {
190 return None;
191 }
192
193 let after_keyword = leading + "require".len();
194
195 let rest_trimmed = rest.trim_start();
196 let quote_offset = rest.len() - rest_trimmed.len();
197
198 if let Some(quote_char) = rest_trimmed.chars().next().filter(|ch| *ch == '"' || *ch == '\'') {
199 let quoted = &rest_trimmed[quote_char.len_utf8()..];
200 let close_idx = quoted.find(quote_char)?;
201 let inner = "ed[..close_idx];
202
203 let token_start = after_keyword + quote_offset + quote_char.len_utf8();
204 let token_end = token_start + inner.len();
205 return Some(ModuleImportHead {
206 kind: ModuleImportKind::Require,
207 token: inner,
208 token_start,
209 token_end,
210 require_form: Some(RequireForm::FilePath),
211 import_list: None,
212 });
213 }
214
215 let (token, token_rel_start, token_rel_end) = first_token_with_range(rest)?;
216 let token_start = after_keyword + token_rel_start;
217 let token_end = after_keyword + token_rel_end;
218
219 Some(ModuleImportHead {
220 kind: ModuleImportKind::Require,
221 token,
222 token_start,
223 token_end,
224 require_form: Some(RequireForm::ModuleName),
225 import_list: None,
226 })
227}
228
229fn classify_use_import_list(rest: &str) -> ImportListForm {
230 let trimmed = rest.trim_start();
231
232 if trimmed.is_empty() || trimmed.starts_with(';') {
233 return ImportListForm::Default;
234 }
235
236 if let Some(after_open) = trimmed.strip_prefix('(')
237 && let Some(close_idx) = after_open.find(')')
238 && after_open[..close_idx].trim().is_empty()
239 {
240 let after_close = after_open[close_idx + 1..].trim_start();
241 if after_close.is_empty() || after_close.starts_with(';') || after_close.starts_with('#') {
242 return ImportListForm::Empty;
243 }
244 }
245
246 ImportListForm::Explicit
247}
248
249fn parse_statement_head<'a>(line: &'a str, keyword: &str) -> Option<(&'a str, usize, usize)> {
250 let trimmed = line.trim_start();
251 let leading = line.len().saturating_sub(trimmed.len());
252
253 let rest = trimmed.strip_prefix(keyword)?;
254 if !rest.chars().next().is_some_and(char::is_whitespace) {
255 return None;
256 }
257
258 let (token, token_rel_start, token_rel_end) = first_token_with_range(rest)?;
259 let token_start = leading + keyword.len() + token_rel_start;
260 let token_end = leading + keyword.len() + token_rel_end;
261
262 Some((token, token_start, token_end))
263}
264
265fn first_token_with_range(input: &str) -> Option<(&str, usize, usize)> {
266 let mut token_start = None;
267
268 for (idx, ch) in input.char_indices() {
269 match token_start {
270 None => {
271 if is_token_delimiter(ch) {
272 continue;
273 }
274 token_start = Some(idx);
275 }
276 Some(start) => {
277 if is_token_delimiter(ch) {
278 if start == idx {
279 return None;
280 }
281 return Some((&input[start..idx], start, idx));
282 }
283 }
284 }
285 }
286
287 if let Some(start) = token_start {
288 if start < input.len() { Some((&input[start..], start, input.len())) } else { None }
289 } else {
290 None
291 }
292}
293
294fn is_token_delimiter(ch: char) -> bool {
295 ch.is_whitespace() || matches!(ch, ';' | '(' | ')')
296}