Skip to main content

just_lsp/
rule_context.rs

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}