1use crate::ast::{Node, SNode, TypedParam};
15use harn_lexer::Span;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum DeclarationKind {
24 Function,
26 Generator,
28 Pipeline,
30 Tool,
32 Method,
34 InterfaceMethod,
36}
37
38impl DeclarationKind {
39 pub const fn as_str(self) -> &'static str {
41 match self {
42 DeclarationKind::Function => "function",
43 DeclarationKind::Generator => "generator",
44 DeclarationKind::Pipeline => "pipeline",
45 DeclarationKind::Tool => "tool",
46 DeclarationKind::Method => "method",
47 DeclarationKind::InterfaceMethod => "interface method",
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
57pub struct UnannotatedParam {
58 pub kind: DeclarationKind,
59 pub owner: String,
61 pub owner_span: Span,
64 pub index: usize,
66 pub name: String,
67 pub span: Span,
70 pub has_default: bool,
71 pub is_rest: bool,
72}
73
74pub fn requires_annotation(kind: DeclarationKind, index: usize, param: &TypedParam) -> bool {
81 if param.type_expr.is_some() {
82 return false;
83 }
84 !is_self_receiver(kind, index, param)
85}
86
87fn is_self_receiver(kind: DeclarationKind, index: usize, param: &TypedParam) -> bool {
88 matches!(
89 kind,
90 DeclarationKind::Method | DeclarationKind::InterfaceMethod
91 ) && index == 0
92 && param.name == "self"
93}
94
95pub fn walk_unannotated_params(program: &[SNode], visit: &mut impl FnMut(UnannotatedParam)) {
102 let mut method_spans: std::collections::HashSet<(usize, usize)> =
103 std::collections::HashSet::new();
104 crate::visit::walk_program(program, &mut |node| match &node.node {
105 Node::ImplBlock { methods, .. } => {
106 for method in methods {
107 method_spans.insert((method.span.start, method.span.end));
108 }
109 }
110 Node::InterfaceDecl { methods, .. } => {
111 for method in methods {
112 report(
113 DeclarationKind::InterfaceMethod,
114 &method.name,
115 method.span,
116 &method.params,
117 visit,
118 );
119 }
120 }
121 Node::FnDecl {
122 name,
123 params,
124 is_stream,
125 ..
126 } => {
127 let kind = if method_spans.contains(&(node.span.start, node.span.end)) {
128 DeclarationKind::Method
129 } else if *is_stream {
130 DeclarationKind::Generator
131 } else {
132 DeclarationKind::Function
133 };
134 report(kind, name, node.span, params, visit);
135 }
136 Node::Pipeline { name, params, .. } => {
137 report(DeclarationKind::Pipeline, name, node.span, params, visit);
138 }
139 Node::ToolDecl { name, params, .. } => {
140 report(DeclarationKind::Tool, name, node.span, params, visit);
141 }
142 _ => {}
143 });
144}
145
146pub fn unannotated_params(program: &[SNode]) -> Vec<UnannotatedParam> {
148 let mut found = Vec::new();
149 walk_unannotated_params(program, &mut |param| found.push(param));
150 found
151}
152
153fn report(
154 kind: DeclarationKind,
155 owner: &str,
156 owner_span: Span,
157 params: &[TypedParam],
158 visit: &mut impl FnMut(UnannotatedParam),
159) {
160 for (index, param) in params.iter().enumerate() {
161 if requires_annotation(kind, index, param) {
162 visit(UnannotatedParam {
163 kind,
164 owner: owner.to_string(),
165 owner_span,
166 index,
167 name: param.name.clone(),
168 span: param.span,
169 has_default: param.default_value.is_some(),
170 is_rest: param.rest,
171 });
172 }
173 }
174}
175
176pub fn annotation_insert_offset(source: &str, param: &UnannotatedParam) -> Option<usize> {
185 let region = source.get(param.span.start..param.span.end)?;
186 let mut from = 0usize;
187 while let Some(relative) = region.get(from..)?.find(¶m.name) {
188 let start = from + relative;
189 let end = start + param.name.len();
190 let before_ok = start == 0
191 || !region
192 .get(..start)?
193 .chars()
194 .next_back()
195 .is_some_and(|c| c.is_alphanumeric() || c == '_');
196 let after_ok = !region
197 .get(end..)?
198 .chars()
199 .next()
200 .is_some_and(|c| c.is_alphanumeric() || c == '_');
201 if before_ok && after_ok {
202 return Some(param.span.start + end);
203 }
204 from = start + param.name.chars().next().map_or(1, char::len_utf8);
205 }
206 None
207}
208
209pub fn message(found: &UnannotatedParam) -> String {
212 format!(
213 "{} `{}` parameter `{}` has no type annotation",
214 found.kind.as_str(),
215 found.owner,
216 found.name
217 )
218}
219
220pub fn help(found: &UnannotatedParam) -> String {
222 format!(
223 "annotate the parameter, for example `{name}: string`, or write `{name}: unknown` and \
224 narrow it at the dynamic boundary. `harn fix --apply` infers the type from the body and the \
225 call sites.",
226 name = found.name
227 )
228}