Skip to content

Commit 2e155fa

Browse files
committed
Make diagnostics for extra paramters configurable
1 parent 16d0455 commit 2e155fa

5 files changed

Lines changed: 174 additions & 62 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- **Document Links.** `require`/`include` paths are now Ctrl+Clickable. Path resolution supports string literals, `__DIR__` concatenation, `dirname(__DIR__)`, `dirname(__FILE__)`, and nested `dirname` with levels.
2323
- **Syntax error diagnostic.** Parse errors from the Mago parser now appear as Error-severity diagnostics instantly as you type.
2424
- **Implementation error diagnostic.** Concrete classes that fail to implement all required methods from their interfaces or abstract parents are now flagged with an Error-severity diagnostic on the class name. The existing "Implement missing methods" quick-fix appears inline alongside the error. Cyclic hierarchies are handled gracefully.
25-
- **Argument count diagnostic.** Flags function and method calls that pass too few or too many arguments. Variadic parameters and argument unpacking are handled correctly.
25+
- **Argument count diagnostic.** Flags function and method calls that pass too few arguments. The "too many arguments" check is off by default (PHP silently ignores extra arguments) and can be enabled with `extra-arguments = true` in the `[diagnostics]` section of `.phpantom.toml`. Variadic parameters and argument unpacking are handled correctly.
2626
- **Change visibility.** Code action on any method, property, constant, or promoted constructor parameter offers to change its visibility (`public`, `protected`, `private`).
2727
- **Update docblock.** Code action on a function or method whose existing docblock is out of sync with its signature. Adds missing `@param` tags, removes stale ones, reorders to match the signature, fixes contradicted types, and removes redundant `@return void`. Refinement types and unrelated tags are preserved. Handles `@param $name` (no type) correctly.
2828
- **PHPDoc block generation.** Typing `/**` above any declaration generates a docblock skeleton. Tags are only emitted when the native type hint needs enrichment. Properties and constants always get `@var`. Class-likes with templated parents or interfaces get `@extends`/`@implements` tags. Uncaught exceptions get `@throws` with auto-import. Works both via completion and on-type formatting.

docs/todo.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ within the same impact tier.
2323

2424
| # | Item | Impact | Effort |
2525
| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ |
26-
| B13 | [Argument count: too many arguments off by default](todo/bugs.md#b13-argument-count-diagnostic-flags-too-many-arguments-by-default) | High | Low |
2726
| E0 | [Switch embedded stubs to master + LanguageLevelTypeAware](todo/external-stubs.md#e0-switch-embedded-stubs-to-master-and-apply-languageleveltypeaware-patches) | High | Low |
2827

2928
## Sprint 4 — Refactoring toolkit

docs/todo/bugs.md

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -55,49 +55,3 @@ In practice this is unlikely because pruning only ever removes entries
5555
**Fix:** Replace the length comparison with a content comparison, or
5656
unconditionally write the pruned set back into the cache (the extra
5757
write is negligible).
58-
59-
---
60-
61-
## B13. Argument count diagnostic flags too many arguments by default
62-
63-
**Impact: High · Effort: Low**
64-
65-
The "too many arguments" half of the argument count diagnostic produces
66-
frequent false positives on real-world PHP codebases. PHP itself does
67-
not error on extra arguments to user-defined functions — the extras are
68-
silently ignored. Many libraries (Laravel in particular) exploit this to
69-
implement flexible APIs: a function declared as `foo(array $items)` is
70-
commonly called as `foo('a', 'b', 'c')` because the caller is actually
71-
passing variadic strings and relying on PHP's permissive argument
72-
handling, or the signature is simply underdocumented.
73-
74-
The "too few arguments" half is genuinely useful — passing too few
75-
arguments always causes a `TypeError` at runtime — and should remain
76-
on by default.
77-
78-
### Desired behaviour
79-
80-
- **Too few arguments:** always reported, `Error` severity (current
81-
behaviour, keep as-is).
82-
- **Too many arguments:** off by default, opt-in via
83-
`[diagnostics] extra-arguments = true` in `.phpantom.toml`.
84-
85-
### Implementation
86-
87-
- Add an `extra_arguments` field to `DiagnosticsConfig` in
88-
`src/config.rs`, defaulting to `false` (same pattern as
89-
`unresolved_member_access`).
90-
- In `src/diagnostics/argument_count.rs`, gate the "too many arguments"
91-
block on `self.config().diagnostics.extra_arguments_enabled()`.
92-
- Add `extra-arguments = true` (commented out) to
93-
`DEFAULT_CONFIG_CONTENT` in `src/config.rs` alongside the existing
94-
`unresolved-member-access` entry.
95-
- Update the test helper `collect()` in `argument_count.rs` tests to
96-
verify that extra-argument diagnostics are suppressed by default and
97-
appear when the config flag is set.
98-
99-
### Files to change
100-
101-
- `src/config.rs` — new field + accessor + default config comment.
102-
- `src/diagnostics/argument_count.rs` — gate the too-many block.
103-
- `tests/` — update or add tests asserting the default-off behaviour.

src/config.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ pub struct DiagnosticsConfig {
5151
/// diagnostics on codebases without comprehensive type annotations.
5252
#[serde(rename = "unresolved-member-access")]
5353
pub unresolved_member_access: Option<bool>,
54+
55+
/// Report calls that pass more arguments than the function accepts.
56+
///
57+
/// Off by default. PHP does not error on extra arguments to
58+
/// user-defined functions (the extras are silently ignored), and
59+
/// many libraries exploit this for flexible APIs. Enable this if
60+
/// you want stricter checking.
61+
#[serde(rename = "extra-arguments")]
62+
pub extra_arguments: Option<bool>,
5463
}
5564

5665
impl DiagnosticsConfig {
@@ -60,6 +69,13 @@ impl DiagnosticsConfig {
6069
pub fn unresolved_member_access_enabled(&self) -> bool {
6170
self.unresolved_member_access.unwrap_or(false)
6271
}
72+
73+
/// Whether the extra-arguments diagnostic is enabled.
74+
///
75+
/// Defaults to `false` (off) when not explicitly set.
76+
pub fn extra_arguments_enabled(&self) -> bool {
77+
self.extra_arguments.unwrap_or(false)
78+
}
6379
}
6480

6581
/// `[formatting]` section — controls the formatting strategy.
@@ -253,6 +269,9 @@ pub const DEFAULT_CONFIG_CONTENT: &str = r#"# PHPantom project configuration
253269
# Report member access on subjects whose type could not be resolved.
254270
# Useful for discovering gaps in type coverage. Off by default.
255271
# unresolved-member-access = true
272+
# Report calls that pass more arguments than the function accepts.
273+
# PHP silently ignores extra arguments, so this is off by default.
274+
# extra-arguments = true
256275
257276
[indexing]
258277
# How PHPantom discovers classes across the workspace.
@@ -406,6 +425,7 @@ mod tests {
406425
let config: Config = toml::from_str(DEFAULT_CONFIG_CONTENT).unwrap();
407426
assert!(config.php.version.is_none());
408427
assert!(!config.diagnostics.unresolved_member_access_enabled());
428+
assert!(!config.diagnostics.extra_arguments_enabled());
409429
assert_eq!(config.indexing.strategy, IndexingStrategy::Composer);
410430
assert!(config.formatting.php_cs_fixer.is_none());
411431
assert!(config.formatting.phpcbf.is_none());
@@ -423,6 +443,7 @@ mod tests {
423443
let config = load_config(dir.path()).unwrap();
424444
assert!(config.php.version.is_none());
425445
assert!(!config.diagnostics.unresolved_member_access_enabled());
446+
assert!(!config.diagnostics.extra_arguments_enabled());
426447
assert_eq!(config.indexing.strategy, IndexingStrategy::Composer);
427448
assert!(config.formatting.php_cs_fixer.is_none());
428449
assert!(config.formatting.phpcbf.is_none());
@@ -437,6 +458,7 @@ mod tests {
437458
let config = load_config(dir.path()).unwrap();
438459
assert!(config.php.version.is_none());
439460
assert!(!config.diagnostics.unresolved_member_access_enabled());
461+
assert!(!config.diagnostics.extra_arguments_enabled());
440462
assert_eq!(config.indexing.strategy, IndexingStrategy::Composer);
441463
assert!(config.formatting.php_cs_fixer.is_none());
442464
assert!(config.formatting.phpcbf.is_none());
@@ -470,6 +492,24 @@ mod tests {
470492
assert!(!config.diagnostics.unresolved_member_access_enabled());
471493
}
472494

495+
#[test]
496+
fn extra_arguments_defaults_to_false() {
497+
let dir = tempfile::tempdir().unwrap();
498+
let path = dir.path().join(CONFIG_FILE_NAME);
499+
std::fs::write(&path, "[diagnostics]\n").unwrap();
500+
let config = load_config(dir.path()).unwrap();
501+
assert!(!config.diagnostics.extra_arguments_enabled());
502+
}
503+
504+
#[test]
505+
fn parses_extra_arguments() {
506+
let dir = tempfile::tempdir().unwrap();
507+
let path = dir.path().join(CONFIG_FILE_NAME);
508+
std::fs::write(&path, "[diagnostics]\nextra-arguments = true\n").unwrap();
509+
let config = load_config(dir.path()).unwrap();
510+
assert!(config.diagnostics.extra_arguments_enabled());
511+
}
512+
473513
#[test]
474514
fn invalid_toml_returns_parse_error() {
475515
let dir = tempfile::tempdir().unwrap();
@@ -567,6 +607,7 @@ version = "8.2"
567607
568608
[diagnostics]
569609
unresolved-member-access = true
610+
extra-arguments = true
570611
571612
[indexing]
572613
strategy = "self"
@@ -586,6 +627,7 @@ timeout = 30000
586627
let config = load_config(dir.path()).unwrap();
587628
assert_eq!(config.php.version.as_deref(), Some("8.2"));
588629
assert!(config.diagnostics.unresolved_member_access_enabled());
630+
assert!(config.diagnostics.extra_arguments_enabled());
589631
assert_eq!(config.indexing.strategy, IndexingStrategy::SelfScan);
590632
assert_eq!(config.formatting.php_cs_fixer.as_deref(), Some(""));
591633
assert_eq!(

src/diagnostics/argument_count.rs

Lines changed: 131 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ impl Backend {
128128
}
129129

130130
// ── Too many arguments ──────────────────────────────────
131+
if !self.config().diagnostics.extra_arguments_enabled() {
132+
continue;
133+
}
134+
131135
if let Some(max) = max_count
132136
&& actual_args > max
133137
{
@@ -174,8 +178,16 @@ mod tests {
174178
use super::*;
175179
use std::collections::HashMap;
176180

181+
/// Enable the `extra-arguments` diagnostic on the given backend.
182+
fn enable_extra_args(backend: &Backend) {
183+
let mut cfg = backend.config.lock().clone();
184+
cfg.diagnostics.extra_arguments = Some(true);
185+
*backend.config.lock() = cfg;
186+
}
187+
177188
/// Helper: create a test backend with minimal function stubs and
178-
/// collect argument-count diagnostics.
189+
/// collect argument-count diagnostics. Extra-arguments checking
190+
/// is **off** (the default).
179191
fn collect(php: &str) -> Vec<Diagnostic> {
180192
let backend = Backend::new_test();
181193
let uri = "file:///test.php";
@@ -185,10 +197,21 @@ mod tests {
185197
out
186198
}
187199

188-
/// Helper that includes minimal stub functions so that built-in
189-
/// functions like `strlen` are resolvable.
190-
fn collect_with_stubs(php: &str) -> Vec<Diagnostic> {
191-
let stub_fn_index: HashMap<&'static str, &'static str> = HashMap::from([
200+
/// Like [`collect`] but with the `extra-arguments` diagnostic
201+
/// enabled so that "too many arguments" errors are reported.
202+
fn collect_extra(php: &str) -> Vec<Diagnostic> {
203+
let backend = Backend::new_test();
204+
enable_extra_args(&backend);
205+
let uri = "file:///test.php";
206+
backend.update_ast(uri, php);
207+
let mut out = Vec::new();
208+
backend.collect_argument_count_diagnostics(uri, php, &mut out);
209+
out
210+
}
211+
212+
/// Minimal stub function index shared by stub-aware helpers.
213+
fn stub_fn_index() -> HashMap<&'static str, &'static str> {
214+
HashMap::from([
192215
("strlen", "<?php\nfunction strlen(string $string): int {}\n"),
193216
(
194217
"array_map",
@@ -214,9 +237,28 @@ mod tests {
214237
"substr",
215238
"<?php\nfunction substr(string $string, int $offset, ?int $length = null): string {}\n",
216239
),
217-
]);
240+
])
241+
}
242+
243+
/// Helper that includes minimal stub functions so that built-in
244+
/// functions like `strlen` are resolvable. Extra-arguments
245+
/// checking is **off** (the default).
246+
fn collect_with_stubs(php: &str) -> Vec<Diagnostic> {
247+
let backend =
248+
Backend::new_test_with_all_stubs(HashMap::new(), stub_fn_index(), HashMap::new());
249+
let uri = "file:///test.php";
250+
backend.update_ast(uri, php);
251+
let mut out = Vec::new();
252+
backend.collect_argument_count_diagnostics(uri, php, &mut out);
253+
out
254+
}
255+
256+
/// Like [`collect_with_stubs`] but with the `extra-arguments`
257+
/// diagnostic enabled.
258+
fn collect_with_stubs_extra(php: &str) -> Vec<Diagnostic> {
218259
let backend =
219-
Backend::new_test_with_all_stubs(HashMap::new(), stub_fn_index, HashMap::new());
260+
Backend::new_test_with_all_stubs(HashMap::new(), stub_fn_index(), HashMap::new());
261+
enable_extra_args(&backend);
220262
let uri = "file:///test.php";
221263
backend.update_ast(uri, php);
222264
let mut out = Vec::new();
@@ -289,16 +331,67 @@ function test(): void {
289331
);
290332
}
291333

292-
// ── Too many arguments ──────────────────────────────────────────
334+
// ── Too many arguments (default off) ────────────────────────────
293335

294336
#[test]
295-
fn flags_too_many_args_to_function() {
337+
fn too_many_args_suppressed_by_default() {
296338
let php = r#"<?php
297339
function test(): void {
298340
strlen("hello", "extra");
299341
}
300342
"#;
301343
let diags = collect_with_stubs(php);
344+
assert!(
345+
diags.is_empty(),
346+
"Extra-arguments diagnostic should be off by default, got: {diags:?}",
347+
);
348+
}
349+
350+
#[test]
351+
fn too_many_args_to_user_function_suppressed_by_default() {
352+
let php = r#"<?php
353+
function myHelper(string $a): void {}
354+
function test(): void {
355+
myHelper("x", "y");
356+
}
357+
"#;
358+
let diags = collect(php);
359+
assert!(
360+
diags.is_empty(),
361+
"Extra-arguments diagnostic should be off by default, got: {diags:?}",
362+
);
363+
}
364+
365+
#[test]
366+
fn too_many_args_to_method_suppressed_by_default() {
367+
let php = r#"<?php
368+
class Greeter {
369+
public function greet(string $name): string {
370+
return "Hello, " . $name;
371+
}
372+
}
373+
function test(): void {
374+
$g = new Greeter();
375+
$g->greet("world", "extra", "more");
376+
}
377+
"#;
378+
let diags = collect(php);
379+
assert!(
380+
diags.is_empty(),
381+
"Extra-arguments diagnostic should be off by default, got: {diags:?}",
382+
);
383+
}
384+
385+
// ── Too many arguments (opt-in) ─────────────────────────────────
386+
387+
#[test]
388+
fn flags_too_many_args_to_function() {
389+
let php = r#"<?php
390+
function test(): void {
391+
strlen("hello", "extra");
392+
}
393+
"#;
394+
let diags = collect_with_stubs_extra(php);
302395
assert_eq!(diags.len(), 1, "got: {diags:?}");
303396
assert!(
304397
diags[0].message.contains("got 2"),
@@ -320,7 +413,7 @@ function test(): void {
320413
$g->greet("world", "extra", "more");
321414
}
322415
"#;
323-
let diags = collect(php);
416+
let diags = collect_extra(php);
324417
assert!(
325418
diags.iter().any(|d| d.message.contains("got 3")),
326419
"Expected too-many-args diagnostic, got: {diags:?}",
@@ -462,7 +555,7 @@ function test(): void {
462555
myHelper("x", "y");
463556
}
464557
"#;
465-
let diags = collect(php);
558+
let diags = collect_extra(php);
466559
assert!(
467560
diags
468561
.iter()
@@ -534,7 +627,7 @@ function test(): void {
534627
new User("Alice", "extra");
535628
}
536629
"#;
537-
let diags = collect(php);
630+
let diags = collect_extra(php);
538631
assert!(
539632
diags.iter().any(|d| d.message.contains("got 2")),
540633
"Expected too-many-args diagnostic for constructor, got: {diags:?}",
@@ -581,7 +674,7 @@ function test(): void {
581674
helper("x", "y", "z");
582675
}
583676
"#;
584-
let diags = collect(php);
677+
let diags = collect_extra(php);
585678
assert!(
586679
diags.iter().any(|d| d.message.contains("at most 2")),
587680
"Expected 'at most' wording, got: {diags:?}",
@@ -600,10 +693,34 @@ function test(): void {
600693
two(1, 2, 3);
601694
}
602695
"#;
603-
let diags = collect(php);
696+
let diags = collect_extra(php);
604697
assert_eq!(diags.len(), 2, "Expected 2 diagnostics, got: {diags:?}",);
605698
}
606699

700+
#[test]
701+
fn too_few_still_reported_when_extra_args_disabled() {
702+
// "Too few" must always fire regardless of the extra-arguments flag.
703+
let php = r#"<?php
704+
function one(int $a): void {}
705+
function two(int $a, int $b): void {}
706+
function test(): void {
707+
one();
708+
two(1, 2, 3);
709+
}
710+
"#;
711+
let diags = collect(php);
712+
assert_eq!(
713+
diags.len(),
714+
1,
715+
"Only the too-few diagnostic should fire by default, got: {diags:?}",
716+
);
717+
assert!(
718+
diags[0].message.contains("got 0"),
719+
"message: {}",
720+
diags[0].message,
721+
);
722+
}
723+
607724
// ── Scope methods (Laravel) ─────────────────────────────────────
608725

609726
#[test]

0 commit comments

Comments
 (0)