just-lsp 0.2.8

A language server for just
use super::*;

pub struct UnresolvedIdentifier {
  pub name: String,
  pub range: lsp::Range,
}

#[derive(Default)]
pub struct IdentifierAnalysis {
  recipe_identifier_usage: HashMap<String, HashSet<String>>,
  unresolved_identifiers: Vec<UnresolvedIdentifier>,
  variable_usage: HashMap<String, bool>,
}

impl IdentifierAnalysis {
  fn new(context: &RuleContext<'_>) -> Self {
    let mut variable_usage = context
      .variables()
      .iter()
      .map(|variable| (variable.name.value.clone(), false))
      .collect::<HashMap<_, _>>();

    let mut recipe_identifier_usage = context
      .recipes()
      .iter()
      .map(|recipe| (recipe.name.clone(), HashSet::new()))
      .collect::<HashMap<_, _>>();

    let mut unresolved_identifiers = Vec::new();

    if let Some(tree) = context.tree() {
      let root = tree.root_node();

      for identifier in root.find_all("expression > value > identifier") {
        Self::record_identifier(
          context,
          &mut recipe_identifier_usage,
          &mut variable_usage,
          &mut unresolved_identifiers,
          identifier,
        );
      }

      for identifier in root.find_all("parameter > value > identifier") {
        Self::record_identifier(
          context,
          &mut recipe_identifier_usage,
          &mut variable_usage,
          &mut unresolved_identifiers,
          identifier,
        );
      }
    }

    Self {
      recipe_identifier_usage,
      unresolved_identifiers,
      variable_usage,
    }
  }

  fn record_identifier(
    context: &RuleContext<'_>,
    recipe_identifier_usage: &mut HashMap<String, HashSet<String>>,
    variable_usage: &mut HashMap<String, bool>,
    unresolved_identifiers: &mut Vec<UnresolvedIdentifier>,
    identifier: Node<'_>,
  ) {
    let document = context.document();
    let recipe_parameters = context.recipe_parameters();
    let value_names = context.variable_and_builtin_names();

    let recipe_name = identifier
      .get_parent("recipe")
      .as_ref()
      .and_then(|recipe_node| recipe_node.find("recipe_header > identifier"))
      .map_or_else(String::new, |identifier_node| {
        document.get_node_text(&identifier_node)
      });

    let identifier_name = document.get_node_text(&identifier);

    if let Some(recipe) = context.recipe(&recipe_name) {
      recipe_identifier_usage
        .entry(recipe.name.clone())
        .or_default()
        .insert(identifier_name.clone());

      if recipe_parameters.get(&recipe.name).is_some_and(|params| {
        params.iter().any(|param| param.name == identifier_name)
      }) {
        return;
      }
    }

    if let Some(usage) = variable_usage.get_mut(&identifier_name) {
      *usage = true;
    }

    if !value_names.contains(&identifier_name) {
      unresolved_identifiers.push(UnresolvedIdentifier {
        name: identifier_name,
        range: identifier.get_range(),
      });
    }
  }
}

pub struct RuleContext<'a> {
  aliases: OnceCell<Vec<Alias>>,
  attributes: OnceCell<Vec<Attribute>>,
  document: &'a Document,
  document_variable_names: OnceCell<HashSet<String>>,
  function_calls: OnceCell<Vec<FunctionCall>>,
  identifier_analysis: OnceCell<IdentifierAnalysis>,
  recipe_names: OnceCell<HashSet<String>>,
  recipe_parameters: OnceCell<HashMap<String, Vec<Parameter>>>,
  recipes: OnceCell<Vec<Recipe>>,
  settings: OnceCell<Vec<Setting>>,
  variable_and_builtin_names: OnceCell<HashSet<String>>,
  variables: OnceCell<Vec<Variable>>,
}

impl<'a> RuleContext<'a> {
  pub fn aliases(&self) -> &[Alias] {
    self
      .aliases
      .get_or_init(|| self.document.aliases())
      .as_slice()
  }

  pub fn attributes(&self) -> &[Attribute] {
    self
      .attributes
      .get_or_init(|| self.document.attributes())
      .as_slice()
  }

  pub fn document(&self) -> &Document {
    self.document
  }

  pub fn document_variable_names(&self) -> &HashSet<String> {
    self.document_variable_names.get_or_init(|| {
      self
        .variables()
        .iter()
        .map(|variable| variable.name.value.clone())
        .collect()
    })
  }

  pub fn function_calls(&self) -> &[FunctionCall] {
    self
      .function_calls
      .get_or_init(|| self.document.function_calls())
      .as_slice()
  }

  fn identifier_analysis(&self) -> &IdentifierAnalysis {
    self
      .identifier_analysis
      .get_or_init(|| IdentifierAnalysis::new(self))
  }

  pub fn new(document: &'a Document) -> Self {
    Self {
      aliases: OnceCell::new(),
      attributes: OnceCell::new(),
      document,
      document_variable_names: OnceCell::new(),
      function_calls: OnceCell::new(),
      identifier_analysis: OnceCell::new(),
      recipe_names: OnceCell::new(),
      recipe_parameters: OnceCell::new(),
      recipes: OnceCell::new(),
      settings: OnceCell::new(),
      variable_and_builtin_names: OnceCell::new(),
      variables: OnceCell::new(),
    }
  }

  pub fn recipe(&self, name: &str) -> Option<&Recipe> {
    self.recipes().iter().find(|recipe| recipe.name == name)
  }

  pub fn recipe_identifier_usage(&self) -> &HashMap<String, HashSet<String>> {
    &self.identifier_analysis().recipe_identifier_usage
  }

  pub fn recipe_names(&self) -> &HashSet<String> {
    self
      .recipe_names
      .get_or_init(|| self.recipes().iter().map(|r| r.name.clone()).collect())
  }

  pub fn recipe_parameters(&self) -> &HashMap<String, Vec<Parameter>> {
    self.recipe_parameters.get_or_init(|| {
      self
        .recipes()
        .iter()
        .map(|recipe| (recipe.name.clone(), recipe.parameters.clone()))
        .collect()
    })
  }

  pub fn recipes(&self) -> &[Recipe] {
    self
      .recipes
      .get_or_init(|| self.document.recipes())
      .as_slice()
  }

  pub fn settings(&self) -> &[Setting] {
    self
      .settings
      .get_or_init(|| self.document.settings())
      .as_slice()
  }

  pub fn tree(&self) -> Option<&Tree> {
    self.document.tree.as_ref()
  }

  pub fn unresolved_identifiers(&self) -> &[UnresolvedIdentifier] {
    &self.identifier_analysis().unresolved_identifiers
  }

  pub fn variable_and_builtin_names(&self) -> &HashSet<String> {
    self.variable_and_builtin_names.get_or_init(|| {
      let mut names = self.document_variable_names().clone();

      names.extend(BUILTINS.into_iter().filter_map(|builtin| match builtin {
        Builtin::Constant { name, .. } => Some(name.to_owned()),
        _ => None,
      }));

      names
    })
  }

  pub fn variable_usage(&self) -> &HashMap<String, bool> {
    &self.identifier_analysis().variable_usage
  }

  pub fn variables(&self) -> &[Variable] {
    self
      .variables
      .get_or_init(|| self.document.variables())
      .as_slice()
  }
}