1use crate::ast::{Node, SNode, TypeExpr, 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
74#[derive(Debug, Clone)]
81pub struct DeclaredParam {
82 pub kind: DeclarationKind,
83 pub owner: String,
84 pub index: usize,
85 pub name: String,
86 pub type_expr: Option<TypeExpr>,
87 pub requires_annotation: bool,
89 owner_span: Span,
90 span: Span,
91 has_default: bool,
92 is_rest: bool,
93}
94
95pub fn requires_annotation(kind: DeclarationKind, index: usize, param: &TypedParam) -> bool {
102 if param.type_expr.is_some() {
103 return false;
104 }
105 !is_self_receiver(kind, index, param)
106}
107
108fn is_self_receiver(kind: DeclarationKind, index: usize, param: &TypedParam) -> bool {
109 matches!(
110 kind,
111 DeclarationKind::Method | DeclarationKind::InterfaceMethod
112 ) && index == 0
113 && param.name == "self"
114}
115
116pub fn walk_unannotated_params(program: &[SNode], visit: &mut impl FnMut(UnannotatedParam)) {
123 walk_declared_params(program, &mut |param| {
124 if !param.requires_annotation {
125 return;
126 }
127 visit(UnannotatedParam {
128 kind: param.kind,
129 owner: param.owner,
130 owner_span: param.owner_span,
131 index: param.index,
132 name: param.name,
133 span: param.span,
134 has_default: param.has_default,
135 is_rest: param.is_rest,
136 });
137 });
138}
139
140pub fn walk_declared_params(program: &[SNode], visit: &mut impl FnMut(DeclaredParam)) {
146 let mut method_spans: std::collections::HashSet<(usize, usize)> =
147 std::collections::HashSet::new();
148 crate::visit::walk_program(program, &mut |node| match &node.node {
149 Node::ImplBlock { methods, .. } => {
150 for method in methods {
151 method_spans.insert((method.span.start, method.span.end));
152 }
153 }
154 Node::InterfaceDecl { methods, .. } => {
155 for method in methods {
156 report_declared(
157 DeclarationKind::InterfaceMethod,
158 &method.name,
159 method.span,
160 &method.params,
161 visit,
162 );
163 }
164 }
165 Node::FnDecl {
166 name,
167 params,
168 is_stream,
169 ..
170 } => {
171 let kind = if method_spans.contains(&(node.span.start, node.span.end)) {
172 DeclarationKind::Method
173 } else if *is_stream {
174 DeclarationKind::Generator
175 } else {
176 DeclarationKind::Function
177 };
178 report_declared(kind, name, node.span, params, visit);
179 }
180 Node::Pipeline { name, params, .. } => {
181 report_declared(DeclarationKind::Pipeline, name, node.span, params, visit);
182 }
183 Node::ToolDecl { name, params, .. } => {
184 report_declared(DeclarationKind::Tool, name, node.span, params, visit);
185 }
186 _ => {}
187 });
188}
189
190pub fn declared_params(program: &[SNode]) -> Vec<DeclaredParam> {
192 let mut found = Vec::new();
193 walk_declared_params(program, &mut |param| found.push(param));
194 found
195}
196
197pub fn unannotated_params(program: &[SNode]) -> Vec<UnannotatedParam> {
199 let mut found = Vec::new();
200 walk_unannotated_params(program, &mut |param| found.push(param));
201 found
202}
203
204fn report_declared(
205 kind: DeclarationKind,
206 owner: &str,
207 owner_span: Span,
208 params: &[TypedParam],
209 visit: &mut impl FnMut(DeclaredParam),
210) {
211 for (index, param) in params.iter().enumerate() {
212 visit(DeclaredParam {
213 kind,
214 owner: owner.to_string(),
215 index,
216 name: param.name.clone(),
217 type_expr: param.type_expr.clone(),
218 requires_annotation: requires_annotation(kind, index, param),
219 owner_span,
220 span: param.span,
221 has_default: param.default_value.is_some(),
222 is_rest: param.rest,
223 });
224 }
225}
226
227pub fn annotation_insert_offset(source: &str, param: &UnannotatedParam) -> Option<usize> {
236 let region = source.get(param.span.start..param.span.end)?;
237 let mut from = 0usize;
238 while let Some(relative) = region.get(from..)?.find(¶m.name) {
239 let start = from + relative;
240 let end = start + param.name.len();
241 let before_ok = start == 0
242 || !region
243 .get(..start)?
244 .chars()
245 .next_back()
246 .is_some_and(|c| c.is_alphanumeric() || c == '_');
247 let after_ok = !region
248 .get(end..)?
249 .chars()
250 .next()
251 .is_some_and(|c| c.is_alphanumeric() || c == '_');
252 if before_ok && after_ok {
253 return Some(param.span.start + end);
254 }
255 from = start + param.name.chars().next().map_or(1, char::len_utf8);
256 }
257 None
258}
259
260pub fn message(found: &UnannotatedParam) -> String {
263 format!(
264 "{} `{}` parameter `{}` has no type annotation",
265 found.kind.as_str(),
266 found.owner,
267 found.name
268 )
269}
270
271pub fn help(found: &UnannotatedParam) -> String {
273 format!(
274 "annotate the parameter, for example `{name}: string`, or write `{name}: unknown` and \
275 narrow it at the dynamic boundary. `harn fix --apply` infers the type from the body and the \
276 call sites.",
277 name = found.name
278 )
279}