use serde_json::Value as JsonValue;
use crate::diagnostics::{
CustomRuleDefinition, CustomRuleId, DiagnosticLocation, DiagnosticRule, LintDiagnostic,
SourcePosition,
};
use super::super::WorkspaceLintSnapshot;
use super::super::index::*;
use super::WorkspaceHover;
use super::common::{
location_contains_position, predicate_op_project_field_value, source_range_size,
string_project_field_value,
};
pub(crate) fn hover(
snapshot: &WorkspaceLintSnapshot,
path: &str,
position: SourcePosition,
) -> Option<WorkspaceHover> {
let mut candidates = Vec::new();
push_diagnostic_hover_candidates(snapshot, path, position, &mut candidates);
push_manifest_hover_candidates(&snapshot.index, path, position, &mut candidates);
push_qualifier_hover_candidates(&snapshot.index, path, position, &mut candidates);
push_variable_hover_candidates(&snapshot.index, path, position, &mut candidates);
push_resource_hover_candidates(&snapshot.index, path, position, &mut candidates);
sort_hover_candidates(&mut candidates);
candidates
.into_iter()
.next()
.map(|candidate| candidate.hover)
.or_else(|| file_hover(&snapshot.index, path))
}
struct HoverCandidate {
priority: u8,
span_size: usize,
hover: WorkspaceHover,
}
fn push_diagnostic_hover_candidates(
snapshot: &WorkspaceLintSnapshot,
path: &str,
position: SourcePosition,
candidates: &mut Vec<HoverCandidate>,
) {
for diagnostic in &snapshot.lint.diagnostics {
let contents = diagnostic_hover_contents(&snapshot.index, diagnostic);
push_hover_candidate(candidates, path, position, &diagnostic.primary, 0, contents);
}
}
fn push_manifest_hover_candidates(
_index: &SemanticIndex,
path: &str,
position: SourcePosition,
candidates: &mut Vec<HoverCandidate>,
) {
let _ = (path, position, candidates);
}
fn push_qualifier_hover_candidates(
index: &SemanticIndex,
path: &str,
position: SourcePosition,
candidates: &mut Vec<HoverCandidate>,
) {
for qualifier in index.qualifiers.values() {
if qualifier.location.path != path {
continue;
}
if let Some(ProjectField::Present(description)) = &qualifier.description {
push_hover_candidate(
candidates,
path,
position,
&description.location,
2,
qualifier_hover_contents(qualifier),
);
}
if let PredicateCollection::Predicates(predicates) = &qualifier.predicates {
for predicate in predicates {
push_hover_candidate(
candidates,
path,
position,
&predicate.location,
3,
predicate_hover_contents(qualifier, predicate),
);
for location in [
Some(predicate.attribute.location()),
Some(predicate.op.location()),
predicate.value.as_ref().map(|value| value.location.clone()),
predicate.salt.as_ref().map(ProjectField::location),
predicate.range.as_ref().map(|range| range.location.clone()),
]
.into_iter()
.flatten()
{
push_hover_candidate(
candidates,
path,
position,
&location,
2,
predicate_hover_contents(qualifier, predicate),
);
}
}
}
}
}
fn push_variable_hover_candidates(
index: &SemanticIndex,
path: &str,
position: SourcePosition,
candidates: &mut Vec<HoverCandidate>,
) {
for variable in index.variables.values() {
if variable.location.path != path {
continue;
}
if let Some(ProjectField::Present(description)) = &variable.description {
push_hover_candidate(
candidates,
path,
position,
&description.location,
2,
variable_hover_contents(variable),
);
}
push_hover_candidate(
candidates,
path,
position,
&variable.type_source.location(),
2,
variable_type_hover_contents(variable),
);
push_hover_candidate(
candidates,
path,
position,
&variable.values.location,
4,
variable_values_hover_contents(variable),
);
for value in variable.values.inline_values.values() {
push_hover_candidate(
candidates,
path,
position,
&value.location,
2,
value_hover_contents(&variable.id, value),
);
}
if let ResolveNode::Resolve {
location,
default,
rules,
} = &variable.resolve
{
push_hover_candidate(
candidates,
path,
position,
&default.location(),
3,
variable_resolve_hover_contents(variable, default),
);
push_hover_candidate(
candidates,
path,
position,
location,
4,
variable_resolve_hover_contents(variable, default),
);
if let RuleCollection::Rules(rules) = rules {
for rule in rules {
push_hover_candidate(
candidates,
path,
position,
&rule.location,
3,
variable_rule_hover_contents(variable, rule),
);
for location in [rule.qualifier.location(), rule.value.location()] {
push_hover_candidate(
candidates,
path,
position,
&location,
2,
variable_rule_hover_contents(variable, rule),
);
}
}
}
}
}
}
fn push_resource_hover_candidates(
index: &SemanticIndex,
path: &str,
position: SourcePosition,
candidates: &mut Vec<HoverCandidate>,
) {
for resource in index.resources.values() {
if resource.location.path != path {
continue;
}
push_hover_candidate(
candidates,
path,
position,
&resource.location,
2,
resource_hover_contents(resource),
);
if let Some(ProjectField::Present(description)) = &resource.description {
push_hover_candidate(
candidates,
path,
position,
&description.location,
2,
resource_hover_contents(resource),
);
}
}
for objects in index.resource_objects.values() {
for object in objects.values() {
push_hover_candidate(
candidates,
path,
position,
&object.location,
2,
resource_object_hover_contents(object),
);
}
}
}
fn push_hover_candidate(
candidates: &mut Vec<HoverCandidate>,
path: &str,
position: SourcePosition,
location: &DiagnosticLocation,
priority: u8,
contents: String,
) {
if !location_contains_position(location, path, position) {
return;
}
candidates.push(HoverCandidate {
priority,
span_size: location.range.map(source_range_size).unwrap_or(usize::MAX),
hover: WorkspaceHover {
contents,
location: location.clone(),
},
});
}
fn sort_hover_candidates(candidates: &mut [HoverCandidate]) {
candidates.sort_by(|left, right| {
left.priority
.cmp(&right.priority)
.then_with(|| left.span_size.cmp(&right.span_size))
.then_with(|| left.hover.contents.cmp(&right.hover.contents))
});
}
fn file_hover(index: &SemanticIndex, path: &str) -> Option<WorkspaceHover> {
index
.variables
.values()
.find(|variable| variable.location.path == path)
.map(|variable| WorkspaceHover {
contents: variable_hover_contents(variable),
location: variable.location.clone(),
})
.or_else(|| {
index
.qualifiers
.values()
.find(|qualifier| qualifier.location.path == path)
.map(|qualifier| WorkspaceHover {
contents: qualifier_hover_contents(qualifier),
location: qualifier.location.clone(),
})
})
.or_else(|| {
index
.resources
.values()
.find(|resource| resource.location.path == path)
.map(|resource| WorkspaceHover {
contents: resource_hover_contents(resource),
location: resource.location.clone(),
})
})
.or_else(|| {
index.resource_objects.values().find_map(|objects| {
objects
.values()
.find(|object| object.location.path == path)
.map(|object| WorkspaceHover {
contents: resource_object_hover_contents(object),
location: object.location.clone(),
})
})
})
}
fn diagnostic_hover_contents(index: &SemanticIndex, diagnostic: &LintDiagnostic) -> String {
let (title, help) = diagnostic_rule_title_help(index, &diagnostic.rule);
format!(
"### {title}\n\n`{}`\n\n{}\n\n{}",
diagnostic.rule.as_string(),
diagnostic.message,
help
)
}
fn diagnostic_rule_title_help(index: &SemanticIndex, rule: &DiagnosticRule) -> (String, String) {
match rule {
DiagnosticRule::Rototo(rule) => {
let meta = rule.meta();
(meta.title.to_owned(), meta.help.to_owned())
}
DiagnosticRule::Custom(rule) => custom_rule_definition(index, rule)
.map(|definition| (definition.title, definition.help))
.unwrap_or_else(|| {
(
rule.as_str().to_owned(),
"Workspace custom lint.".to_owned(),
)
}),
}
}
fn custom_rule_definition(
index: &SemanticIndex,
rule: &CustomRuleId,
) -> Option<CustomRuleDefinition> {
index
.custom_lints
.rules
.get(rule)
.map(|rule| rule.definition.clone())
}
fn qualifier_hover_contents(qualifier: &QualifierNode) -> String {
let mut contents = format!("### Qualifier `{}`", qualifier.id);
if let Some(description) = project_field_string(&qualifier.description) {
contents.push_str("\n\n");
contents.push_str(description);
}
contents
}
fn predicate_hover_contents(qualifier: &QualifierNode, predicate: &PredicateNode) -> String {
let mut contents = format!(
"### Predicate {} for `{}`\n\n{}",
predicate.index + 1,
qualifier.id,
predicate_summary(predicate)
);
if let Some(value) = &predicate.value {
contents.push_str("\n\nValue shape: `");
contents.push_str(value.shape.as_str());
contents.push('`');
}
contents
}
fn predicate_summary(predicate: &PredicateNode) -> String {
match (
string_project_field_value(&predicate.attribute),
predicate_op_project_field_value(&predicate.op),
) {
(Some(attribute), Some(op)) => format!("`{attribute}` `{op}`"),
(Some(attribute), None) => format!("`{attribute}`"),
(None, Some(op)) => format!("operator `{op}`"),
(None, None) => "Incomplete predicate".to_owned(),
}
}
fn variable_hover_contents(variable: &VariableNode) -> String {
let mut contents = format!(
"### Variable `{}`\n\n{}",
variable.id,
type_source_summary(variable)
);
if let Some(description) = project_field_string(&variable.description) {
contents.push_str("\n\n");
contents.push_str(description);
}
let values = variable_value_keys(variable);
if !values.is_empty() {
contents.push_str("\n\nValues: ");
contents.push_str(&values.join(", "));
}
contents
}
fn variable_type_hover_contents(variable: &VariableNode) -> String {
format!(
"### Variable `{}`\n\n{}",
variable.id,
type_source_summary(variable)
)
}
fn variable_values_hover_contents(variable: &VariableNode) -> String {
let values = variable_value_keys(variable);
if values.is_empty() {
return format!("### Values for `{}`\n\nNo values declared.", variable.id);
}
format!("### Values for `{}`\n\n{}", variable.id, values.join(", "))
}
fn value_hover_contents(variable_id: &str, value: &ValueNode) -> String {
format!(
"### Value `{}`\n\nVariable: `{}`\n\nJSON shape: `{}`",
value.key,
variable_id,
json_shape_label(&value.value)
)
}
fn resource_hover_contents(resource: &ResourceNode) -> String {
let mut contents = format!("### Resource `{}`", resource.id);
if let Some(description) = project_field_string(&resource.description) {
contents.push_str("\n\n");
contents.push_str(description);
}
contents
}
fn resource_object_hover_contents(object: &ResourceObjectNode) -> String {
format!(
"### Resource object `{}`\n\nResource: `{}`\n\nJSON shape: `{}`",
object.key,
object.resource_id,
json_shape_label(&object.value)
)
}
fn variable_resolve_hover_contents(
variable: &VariableNode,
default: &ProjectField<String>,
) -> String {
match string_project_field_value(default) {
Some(value) => format!(
"### Resolve for `{}`\n\nDefault value: `{}`",
variable.id, value
),
None => format!("### Resolve for `{}`", variable.id),
}
}
fn variable_rule_hover_contents(variable: &VariableNode, rule: &VariableRuleNode) -> String {
format!(
"### Rule {} for `{}`\n\n{}",
rule.index + 1,
variable.id,
variable_rule_summary(rule)
)
}
fn variable_rule_summary(rule: &VariableRuleNode) -> String {
match (
string_project_field_value(&rule.qualifier),
string_project_field_value(&rule.value),
) {
(Some(qualifier), Some(value)) => {
format!("Qualifier `{qualifier}` selects value `{value}`.")
}
(Some(qualifier), None) => format!("Qualifier `{qualifier}`."),
(None, Some(value)) => format!("Selects value `{value}`."),
(None, None) => "Incomplete rule.".to_owned(),
}
}
fn type_source_summary(variable: &VariableNode) -> String {
match &variable.type_source {
TypeSourceNode::Primitive(type_name) => format!("Type: `{}`", type_name.value),
TypeSourceNode::Resource(resource) => format!("Resource type: `{}`", resource.value),
TypeSourceNode::Schema(schema) => format!("Schema: `{}`", schema.value),
TypeSourceNode::Missing { .. } => "Type: missing".to_owned(),
TypeSourceNode::Conflict { .. } => "Type: conflicting declarations".to_owned(),
TypeSourceNode::Invalid { .. } => "Type: invalid".to_owned(),
}
}
fn variable_value_keys(variable: &VariableNode) -> Vec<String> {
variable
.values
.inline_values
.keys()
.map(|value| format!("`{value}`"))
.collect()
}
fn project_field_string(field: &Option<ProjectField<String>>) -> Option<&str> {
let Some(ProjectField::Present(value)) = field else {
return None;
};
Some(&value.value)
}
fn json_shape_label(value: &JsonValue) -> &'static str {
match value {
JsonValue::Null => "null",
JsonValue::Bool(_) => "bool",
JsonValue::Number(number) if number.is_i64() || number.is_u64() => "int",
JsonValue::Number(_) => "number",
JsonValue::String(_) => "string",
JsonValue::Array(_) => "list",
JsonValue::Object(_) => "object",
}
}