From cba3e19b591a861d0979caaa938fc27021c8fa0d Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Fri, 16 Oct 2026 23:39:42 +0200 Subject: [PATCH] Zend: suggest similar names for undefined function/method/class errors When a function, method, class, interface or trait cannot be found, append a "(did you mean X?)" hint naming the closest existing match by Levenshtein distance: Call to undefined function strlenn() (did you mean strlen?) Call to undefined method Calculator::addd() (did you mean add?) Class "ArrayObjekt" not found (did you mean ArrayObject?) Names shorter than 3 characters are not matched. The allowed distance is 1, or 2 for names of 8 or more characters. Matching is case-insensitive and the suggestion uses the declared casing. Function suggestions cover direct and dynamic calls, constant expressions and the opcache JIT. For unqualified calls in a namespace, names in the current namespace are tried first, falling back to global names, mirroring function lookup. --- UPGRADING | 6 ++ Zend/tests/exceptions/bug31102.phpt | 2 +- Zend/tests/is_callable/bug69315.phpt | 2 +- .../suggestion_undefined_class.phpt | 35 ++++++++ .../suggestion_undefined_fcc.phpt | 28 ++++++ .../suggestion_undefined_function.phpt | 26 ++++++ ...gestion_undefined_function_namespaced.phpt | 33 +++++++ .../suggestion_undefined_method.phpt | 29 +++++++ Zend/tests/nullsafe_operator/003.phpt | 4 +- Zend/tests/nullsafe_operator/033.phpt | 2 +- Zend/zend_ast.c | 16 +++- Zend/zend_execute.c | 87 ++++++++++++++++++- Zend/zend_execute.h | 3 + Zend/zend_execute_API.c | 13 +-- Zend/zend_string.c | 56 ++++++++++++ Zend/zend_string.h | 2 + Zend/zend_vm_def.h | 4 +- Zend/zend_vm_execute.h | 8 +- ext/opcache/jit/zend_jit_ir.c | 15 +--- ext/opcache/tests/bug76796.phpt | 2 +- .../jit/undefined_function_suggestion.phpt | 29 +++++++ ext/spl/tests/autoloading/bug73896.phpt | 2 +- ext/standard/levenshtein.c | 60 +------------ 23 files changed, 367 insertions(+), 97 deletions(-) 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_function_namespaced.phpt create mode 100644 Zend/tests/name_suggestion/suggestion_undefined_method.phpt create mode 100644 ext/opcache/tests/jit/undefined_function_suggestion.phpt diff --git a/UPGRADING b/UPGRADING index fd3efda9df7a..89573c5ba520 100644 --- a/UPGRADING +++ b/UPGRADING @@ -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 ======================================== diff --git a/Zend/tests/exceptions/bug31102.phpt b/Zend/tests/exceptions/bug31102.phpt index 17beec6ba389..aded6736a32e 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/is_callable/bug69315.phpt b/Zend/tests/is_callable/bug69315.phpt index f20d0d1c0357..9b640f9f5cfa 100644 --- a/Zend/tests/is_callable/bug69315.phpt +++ b/Zend/tests/is_callable/bug69315.phpt @@ -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() 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..01cc423bff7d --- /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_function_namespaced.phpt b/Zend/tests/name_suggestion/suggestion_undefined_function_namespaced.phpt new file mode 100644 index 000000000000..36b073642f17 --- /dev/null +++ b/Zend/tests/name_suggestion/suggestion_undefined_function_namespaced.phpt @@ -0,0 +1,33 @@ +--TEST-- +Levenshtein suggestion for undefined function calls in a namespace +--FILE-- +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?) 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?) diff --git a/Zend/tests/nullsafe_operator/003.phpt b/Zend/tests/nullsafe_operator/003.phpt index 4164ffab9f97..726208b91ca6 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" -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?) diff --git a/Zend/tests/nullsafe_operator/033.phpt b/Zend/tests/nullsafe_operator/033.phpt index 58579d325e04..9b52d8bfae1a 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" -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 diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index b81296599d6d..d9bfbc779b5d 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" @@ -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; } diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index e7923d76996c..661ac3f8653a 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -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) @@ -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; diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index 78f33e2b5bd8..10bfed949452 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -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); diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index beb3f9f995af..bd94612f9aa9 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -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)); } } diff --git a/Zend/zend_string.c b/Zend/zend_string.c index b1b4a0e17a69..717210aeaeca 100644 --- a/Zend/zend_string.c +++ b/Zend/zend_string.c @@ -63,6 +63,62 @@ 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 cost_ins, zend_long cost_rep, zend_long cost_del) +{ + zend_long *p1, *p2, *tmp; + zend_long c0, c1, c2; + size_t i1, i2; + + if (l1 == 0) { + return l2 * cost_ins; + } + if (l2 == 0) { + return l1 * cost_del; + } + + /* With equal costs the distance is symmetric, so use the shorter string for the rows */ + if (l1 < l2 && cost_ins == cost_rep && cost_rep == cost_del) { + const char *tmp_s = s1; + s1 = s2; + s2 = tmp_s; + size_t tmp_l = l1; + l1 = l2; + l2 = tmp_l; + } + + p1 = safe_emalloc((l2 + 1), sizeof(zend_long), 0); + p2 = safe_emalloc((l2 + 1), sizeof(zend_long), 0); + + for (i2 = 0; i2 <= l2; i2++) { + p1[i2] = i2 * cost_ins; + } + for (i1 = 0; i1 < l1; i1++) { + p2[0] = p1[0] + cost_del; + + for (i2 = 0; i2 < l2; i2++) { + c0 = p1[i2] + ((s1[i1] == s2[i2]) ? 0 : cost_rep); + c1 = p1[i2 + 1] + cost_del; + if (c1 < c0) { + c0 = c1; + } + c2 = p2[i2] + cost_ins; + if (c2 < c0) { + c0 = c2; + } + p2[i2 + 1] = c0; + } + 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 dfdf73a2e356..8ba0a2988a57 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_long cost_ins, zend_long cost_rep, zend_long cost_del); + 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 cf8d626bbdbc..45214743a647 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -998,11 +998,9 @@ ZEND_VM_COLD_HELPER(zend_this_not_in_object_context_helper, ANY, ANY) ZEND_VM_COLD_HELPER(zend_undefined_function_helper, ANY, ANY) { USE_OPLINE - zval *function_name; SAVE_OPLINE(); - function_name = RT_CONSTANT(opline, opline->op2); - zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(function_name)); + zend_undefined_function_error(opline); HANDLE_EXCEPTION(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 7fee6dbde454..2264b4e0bcae 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -769,11 +769,9 @@ static zend_never_inline ZEND_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_F static zend_never_inline ZEND_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE - zval *function_name; SAVE_OPLINE(); - function_name = RT_CONSTANT(opline, opline->op2); - zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(function_name)); + zend_undefined_function_error(opline); HANDLE_EXCEPTION(); } @@ -53782,11 +53780,9 @@ static zend_never_inline ZEND_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_C static zend_never_inline ZEND_COLD ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_undefined_function_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE - zval *function_name; SAVE_OPLINE(); - function_name = RT_CONSTANT(opline, opline->op2); - zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(function_name)); + zend_undefined_function_error(opline); HANDLE_EXCEPTION(); } diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index bfa5297aa6db..f7105da9331f 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -2121,21 +2121,8 @@ 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))); - if (sizeof(void*) == 8) { - arg3 = ir_LOAD_A(ir_ADD_A(ref, ir_SEXT_A(arg3))); - } else { - arg3 = ir_LOAD_A(arg3); - } - 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), ref); ir_IJMP(jit_STUB_ADDR(jit, jit_stub_exception_handler)); diff --git a/ext/opcache/tests/bug76796.phpt b/ext/opcache/tests/bug76796.phpt index bc10f1fc6289..5dd22522b013 100644 --- a/ext/opcache/tests/bug76796.phpt +++ b/ext/opcache/tests/bug76796.phpt @@ -18,4 +18,4 @@ try { ?> --EXPECT-- -Error: Call to undefined function strpos() +Error: Call to undefined function strpos() (did you mean stripos?) diff --git a/ext/opcache/tests/jit/undefined_function_suggestion.phpt b/ext/opcache/tests/jit/undefined_function_suggestion.phpt new file mode 100644 index 000000000000..812fb9307dd3 --- /dev/null +++ b/ext/opcache/tests/jit/undefined_function_suggestion.phpt @@ -0,0 +1,29 @@ +--TEST-- +JIT: undefined function errors include a name suggestion +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=1205 +opcache.jit_buffer_size=64M +--FILE-- +getMessage(), "\n"; } + try { strlenn("x"); } catch (\Error $e) { echo $e->getMessage(), "\n"; } + try { helpr(); } catch (\Error $e) { echo $e->getMessage(), "\n"; } + try { nonexistentfunc(); } catch (\Error $e) { echo $e->getMessage(), "\n"; } +} + +test(); +?> +--EXPECT-- +Call to undefined function strlenn() (did you mean strlen?) +Call to undefined function App\strlenn() (did you mean strlen?) +Call to undefined function App\helpr() (did you mean App\helper?) +Call to undefined function App\nonexistentfunc() diff --git a/ext/spl/tests/autoloading/bug73896.phpt b/ext/spl/tests/autoloading/bug73896.phpt index c31d3285803d..1eb39c9926e8 100644 --- a/ext/spl/tests/autoloading/bug73896.phpt +++ b/ext/spl/tests/autoloading/bug73896.phpt @@ -35,4 +35,4 @@ try { } ?> --EXPECT-- -Error: Class "teException" not found +Error: Class "teException" not found (did you mean "Exception"?) diff --git a/ext/standard/levenshtein.c b/ext/standard/levenshtein.c index 9ac7689d3bf5..d815dbd74354 100644 --- a/ext/standard/levenshtein.c +++ b/ext/standard/levenshtein.c @@ -14,64 +14,6 @@ #include "php.h" -/* {{{ reference_levdist - * reference implementation, only optimized for memory usage, not speed */ -static zend_long reference_levdist(const zend_string *string1, const zend_string *string2, zend_long cost_ins, zend_long cost_rep, zend_long cost_del ) -{ - zend_long *p1, *p2, *tmp; - zend_long c0, c1, c2; - size_t i1, i2; - - if (ZSTR_LEN(string1) == 0) { - return ZSTR_LEN(string2) * cost_ins; - } - if (ZSTR_LEN(string2) == 0) { - return ZSTR_LEN(string1) * cost_del; - } - - /* When all costs are equal, levenshtein fulfills the requirements of a metric, which means - * that the distance is symmetric. If string1 is shorter than string 2 we can save memory (and CPU time) - * by having shorter rows (p1 & p2). */ - if (ZSTR_LEN(string1) < ZSTR_LEN(string2) && cost_ins == cost_rep && cost_rep == cost_del) { - const zend_string *tmp = string1; - string1 = string2; - string2 = tmp; - } - - p1 = safe_emalloc((ZSTR_LEN(string2) + 1), sizeof(zend_long), 0); - p2 = safe_emalloc((ZSTR_LEN(string2) + 1), sizeof(zend_long), 0); - - for (i2 = 0; i2 <= ZSTR_LEN(string2); i2++) { - p1[i2] = i2 * cost_ins; - } - for (i1 = 0; i1 < ZSTR_LEN(string1) ; i1++) { - p2[0] = p1[0] + cost_del; - - for (i2 = 0; i2 < ZSTR_LEN(string2); i2++) { - c0 = p1[i2] + ((ZSTR_VAL(string1)[i1] == ZSTR_VAL(string2)[i2]) ? 0 : cost_rep); - c1 = p1[i2 + 1] + cost_del; - if (c1 < c0) { - c0 = c1; - } - c2 = p2[i2] + cost_ins; - if (c2 < c0) { - c0 = c2; - } - p2[i2 + 1] = c0; - } - tmp = p1; - p1 = p2; - p2 = tmp; - } - c0 = p1[ZSTR_LEN(string2)]; - - efree(p1); - efree(p2); - - return c0; -} -/* }}} */ - /* {{{ Calculate Levenshtein distance between two strings */ PHP_FUNCTION(levenshtein) { @@ -89,6 +31,6 @@ PHP_FUNCTION(levenshtein) Z_PARAM_LONG(cost_del) ZEND_PARSE_PARAMETERS_END(); - RETURN_LONG(reference_levdist(string1, string2, cost_ins, cost_rep, cost_del)); + RETURN_LONG(zend_levenshtein(ZSTR_VAL(string1), ZSTR_LEN(string1), ZSTR_VAL(string2), ZSTR_LEN(string2), cost_ins, cost_rep, cost_del)); } /* }}} */