Skip to content

Commit 896baa7

Browse files
committed
Check call argument names against signatures
1 parent 3aa40f4 commit 896baa7

4 files changed

Lines changed: 106 additions & 3 deletions

File tree

src/checks/calls.rs

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::{HashMap, HashSet};
22

33
use crate::analyzer::Analyzer;
4-
use crate::diagnostic::{Diagnostic, code};
4+
use crate::diagnostic::{Diagnostic, Span, code};
55
use crate::syntax::ast::{Block, CallArg, Callee, DataEffect, Expr, Item, LetKind, Stmt};
66

77
pub(crate) fn check(analyzer: &mut Analyzer<'_>) {
@@ -53,8 +53,10 @@ fn check_block(analyzer: &mut Analyzer<'_>, block: &Block, locals: &HashSet<Stri
5353

5454
fn check_expr(analyzer: &mut Analyzer<'_>, expr: &Expr, locals: &HashSet<String>) {
5555
match expr {
56-
Expr::Call { callee, args, .. } => {
57-
check_call_args(analyzer, callee, args, locals);
56+
Expr::Call {
57+
callee, args, span, ..
58+
} => {
59+
check_call_args(analyzer, callee, args, span, locals);
5860
for arg in args {
5961
check_expr(analyzer, &arg.value, locals);
6062
}
@@ -72,6 +74,7 @@ fn check_call_args(
7274
analyzer: &mut Analyzer<'_>,
7375
callee: &Callee,
7476
args: &[CallArg],
77+
call_span: &Span,
7578
locals: &HashSet<String>,
7679
) {
7780
let call_name = callee_name(callee);
@@ -101,6 +104,7 @@ fn check_call_args(
101104
let Some(signature) = analyzer.resolve_callee(callee) else {
102105
return;
103106
};
107+
let signature_params = signature.params.clone();
104108
let param_effects: HashMap<String, &'static str> = signature
105109
.params
106110
.iter()
@@ -111,6 +115,63 @@ fn check_call_args(
111115
})
112116
.collect();
113117
let retained_params = signature.retained_params.clone();
118+
let param_names: HashSet<String> = signature_params
119+
.iter()
120+
.map(|param| param.name.clone())
121+
.collect();
122+
123+
for arg in args {
124+
let Some(name) = &arg.name else {
125+
continue;
126+
};
127+
if !param_names.contains(name) {
128+
analyzer.diagnostics.push(
129+
Diagnostic::error(
130+
code::UNKNOWN_ARGUMENT,
131+
format!("call to `{call_name}` has no argument named `{name}`."),
132+
arg.span.clone(),
133+
"unknown argument",
134+
)
135+
.with_cause(format!(
136+
"`{call_name}` does not declare a parameter named `{name}`."
137+
))
138+
.with_fix(
139+
"rename_argument",
140+
format!("Use one of: {}.", join_param_names(&signature_params)),
141+
"manual",
142+
),
143+
);
144+
}
145+
}
146+
147+
if args.iter().all(|arg| arg.name.is_some()) {
148+
let provided_names: HashSet<&str> =
149+
args.iter().filter_map(|arg| arg.name.as_deref()).collect();
150+
for param in &signature_params {
151+
if !provided_names.contains(param.name.as_str()) {
152+
analyzer.diagnostics.push(
153+
Diagnostic::error(
154+
code::MISSING_ARGUMENT,
155+
format!(
156+
"call to `{call_name}` is missing required argument `{}`.",
157+
param.name
158+
),
159+
call_span.clone(),
160+
"missing argument",
161+
)
162+
.with_cause(format!(
163+
"`{call_name}` requires a named argument `{}`.",
164+
param.name
165+
))
166+
.with_fix(
167+
"add_argument",
168+
format!("Add `{}: ...` to the call.", param.name),
169+
"manual",
170+
),
171+
);
172+
}
173+
}
174+
}
114175

115176
for arg in args {
116177
let Some(name) = &arg.name else {
@@ -178,6 +239,14 @@ fn is_enum_variant_call(name: &str) -> bool {
178239
matches!(name, "Ok" | "Err" | "Some" | "None" | "Result" | "Option")
179240
}
180241

242+
fn join_param_names(params: &[crate::hir::ParamSig]) -> String {
243+
params
244+
.iter()
245+
.map(|param| param.name.as_str())
246+
.collect::<Vec<_>>()
247+
.join(", ")
248+
}
249+
181250
fn callee_name(callee: &Callee) -> String {
182251
match callee {
183252
Callee::Name(name) => name.clone(),

src/diagnostic.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ pub mod code {
88
pub const FILE_MODE_VIOLATION: &str = "RS0101";
99
pub const UNNAMED_ARGUMENT: &str = "RS0201";
1010
pub const MISSING_DATA_EFFECT: &str = "RS0202";
11+
pub const UNKNOWN_ARGUMENT: &str = "RS0203";
12+
pub const MISSING_ARGUMENT: &str = "RS0204";
1113
pub const MANAGED_TO_LOCAL: &str = "RS0301";
1214
pub const USE_AFTER_MANAGE: &str = "RS0401";
1315
pub const LOCAL_VALUE_RETAINED: &str = "RS0501";
@@ -219,6 +221,16 @@ static DIAGNOSTIC_EXPLANATIONS: &[DiagnosticExplanation] = &[
219221
title: "missing call-site data effect",
220222
explanation: "Arguments for non-Copy parameters must use an explicit `read`, `mut`, or `take` effect matching the callee signature.",
221223
},
224+
DiagnosticExplanation {
225+
code: code::UNKNOWN_ARGUMENT,
226+
title: "unknown named argument",
227+
explanation: "Named call arguments must match the resolved callee signature. This catches misspellings and stale call sites after API changes.",
228+
},
229+
DiagnosticExplanation {
230+
code: code::MISSING_ARGUMENT,
231+
title: "missing required argument",
232+
explanation: "Every parameter in the resolved callee signature must be supplied by name at the call site.",
233+
},
222234
DiagnosticExplanation {
223235
code: code::MANAGED_TO_LOCAL,
224236
title: "managed-to-local conversion",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// expect: RS0204
2+
mode: uses-local
3+
4+
struct Image {
5+
pixels: Buffer
6+
}
7+
8+
fn bad_resize_missing_width(path: read Path) -> Unit {
9+
local image = Image.load(path: read path)
10+
Image.resize(image: mut image, height: 600)
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// expect: RS0203
2+
mode: uses-local
3+
4+
struct Image {
5+
pixels: Buffer
6+
}
7+
8+
fn bad_resize_argument_name(path: read Path) -> Unit {
9+
local image = Image.load(path: read path)
10+
Image.resize(image: mut image, widt: 800, height: 600)
11+
}

0 commit comments

Comments
 (0)