just-lsp 0.3.1

A language server for just
use super::*;

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Recipe {
  pub(crate) attributes: Vec<Attribute>,
  pub(crate) content: String,
  pub(crate) dependencies: Vec<Dependency>,
  pub(crate) name: TextNode,
  pub(crate) parameters: Vec<Parameter>,
  pub(crate) range: lsp::Range,
  pub(crate) shebang: Option<TextNode>,
}

impl Recipe {
  #[must_use]
  pub(crate) fn find_attribute(&self, name: &str) -> Option<&Attribute> {
    self
      .attributes
      .iter()
      .find(|attribute| attribute.name.value == name)
  }

  #[must_use]
  pub(crate) fn groups(&self) -> HashSet<Group> {
    let mut groups = HashSet::new();

    for attribute in &self.attributes {
      let attribute_name = attribute.name.value.as_str();

      if let Some(targets) = Group::targets(attribute_name) {
        groups.extend(targets);
      }
    }

    if groups.is_empty() {
      groups.insert(Group::Any);
    }

    groups
  }

  #[must_use]
  pub(crate) fn has_attribute(&self, name: &str) -> bool {
    self
      .attributes
      .iter()
      .any(|attribute| attribute.name.value == name)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  fn create_range(
    start_line: u32,
    start_char: u32,
    end_line: u32,
    end_char: u32,
  ) -> lsp::Range {
    lsp::Range {
      start: lsp::Position {
        line: start_line,
        character: start_char,
      },
      end: lsp::Position {
        line: end_line,
        character: end_char,
      },
    }
  }

  #[test]
  fn recipe_groups_no_attributes() {
    let recipe = Recipe {
      name: TextNode {
        value: "test".into(),
        range: create_range(0, 0, 0, 4),
      },
      attributes: vec![],
      dependencies: vec![],
      shebang: None,
      parameters: vec![],
      content: "test:\n  echo test".to_string(),
      range: create_range(0, 0, 2, 0),
    };

    assert_eq!(recipe.groups(), HashSet::from([Group::Any]));
  }

  #[test]
  fn recipe_groups_single_attribute() {
    let recipe = Recipe {
      name: TextNode {
        value: "test".into(),
        range: create_range(1, 0, 1, 4),
      },
      attributes: vec![Attribute {
        name: TextNode {
          value: "linux".to_string(),
          range: create_range(0, 1, 0, 6),
        },
        arguments: vec![],
        target: Some(AttributeTarget::Recipe),
        range: create_range(0, 0, 1, 0),
      }],
      dependencies: vec![],
      shebang: None,
      parameters: vec![],
      content: "[linux]\ntest:\n  echo test".to_string(),
      range: create_range(0, 0, 3, 0),
    };

    assert_eq!(recipe.groups(), HashSet::from([Group::Linux]));
  }

  #[test]
  fn recipe_groups_multiple_attributes() {
    let recipe = Recipe {
      name: TextNode {
        value: "test".into(),
        range: create_range(2, 0, 2, 4),
      },
      attributes: vec![
        Attribute {
          name: TextNode {
            value: "linux".to_string(),
            range: create_range(0, 1, 0, 6),
          },
          arguments: vec![],
          target: Some(AttributeTarget::Recipe),
          range: create_range(0, 0, 1, 0),
        },
        Attribute {
          name: TextNode {
            value: "windows".to_string(),
            range: create_range(1, 1, 1, 8),
          },
          arguments: vec![],
          target: Some(AttributeTarget::Recipe),
          range: create_range(1, 0, 2, 0),
        },
      ],
      dependencies: vec![],
      shebang: None,
      parameters: vec![],
      content: "[linux]\n[windows]\ntest:\n  echo test".to_string(),
      range: create_range(0, 0, 4, 0),
    };

    assert_eq!(
      recipe.groups(),
      HashSet::from([Group::Linux, Group::Windows])
    );
  }

  #[test]
  fn recipe_groups_all_attributes() {
    let recipe = Recipe {
      name: TextNode {
        value: "test".into(),
        range: create_range(5, 0, 5, 4),
      },
      attributes: vec![
        Attribute {
          name: TextNode {
            value: "linux".to_string(),
            range: create_range(0, 1, 0, 6),
          },
          arguments: vec![],
          target: Some(AttributeTarget::Recipe),
          range: create_range(0, 0, 1, 0),
        },
        Attribute {
          name: TextNode {
            value: "windows".to_string(),
            range: create_range(1, 1, 1, 8),
          },
          arguments: vec![],
          target: Some(AttributeTarget::Recipe),
          range: create_range(1, 0, 2, 0),
        },
        Attribute {
          name: TextNode {
            value: "macos".to_string(),
            range: create_range(2, 1, 2, 6),
          },
          arguments: vec![],
          target: Some(AttributeTarget::Recipe),
          range: create_range(2, 0, 3, 0),
        },
        Attribute {
          name: TextNode {
            value: "unix".to_string(),
            range: create_range(3, 1, 3, 5),
          },
          arguments: vec![],
          target: Some(AttributeTarget::Recipe),
          range: create_range(3, 0, 4, 0),
        },
        Attribute {
          name: TextNode {
            value: "openbsd".to_string(),
            range: create_range(4, 1, 4, 8),
          },
          arguments: vec![],
          target: Some(AttributeTarget::Recipe),
          range: create_range(4, 0, 5, 0),
        },
      ],
      dependencies: vec![],
      shebang: None,
      parameters: vec![],
      content:
        "[linux]\n[windows]\n[macos]\n[unix]\n[openbsd]\ntest:\n  echo test"
          .to_string(),
      range: create_range(0, 0, 7, 0),
    };

    assert_eq!(
      recipe.groups(),
      HashSet::from([
        Group::Linux,
        Group::Windows,
        Group::Macos,
        Group::Openbsd
      ])
    );
  }

  #[test]
  fn recipe_groups_non_os_attributes() {
    let recipe = Recipe {
      name: TextNode {
        value: "test".into(),
        range: create_range(1, 0, 1, 4),
      },
      attributes: vec![Attribute {
        name: TextNode {
          value: "private".to_string(),
          range: create_range(0, 1, 0, 8),
        },
        arguments: vec![],
        target: Some(AttributeTarget::Recipe),
        range: create_range(0, 0, 1, 0),
      }],
      dependencies: vec![],
      shebang: None,
      parameters: vec![],
      content: "[private]\ntest:\n  echo test".to_string(),
      range: create_range(0, 0, 3, 0),
    };

    assert_eq!(recipe.groups(), HashSet::from([Group::Any]));
  }
}