use super::*;
#[derive(Debug)]
pub(crate) struct Resolver<'a> {
document: &'a Document,
}
impl<'a> Resolver<'a> {
#[must_use]
pub(crate) fn new(document: &'a Document) -> Self {
Self { document }
}
#[must_use]
pub(crate) fn resolve_identifier_definition(
&self,
identifier: &Node,
) -> Option<lsp::Location> {
Some(lsp::Location {
range: match self.resolve_symbol(identifier)? {
Symbol::Builtin(_) => identifier.get_range(self.document),
Symbol::Function(function) => function.name.range,
Symbol::FunctionParameter(parameter) => parameter.range,
Symbol::Parameter(parameter) => parameter.range,
Symbol::Recipe(recipe) => recipe.range,
Symbol::Variable(variable) => variable.range,
},
uri: self.document.uri.clone(),
})
}
#[must_use]
pub(crate) fn resolve_identifier_hover(
&self,
identifier: &Node,
) -> Option<lsp::Hover> {
Some(lsp::Hover {
contents: lsp::HoverContents::Markup(
match self.resolve_symbol(identifier)? {
Symbol::Builtin(builtin) => builtin.description(),
Symbol::Function(function) => lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: function.content,
},
Symbol::FunctionParameter(parameter) => lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: parameter.value,
},
Symbol::Parameter(parameter) => lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: parameter.content,
},
Symbol::Recipe(recipe) => lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: recipe.content,
},
Symbol::Variable(variable) => lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: variable.content,
},
},
),
range: Some(identifier.get_range(self.document)),
})
}
#[must_use]
pub(crate) fn resolve_identifier_references(
&self,
identifier: &Node,
) -> Vec<lsp::Location> {
let name = self.document.get_node_text(identifier);
let Some(symbol) = self.resolve_symbol(identifier) else {
return Vec::new();
};
let root = match &self.document.tree {
Some(tree) => tree.root_node(),
None => return Vec::new(),
};
root
.find_all("identifier")
.into_iter()
.filter(|candidate| {
if candidate.id() == identifier.id() {
return true;
}
if self.document.get_node_text(candidate) != name {
return false;
}
let Some(candidate_parent) = candidate.parent() else {
return false;
};
let candidate_parent_kind = candidate_parent.kind();
match &symbol {
Symbol::Builtin(_) => false,
Symbol::Function(_) => {
candidate_parent_kind == "function_call"
|| candidate_parent_kind == "function_definition"
}
Symbol::FunctionParameter(_) => {
let in_same_function = matches!(
(
identifier.get_parent("function_definition"),
candidate.get_parent("function_definition"),
),
(Some(f1), Some(f2)) if f1.id() == f2.id()
);
in_same_function
&& ["value", "function_parameters"]
.contains(&candidate_parent_kind)
}
Symbol::Parameter(_) => {
let in_same_recipe = matches!(
(identifier.get_parent("recipe"), candidate.get_parent("recipe")),
(Some(r1), Some(r2)) if r1.id() == r2.id()
);
in_same_recipe
&& ["value", "parameter", "variadic_parameter"]
.contains(&candidate_parent_kind)
}
Symbol::Recipe(_) => [
"alias",
"dependency",
"dependency_expression",
"recipe_header",
]
.contains(&candidate_parent_kind),
Symbol::Variable(_) => {
if candidate_parent_kind == "assignment" {
return true;
}
if candidate_parent_kind != "value" {
return false;
}
let containing_parameter = candidate
.get_parent("parameter")
.or_else(|| candidate.get_parent("variadic_parameter"));
if let Some(containing_parameter) = containing_parameter {
let containing_parameter_name = self.document.get_node_text(
&containing_parameter.find("identifier").unwrap(),
);
let shadowed_by_preceding_parameter =
candidate.get_recipe(self.document).is_some_and(|recipe| {
recipe
.parameters
.iter()
.take_while(|parameter| {
parameter.name != containing_parameter_name
})
.any(|parameter| parameter.name == name)
});
return !shadowed_by_preceding_parameter;
}
if let Some(recipe) = candidate.get_recipe(self.document) {
return !recipe
.parameters
.iter()
.any(|parameter| parameter.name == name);
}
if let Some(function) = candidate.get_function(self.document) {
return !function
.parameters
.iter()
.any(|parameter| parameter.value == name);
}
true
}
}
})
.map(|found| lsp::Location {
uri: self.document.uri.clone(),
range: found.get_range(self.document),
})
.collect()
}
fn resolve_symbol(&self, identifier: &Node) -> Option<Symbol> {
let name = self.document.get_node_text(identifier);
let parent_kind = identifier.parent()?.kind();
let builtin_constant = |name: &str| {
BUILTINS
.iter()
.find(|builtin| matches!(
builtin,
Builtin::Constant { name: builtin_name, .. } if name == *builtin_name
))
.map(Symbol::Builtin)
};
match parent_kind {
"alias" | "dependency" | "dependency_expression" | "recipe_header" => {
self.document.find_recipe(&name).map(Symbol::Recipe)
}
"assignment" => self.document.find_variable(&name).map(Symbol::Variable),
"function_call" => {
self
.document
.find_function(&name)
.map(Symbol::Function)
.or_else(|| {
BUILTINS
.iter()
.find(|builtin| matches!(
builtin,
Builtin::Function { name: function_name, aliases, .. }
if name == *function_name || aliases.contains(&name.as_str())
))
.map(Symbol::Builtin)
})
}
"function_definition" => {
self.document.find_function(&name).map(Symbol::Function)
}
"function_parameters" => {
identifier.get_function(self.document).and_then(|function| {
function
.parameters
.iter()
.find(|parameter| parameter.value == name)
.cloned()
.map(Symbol::FunctionParameter)
})
}
"parameter" | "variadic_parameter" => {
identifier.get_recipe(self.document).and_then(|recipe| {
recipe
.parameters
.iter()
.find(|parameter| parameter.name == name)
.cloned()
.map(Symbol::Parameter)
})
}
"value" => {
let containing_parameter = identifier
.get_parent("parameter")
.or_else(|| identifier.get_parent("variadic_parameter"));
match containing_parameter {
None => identifier
.get_recipe(self.document)
.and_then(|recipe| {
recipe
.parameters
.iter()
.find(|parameter| parameter.name == name)
.cloned()
.map(Symbol::Parameter)
})
.or_else(|| {
identifier.get_function(self.document).and_then(|function| {
function
.parameters
.iter()
.find(|parameter| parameter.value == name)
.cloned()
.map(Symbol::FunctionParameter)
})
})
.or_else(|| {
self.document.find_variable(&name).map(Symbol::Variable)
})
.or_else(|| builtin_constant(&name)),
Some(containing_parameter) => {
let containing_parameter_name = self
.document
.get_node_text(&containing_parameter.find("identifier")?);
identifier
.get_recipe(self.document)
.and_then(|recipe| {
recipe
.parameters
.iter()
.take_while(|parameter| {
parameter.name != containing_parameter_name
})
.find(|parameter| parameter.name == name)
.cloned()
.map(Symbol::Parameter)
})
.or_else(|| {
self.document.find_variable(&name).map(Symbol::Variable)
})
.or_else(|| builtin_constant(&name))
}
}
}
_ => BUILTINS
.iter()
.find(|builtin| match builtin {
Builtin::Attribute {
name: attribute_name,
..
} => parent_kind == "attribute" && name == *attribute_name,
Builtin::Setting {
name: setting_name, ..
} => parent_kind == "setting" && name == *setting_name,
_ => false,
})
.map(Symbol::Builtin),
}
}
}
#[cfg(test)]
mod tests {
use {super::*, indoc::indoc, pretty_assertions::assert_eq};
#[test]
fn resolve_shadowed_parameter_default_definition() {
let document = Document::from(indoc! {
"
a := 'foo'
b a=a:
echo {{ a }}
"
});
let definition = Resolver::new(&document)
.resolve_identifier_definition(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("parameter > value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
definition,
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 1, 0),
}
);
}
#[test]
fn resolve_dependency_expression_definition() {
let document = Document::from(indoc! {
"
foo:
echo foo
bar: (foo)
echo bar
"
});
let definition = Resolver::new(&document)
.resolve_identifier_definition(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("dependency_expression > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
definition,
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 3, 0),
}
);
}
#[test]
fn resolve_recipe_definition() {
let document = Document::from(indoc! {
"
foo:
echo \"foo\"
bar: foo
echo \"bar\"
"
});
let definition = Resolver::new(&document)
.resolve_identifier_definition(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("dependency > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
definition,
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 3, 0),
}
);
}
#[test]
fn resolve_variable_definition() {
let document = Document::from(indoc! {
"
var := \"value\"
foo:
echo {{ var }}
"
});
let definition = Resolver::new(&document)
.resolve_identifier_definition(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
definition,
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 1, 0),
}
);
}
#[test]
fn resolve_parameter_definition() {
let document = Document::from(indoc! {
"
foo param=\"default\":
echo {{ param }}
"
});
let definition = Resolver::new(&document)
.resolve_identifier_definition(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
definition,
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 4, 0, 19),
}
);
}
#[test]
fn resolve_builtin_identifier_definition() {
let document = Document::from(indoc! {
"
foo:
echo {{ arch() }}
"
});
let definition = Resolver::new(&document)
.resolve_identifier_definition(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("function_call > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
definition,
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(1, 10, 1, 14),
}
);
}
#[test]
fn resolve_self_definition() {
let document = Document::from(indoc! {
"
foo:
echo \"foo\"
"
});
let definition = Resolver::new(&document)
.resolve_identifier_definition(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("recipe_header > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
definition,
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 2, 0),
}
);
}
#[test]
fn resolve_user_function_definition() {
let document = Document::from(indoc! {
"
foo(x) := x + \"!\"
bar:
echo {{ foo(\"baz\") }}
"
});
let definition = Resolver::new(&document)
.resolve_identifier_definition(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("function_call > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
definition,
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 3),
}
);
}
#[test]
fn resolve_user_function_parameter_definition() {
let document = Document::from(indoc! {
"
foo(x) := x
"
});
let definition = Resolver::new(&document)
.resolve_identifier_definition(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
definition,
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 4, 0, 5),
}
);
}
#[test]
fn resolve_dependency_expression_hover() {
let document = Document::from(indoc! {
"
foo:
echo foo
bar: (foo)
echo bar
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("dependency_expression > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "foo:\n echo foo".to_string(),
})
);
}
#[test]
fn resolve_recipe_hover() {
let document = Document::from(indoc! {
"
foo:
echo \"foo\"
bar: foo
echo \"bar\"
"
});
let resolver = Resolver::new(&document);
let root = document.tree.as_ref().unwrap().root_node();
let hover = resolver
.resolve_identifier_hover(
&root.find("recipe_header > identifier").unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "foo:\n echo \"foo\"".to_string(),
})
);
let hover = resolver
.resolve_identifier_hover(&root.find("dependency > identifier").unwrap())
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "foo:\n echo \"foo\"".to_string(),
})
);
}
#[test]
fn resolve_recipe_hover_in_alias() {
let document = Document::from(indoc! {
"
foo:
echo \"foo\"
alias f := foo
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("alias > identifier[1]")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "foo:\n echo \"foo\"".to_string(),
})
);
}
#[test]
fn resolve_parameter_hover() {
let document = Document::from(indoc! {
"
foo param=\"default\":
echo {{ param }}
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "param=\"default\"".to_string(),
})
);
}
#[test]
fn resolve_variadic_parameter_hover() {
let document = Document::from(indoc! {
"
foo +args:
echo {{ args }}
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "+args".to_string(),
})
);
}
#[test]
fn resolve_export_parameter_hover() {
let document = Document::from(indoc! {
"
foo $env_var:
echo {{ env_var }}
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "$env_var".to_string(),
})
);
}
#[test]
fn resolve_variable_hover() {
let document = Document::from(indoc! {
"
var := \"value\"
foo:
echo {{ var }}
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "var := \"value\"".to_string(),
})
);
}
#[test]
fn resolve_export_variable_hover() {
let document = Document::from(indoc! {
"
export VERSION := \"1.0.0\"
foo:
echo {{ VERSION }}
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "VERSION := \"1.0.0\"".to_string(),
})
);
}
#[test]
fn resolve_builtin_function_hover() {
let document = Document::from(indoc! {
"
foo:
echo {{ arch() }}
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("function_call > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: indoc! {
"
Instruction set architecture of the host machine.
Returns one of: `aarch64`, `arm`, `asmjs`, `hexagon`, `mips`,
`msp430`, `powerpc`, `powerpc64`, `s390x`, `sparc`, `wasm32`,
`x86`, `x86_64`, or `xcore`.
```just
system-info:
@echo This is an {{arch()}} machine.
```
"
}
.to_string(),
})
);
}
#[test]
fn resolve_builtin_constant_hover() {
let document = Document::from(indoc! {
"
foo:
echo {{ RED }}
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: indoc! {
"
ANSI escape sequence for red foreground text: `\\e[31m`.
Terminate styled output with `NORMAL` to reset.
"
}
.to_string(),
})
);
}
#[test]
fn resolve_builtin_attribute_hover() {
let document = Document::from(indoc! {
"
[no-cd]
foo:
echo \"foo\"
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("attribute > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: indoc! {
"
Don't change directory before executing the recipe.
Normally `just` runs recipes with the current directory set to
the directory containing the `justfile`. With `[no-cd]`, the
recipe runs with the current directory unchanged, so it can use
paths relative to the invocation directory or operate on the
user's current directory.
```just
[no-cd]
commit file:
git add {{file}}
git commit
```
"
}
.to_string(),
})
);
}
#[test]
fn resolve_builtin_setting_hover() {
let document = Document::from(indoc! {
"
set export
foo:
echo \"foo\"
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("setting > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: indoc! {
"
Export every top-level `just` variable as an environment
variable.
Equivalent to prefixing each assignment with `export`, so
recipes and backticks see the variables as `$NAME` rather than
needing `{{ name }}` interpolation.
```just
set export
a := \"hello\"
@foo b:
echo $a
echo $b
```
"
}
.to_string(),
})
);
}
#[test]
fn resolve_same_name_confusion() {
let document = Document::from(indoc! {
"
arch := \"custom_arch\"
foo:
echo {{ arch }}
echo {{ arch() }}
"
});
let resolver = Resolver::new(&document);
let root = document.tree.as_ref().unwrap().root_node();
let hover = resolver
.resolve_identifier_hover(&root.find("value > identifier").unwrap())
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "arch := \"custom_arch\"".to_string(),
})
);
let hover = resolver
.resolve_identifier_hover(
&root.find("function_call > identifier").unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: indoc! {
"
Instruction set architecture of the host machine.
Returns one of: `aarch64`, `arm`, `asmjs`, `hexagon`, `mips`,
`msp430`, `powerpc`, `powerpc64`, `s390x`, `sparc`, `wasm32`,
`x86`, `x86_64`, or `xcore`.
```just
system-info:
@echo This is an {{arch()}} machine.
```
"
}
.to_string(),
})
);
}
#[test]
fn resolve_parameter_over_variable() {
let document = Document::from(indoc! {
"
param := \"global value\"
foo param=\"local value\":
echo {{ param }}
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "param=\"local value\"".to_string(),
})
);
}
#[test]
fn resolve_hover_non_identifier() {
let document = Document::from(indoc! {
"
foo:
echo \"foo\"
"
});
let hover = Resolver::new(&document).resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("text")
.unwrap(),
);
assert_eq!(hover, None);
}
#[test]
fn resolve_hover_nonexistent_variable() {
let document = Document::from(indoc! {
"
foo:
echo {{ nonexistent }}
"
});
let hover = Resolver::new(&document).resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
);
assert_eq!(hover, None);
}
#[test]
fn resolve_user_function_hover() {
let document = Document::from(indoc! {
"
foo(x) := x + \"!\"
bar:
echo {{ foo(\"baz\") }}
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("function_call > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "foo(x) := x + \"!\"".to_string(),
})
);
}
#[test]
fn resolve_user_function_parameter_hover() {
let document = Document::from(indoc! {
"
foo(x) := x
"
});
let hover = Resolver::new(&document)
.resolve_identifier_hover(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
)
.unwrap();
assert_eq!(
hover.contents,
lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: "x".to_string(),
})
);
}
#[test]
fn resolve_recipe_references() {
let document = Document::from(indoc! {
"
foo:
echo \"foo\"
bar foo: foo
echo \"bar\"
alias baz := foo
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("recipe_header > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 3),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(3, 9, 3, 12),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(6, 13, 6, 16),
},
]
);
}
#[test]
fn resolve_recipe_parameter_references() {
let document = Document::from(indoc! {
"
foo := 'bar'
foo:
echo {{ foo }}
bar foo: foo
echo {{ foo }}
echo {{ foo }}
alias baz := foo
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("parameter > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(5, 4, 5, 7),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(6, 10, 6, 13),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(7, 10, 7, 13),
},
]
);
}
#[test]
fn resolve_value_references() {
let document = Document::from(indoc! {
"
foo := \"foo\"
foo foo:
echo {{ foo }}
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(2, 4, 2, 7),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(3, 10, 3, 13),
},
]
);
let document = Document::from(indoc! {
"
foo := \"foo\"
foo:
echo {{ foo / foo }}
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("value > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 3),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(3, 10, 3, 13),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(3, 16, 3, 19),
},
]
);
}
#[test]
fn resolve_variable_references() {
let document = Document::from(indoc! {
"
foo := 'bar'
foo:
echo {{ foo }}
bar foo: foo
echo {{ foo }}
echo {{ foo }}
quux:
echo {{ foo }}
echo {{ foo }}
alias baz := foo
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("assignment > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 3),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(3, 10, 3, 13),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(10, 10, 10, 13),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(11, 10, 11, 13),
},
]
);
}
#[test]
fn resolve_shadowed_parameter_default_references() {
let document = Document::from(indoc! {
"
a := 'foo'
b a=a:
echo {{ a }}
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("assignment > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 1),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(2, 4, 2, 5),
},
]
);
}
#[test]
fn resolve_variable_excludes_parameter_default_shadowed_by_preceding_parameter()
{
let document = Document::from(indoc! {
"
a := 'foo'
bar a b=a:
echo {{ b }}
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("assignment > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 1),
}]
);
}
#[test]
fn resolve_variable_references_in_assignment_value() {
let document = Document::from(indoc! {
"
foo := 'x'
bar := foo
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("assignment > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 3),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(1, 7, 1, 10),
},
]
);
}
#[test]
fn resolve_variable_references_in_user_function_body() {
let document = Document::from(indoc! {
"
base := 'x'
join(ext) := base + ext
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("assignment > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 4),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(2, 13, 2, 17),
},
]
);
}
#[test]
fn resolve_variable_excludes_user_function_parameter_shadow() {
let document = Document::from(indoc! {
"
base := 'global'
join(base) := base + '!'
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("assignment > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 4),
}]
);
}
#[test]
fn resolve_dependency_references() {
let document = Document::from(indoc! {
"
all: foo
foo:
echo foo
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("dependency > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 5, 0, 8),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(2, 0, 2, 3),
},
]
);
}
#[test]
fn resolve_dependency_argument_references() {
let document = Document::from(indoc! {
"
a := 'foo'
[group: 'test']
foo: (bar a)
bar a:
echo {{ a }}
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("dependency_expression > expression > value > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 1),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(3, 10, 3, 11),
},
]
);
}
#[test]
fn resolve_dependency_expression_references() {
let document = Document::from(indoc! {
"
foo:
echo foo
bar: (foo)
echo bar
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("dependency_expression > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 3),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(3, 6, 3, 9),
},
]
);
}
#[test]
fn resolve_user_function_references() {
let document = Document::from(indoc! {
"
foo(x) := x + \"!\"
bar:
echo {{ foo(\"a\") }}
echo {{ foo(\"b\") }}
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("function_definition > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 0, 0, 3),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(3, 10, 3, 13),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(4, 10, 4, 13),
},
]
);
}
#[test]
fn resolve_user_function_parameter_references() {
let document = Document::from(indoc! {
"
foo(x) := x + x
bar(x) := x
"
});
let references = Resolver::new(&document).resolve_identifier_references(
&document
.tree
.as_ref()
.unwrap()
.root_node()
.find("function_parameters > identifier")
.unwrap(),
);
assert_eq!(
references,
vec![
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 4, 0, 5),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 10, 0, 11),
},
lsp::Location {
uri: document.uri.clone(),
range: lsp::Range::at(0, 14, 0, 15),
},
]
);
}
}