use crate::ast::{Node, SNode, TypedParam};
use harn_lexer::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeclarationKind {
Function,
Generator,
Pipeline,
Tool,
Method,
InterfaceMethod,
}
impl DeclarationKind {
pub const fn as_str(self) -> &'static str {
match self {
DeclarationKind::Function => "function",
DeclarationKind::Generator => "generator",
DeclarationKind::Pipeline => "pipeline",
DeclarationKind::Tool => "tool",
DeclarationKind::Method => "method",
DeclarationKind::InterfaceMethod => "interface method",
}
}
}
#[derive(Debug, Clone)]
pub struct UnannotatedParam {
pub kind: DeclarationKind,
pub owner: String,
pub owner_span: Span,
pub index: usize,
pub name: String,
pub span: Span,
pub has_default: bool,
pub is_rest: bool,
}
pub fn requires_annotation(kind: DeclarationKind, index: usize, param: &TypedParam) -> bool {
if param.type_expr.is_some() {
return false;
}
!is_self_receiver(kind, index, param)
}
fn is_self_receiver(kind: DeclarationKind, index: usize, param: &TypedParam) -> bool {
matches!(
kind,
DeclarationKind::Method | DeclarationKind::InterfaceMethod
) && index == 0
&& param.name == "self"
}
pub fn walk_unannotated_params(program: &[SNode], visit: &mut impl FnMut(UnannotatedParam)) {
let mut method_spans: std::collections::HashSet<(usize, usize)> =
std::collections::HashSet::new();
crate::visit::walk_program(program, &mut |node| match &node.node {
Node::ImplBlock { methods, .. } => {
for method in methods {
method_spans.insert((method.span.start, method.span.end));
}
}
Node::InterfaceDecl { methods, .. } => {
for method in methods {
report(
DeclarationKind::InterfaceMethod,
&method.name,
method.span,
&method.params,
visit,
);
}
}
Node::FnDecl {
name,
params,
is_stream,
..
} => {
let kind = if method_spans.contains(&(node.span.start, node.span.end)) {
DeclarationKind::Method
} else if *is_stream {
DeclarationKind::Generator
} else {
DeclarationKind::Function
};
report(kind, name, node.span, params, visit);
}
Node::Pipeline { name, params, .. } => {
report(DeclarationKind::Pipeline, name, node.span, params, visit);
}
Node::ToolDecl { name, params, .. } => {
report(DeclarationKind::Tool, name, node.span, params, visit);
}
_ => {}
});
}
pub fn unannotated_params(program: &[SNode]) -> Vec<UnannotatedParam> {
let mut found = Vec::new();
walk_unannotated_params(program, &mut |param| found.push(param));
found
}
fn report(
kind: DeclarationKind,
owner: &str,
owner_span: Span,
params: &[TypedParam],
visit: &mut impl FnMut(UnannotatedParam),
) {
for (index, param) in params.iter().enumerate() {
if requires_annotation(kind, index, param) {
visit(UnannotatedParam {
kind,
owner: owner.to_string(),
owner_span,
index,
name: param.name.clone(),
span: param.span,
has_default: param.default_value.is_some(),
is_rest: param.rest,
});
}
}
}
pub fn annotation_insert_offset(source: &str, param: &UnannotatedParam) -> Option<usize> {
let region = source.get(param.span.start..param.span.end)?;
let mut from = 0usize;
while let Some(relative) = region.get(from..)?.find(¶m.name) {
let start = from + relative;
let end = start + param.name.len();
let before_ok = start == 0
|| !region
.get(..start)?
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '_');
let after_ok = !region
.get(end..)?
.chars()
.next()
.is_some_and(|c| c.is_alphanumeric() || c == '_');
if before_ok && after_ok {
return Some(param.span.start + end);
}
from = start + param.name.chars().next().map_or(1, char::len_utf8);
}
None
}
pub fn message(found: &UnannotatedParam) -> String {
format!(
"{} `{}` parameter `{}` has no type annotation",
found.kind.as_str(),
found.owner,
found.name
)
}
pub fn help(found: &UnannotatedParam) -> String {
format!(
"annotate the parameter, for example `{name}: string`, or write `{name}: unknown` and \
narrow it at the dynamic boundary. `harn fix --apply` infers the type from the body and the \
call sites.",
name = found.name
)
}