Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ PHP 8.7 UPGRADE NOTES
13. Other Changes
========================================

- Core:
. Errors for undefined functions, undefined methods and classes, interfaces
or traits that were not found now suggest a similarly named one, e.g.
'Call to undefined function strlenn() (did you mean strlen?)'. Code that
matches on the exact error message may need to be updated.

========================================
14. Performance Improvements
========================================
Expand Down
2 changes: 1 addition & 1 deletion Zend/tests/exceptions/bug31102.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Caught: Test1::__construct
Caught: {closure:%s:%d}
{closure:%s:%d}(Test3,3)

Fatal error: Uncaught Error: Class "Test3" not found in %s:%d
Fatal error: Uncaught Error: Class "Test3" not found (did you mean "Test1"?) in %s:%d
Stack trace:
#0 %s(%d): eval()
#1 {main}
Expand Down
2 changes: 1 addition & 1 deletion Zend/tests/is_callable/bug69315.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ try {
bool(false)
bool(false)
Error: Call to undefined function strlen()
Error: Call to undefined function defined()
Error: Call to undefined function defined() (did you mean define?)
Error: Call to undefined function constant()
Error: Call to undefined function call_user_func()
Error: Call to undefined function is_string()
Expand Down
35 changes: 35 additions & 0 deletions Zend/tests/name_suggestion/suggestion_undefined_class.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--TEST--
Levenshtein suggestion for undefined class, interface and trait lookups
--FILE--
<?php
// One edit away — should suggest
try { new ArrayObjekt(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
try { new StdClas(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// One edit away for a longer name — should suggest
try { new ArrayIteratr(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Two edits away for a name >= 8 chars — adaptive threshold should suggest
try { new ArryObjct(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Completely wrong name — no suggestion
try { new Unicorn(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Interface
try {
$c = new class() implements Countble {};
} catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Trait
try {
eval('class T { use NonExistntTrait; }');
} catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
?>
--EXPECTF--
Error: Class "ArrayObjekt" not found (did you mean "ArrayObject"?)
Error: Class "StdClas" not found (did you mean "stdClass"?)
Error: Class "ArrayIteratr" not found (did you mean "ArrayIterator"?)
Error: Class "ArryObjct" not found (did you mean "ArrayObject"?)
Error: Class "Unicorn" not found
Error: Interface "Countble" not found (did you mean "Countable"?)
Error: Trait "NonExistntTrait" not found
28 changes: 28 additions & 0 deletions Zend/tests/name_suggestion/suggestion_undefined_fcc.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Levenshtein suggestion for undefined function in FCC context
--FILE--
<?php
// Test 1: In default argument position

function test(Closure $name = array_pussh(...)) {
var_dump($name);
}

try {
test();
} catch (Error $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}

// Test 2: In constant initializer (via eval so the fatal can be caught for combined output)

try {
eval("const Closure2 = strlenn(...);");
} catch (Error $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}

?>
--EXPECT--
Error: Call to undefined function array_pussh() (did you mean array_push?)
Error: Call to undefined function strlenn() (did you mean strlen?)
26 changes: 26 additions & 0 deletions Zend/tests/name_suggestion/suggestion_undefined_function.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Levenshtein suggestion for undefined function calls
--FILE--
<?php
// One edit away — should suggest
try { strlenn("x"); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// One edit away for a longer name — should suggest
try { array_pussh([], 1); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Two edits away for a name >= 8 chars — adaptive threshold should suggest
try { arry_pussh([], 1); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Completely wrong name — no suggestion
try { nonexistentfunc(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Dynamic call — same suggestion logic applies
$f = "strlenx";
try { $f("x"); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
?>
--EXPECT--
Error: Call to undefined function strlenn() (did you mean strlen?)
Error: Call to undefined function array_pussh() (did you mean array_push?)
Error: Call to undefined function arry_pussh() (did you mean array_push?)
Error: Call to undefined function nonexistentfunc()
Error: Call to undefined function strlenx() (did you mean strlen?)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
Levenshtein suggestion for undefined function calls in a namespace
--FILE--
<?php
namespace App;

function helper() {}

// Unqualified call — falls back to a similar global function
try { strlenn("x"); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Unqualified call — similar function in the current namespace wins
try { helpr(); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Qualified call — only namespaced functions are considered
try { \App\helpr(); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
try { \App\strlenn("x"); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Dynamic call with a namespaced name
$f = 'App\helpr';
try { $f(); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// FCC in a default argument — falls back to a similar global function
function test(\Closure $c = strlenn(...)) {}
try { test(); } catch (\Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
?>
--EXPECT--
Error: Call to undefined function App\strlenn() (did you mean strlen?)
Error: Call to undefined function App\helpr() (did you mean App\helper?)
Error: Call to undefined function App\helpr() (did you mean App\helper?)
Error: Call to undefined function App\strlenn()
Error: Call to undefined function App\helpr() (did you mean App\helper?)
Error: Call to undefined function App\strlenn() (did you mean strlen?)
29 changes: 29 additions & 0 deletions Zend/tests/name_suggestion/suggestion_undefined_method.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
Levenshtein suggestion for undefined method calls
--FILE--
<?php
class Calculator {
public function add(int $a, int $b): int { return $a + $b; }
public function subtract(int $a, int $b): int { return $a - $b; }
public function multiply(int $a, int $b): int { return $a * $b; }
}

$calc = new Calculator();

// One edit away — should suggest
try { $calc->addd(1, 2); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Two edits away for a name >= 8 chars — adaptive threshold should suggest
try { $calc->subtarct(5, 3); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Completely wrong name — no suggestion
try { $calc->nonexistent(); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }

// Static call — same logic applies
try { Calculator::addd(1, 2); } catch (Error $e) { echo $e::class, ': ', $e->getMessage(), "\n"; }
?>
--EXPECT--
Error: Call to undefined method Calculator::addd() (did you mean add?)
Error: Call to undefined method Calculator::subtarct() (did you mean subtract?)
Error: Call to undefined method Calculator::nonexistent()
Error: Call to undefined method Calculator::addd() (did you mean add?)
4 changes: 2 additions & 2 deletions Zend/tests/nullsafe_operator/003.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ string(3) "bar"
Warning: Undefined property: Foo::$baz in %s.php on line 20
NULL
string(3) "qux"
Error: Call to undefined method Foo::quux()
Error: Call to undefined method Foo::quux() (did you mean qux?)
string(3) "bar"

Warning: Undefined property: Foo::$baz in %s.php on line 29
NULL
string(3) "qux"
Error: Call to undefined method Foo::quux()
Error: Call to undefined method Foo::quux() (did you mean qux?)
2 changes: 1 addition & 1 deletion Zend/tests/nullsafe_operator/033.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ string(3) "bar"
Warning: Undefined property: Foo::$baz in %s.php on line 20
string(0) ""
string(3) "qux"
Error: Call to undefined method Foo::quux()
Error: Call to undefined method Foo::quux() (did you mean qux?)
string(3) "bar"

Warning: Undefined property: Foo::$baz in %s.php on line 29
Expand Down
16 changes: 15 additions & 1 deletion Zend/zend_ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "zend_ast.h"
#include "zend_API.h"
#include "zend_execute.h"
#include "zend_operators.h"
#include "zend_language_parser.h"
#include "zend_smart_str.h"
Expand Down Expand Up @@ -1259,7 +1260,20 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
}

if (!fptr) {
zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(function_name));
zend_string *lc_name = zend_string_tolower(function_name);
zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_name), ZSTR_LEN(lc_name));
if (!suggestion && ast->child[0]->attr != ZEND_NAME_FQ) {
const char *backslash = zend_memrchr(ZSTR_VAL(lc_name), '\\', ZSTR_LEN(lc_name));
if (backslash) {
suggestion = zend_find_similar_function(backslash + 1, ZSTR_LEN(lc_name) - (backslash - ZSTR_VAL(lc_name) + 1));
}
}
if (suggestion) {
zend_throw_error(NULL, "Call to undefined function %s() (did you mean %s?)", ZSTR_VAL(function_name), ZSTR_VAL(suggestion));
} else {
zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(function_name));
}
zend_string_release(lc_name);
return FAILURE;
}

Expand Down
87 changes: 85 additions & 2 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -2582,9 +2582,80 @@ ZEND_API ZEND_COLD zval* ZEND_FASTCALL zend_undefined_index_write(HashTable *ht,
return retval;
}

static zval *zend_find_similar_entry(const HashTable *ht, const char *lcname, size_t lcname_len)
{
if (lcname_len < 3) {
return NULL;
}

size_t threshold = lcname_len >= 8 ? 2 : 1;
zend_long best_dist = threshold + 1;
zval *best = NULL;

ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(ht, zend_string *key, zval *val) {
if (!key || ZSTR_VAL(key)[0] == '\0' || Z_TYPE_P(val) == IS_ALIAS_PTR) {
continue;
}
if (ZSTR_LEN(key) > lcname_len + threshold || lcname_len > ZSTR_LEN(key) + threshold) {
continue;
}
zend_long dist = zend_levenshtein(lcname, lcname_len, ZSTR_VAL(key), ZSTR_LEN(key), 1, 1, 1);
if (dist > 0 && dist < best_dist) {
best_dist = dist;
best = val;
}
} ZEND_HASH_FOREACH_END();

return best;
}

zend_string *zend_find_similar_function(const char *lcname, size_t lcname_len)
{
const zval *zv = zend_find_similar_entry(EG(function_table), lcname, lcname_len);
return zv ? Z_FUNC_P(zv)->common.function_name : NULL;
}

zend_string *zend_find_similar_class(const zend_string *class_name)
{
zend_string *lc_name = zend_string_alloc(ZSTR_LEN(class_name), 0);
zend_str_tolower_copy(ZSTR_VAL(lc_name), ZSTR_VAL(class_name), ZSTR_LEN(class_name));
const zval *zv = zend_find_similar_entry(EG(class_table), ZSTR_VAL(lc_name), ZSTR_LEN(lc_name));
zend_string_release(lc_name);
return zv ? Z_CE_P(zv)->name : NULL;
}

static zend_string *zend_find_similar_method(const zend_class_entry *ce, const zend_string *method)
{
zend_string *lc_method = zend_string_alloc(ZSTR_LEN(method), 0);
zend_str_tolower_copy(ZSTR_VAL(lc_method), ZSTR_VAL(method), ZSTR_LEN(method));
const zval *zv = zend_find_similar_entry(&ce->function_table, ZSTR_VAL(lc_method), ZSTR_LEN(lc_method));
zend_string_release(lc_method);
return zv ? Z_FUNC_P(zv)->common.function_name : NULL;
}

ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_undefined_method(const zend_class_entry *ce, const zend_string *method)
{
zend_throw_error(NULL, "Call to undefined method %s::%s()", ZSTR_VAL(ce->name), ZSTR_VAL(method));
zend_string *suggestion = zend_find_similar_method(ce, method);
if (suggestion) {
zend_throw_error(NULL, "Call to undefined method %s::%s() (did you mean %s?)", ZSTR_VAL(ce->name), ZSTR_VAL(method), ZSTR_VAL(suggestion));
} else {
zend_throw_error(NULL, "Call to undefined method %s::%s()", ZSTR_VAL(ce->name), ZSTR_VAL(method));
}
}

/* op2 is followed by the lowercased name and, for INIT_NS_FCALL_BY_NAME, the global fallback */
ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_undefined_function_error(const zend_op *opline)
{
const zval *function_name = RT_CONSTANT(opline, opline->op2);
zend_string *suggestion = zend_find_similar_function(Z_STRVAL_P(function_name + 1), Z_STRLEN_P(function_name + 1));
if (!suggestion && opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME) {
suggestion = zend_find_similar_function(Z_STRVAL_P(function_name + 2), Z_STRLEN_P(function_name + 2));
}
if (suggestion) {
zend_throw_error(NULL, "Call to undefined function %s() (did you mean %s?)", Z_STRVAL_P(function_name), ZSTR_VAL(suggestion));
} else {
zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(function_name));
}
}

static zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_invalid_method_call(const zval *object, const zval *function_name)
Expand Down Expand Up @@ -5185,7 +5256,19 @@ static zend_never_inline zend_execute_data *zend_init_dynamic_call_string(zend_s
} else {
fbc = zend_fetch_function(function);
if (UNEXPECTED(fbc == NULL)) {
zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(function));
if (ZSTR_VAL(function)[0] == '\\') {
lcname = zend_string_alloc(ZSTR_LEN(function) - 1, 0);
zend_str_tolower_copy(ZSTR_VAL(lcname), ZSTR_VAL(function) + 1, ZSTR_LEN(function) - 1);
} else {
lcname = zend_string_tolower(function);
}
zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lcname), ZSTR_LEN(lcname));
if (suggestion) {
zend_throw_error(NULL, "Call to undefined function %s() (did you mean %s?)", ZSTR_VAL(function), ZSTR_VAL(suggestion));
} else {
zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(function));
}
zend_string_release_ex(lcname, 0);
return NULL;
}
called_scope = NULL;
Expand Down
3 changes: 3 additions & 0 deletions Zend/zend_execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,9 @@ ZEND_API ZEND_ATTRIBUTE_DEPRECATED HashTable *zend_unfinished_execution_gc(zend_
ZEND_API HashTable *zend_unfinished_execution_gc_ex(zend_execute_data *execute_data, zend_execute_data *call, zend_get_gc_buffer *gc_buffer, bool suspended_by_yield);
ZEND_API zval* ZEND_FASTCALL zend_fetch_static_property(zend_execute_data *ex, int fetch_type);
ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_undefined_method(const zend_class_entry *ce, const zend_string *method);
ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_undefined_function_error(const zend_op *opline);
zend_string *zend_find_similar_function(const char *lcname, size_t lcname_len);
zend_string *zend_find_similar_class(const zend_string *class_name);
ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_non_static_method_call(const zend_function *fbc);

ZEND_API void zend_frameless_observed_call(zend_execute_data *execute_data);
Expand Down
13 changes: 8 additions & 5 deletions Zend/zend_execute_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -1752,12 +1752,15 @@ static ZEND_COLD void report_class_fetch_error(const zend_string *class_name, ui
return;
}

if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_INTERFACE) {
zend_throw_or_error(fetch_type, NULL, "Interface \"%s\" not found", ZSTR_VAL(class_name));
} else if ((fetch_type & ZEND_FETCH_CLASS_MASK) == ZEND_FETCH_CLASS_TRAIT) {
zend_throw_or_error(fetch_type, NULL, "Trait \"%s\" not found", ZSTR_VAL(class_name));
uint32_t mask = fetch_type & ZEND_FETCH_CLASS_MASK;
const char *kind = mask == ZEND_FETCH_CLASS_INTERFACE ? "Interface"
: mask == ZEND_FETCH_CLASS_TRAIT ? "Trait"
: "Class";
zend_string *suggestion = zend_find_similar_class(class_name);
if (suggestion) {
zend_throw_or_error(fetch_type, NULL, "%s \"%s\" not found (did you mean \"%s\"?)", kind, ZSTR_VAL(class_name), ZSTR_VAL(suggestion));
} else {
zend_throw_or_error(fetch_type, NULL, "Class \"%s\" not found", ZSTR_VAL(class_name));
zend_throw_or_error(fetch_type, NULL, "%s \"%s\" not found", kind, ZSTR_VAL(class_name));
}
}

Expand Down
Loading
Loading