From 4f0b5adb83a37dbd4f28e87f30f9424c884c06e5 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Sun, 17 May 2026 13:35:11 +0200 Subject: [PATCH 1/3] 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 1b28ce25fe37..ad282070f058 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2551,9 +2551,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) @@ -5154,7 +5198,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 71e0c56a51c8..b65268adf373 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1687,6 +1687,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) { @@ -1700,12 +1732,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 52fca0cd4346..3a2f191417a1 100644 --- a/Zend/zend_string.c +++ b/Zend/zend_string.c @@ -52,6 +52,43 @@ ZEND_API zend_string *zend_empty_string = NULL; ZEND_API zend_string *zend_one_char_string[256]; ZEND_API zend_string **zend_known_strings = NULL; +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 971902cabd2b..29677bdb5c17 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -44,6 +44,8 @@ ZEND_API zend_ulong ZEND_FASTCALL zend_string_hash_func(zend_string *str); ZEND_API zend_ulong ZEND_FASTCALL zend_hash_func(const char *str, size_t len); ZEND_API zend_string* ZEND_FASTCALL zend_interned_string_find_permanent(zend_string *str); +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 a0bfad488f85..d6e8e0dfcbeb 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 cedc735bbb1e..deaeae82dc4f 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -772,7 +772,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(); } @@ -53611,7 +53618,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 54ccec4340c098948a8f32d5b3ca313373c2fb6f Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Sat, 4 Jul 2026 17:12:07 +0200 Subject: [PATCH 2/3] 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 ad282070f058..15a030e5402d 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2600,6 +2600,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 ba48b19bcfe1..48abde56c251 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -511,6 +511,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 d6e8e0dfcbeb..1ad3adaf6705 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 deaeae82dc4f..c7de67d72578 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -772,14 +772,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(); } @@ -53618,14 +53611,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 d62ef95b5513..ba0294accb91 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -2166,21 +2166,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 ad14e873b73c90ced8e6932ba13999c0ebebf822 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Sat, 4 Jul 2026 17:45:24 +0200 Subject: [PATCH 3/3] 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 15a030e5402d..2114b770188d 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2551,17 +2551,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; @@ -2574,17 +2576,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 b65268adf373..d95ec72d7cab 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1692,12 +1692,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; @@ -1710,8 +1712,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 3a2f191417a1..5d4563a15455 100644 --- a/Zend/zend_string.c +++ b/Zend/zend_string.c @@ -66,8 +66,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;