1use super::*;
2
3type BuiltinRef = &'static Builtin<'static>;
4
5pub struct RuleContext<'a> {
6 aliases: OnceLock<Vec<Alias>>,
7 attributes: OnceLock<Vec<Attribute>>,
8 builtin_attributes_map: OnceLock<HashMap<&'static str, Vec<BuiltinRef>>>,
9 builtin_function_map: OnceLock<HashMap<&'static str, BuiltinRef>>,
10 builtin_setting_map: OnceLock<HashMap<&'static str, BuiltinRef>>,
11 document: &'a Document,
12 document_variable_names: OnceLock<HashSet<String>>,
13 function_calls: OnceLock<Vec<FunctionCall>>,
14 functions: OnceLock<Vec<Function>>,
15 imported_documents: Vec<Document>,
16 recipe_names: OnceLock<HashSet<String>>,
17 recipe_parameters: OnceLock<HashMap<String, Vec<Parameter>>>,
18 recipes: OnceLock<Vec<Recipe>>,
19 scope: OnceLock<Scope<'a>>,
20 settings: OnceLock<Vec<Setting>>,
21 user_function_names: OnceLock<HashSet<String>>,
22 variable_and_builtin_names: OnceLock<HashSet<String>>,
23 variables: OnceLock<Vec<Variable>>,
24}
25
26impl<'a> RuleContext<'a> {
27 pub fn aliases(&self) -> &[Alias] {
28 self
29 .aliases
30 .get_or_init(|| self.documents().flat_map(Document::aliases).collect())
31 .as_slice()
32 }
33
34 pub fn attributes(&self) -> &[Attribute] {
35 self
36 .attributes
37 .get_or_init(|| self.document.attributes())
38 .as_slice()
39 }
40
41 pub fn builtin_attributes(&self, name: &str) -> &[&'static Builtin<'static>] {
42 self
43 .builtin_attributes_map()
44 .get(name)
45 .map_or(&[], Vec::as_slice)
46 }
47
48 fn builtin_attributes_map(
49 &self,
50 ) -> &HashMap<&'static str, Vec<&'static Builtin<'static>>> {
51 self.builtin_attributes_map.get_or_init(|| {
52 let mut map = HashMap::new();
53
54 for builtin in BUILTINS {
55 if let Builtin::Attribute { name, .. } = builtin {
56 map.entry(*name).or_insert_with(Vec::new).push(builtin);
57 }
58 }
59
60 map
61 })
62 }
63
64 pub fn builtin_function(
65 &self,
66 name: &str,
67 ) -> Option<&'static Builtin<'static>> {
68 self.builtin_function_map().get(name).copied()
69 }
70
71 fn builtin_function_map(
72 &self,
73 ) -> &HashMap<&'static str, &'static Builtin<'static>> {
74 self.builtin_function_map.get_or_init(|| {
75 let mut map = HashMap::new();
76
77 for builtin in BUILTINS {
78 if let Builtin::Function { name, aliases, .. } = builtin {
79 map.entry(*name).or_insert(builtin);
80
81 for alias in *aliases {
82 map.entry(*alias).or_insert(builtin);
83 }
84 }
85 }
86
87 map
88 })
89 }
90
91 pub fn builtin_setting(
92 &self,
93 name: &str,
94 ) -> Option<&'static Builtin<'static>> {
95 self.builtin_setting_map().get(name).copied()
96 }
97
98 fn builtin_setting_map(
99 &self,
100 ) -> &HashMap<&'static str, &'static Builtin<'static>> {
101 self.builtin_setting_map.get_or_init(|| {
102 let mut map = HashMap::new();
103
104 for builtin in BUILTINS {
105 if let Builtin::Setting { name, .. } = builtin {
106 map.entry(*name).or_insert(builtin);
107 }
108 }
109
110 map
111 })
112 }
113
114 pub fn document(&self) -> &'a Document {
115 self.document
116 }
117
118 pub fn document_variable_names(&self) -> &HashSet<String> {
119 self.document_variable_names.get_or_init(|| {
120 self
121 .variables()
122 .iter()
123 .map(|variable| variable.name.value.clone())
124 .collect()
125 })
126 }
127
128 fn documents(&self) -> impl Iterator<Item = &Document> {
129 once(self.document).chain(self.imported_documents.iter())
130 }
131
132 pub fn function_calls(&self) -> &[FunctionCall] {
133 self
134 .function_calls
135 .get_or_init(|| self.document.function_calls())
136 .as_slice()
137 }
138
139 pub fn functions(&self) -> &[Function] {
140 self
141 .functions
142 .get_or_init(|| self.documents().flat_map(Document::functions).collect())
143 .as_slice()
144 }
145
146 #[must_use]
147 pub fn new(document: &'a Document) -> Self {
148 Self {
149 aliases: OnceLock::new(),
150 attributes: OnceLock::new(),
151 builtin_attributes_map: OnceLock::new(),
152 builtin_function_map: OnceLock::new(),
153 builtin_setting_map: OnceLock::new(),
154 document,
155 document_variable_names: OnceLock::new(),
156 function_calls: OnceLock::new(),
157 functions: OnceLock::new(),
158 imported_documents: Self::resolve_imports(document),
159 recipe_names: OnceLock::new(),
160 recipe_parameters: OnceLock::new(),
161 recipes: OnceLock::new(),
162 scope: OnceLock::new(),
163 settings: OnceLock::new(),
164 user_function_names: OnceLock::new(),
165 variable_and_builtin_names: OnceLock::new(),
166 variables: OnceLock::new(),
167 }
168 }
169
170 pub fn recipe(&self, name: &str) -> Option<&Recipe> {
171 self
172 .recipes()
173 .iter()
174 .find(|recipe| recipe.name.value == name)
175 }
176
177 pub fn recipe_names(&self) -> &HashSet<String> {
178 self.recipe_names.get_or_init(|| {
179 self
180 .recipes()
181 .iter()
182 .map(|recipe| recipe.name.value.clone())
183 .collect()
184 })
185 }
186
187 pub fn recipe_parameters(&self) -> &HashMap<String, Vec<Parameter>> {
188 self.recipe_parameters.get_or_init(|| {
189 self
190 .recipes()
191 .iter()
192 .map(|recipe| (recipe.name.value.clone(), recipe.parameters.clone()))
193 .collect()
194 })
195 }
196
197 pub fn recipes(&self) -> &[Recipe] {
198 self
199 .recipes
200 .get_or_init(|| self.documents().flat_map(Document::recipes).collect())
201 .as_slice()
202 }
203
204 fn resolve_imports(document: &Document) -> Vec<Document> {
205 let mut documents = Vec::new();
206 let mut seen = HashSet::new();
207
208 if let Ok(path) = document.uri.to_file_path() {
209 seen.insert(path);
210 }
211
212 Self::resolve_imports_recursive(document, &mut documents, &mut seen);
213
214 documents
215 }
216
217 fn resolve_imports_recursive(
218 document: &Document,
219 documents: &mut Vec<Document>,
220 seen: &mut HashSet<PathBuf>,
221 ) {
222 for import in document.imports() {
223 let Some(path) = import.resolve(&document.uri) else {
224 continue;
225 };
226
227 if !seen.insert(path.clone()) {
228 continue;
229 }
230
231 let Ok(content) = fs::read_to_string(&path) else {
232 if !import.optional {
233 log::warn!("failed to read import: {}", path.display());
234 }
235
236 continue;
237 };
238
239 let Ok(uri) = lsp::Url::from_file_path(&path) else {
240 continue;
241 };
242
243 let mut imported = Document {
244 content: Rope::from_str(&content),
245 tree: None,
246 uri,
247 version: 0,
248 };
249
250 if imported.parse().is_err() {
251 continue;
252 }
253
254 Self::resolve_imports_recursive(&imported, documents, seen);
255
256 documents.push(imported);
257 }
258 }
259
260 pub fn scope(&self) -> &Scope<'_> {
261 self.scope.get_or_init(|| Scope::analyze(self))
262 }
263
264 pub fn setting_enabled(&self, name: &str) -> bool {
265 self.settings().iter().any(|setting| {
266 setting.name.value == name
267 && matches!(setting.kind, SettingKind::Boolean(true))
268 })
269 }
270
271 pub fn settings(&self) -> &[Setting] {
272 self
273 .settings
274 .get_or_init(|| self.documents().flat_map(Document::settings).collect())
275 .as_slice()
276 }
277
278 pub fn tree(&self) -> Option<&Tree> {
279 self.document.tree.as_ref()
280 }
281
282 pub fn user_function_names(&self) -> &HashSet<String> {
283 self.user_function_names.get_or_init(|| {
284 self
285 .functions()
286 .iter()
287 .map(|function| function.name.value.clone())
288 .collect()
289 })
290 }
291
292 pub fn variable_and_builtin_names(&self) -> &HashSet<String> {
293 self.variable_and_builtin_names.get_or_init(|| {
294 let mut names = self.document_variable_names().clone();
295
296 names.extend(BUILTINS.iter().filter_map(|builtin| match builtin {
297 Builtin::Constant { name, .. } => Some((*name).to_owned()),
298 _ => None,
299 }));
300
301 names
302 })
303 }
304
305 pub fn variables(&self) -> &[Variable] {
306 self
307 .variables
308 .get_or_init(|| self.documents().flat_map(Document::variables).collect())
309 .as_slice()
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use {
316 super::*, indoc::indoc, pretty_assertions::assert_eq, tempfile::Builder,
317 };
318
319 #[test]
320 fn imported_recipes_are_merged() {
321 let dir = Builder::new().prefix("just-lsp").tempdir().unwrap();
322
323 fs::write(
324 dir.path().join("bar.just"),
325 indoc! {
326 "
327 bar:
328 echo bar
329 "
330 },
331 )
332 .unwrap();
333
334 fs::write(
335 dir.path().join("justfile"),
336 indoc! {
337 "
338 import 'bar.just'
339
340 foo:
341 echo foo
342 "
343 },
344 )
345 .unwrap();
346
347 let uri = lsp::Url::from_file_path(dir.path().join("justfile")).unwrap();
348
349 let mut document = Document {
350 content: Rope::from_str(
351 &fs::read_to_string(dir.path().join("justfile")).unwrap(),
352 ),
353 tree: None,
354 uri,
355 version: 1,
356 };
357
358 document.parse().unwrap();
359
360 let context = RuleContext::new(&document);
361
362 let recipe_names = context
363 .recipes()
364 .iter()
365 .map(|recipe| recipe.name.value.as_str())
366 .collect::<Vec<_>>();
367
368 assert_eq!(recipe_names, ["foo", "bar"]);
369 }
370
371 #[test]
372 fn imported_variables_are_merged() {
373 let dir = Builder::new().prefix("just-lsp").tempdir().unwrap();
374
375 fs::write(dir.path().join("bar.just"), "bar := 'baz'\n").unwrap();
376
377 fs::write(
378 dir.path().join("justfile"),
379 indoc! {
380 "
381 import 'bar.just'
382
383 foo := 'qux'
384 "
385 },
386 )
387 .unwrap();
388
389 let uri = lsp::Url::from_file_path(dir.path().join("justfile")).unwrap();
390
391 let mut document = Document {
392 content: Rope::from_str(
393 &fs::read_to_string(dir.path().join("justfile")).unwrap(),
394 ),
395 tree: None,
396 uri,
397 version: 1,
398 };
399
400 document.parse().unwrap();
401
402 let context = RuleContext::new(&document);
403
404 let variable_names = context
405 .variables()
406 .iter()
407 .map(|variable| variable.name.value.as_str())
408 .collect::<Vec<_>>();
409
410 assert_eq!(variable_names, ["foo", "bar"]);
411 }
412
413 #[test]
414 fn imported_settings_are_merged() {
415 let dir = Builder::new().prefix("just-lsp").tempdir().unwrap();
416
417 fs::write(dir.path().join("bar.just"), "set export\n").unwrap();
418
419 fs::write(
420 dir.path().join("justfile"),
421 indoc! {
422 "
423 import 'bar.just'
424
425 set dotenv-load
426 "
427 },
428 )
429 .unwrap();
430
431 let uri = lsp::Url::from_file_path(dir.path().join("justfile")).unwrap();
432
433 let mut document = Document {
434 content: Rope::from_str(
435 &fs::read_to_string(dir.path().join("justfile")).unwrap(),
436 ),
437 tree: None,
438 uri,
439 version: 1,
440 };
441
442 document.parse().unwrap();
443
444 let context = RuleContext::new(&document);
445
446 let setting_names = context
447 .settings()
448 .iter()
449 .map(|s| s.name.value.as_str())
450 .collect::<Vec<_>>();
451
452 assert_eq!(setting_names, ["dotenv-load", "export"]);
453 }
454
455 #[test]
456 fn optional_missing_import_is_skipped() {
457 let dir = Builder::new().prefix("just-lsp").tempdir().unwrap();
458
459 fs::write(
460 dir.path().join("justfile"),
461 indoc! {
462 "
463 import? 'nonexistent.just'
464
465 foo:
466 echo foo
467 "
468 },
469 )
470 .unwrap();
471
472 let uri = lsp::Url::from_file_path(dir.path().join("justfile")).unwrap();
473
474 let mut document = Document {
475 content: Rope::from_str(
476 &fs::read_to_string(dir.path().join("justfile")).unwrap(),
477 ),
478 tree: None,
479 uri,
480 version: 1,
481 };
482
483 document.parse().unwrap();
484
485 let context = RuleContext::new(&document);
486
487 let recipe_names = context
488 .recipes()
489 .iter()
490 .map(|recipe| recipe.name.value.as_str())
491 .collect::<Vec<_>>();
492
493 assert_eq!(recipe_names, ["foo"]);
494 }
495
496 #[test]
497 fn recursive_imports_are_resolved() {
498 let dir = Builder::new().prefix("just-lsp").tempdir().unwrap();
499
500 fs::write(
501 dir.path().join("baz.just"),
502 indoc! {
503 "
504 baz:
505 echo baz
506 "
507 },
508 )
509 .unwrap();
510
511 fs::write(
512 dir.path().join("bar.just"),
513 indoc! {
514 "
515 import 'baz.just'
516
517 bar:
518 echo bar
519 "
520 },
521 )
522 .unwrap();
523
524 fs::write(
525 dir.path().join("justfile"),
526 indoc! {
527 "
528 import 'bar.just'
529
530 foo:
531 echo foo
532 "
533 },
534 )
535 .unwrap();
536
537 let uri = lsp::Url::from_file_path(dir.path().join("justfile")).unwrap();
538
539 let mut document = Document {
540 content: Rope::from_str(
541 &fs::read_to_string(dir.path().join("justfile")).unwrap(),
542 ),
543 tree: None,
544 uri,
545 version: 1,
546 };
547
548 document.parse().unwrap();
549
550 let context = RuleContext::new(&document);
551
552 let recipe_names = context
553 .recipes()
554 .iter()
555 .map(|recipe| recipe.name.value.as_str())
556 .collect::<Vec<_>>();
557
558 assert_eq!(recipe_names, ["foo", "baz", "bar"]);
559 }
560
561 #[test]
562 fn circular_imports_are_handled() {
563 let dir = Builder::new().prefix("just-lsp").tempdir().unwrap();
564
565 fs::write(
566 dir.path().join("bar.just"),
567 indoc! {
568 "
569 import 'justfile'
570
571 bar:
572 echo bar
573 "
574 },
575 )
576 .unwrap();
577
578 fs::write(
579 dir.path().join("justfile"),
580 indoc! {
581 "
582 import 'bar.just'
583
584 foo:
585 echo foo
586 "
587 },
588 )
589 .unwrap();
590
591 let uri = lsp::Url::from_file_path(dir.path().join("justfile")).unwrap();
592
593 let mut document = Document {
594 content: Rope::from_str(
595 &fs::read_to_string(dir.path().join("justfile")).unwrap(),
596 ),
597 tree: None,
598 uri,
599 version: 1,
600 };
601
602 document.parse().unwrap();
603
604 let context = RuleContext::new(&document);
605
606 let recipe_names = context
607 .recipes()
608 .iter()
609 .map(|recipe| recipe.name.value.as_str())
610 .collect::<Vec<_>>();
611
612 assert_eq!(recipe_names, ["foo", "bar"]);
613 }
614}