From 11e41df8909bc835ba34a269e70b769c5739a607 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Sun, 17 May 2026 13:35:11 +0200 Subject: [PATCH 1/6] Zend: suggest similar names for undefined function/method/class errors --- Zend/tests/bug69315.phpt | 2 +- Zend/tests/exceptions/bug31102.phpt | 2 +- Zend/tests/levenshtein_suggest_class.phpt | 35 +++++++++++++ Zend/tests/levenshtein_suggest_function.phpt | 26 ++++++++++ Zend/tests/levenshtein_suggest_method.phpt | 29 +++++++++++ Zend/tests/nullsafe_operator/003.phpt | 4 +- Zend/tests/nullsafe_operator/033.phpt | 2 +- Zend/zend_execute.c | 53 +++++++++++++++++++- Zend/zend_execute_API.c | 45 +++++++++++++++-- Zend/zend_string.c | 37 ++++++++++++++ Zend/zend_string.h | 2 + Zend/zend_vm_def.h | 9 +++- Zend/zend_vm_execute.h | 18 ++++++- ext/opcache/tests/bug76796.phpt | 2 +- ext/spl/tests/autoloading/bug73896.phpt | 2 +- 15 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 Zend/tests/levenshtein_suggest_class.phpt create mode 100644 Zend/tests/levenshtein_suggest_function.phpt create mode 100644 Zend/tests/levenshtein_suggest_method.phpt diff --git a/Zend/tests/bug69315.phpt b/Zend/tests/bug69315.phpt index 82017c055241..eced311bbe12 100644 --- a/Zend/tests/bug69315.phpt +++ b/Zend/tests/bug69315.phpt @@ -43,7 +43,7 @@ try { bool(false) bool(false) Call to undefined function strlen() -Call to undefined function defined() +Call to undefined function defined() (did you mean define()?) Call to undefined function constant() Call to undefined function call_user_func() Call to undefined function is_string() diff --git a/Zend/tests/exceptions/bug31102.phpt b/Zend/tests/exceptions/bug31102.phpt index 17beec6ba389..99147c04c258 100644 --- a/Zend/tests/exceptions/bug31102.phpt +++ b/Zend/tests/exceptions/bug31102.phpt @@ -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} diff --git a/Zend/tests/levenshtein_suggest_class.phpt b/Zend/tests/levenshtein_suggest_class.phpt new file mode 100644 index 000000000000..9e2a42aaf7da --- /dev/null +++ b/Zend/tests/levenshtein_suggest_class.phpt @@ -0,0 +1,35 @@ +--TEST-- +Levenshtein suggestion for undefined class, interface and trait lookups +--FILE-- +getMessage(), "\n"; } +try { new StdClas(); } catch (Error $e) { echo $e->getMessage(), "\n"; } + +// One edit away for a longer name — should suggest +try { new ArrayIteratr(); } catch (Error $e) { echo $e->getMessage(), "\n"; } + +// Two edits away for a name >= 8 chars — adaptive threshold should suggest +try { new ArryObjct(); } catch (Error $e) { echo $e->getMessage(), "\n"; } + +// Completely wrong name — no suggestion +try { new Unicorn(); } catch (Error $e) { echo $e->getMessage(), "\n"; } + +// Interface +try { + $c = new class() implements Countble {}; +} catch (Error $e) { echo $e->getMessage(), "\n"; } + +// Trait +try { + eval('class T { use NonExistntTrait; }'); +} catch (Error $e) { echo $e->getMessage(), "\n"; } +?> +--EXPECTF-- +Class "ArrayObjekt" not found (did you mean ArrayObject?) +Class "StdClas" not found (did you mean stdClass?) +Class "ArrayIteratr" not found (did you mean ArrayIterator?) +Class "ArryObjct" not found (did you mean ArrayObject?) +Class "Unicorn" not found +Interface "Countble" not found (did you mean Countable?) +Trait "NonExistntTrait" not found diff --git a/Zend/tests/levenshtein_suggest_function.phpt b/Zend/tests/levenshtein_suggest_function.phpt new file mode 100644 index 000000000000..308b70f33b88 --- /dev/null +++ b/Zend/tests/levenshtein_suggest_function.phpt @@ -0,0 +1,26 @@ +--TEST-- +Levenshtein suggestion for undefined function calls +--FILE-- +getMessage(), "\n"; } + +// One edit away for a longer name — should suggest +try { array_pussh([], 1); } catch (Error $e) { echo $e->getMessage(), "\n"; } + +// Two edits away for a name >= 8 chars — adaptive threshold should suggest +try { arry_pussh([], 1); } catch (Error $e) { echo $e->getMessage(), "\n"; } + +// Completely wrong name — no suggestion +try { nonexistentfunc(); } catch (Error $e) { echo $e->getMessage(), "\n"; } + +// Dynamic call — same suggestion logic applies +$f = "strlenx"; +try { $f("x"); } catch (Error $e) { echo $e->getMessage(), "\n"; } +?> +--EXPECT-- +Call to undefined function strlenn() (did you mean strlen()?) +Call to undefined function array_pussh() (did you mean array_push()?) +Call to undefined function arry_pussh() (did you mean array_push()?) +Call to undefined function nonexistentfunc() +Call to undefined function strlenx() (did you mean strlen()?) diff --git a/Zend/tests/levenshtein_suggest_method.phpt b/Zend/tests/levenshtein_suggest_method.phpt new file mode 100644 index 000000000000..89f6053b1ca5 --- /dev/null +++ b/Zend/tests/levenshtein_suggest_method.phpt @@ -0,0 +1,29 @@ +--TEST-- +Levenshtein suggestion for undefined method calls +--FILE-- +addd(1, 2); } catch (Error $e) { echo $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->getMessage(), "\n"; } + +// Completely wrong name — no suggestion +try { $calc->nonexistent(); } catch (Error $e) { echo $e->getMessage(), "\n"; } + +// Static call — same logic applies +try { Calculator::addd(1, 2); } catch (Error $e) { echo $e->getMessage(), "\n"; } +?> +--EXPECT-- +Call to undefined method Calculator::addd() (did you mean add()?) +Call to undefined method Calculator::subtarct() (did you mean subtract()?) +Call to undefined method Calculator::nonexistent() +Call to undefined method Calculator::addd() (did you mean add()?) diff --git a/Zend/tests/nullsafe_operator/003.phpt b/Zend/tests/nullsafe_operator/003.phpt index 83c2863d1a10..5087a9c56ca6 100644 --- a/Zend/tests/nullsafe_operator/003.phpt +++ b/Zend/tests/nullsafe_operator/003.phpt @@ -48,10 +48,10 @@ string(3) "bar" Warning: Undefined property: Foo::$baz in %s.php on line 20 NULL string(3) "qux" -string(36) "Call to undefined method Foo::quux()" +string(58) "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" -string(36) "Call to undefined method Foo::quux()" +string(58) "Call to undefined method Foo::quux() (did you mean qux()?)" diff --git a/Zend/tests/nullsafe_operator/033.phpt b/Zend/tests/nullsafe_operator/033.phpt index 63ee2f850a56..ff0a9c4824b7 100644 --- a/Zend/tests/nullsafe_operator/033.phpt +++ b/Zend/tests/nullsafe_operator/033.phpt @@ -48,7 +48,7 @@ string(3) "bar" Warning: Undefined property: Foo::$baz in %s.php on line 20 string(0) "" string(3) "qux" -string(36) "Call to undefined method Foo::quux()" +string(58) "Call to undefined method Foo::quux() (did you mean qux()?)" string(3) "bar" Warning: Undefined property: Foo::$baz in %s.php on line 29 diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index bc93aa8892ac..bd0a153c87eb 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2558,9 +2558,53 @@ ZEND_API ZEND_COLD zval* ZEND_FASTCALL zend_undefined_index_write(HashTable *ht, return retval; } +static zend_string *zend_find_similar_in_function_table(HashTable *ht, const char *lcname, size_t lcname_len) +{ + zend_long threshold = lcname_len >= 8 ? 2 : 1; + zend_long best_dist = threshold + 1; + zend_string *best = NULL; + zend_string *key; + zval *val; + + ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(ht, key, val) { + if (!key || ZSTR_VAL(key)[0] == '\0') continue; + if (llabs((zend_long)lcname_len - (zend_long)ZSTR_LEN(key)) > threshold) continue; + zend_long dist = zend_levenshtein(lcname, lcname_len, ZSTR_VAL(key), ZSTR_LEN(key)); + if (dist > 0 && dist <= threshold && dist < best_dist) { + best_dist = dist; + best = Z_FUNC_P(val)->common.function_name; + } + } ZEND_HASH_FOREACH_END(); + + return best; +} + +static zend_string *zend_find_similar_function(const char *lcname, size_t lcname_len) +{ + if (memchr(lcname, '\\', lcname_len)) return NULL; + if (lcname_len < 3) return NULL; + return zend_find_similar_in_function_table(EG(function_table), lcname, lcname_len); +} + +static zend_string *zend_find_similar_method(const zend_class_entry *ce, const zend_string *method) +{ + zend_string *lc_method = zend_string_tolower((zend_string *)method); + zend_string *best = NULL; + if (ZSTR_LEN(lc_method) >= 3) { + best = zend_find_similar_in_function_table((HashTable *)&ce->function_table, ZSTR_VAL(lc_method), ZSTR_LEN(lc_method)); + } + zend_string_release(lc_method); + return best; +} + 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)); + } } static zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_invalid_method_call(const zval *object, const zval *function_name) @@ -5167,7 +5211,12 @@ static zend_never_inline zend_execute_data *zend_init_dynamic_call_string(zend_s lcname = zend_string_tolower(function); } if (UNEXPECTED((func = zend_hash_find(EG(function_table), lcname)) == NULL)) { - zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(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; } diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 83ba5e7490cf..1526fe88bc26 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1699,6 +1699,38 @@ void zend_unset_timeout(void) /* {{{ */ } /* }}} */ +static zend_string *zend_find_similar_in_class_table(const char *lcname, size_t lcname_len) +{ + zend_long threshold = lcname_len >= 8 ? 2 : 1; + zend_long best_dist = threshold + 1; + zend_string *best = NULL; + zend_string *key; + zval *val; + + ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(EG(class_table), key, val) { + if (!key || ZSTR_VAL(key)[0] == '\0' || Z_TYPE_P(val) == IS_ALIAS_PTR) continue; + if (llabs((zend_long)lcname_len - (zend_long)ZSTR_LEN(key)) > threshold) continue; + zend_long dist = zend_levenshtein(lcname, lcname_len, ZSTR_VAL(key), ZSTR_LEN(key)); + if (dist > 0 && dist <= threshold && dist < best_dist) { + best_dist = dist; + best = ((zend_class_entry *)Z_PTR_P(val))->name; + } + } ZEND_HASH_FOREACH_END(); + + return best; +} + +static zend_string *zend_find_similar_class(const zend_string *class_name) +{ + zend_string *lc_name = zend_string_tolower((zend_string *)class_name); + zend_string *best = NULL; + if (ZSTR_LEN(lc_name) >= 3) { + best = zend_find_similar_in_class_table(ZSTR_VAL(lc_name), ZSTR_LEN(lc_name)); + } + zend_string_release(lc_name); + return best; +} + static ZEND_COLD void report_class_fetch_error(const zend_string *class_name, uint32_t fetch_type) { if (fetch_type & ZEND_FETCH_CLASS_SILENT) { @@ -1712,12 +1744,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)); } } diff --git a/Zend/zend_string.c b/Zend/zend_string.c index b1b4a0e17a69..3949370ddd1d 100644 --- a/Zend/zend_string.c +++ b/Zend/zend_string.c @@ -63,6 +63,43 @@ static zend_always_inline void zend_bin2hex_impl(char *out, const unsigned char } } +ZEND_API zend_long zend_levenshtein(const char *s1, size_t l1, const char *s2, size_t l2) +{ + zend_long *p1, *p2, *tmp; + zend_long c0, c1, c2; + size_t i1, i2; + + if (l1 == 0) return (zend_long)l2; + if (l2 == 0) return (zend_long)l1; + + if (l1 < l2) { + const char *tmp_s = s1; s1 = s2; s2 = tmp_s; + size_t tmp_l = l1; l1 = l2; l2 = tmp_l; + } + + p1 = emalloc((l2 + 1) * sizeof(zend_long)); + p2 = emalloc((l2 + 1) * sizeof(zend_long)); + + for (i2 = 0; i2 <= l2; i2++) { + p1[i2] = (zend_long)i2; + } + for (i1 = 0; i1 < l1; i1++) { + p2[0] = (zend_long)(i1 + 1); + for (i2 = 0; i2 < l2; i2++) { + c0 = p1[i2] + (s1[i1] == s2[i2] ? 0 : 1); + c1 = p1[i2 + 1] + 1; + c2 = p2[i2] + 1; + p2[i2 + 1] = c0 < c1 ? (c0 < c2 ? c0 : c2) : (c1 < c2 ? c1 : c2); + } + tmp = p1; p1 = p2; p2 = tmp; + } + + c0 = p1[l2]; + efree(p1); + efree(p2); + return c0; +} + ZEND_API zend_ulong ZEND_FASTCALL zend_string_hash_func(zend_string *str) { return ZSTR_H(str) = zend_hash_func(ZSTR_VAL(str), ZSTR_LEN(str)); diff --git a/Zend/zend_string.h b/Zend/zend_string.h index d9efea00e7bd..d4a4056932ff 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -46,6 +46,8 @@ ZEND_API zend_string* ZEND_FASTCALL zend_interned_string_find_permanent(zend_str ZEND_API void ZEND_FASTCALL zend_bin2hex(char *out, const unsigned char *in, size_t in_len); ZEND_API zend_string *zend_bin2hex_str(const unsigned char *in, size_t in_len); +ZEND_API zend_long zend_levenshtein(const char *s1, size_t l1, const char *s2, size_t l2); + ZEND_API zend_string *zend_string_concat2( const char *str1, size_t str1_len, const char *str2, size_t str2_len); diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index d14230514b34..a6048355ad2d 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -1000,7 +1000,14 @@ ZEND_VM_COLD_HELPER(zend_undefined_function_helper, ANY, ANY) SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); - zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(function_name)); + /* For INIT_NS_FCALL_BY_NAME with a namespace prefix, op2+2 is the global fallback */ + zend_string *lc_key = Z_STR_P(function_name + 1); + zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_key), ZSTR_LEN(lc_key)); + 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)); + } HANDLE_EXCEPTION(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 53bcdccd9719..812639be72b8 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -773,7 +773,14 @@ static zend_never_inline ZEND_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_F SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); - zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(function_name)); + /* For INIT_NS_FCALL_BY_NAME with a namespace prefix, op2+2 is the global fallback */ + zend_string *lc_key = Z_STR_P(function_name + 1); + zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_key), ZSTR_LEN(lc_key)); + 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)); + } HANDLE_EXCEPTION(); } @@ -53755,7 +53762,14 @@ static zend_never_inline ZEND_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_C SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); - zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(function_name)); + /* For INIT_NS_FCALL_BY_NAME with a namespace prefix, op2+2 is the global fallback */ + zend_string *lc_key = Z_STR_P(function_name + 1); + zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_key), ZSTR_LEN(lc_key)); + 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)); + } HANDLE_EXCEPTION(); } diff --git a/ext/opcache/tests/bug76796.phpt b/ext/opcache/tests/bug76796.phpt index b9efbc3ce70f..3510fd41c6d5 100644 --- a/ext/opcache/tests/bug76796.phpt +++ b/ext/opcache/tests/bug76796.phpt @@ -18,4 +18,4 @@ try { ?> --EXPECT-- -Call to undefined function strpos() +Call to undefined function strpos() (did you mean stripos()?) diff --git a/ext/spl/tests/autoloading/bug73896.phpt b/ext/spl/tests/autoloading/bug73896.phpt index 657f30c50dbe..98b71333aa05 100644 --- a/ext/spl/tests/autoloading/bug73896.phpt +++ b/ext/spl/tests/autoloading/bug73896.phpt @@ -35,4 +35,4 @@ try { } ?> --EXPECT-- -Exception: Class "teException" not found +Exception: Class "teException" not found (did you mean Exception?) From a1cbd3c46c9211579d408692f2d5089f8a92d2a6 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Sat, 4 Jul 2026 17:12:07 +0200 Subject: [PATCH 2/6] Zend/JIT: fix undefined function suggestion under opcache JIT The JIT compiler had its own hand-written stub for raising "Call to undefined function" errors that bypassed the new Levenshtein suggestion logic, since it duplicated the error message instead of reusing the interpreter's helper. Extract the shared logic into zend_undefined_function_error() and call it from both the VM helper and the JIT stub. --- Zend/zend_execute.c | 13 +++++++++++++ Zend/zend_execute.h | 1 + Zend/zend_vm_def.h | 9 +-------- Zend/zend_vm_execute.h | 18 ++---------------- ext/opcache/jit/zend_jit_ir.c | 14 +++----------- 5 files changed, 20 insertions(+), 35 deletions(-) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index bd0a153c87eb..8e156e1f5021 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2607,6 +2607,19 @@ ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_undefined_method(co } } +/* function_name and function_name[1] (the lowercased key) are adjacent RT_CONSTANT literals; + * for INIT_NS_FCALL_BY_NAME with a namespace prefix, function_name[2] is the global fallback */ +ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_undefined_function_error(zval *function_name) +{ + zend_string *lc_key = Z_STR_P(function_name + 1); + zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_key), ZSTR_LEN(lc_key)); + 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) { zend_throw_error(NULL, "Call to a member function %s() on %s", diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index eb73b3f3de9e..f4c9c6dddf0e 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -513,6 +513,7 @@ 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(zval *function_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); diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index a6048355ad2d..02d0a6855667 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -1000,14 +1000,7 @@ ZEND_VM_COLD_HELPER(zend_undefined_function_helper, ANY, ANY) SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); - /* For INIT_NS_FCALL_BY_NAME with a namespace prefix, op2+2 is the global fallback */ - zend_string *lc_key = Z_STR_P(function_name + 1); - zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_key), ZSTR_LEN(lc_key)); - 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)); - } + zend_undefined_function_error(function_name); HANDLE_EXCEPTION(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 812639be72b8..18665bcd381e 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -773,14 +773,7 @@ static zend_never_inline ZEND_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_F SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); - /* For INIT_NS_FCALL_BY_NAME with a namespace prefix, op2+2 is the global fallback */ - zend_string *lc_key = Z_STR_P(function_name + 1); - zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_key), ZSTR_LEN(lc_key)); - 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)); - } + zend_undefined_function_error(function_name); HANDLE_EXCEPTION(); } @@ -53762,14 +53755,7 @@ static zend_never_inline ZEND_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_C SAVE_OPLINE(); function_name = RT_CONSTANT(opline, opline->op2); - /* For INIT_NS_FCALL_BY_NAME with a namespace prefix, op2+2 is the global fallback */ - zend_string *lc_key = Z_STR_P(function_name + 1); - zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_key), ZSTR_LEN(lc_key)); - 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)); - } + zend_undefined_function_error(function_name); HANDLE_EXCEPTION(); } diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 2c904edd4cfc..308aaee90293 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -2168,21 +2168,13 @@ static int zend_jit_undefined_function_stub(zend_jit_ctx *jit) { // JIT: load EX(opline) ir_ref ref = ir_LOAD_A(jit_FP(jit)); - ir_ref arg3 = ir_LOAD_U32(ir_ADD_OFFSET(ref, offsetof(zend_op, op2.constant))); + ir_ref arg1 = ir_LOAD_U32(ir_ADD_OFFSET(ref, offsetof(zend_op, op2.constant))); if (sizeof(void*) == 8) { - arg3 = ir_LOAD_A(ir_ADD_A(ref, ir_SEXT_A(arg3))); - } else { - arg3 = ir_LOAD_A(arg3); + arg1 = ir_ADD_A(ref, ir_SEXT_A(arg1)); } - arg3 = ir_ADD_OFFSET(arg3, offsetof(zend_string, val)); - ir_CALL_3(IR_VOID, - ir_CONST_FUNC_PROTO(zend_throw_error, - ir_proto_2(&jit->ctx, IR_VARARG_FUNC, IR_VOID, IR_ADDR, IR_ADDR)), - IR_NULL, - ir_CONST_ADDR("Call to undefined function %s()"), - arg3); + ir_CALL_1(IR_VOID, ir_CONST_FC_FUNC(zend_undefined_function_error), arg1); ir_IJMP(jit_STUB_ADDR(jit, jit_stub_exception_handler)); From 3c111b3f448fdbe0521a2e0634c2b32f827abcce Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Sat, 4 Jul 2026 17:45:24 +0200 Subject: [PATCH 3/6] apply review comments --- Zend/zend_execute.c | 27 +++++++++++++++++---------- Zend/zend_execute_API.c | 15 +++++++++------ Zend/zend_string.c | 4 ++-- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 8e156e1f5021..07176903a0dc 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2558,17 +2558,19 @@ ZEND_API ZEND_COLD zval* ZEND_FASTCALL zend_undefined_index_write(HashTable *ht, return retval; } -static zend_string *zend_find_similar_in_function_table(HashTable *ht, const char *lcname, size_t lcname_len) +static zend_string *zend_find_similar_in_function_table(const HashTable *ht, const char *lcname, size_t lcname_len) { zend_long threshold = lcname_len >= 8 ? 2 : 1; zend_long best_dist = threshold + 1; zend_string *best = NULL; - zend_string *key; - zval *val; - ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(ht, key, val) { - if (!key || ZSTR_VAL(key)[0] == '\0') continue; - if (llabs((zend_long)lcname_len - (zend_long)ZSTR_LEN(key)) > threshold) continue; + ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(ht, zend_string *key, zval *val) { + if (!key || ZSTR_VAL(key)[0] == '\0') { + continue; + } + if (llabs((zend_long)lcname_len - (zend_long)ZSTR_LEN(key)) > threshold) { + continue; + } zend_long dist = zend_levenshtein(lcname, lcname_len, ZSTR_VAL(key), ZSTR_LEN(key)); if (dist > 0 && dist <= threshold && dist < best_dist) { best_dist = dist; @@ -2581,17 +2583,22 @@ static zend_string *zend_find_similar_in_function_table(HashTable *ht, const cha static zend_string *zend_find_similar_function(const char *lcname, size_t lcname_len) { - if (memchr(lcname, '\\', lcname_len)) return NULL; - if (lcname_len < 3) return NULL; + if (memchr(lcname, '\\', lcname_len)) { + return NULL; + } + if (lcname_len < 3) { + return NULL; + } return zend_find_similar_in_function_table(EG(function_table), lcname, lcname_len); } static zend_string *zend_find_similar_method(const zend_class_entry *ce, const zend_string *method) { - zend_string *lc_method = zend_string_tolower((zend_string *)method); + zend_string *lc_method = zend_string_alloc(ZSTR_LEN(method), 0); zend_string *best = NULL; + zend_str_tolower_copy(ZSTR_VAL(lc_method), ZSTR_VAL(method), ZSTR_LEN(method)); if (ZSTR_LEN(lc_method) >= 3) { - best = zend_find_similar_in_function_table((HashTable *)&ce->function_table, ZSTR_VAL(lc_method), ZSTR_LEN(lc_method)); + best = zend_find_similar_in_function_table(&ce->function_table, ZSTR_VAL(lc_method), ZSTR_LEN(lc_method)); } zend_string_release(lc_method); return best; diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 1526fe88bc26..e0c90afce2ff 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1704,12 +1704,14 @@ static zend_string *zend_find_similar_in_class_table(const char *lcname, size_t zend_long threshold = lcname_len >= 8 ? 2 : 1; zend_long best_dist = threshold + 1; zend_string *best = NULL; - zend_string *key; - zval *val; - ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(EG(class_table), key, val) { - if (!key || ZSTR_VAL(key)[0] == '\0' || Z_TYPE_P(val) == IS_ALIAS_PTR) continue; - if (llabs((zend_long)lcname_len - (zend_long)ZSTR_LEN(key)) > threshold) continue; + ZEND_HASH_MAP_FOREACH_STR_KEY_VAL(EG(class_table), zend_string *key, zval *val) { + if (!key || ZSTR_VAL(key)[0] == '\0' || Z_TYPE_P(val) == IS_ALIAS_PTR) { + continue; + } + if (llabs((zend_long)lcname_len - (zend_long)ZSTR_LEN(key)) > threshold) { + continue; + } zend_long dist = zend_levenshtein(lcname, lcname_len, ZSTR_VAL(key), ZSTR_LEN(key)); if (dist > 0 && dist <= threshold && dist < best_dist) { best_dist = dist; @@ -1722,8 +1724,9 @@ static zend_string *zend_find_similar_in_class_table(const char *lcname, size_t static zend_string *zend_find_similar_class(const zend_string *class_name) { - zend_string *lc_name = zend_string_tolower((zend_string *)class_name); + zend_string *lc_name = zend_string_alloc(ZSTR_LEN(class_name), 0); zend_string *best = NULL; + zend_str_tolower_copy(ZSTR_VAL(lc_name), ZSTR_VAL(class_name), ZSTR_LEN(class_name)); if (ZSTR_LEN(lc_name) >= 3) { best = zend_find_similar_in_class_table(ZSTR_VAL(lc_name), ZSTR_LEN(lc_name)); } diff --git a/Zend/zend_string.c b/Zend/zend_string.c index 3949370ddd1d..d9a0022d7a69 100644 --- a/Zend/zend_string.c +++ b/Zend/zend_string.c @@ -77,8 +77,8 @@ ZEND_API zend_long zend_levenshtein(const char *s1, size_t l1, const char *s2, s size_t tmp_l = l1; l1 = l2; l2 = tmp_l; } - p1 = emalloc((l2 + 1) * sizeof(zend_long)); - p2 = emalloc((l2 + 1) * sizeof(zend_long)); + p1 = safe_emalloc(l2 + 1, sizeof(*p1), 0); + p2 = safe_emalloc(l2 + 1, sizeof(*p2), 0); for (i2 = 0; i2 <= l2; i2++) { p1[i2] = (zend_long)i2; From 96bd85dde0e78e896350901301bf4cd44f703da4 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Wed, 5 Aug 2026 00:57:36 +0200 Subject: [PATCH 4/6] Zend: suggest similar function names for undefined-function errors in constant expressions zend_ast_evaluate_inner() (first-class callables used as default parameter values, const initializers, etc.) still threw the plain "Call to undefined function %s()" message, unlike the VM/JIT and dynamic-call paths. Reuse zend_find_similar_function() there too. --- .../error_unknown_function_suggestion.phpt | 15 +++++++++++++++ ...nknown_function_suggestion_default_arg.phpt | 18 ++++++++++++++++++ Zend/zend_ast.c | 10 +++++++++- Zend/zend_execute.c | 2 +- Zend/zend_execute.h | 1 + 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion.phpt create mode 100644 Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion_default_arg.phpt diff --git a/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion.phpt b/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion.phpt new file mode 100644 index 000000000000..2e1c583f80e4 --- /dev/null +++ b/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion.phpt @@ -0,0 +1,15 @@ +--TEST-- +FCC in initializer suggests a similar function name for a missing function. +--FILE-- + +--EXPECTF-- +Fatal error: Uncaught Error: Call to undefined function strlenn() (did you mean strlen()?) in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion_default_arg.phpt b/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion_default_arg.phpt new file mode 100644 index 000000000000..71463b920f2e --- /dev/null +++ b/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion_default_arg.phpt @@ -0,0 +1,18 @@ +--TEST-- +FCC in default argument suggests a similar function name for a missing function. +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +Call to undefined function array_pussh() (did you mean array_push()?) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 4b070d9d5d58..c45b2d35e3d1 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -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" @@ -1252,7 +1253,14 @@ 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) { + 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; } diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 07176903a0dc..f040669cc934 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2581,7 +2581,7 @@ static zend_string *zend_find_similar_in_function_table(const HashTable *ht, con return best; } -static zend_string *zend_find_similar_function(const char *lcname, size_t lcname_len) +ZEND_API zend_string *zend_find_similar_function(const char *lcname, size_t lcname_len) { if (memchr(lcname, '\\', lcname_len)) { return NULL; diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index f4c9c6dddf0e..840a92c1ea71 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -514,6 +514,7 @@ ZEND_API HashTable *zend_unfinished_execution_gc_ex(zend_execute_data *execute_d 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(zval *function_name); +ZEND_API zend_string *zend_find_similar_function(const char *lcname, size_t lcname_len); 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); From 8a477d336c173b1df0669986b1f59d3b0aefdf76 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Wed, 5 Aug 2026 01:04:16 +0200 Subject: [PATCH 5/6] Zend: drop redundant () from function/method name suggestions "(did you mean strlen()?)" reads oddly since the () is already part of the call syntax right before it. Use "(did you mean strlen?)" instead, matching the class suggestion format which never had parens. --- Zend/tests/bug69315.phpt | 2 +- .../constexpr/error_unknown_function_suggestion.phpt | 2 +- .../error_unknown_function_suggestion_default_arg.phpt | 2 +- Zend/tests/levenshtein_suggest_function.phpt | 8 ++++---- Zend/tests/levenshtein_suggest_method.phpt | 6 +++--- Zend/tests/nullsafe_operator/003.phpt | 4 ++-- Zend/tests/nullsafe_operator/033.phpt | 2 +- Zend/zend_ast.c | 2 +- Zend/zend_execute.c | 6 +++--- ext/opcache/tests/bug76796.phpt | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Zend/tests/bug69315.phpt b/Zend/tests/bug69315.phpt index eced311bbe12..ac5181d954a5 100644 --- a/Zend/tests/bug69315.phpt +++ b/Zend/tests/bug69315.phpt @@ -43,7 +43,7 @@ try { bool(false) bool(false) Call to undefined function strlen() -Call to undefined function defined() (did you mean define()?) +Call to undefined function defined() (did you mean define?) Call to undefined function constant() Call to undefined function call_user_func() Call to undefined function is_string() diff --git a/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion.phpt b/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion.phpt index 2e1c583f80e4..b5205e078b59 100644 --- a/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion.phpt +++ b/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion.phpt @@ -9,7 +9,7 @@ var_dump(Closure); ?> --EXPECTF-- -Fatal error: Uncaught Error: Call to undefined function strlenn() (did you mean strlen()?) in %s:%d +Fatal error: Uncaught Error: Call to undefined function strlenn() (did you mean strlen?) in %s:%d Stack trace: #0 {main} thrown in %s on line %d diff --git a/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion_default_arg.phpt b/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion_default_arg.phpt index 71463b920f2e..ceb470dd4f0c 100644 --- a/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion_default_arg.phpt +++ b/Zend/tests/first_class_callable/constexpr/error_unknown_function_suggestion_default_arg.phpt @@ -15,4 +15,4 @@ try { ?> --EXPECT-- -Call to undefined function array_pussh() (did you mean array_push()?) +Call to undefined function array_pussh() (did you mean array_push?) diff --git a/Zend/tests/levenshtein_suggest_function.phpt b/Zend/tests/levenshtein_suggest_function.phpt index 308b70f33b88..7b1d5b3a29ad 100644 --- a/Zend/tests/levenshtein_suggest_function.phpt +++ b/Zend/tests/levenshtein_suggest_function.phpt @@ -19,8 +19,8 @@ $f = "strlenx"; try { $f("x"); } catch (Error $e) { echo $e->getMessage(), "\n"; } ?> --EXPECT-- -Call to undefined function strlenn() (did you mean strlen()?) -Call to undefined function array_pussh() (did you mean array_push()?) -Call to undefined function arry_pussh() (did you mean array_push()?) +Call to undefined function strlenn() (did you mean strlen?) +Call to undefined function array_pussh() (did you mean array_push?) +Call to undefined function arry_pussh() (did you mean array_push?) Call to undefined function nonexistentfunc() -Call to undefined function strlenx() (did you mean strlen()?) +Call to undefined function strlenx() (did you mean strlen?) diff --git a/Zend/tests/levenshtein_suggest_method.phpt b/Zend/tests/levenshtein_suggest_method.phpt index 89f6053b1ca5..d182d7d04383 100644 --- a/Zend/tests/levenshtein_suggest_method.phpt +++ b/Zend/tests/levenshtein_suggest_method.phpt @@ -23,7 +23,7 @@ try { $calc->nonexistent(); } catch (Error $e) { echo $e->getMessage(), "\n"; } try { Calculator::addd(1, 2); } catch (Error $e) { echo $e->getMessage(), "\n"; } ?> --EXPECT-- -Call to undefined method Calculator::addd() (did you mean add()?) -Call to undefined method Calculator::subtarct() (did you mean subtract()?) +Call to undefined method Calculator::addd() (did you mean add?) +Call to undefined method Calculator::subtarct() (did you mean subtract?) Call to undefined method Calculator::nonexistent() -Call to undefined method Calculator::addd() (did you mean add()?) +Call to undefined method Calculator::addd() (did you mean add?) diff --git a/Zend/tests/nullsafe_operator/003.phpt b/Zend/tests/nullsafe_operator/003.phpt index 5087a9c56ca6..847376f89704 100644 --- a/Zend/tests/nullsafe_operator/003.phpt +++ b/Zend/tests/nullsafe_operator/003.phpt @@ -48,10 +48,10 @@ string(3) "bar" Warning: Undefined property: Foo::$baz in %s.php on line 20 NULL string(3) "qux" -string(58) "Call to undefined method Foo::quux() (did you mean qux()?)" +string(56) "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" -string(58) "Call to undefined method Foo::quux() (did you mean qux()?)" +string(56) "Call to undefined method Foo::quux() (did you mean qux?)" diff --git a/Zend/tests/nullsafe_operator/033.phpt b/Zend/tests/nullsafe_operator/033.phpt index ff0a9c4824b7..4e05b815e758 100644 --- a/Zend/tests/nullsafe_operator/033.phpt +++ b/Zend/tests/nullsafe_operator/033.phpt @@ -48,7 +48,7 @@ string(3) "bar" Warning: Undefined property: Foo::$baz in %s.php on line 20 string(0) "" string(3) "qux" -string(58) "Call to undefined method Foo::quux() (did you mean qux()?)" +string(56) "Call to undefined method Foo::quux() (did you mean qux?)" string(3) "bar" Warning: Undefined property: Foo::$baz in %s.php on line 29 diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index c45b2d35e3d1..67e2d31307e0 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1256,7 +1256,7 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( 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) { - zend_throw_error(NULL, "Call to undefined function %s() (did you mean %s()?)", ZSTR_VAL(function_name), ZSTR_VAL(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)); } diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index f040669cc934..9a066996155b 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2608,7 +2608,7 @@ ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_undefined_method(co { 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)); + 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)); } @@ -2621,7 +2621,7 @@ ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_undefined_function_ zend_string *lc_key = Z_STR_P(function_name + 1); zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_key), ZSTR_LEN(lc_key)); if (suggestion) { - zend_throw_error(NULL, "Call to undefined function %s() (did you mean %s()?)", Z_STRVAL_P(function_name), ZSTR_VAL(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)); } @@ -5233,7 +5233,7 @@ static zend_never_inline zend_execute_data *zend_init_dynamic_call_string(zend_s if (UNEXPECTED((func = zend_hash_find(EG(function_table), lcname)) == NULL)) { 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)); + 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)); } diff --git a/ext/opcache/tests/bug76796.phpt b/ext/opcache/tests/bug76796.phpt index 3510fd41c6d5..4eee0432bc23 100644 --- a/ext/opcache/tests/bug76796.phpt +++ b/ext/opcache/tests/bug76796.phpt @@ -18,4 +18,4 @@ try { ?> --EXPECT-- -Call to undefined function strpos() (did you mean stripos()?) +Call to undefined function strpos() (did you mean stripos?) From 160adb0102f9cf24ce484ba4ad29d4880e1c8aeb Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Wed, 5 Aug 2026 23:11:27 +0200 Subject: [PATCH 6/6] Zend/tests: organize name suggestion test files in consistent directory --- Zend/tests/levenshtein_suggest_class.phpt | 35 ------------------- Zend/tests/levenshtein_suggest_function.phpt | 26 -------------- Zend/tests/levenshtein_suggest_method.phpt | 29 --------------- .../suggestion_undefined_class.phpt | 35 +++++++++++++++++++ .../suggestion_undefined_fcc.phpt | 28 +++++++++++++++ .../suggestion_undefined_function.phpt | 26 ++++++++++++++ .../suggestion_undefined_method.phpt | 29 +++++++++++++++ 7 files changed, 118 insertions(+), 90 deletions(-) delete mode 100644 Zend/tests/levenshtein_suggest_class.phpt delete mode 100644 Zend/tests/levenshtein_suggest_function.phpt delete mode 100644 Zend/tests/levenshtein_suggest_method.phpt create mode 100644 Zend/tests/name_suggestion/suggestion_undefined_class.phpt create mode 100644 Zend/tests/name_suggestion/suggestion_undefined_fcc.phpt create mode 100644 Zend/tests/name_suggestion/suggestion_undefined_function.phpt create mode 100644 Zend/tests/name_suggestion/suggestion_undefined_method.phpt diff --git a/Zend/tests/levenshtein_suggest_class.phpt b/Zend/tests/levenshtein_suggest_class.phpt deleted file mode 100644 index 9e2a42aaf7da..000000000000 --- a/Zend/tests/levenshtein_suggest_class.phpt +++ /dev/null @@ -1,35 +0,0 @@ ---TEST-- -Levenshtein suggestion for undefined class, interface and trait lookups ---FILE-- -getMessage(), "\n"; } -try { new StdClas(); } catch (Error $e) { echo $e->getMessage(), "\n"; } - -// One edit away for a longer name — should suggest -try { new ArrayIteratr(); } catch (Error $e) { echo $e->getMessage(), "\n"; } - -// Two edits away for a name >= 8 chars — adaptive threshold should suggest -try { new ArryObjct(); } catch (Error $e) { echo $e->getMessage(), "\n"; } - -// Completely wrong name — no suggestion -try { new Unicorn(); } catch (Error $e) { echo $e->getMessage(), "\n"; } - -// Interface -try { - $c = new class() implements Countble {}; -} catch (Error $e) { echo $e->getMessage(), "\n"; } - -// Trait -try { - eval('class T { use NonExistntTrait; }'); -} catch (Error $e) { echo $e->getMessage(), "\n"; } -?> ---EXPECTF-- -Class "ArrayObjekt" not found (did you mean ArrayObject?) -Class "StdClas" not found (did you mean stdClass?) -Class "ArrayIteratr" not found (did you mean ArrayIterator?) -Class "ArryObjct" not found (did you mean ArrayObject?) -Class "Unicorn" not found -Interface "Countble" not found (did you mean Countable?) -Trait "NonExistntTrait" not found diff --git a/Zend/tests/levenshtein_suggest_function.phpt b/Zend/tests/levenshtein_suggest_function.phpt deleted file mode 100644 index 7b1d5b3a29ad..000000000000 --- a/Zend/tests/levenshtein_suggest_function.phpt +++ /dev/null @@ -1,26 +0,0 @@ ---TEST-- -Levenshtein suggestion for undefined function calls ---FILE-- -getMessage(), "\n"; } - -// One edit away for a longer name — should suggest -try { array_pussh([], 1); } catch (Error $e) { echo $e->getMessage(), "\n"; } - -// Two edits away for a name >= 8 chars — adaptive threshold should suggest -try { arry_pussh([], 1); } catch (Error $e) { echo $e->getMessage(), "\n"; } - -// Completely wrong name — no suggestion -try { nonexistentfunc(); } catch (Error $e) { echo $e->getMessage(), "\n"; } - -// Dynamic call — same suggestion logic applies -$f = "strlenx"; -try { $f("x"); } catch (Error $e) { echo $e->getMessage(), "\n"; } -?> ---EXPECT-- -Call to undefined function strlenn() (did you mean strlen?) -Call to undefined function array_pussh() (did you mean array_push?) -Call to undefined function arry_pussh() (did you mean array_push?) -Call to undefined function nonexistentfunc() -Call to undefined function strlenx() (did you mean strlen?) diff --git a/Zend/tests/levenshtein_suggest_method.phpt b/Zend/tests/levenshtein_suggest_method.phpt deleted file mode 100644 index d182d7d04383..000000000000 --- a/Zend/tests/levenshtein_suggest_method.phpt +++ /dev/null @@ -1,29 +0,0 @@ ---TEST-- -Levenshtein suggestion for undefined method calls ---FILE-- -addd(1, 2); } catch (Error $e) { echo $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->getMessage(), "\n"; } - -// Completely wrong name — no suggestion -try { $calc->nonexistent(); } catch (Error $e) { echo $e->getMessage(), "\n"; } - -// Static call — same logic applies -try { Calculator::addd(1, 2); } catch (Error $e) { echo $e->getMessage(), "\n"; } -?> ---EXPECT-- -Call to undefined method Calculator::addd() (did you mean add?) -Call to undefined method Calculator::subtarct() (did you mean subtract?) -Call to undefined method Calculator::nonexistent() -Call to undefined method Calculator::addd() (did you mean add?) diff --git a/Zend/tests/name_suggestion/suggestion_undefined_class.phpt b/Zend/tests/name_suggestion/suggestion_undefined_class.phpt new file mode 100644 index 000000000000..e5da8e9c0544 --- /dev/null +++ b/Zend/tests/name_suggestion/suggestion_undefined_class.phpt @@ -0,0 +1,35 @@ +--TEST-- +Levenshtein suggestion for undefined class, interface and trait lookups +--FILE-- +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 diff --git a/Zend/tests/name_suggestion/suggestion_undefined_fcc.phpt b/Zend/tests/name_suggestion/suggestion_undefined_fcc.phpt new file mode 100644 index 000000000000..bf77dab81527 --- /dev/null +++ b/Zend/tests/name_suggestion/suggestion_undefined_fcc.phpt @@ -0,0 +1,28 @@ +--TEST-- +Levenshtein suggestion for undefined function in FCC context +--FILE-- +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?) diff --git a/Zend/tests/name_suggestion/suggestion_undefined_function.phpt b/Zend/tests/name_suggestion/suggestion_undefined_function.phpt new file mode 100644 index 000000000000..25da0d4bc077 --- /dev/null +++ b/Zend/tests/name_suggestion/suggestion_undefined_function.phpt @@ -0,0 +1,26 @@ +--TEST-- +Levenshtein suggestion for undefined function calls +--FILE-- +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?) diff --git a/Zend/tests/name_suggestion/suggestion_undefined_method.phpt b/Zend/tests/name_suggestion/suggestion_undefined_method.phpt new file mode 100644 index 000000000000..6b9d6801533a --- /dev/null +++ b/Zend/tests/name_suggestion/suggestion_undefined_method.phpt @@ -0,0 +1,29 @@ +--TEST-- +Levenshtein suggestion for undefined method calls +--FILE-- +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?)