Skip to content

Commit cba3e19

Browse files
committed
Zend: suggest similar names for undefined function/method/class errors
When a function, method, class, interface or trait cannot be found, append a "(did you mean X?)" hint naming the closest existing match by Levenshtein distance: Call to undefined function strlenn() (did you mean strlen?) Call to undefined method Calculator::addd() (did you mean add?) Class "ArrayObjekt" not found (did you mean ArrayObject?) Names shorter than 3 characters are not matched. The allowed distance is 1, or 2 for names of 8 or more characters. Matching is case-insensitive and the suggestion uses the declared casing. Function suggestions cover direct and dynamic calls, constant expressions and the opcache JIT. For unqualified calls in a namespace, names in the current namespace are tried first, falling back to global names, mirroring function lookup.
1 parent 070ce2f commit cba3e19

23 files changed

Lines changed: 367 additions & 97 deletions

‎UPGRADING‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ PHP 8.7 UPGRADE NOTES
113113
13. Other Changes
114114
========================================
115115

116+
- Core:
117+
. Errors for undefined functions, undefined methods and classes, interfaces
118+
or traits that were not found now suggest a similarly named one, e.g.
119+
'Call to undefined function strlenn() (did you mean strlen?)'. Code that
120+
matches on the exact error message may need to be updated.
121+
116122
========================================
117123
14. Performance Improvements
118124
========================================

‎Zend/tests/exceptions/bug31102.phpt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Caught: Test1::__construct
4343
Caught: {closure:%s:%d}
4444
{closure:%s:%d}(Test3,3)
4545

46-
Fatal error: Uncaught Error: Class "Test3" not found in %s:%d
46+
Fatal error: Uncaught Error: Class "Test3" not found (did you mean "Test1"?) in %s:%d
4747
Stack trace:
4848
#0 %s(%d): eval()
4949
#1 {main}

‎Zend/tests/is_callable/bug69315.phpt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ try {
4343
bool(false)
4444
bool(false)
4545
Error: Call to undefined function strlen()
46-
Error: Call to undefined function defined()
46+
Error: Call to undefined function defined() (did you mean define?)
4747
Error: Call to undefined function constant()
4848
Error: Call to undefined function call_user_func()
4949
Error: Call to undefined function is_string()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
Levenshtein suggestion for undefined class, interface and trait lookups
3+
--FILE--
4+
<?php
5+
// One edit away — should suggest
6+
try { new ArrayObjekt(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
7+
try { new StdClas(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
8+
9+
// One edit away for a longer name — should suggest
10+
try { new ArrayIteratr(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
11+
12+
// Two edits away for a name >= 8 chars — adaptive threshold should suggest
13+
try { new ArryObjct(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
14+
15+
// Completely wrong name — no suggestion
16+
try { new Unicorn(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
17+
18+
// Interface
19+
try {
20+
$c = new class() implements Countble {};
21+
} catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
22+
23+
// Trait
24+
try {
25+
eval('class T { use NonExistntTrait; }');
26+
} catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
27+
?>
28+
--EXPECTF--
29+
Error: Class "ArrayObjekt" not found (did you mean "ArrayObject"?)
30+
Error: Class "StdClas" not found (did you mean "stdClass"?)
31+
Error: Class "ArrayIteratr" not found (did you mean "ArrayIterator"?)
32+
Error: Class "ArryObjct" not found (did you mean "ArrayObject"?)
33+
Error: Class "Unicorn" not found
34+
Error: Interface "Countble" not found (did you mean "Countable"?)
35+
Error: Trait "NonExistntTrait" not found
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
Levenshtein suggestion for undefined function in FCC context
3+
--FILE--
4+
<?php
5+
// Test 1: In default argument position
6+
7+
function test(Closure $name = array_pussh(...)) {
8+
var_dump($name);
9+
}
10+
11+
try {
12+
test();
13+
} catch (Error $e) {
14+
echo $e::class, ': ', $e->getMessage(), "\n";
15+
}
16+
17+
// Test 2: In constant initializer (via eval so the fatal can be caught for combined output)
18+
19+
try {
20+
eval("const Closure2 = strlenn(...);");
21+
} catch (Error $e) {
22+
echo $e::class, ': ', $e->getMessage(), "\n";
23+
}
24+
25+
?>
26+
--EXPECT--
27+
Error: Call to undefined function array_pussh() (did you mean array_push?)
28+
Error: Call to undefined function strlenn() (did you mean strlen?)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
--TEST--
2+
Levenshtein suggestion for undefined function calls
3+
--FILE--
4+
<?php
5+
// One edit away — should suggest
6+
try { strlenn("x"); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
7+
8+
// One edit away for a longer name — should suggest
9+
try { array_pussh([], 1); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
10+
11+
// Two edits away for a name >= 8 chars — adaptive threshold should suggest
12+
try { arry_pussh([], 1); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
13+
14+
// Completely wrong name — no suggestion
15+
try { nonexistentfunc(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
16+
17+
// Dynamic call — same suggestion logic applies
18+
$f = "strlenx";
19+
try { $f("x"); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
20+
?>
21+
--EXPECT--
22+
Error: Call to undefined function strlenn() (did you mean strlen?)
23+
Error: Call to undefined function array_pussh() (did you mean array_push?)
24+
Error: Call to undefined function arry_pussh() (did you mean array_push?)
25+
Error: Call to undefined function nonexistentfunc()
26+
Error: Call to undefined function strlenx() (did you mean strlen?)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
Levenshtein suggestion for undefined function calls in a namespace
3+
--FILE--
4+
<?php
5+
namespace App;
6+
7+
function helper() {}
8+
9+
// Unqualified call — falls back to a similar global function
10+
try { strlenn("x"); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
11+
12+
// Unqualified call — similar function in the current namespace wins
13+
try { helpr(); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
14+
15+
// Qualified call — only namespaced functions are considered
16+
try { \App\helpr(); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
17+
try { \App\strlenn("x"); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
18+
19+
// Dynamic call with a namespaced name
20+
$f = 'App\helpr';
21+
try { $f(); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
22+
23+
// FCC in a default argument — falls back to a similar global function
24+
function test(\Closure $c = strlenn(...)) {}
25+
try { test(); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
26+
?>
27+
--EXPECT--
28+
Error: Call to undefined function App\strlenn() (did you mean strlen?)
29+
Error: Call to undefined function App\helpr() (did you mean App\helper?)
30+
Error: Call to undefined function App\helpr() (did you mean App\helper?)
31+
Error: Call to undefined function App\strlenn()
32+
Error: Call to undefined function App\helpr() (did you mean App\helper?)
33+
Error: Call to undefined function App\strlenn() (did you mean strlen?)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
Levenshtein suggestion for undefined method calls
3+
--FILE--
4+
<?php
5+
class Calculator {
6+
public function add(int $a, int $b): int { return $a + $b; }
7+
public function subtract(int $a, int $b): int { return $a - $b; }
8+
public function multiply(int $a, int $b): int { return $a * $b; }
9+
}
10+
11+
$calc = new Calculator();
12+
13+
// One edit away — should suggest
14+
try { $calc->addd(1, 2); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
15+
16+
// Two edits away for a name >= 8 chars — adaptive threshold should suggest
17+
try { $calc->subtarct(5, 3); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
18+
19+
// Completely wrong name — no suggestion
20+
try { $calc->nonexistent(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
21+
22+
// Static call — same logic applies
23+
try { Calculator::addd(1, 2); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
24+
?>
25+
--EXPECT--
26+
Error: Call to undefined method Calculator::addd() (did you mean add?)
27+
Error: Call to undefined method Calculator::subtarct() (did you mean subtract?)
28+
Error: Call to undefined method Calculator::nonexistent()
29+
Error: Call to undefined method Calculator::addd() (did you mean add?)

‎Zend/tests/nullsafe_operator/003.phpt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ string(3) "bar"
4848
Warning: Undefined property: Foo::$baz in %s.php on line 20
4949
NULL
5050
string(3) "qux"
51-
Error: Call to undefined method Foo::quux()
51+
Error: Call to undefined method Foo::quux() (did you mean qux?)
5252
string(3) "bar"
5353

5454
Warning: Undefined property: Foo::$baz in %s.php on line 29
5555
NULL
5656
string(3) "qux"
57-
Error: Call to undefined method Foo::quux()
57+
Error: Call to undefined method Foo::quux() (did you mean qux?)

‎Zend/tests/nullsafe_operator/033.phpt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ string(3) "bar"
4848
Warning: Undefined property: Foo::$baz in %s.php on line 20
4949
string(0) ""
5050
string(3) "qux"
51-
Error: Call to undefined method Foo::quux()
51+
Error: Call to undefined method Foo::quux() (did you mean qux?)
5252
string(3) "bar"
5353

5454
Warning: Undefined property: Foo::$baz in %s.php on line 29

0 commit comments

Comments
 (0)