1#![deny(unsafe_code)]
7#![warn(rust_2018_idioms)]
8#![warn(missing_docs)]
9#![warn(clippy::all)]
10
11use perl_module_import::{ModuleImportKind, parse_module_import_head};
12use perl_module_path::module_name_to_path;
13use serde_json::{Value, json};
14use url::Url;
15
16#[must_use]
22pub fn compute_links(uri: &str, text: &str, _roots: &[Url]) -> Vec<Value> {
23 let mut out = Vec::new();
24
25 for (i, line) in text.lines().enumerate() {
26 if let Some(import) = parse_module_import_head(line) {
27 match import.kind {
28 ModuleImportKind::Use => {
29 if !is_pragma(import.token)
30 && let Some(link) = make_deferred_module_link(
31 uri,
32 i as u32,
33 import.token,
34 import.token_start as u32,
35 import.token_end as u32,
36 )
37 {
38 out.push(link);
39 }
40 }
41 ModuleImportKind::Require => {
42 if !import.token.starts_with('"')
43 && !import.token.starts_with('\'')
44 && import.token.contains("::")
45 && !is_pragma(import.token)
46 && let Some(link) = make_deferred_module_link(
47 uri,
48 i as u32,
49 import.token,
50 import.token_start as u32,
51 import.token_end as u32,
52 )
53 {
54 out.push(link);
55 }
56 }
57 ModuleImportKind::UseParent | ModuleImportKind::UseBase => {}
58 }
59 }
60
61 if let Some(idx) = line.find("require ") {
62 let rest = &line[idx + 8..];
63 if let Some(start) = rest.find('"').or_else(|| rest.find('\'')) {
64 let quote_char = match rest.get(start..).and_then(|s| s.chars().next()) {
65 Some(c) => c,
66 None => continue,
67 };
68 let s = start + 1;
69 if let Some(end) = rest[s..].find(quote_char) {
70 let req = &rest[s..s + end];
71 let col_start = (idx + 8 + start + 1) as u32;
72 let col_end = (idx + 8 + start + 1 + end) as u32;
73 out.push(json!({
74 "range": {
75 "start": {"line": i as u32, "character": col_start},
76 "end": {"line": i as u32, "character": col_end}
77 },
78 "tooltip": format!("Open {}", req),
79 "data": {
80 "type": "file",
81 "path": req,
82 "baseUri": uri
83 }
84 }));
85 }
86 }
87 }
88 }
89 out
90}
91
92fn make_deferred_module_link(
93 uri: &str,
94 line: u32,
95 module: &str,
96 col_start: u32,
97 col_end: u32,
98) -> Option<Value> {
99 if module.is_empty() || col_start >= col_end {
100 return None;
101 }
102
103 Some(json!({
104 "range": {
105 "start": {"line": line, "character": col_start},
106 "end": {"line": line, "character": col_end}
107 },
108 "tooltip": format!("Open {}", module),
109 "data": {
110 "type": "module",
111 "module": module,
112 "baseUri": uri
113 }
114 }))
115}
116
117fn is_pragma(pkg: &str) -> bool {
118 matches!(
119 pkg,
120 "strict"
121 | "warnings"
122 | "utf8"
123 | "bytes"
124 | "integer"
125 | "feature"
126 | "constant"
127 | "lib"
128 | "vars"
129 | "subs"
130 | "overload"
131 | "parent"
132 | "base"
133 | "fields"
134 | "if"
135 | "attributes"
136 | "autouse"
137 | "autodie"
138 | "bigint"
139 | "bignum"
140 | "bigrat"
141 | "blib"
142 | "charnames"
143 | "diagnostics"
144 | "encoding"
145 | "filetest"
146 | "locale"
147 | "open"
148 | "ops"
149 | "re"
150 | "sigtrap"
151 | "sort"
152 | "threads"
153 | "vmsish"
154 )
155}
156
157#[allow(dead_code)]
158fn resolve_pkg(pkg: &str, roots: &[Url]) -> Option<String> {
159 let rel = module_name_to_path(pkg);
160 if let Some(base) = roots.first() {
161 let mut u = base.clone();
162 let mut p = u.path().to_string();
163 if !p.ends_with('/') {
164 p.push('/');
165 }
166 if let Some(lib_dir) = ["lib/", "blib/lib/", ""].first() {
167 let full_path = format!("{}{}{}", p, lib_dir, rel);
168 u.set_path(&full_path);
169 return Some(u.to_string());
170 }
171 }
172 None
173}
174
175#[allow(dead_code)]
176fn resolve_file(path: &str, roots: &[Url]) -> Option<String> {
177 if let Some(base) = roots.first() {
178 let mut u = base.clone();
179 let mut p = u.path().to_string();
180 if !p.ends_with('/') {
181 p.push('/');
182 }
183 p.push_str(path);
184 u.set_path(&p);
185 return Some(u.to_string());
186 }
187 None
188}
189
190#[allow(dead_code)]
191fn make_link(_src: &str, line: u32, line_text: &str, pkg: &str, target: String) -> Option<Value> {
192 if let Some(idx) = line_text.find(pkg) {
193 let start = idx as u32;
194 let end = (idx + pkg.len()) as u32;
195 Some(json!({
196 "range": {
197 "start": {"line": line, "character": start},
198 "end": {"line": line, "character": end}
199 },
200 "target": target,
201 "tooltip": format!("Open {}", pkg)
202 }))
203 } else {
204 None
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::compute_links;
211 use serde_json::Value;
212
213 fn uri() -> &'static str {
214 "file:///workspace/test.pl"
215 }
216
217 #[test]
220 fn emits_module_link_for_use_statement() {
221 let links = compute_links(uri(), "use Foo::Bar;\n", &[]);
222 assert_eq!(links.len(), 1);
223 if let Some(link) = links.first() {
224 assert_eq!(link.pointer("/data/type").and_then(Value::as_str), Some("module"));
225 assert_eq!(link.pointer("/data/module").and_then(Value::as_str), Some("Foo::Bar"));
226 }
227 }
228
229 #[test]
230 fn does_not_emit_link_for_pragma_use_strict() {
231 let links = compute_links(uri(), "use strict;\n", &[]);
232 assert!(links.is_empty(), "pragmas should not produce document links");
233 }
234
235 #[test]
236 fn does_not_emit_link_for_pragma_use_warnings() {
237 let links = compute_links(uri(), "use warnings;\n", &[]);
238 assert!(links.is_empty(), "pragmas should not produce document links");
239 }
240
241 #[test]
242 fn does_not_emit_link_for_use_feature_pragma() {
243 let links = compute_links(uri(), "use feature 'say';\n", &[]);
244 assert!(links.is_empty(), "'feature' is a pragma");
245 }
246
247 #[test]
250 fn does_not_emit_module_link_for_use_parent_statement() {
251 let links = compute_links(uri(), "use parent 'Foo::Bar';\n", &[]);
252 assert!(links.is_empty());
253 }
254
255 #[test]
256 fn does_not_emit_module_link_for_use_base_statement() {
257 let links = compute_links(uri(), "use base 'Foo::Bar';\n", &[]);
258 assert!(links.is_empty(), "use base is a base-class declaration, not a module link");
259 }
260
261 #[test]
264 fn emits_module_link_for_module_form_require_statement() {
265 let links = compute_links(uri(), "require Foo::Bar;\n", &[]);
266 assert_eq!(links.len(), 1);
267 if let Some(link) = links.first() {
268 assert_eq!(link.pointer("/data/type").and_then(Value::as_str), Some("module"));
269 assert_eq!(link.pointer("/data/module").and_then(Value::as_str), Some("Foo::Bar"));
270 }
271 }
272
273 #[test]
274 fn emits_file_link_for_require_with_double_quoted_string() {
275 let links = compute_links(uri(), r#"require "my/file.pm";"#, &[]);
276 assert_eq!(links.len(), 1, "require with file string should emit a file link");
277 if let Some(link) = links.first() {
278 assert_eq!(link.pointer("/data/type").and_then(Value::as_str), Some("file"));
279 assert_eq!(link.pointer("/data/path").and_then(Value::as_str), Some("my/file.pm"));
280 }
281 }
282
283 #[test]
284 fn emits_file_link_for_require_with_single_quoted_string() {
285 let links = compute_links(uri(), "require 'lib/helper.pm';", &[]);
286 assert_eq!(links.len(), 1, "require with single-quoted file should emit a file link");
287 if let Some(link) = links.first() {
288 assert_eq!(link.pointer("/data/type").and_then(Value::as_str), Some("file"));
289 }
290 }
291
292 #[test]
293 fn does_not_emit_link_for_require_bare_word_without_colons() {
294 let links = compute_links(uri(), "require Something;\n", &[]);
296 assert!(links.is_empty(), "bare require without '::' should not emit a module link");
297 }
298
299 #[test]
302 fn link_range_is_on_correct_line() {
303 let text = "# comment\nuse Foo::Bar;\n";
304 let links = compute_links(uri(), text, &[]);
305 assert_eq!(links.len(), 1);
306 if let Some(link) = links.first() {
307 let line = link.pointer("/range/start/line").and_then(Value::as_u64);
308 assert_eq!(line, Some(1), "link should be on line 1 (0-indexed)");
309 }
310 }
311
312 #[test]
313 fn link_tooltip_contains_module_name() {
314 let links = compute_links(uri(), "use Foo::Bar;\n", &[]);
315 assert_eq!(links.len(), 1);
316 if let Some(link) = links.first() {
317 let tooltip = link.pointer("/tooltip").and_then(Value::as_str).unwrap_or("");
318 assert!(tooltip.contains("Foo::Bar"), "tooltip should reference the module name");
319 }
320 }
321
322 #[test]
325 fn emits_link_for_each_use_statement_in_multi_line_file() {
326 let text = "use Foo;\nuse Bar::Baz;\nuse strict;\n";
327 let links = compute_links(uri(), text, &[]);
328 let has_strict = links
331 .iter()
332 .any(|l| l.pointer("/data/module").and_then(Value::as_str) == Some("strict"));
333 assert!(!has_strict, "strict pragma must not appear in links");
334 }
335}