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..2114b770188d 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -2551,9 +2551,73 @@ ZEND_API ZEND_COLD zval* ZEND_FASTCALL zend_undefined_index_write(HashTable *ht, return retval; } +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_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; + 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_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(&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)); + } +} + +/* 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) @@ -5154,7 +5218,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.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_execute_API.c b/Zend/zend_execute_API.c index 71e0c56a51c8..d95ec72d7cab 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1687,6 +1687,41 @@ 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_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; + 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_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)); + } + 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 +1735,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..5d4563a15455 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 = 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; + } + 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..1ad3adaf6705 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -1000,7 +1000,7 @@ 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)); + zend_undefined_function_error(function_name); HANDLE_EXCEPTION(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index cedc735bbb1e..c7de67d72578 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -772,7 +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); - zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(function_name)); + zend_undefined_function_error(function_name); HANDLE_EXCEPTION(); } @@ -53611,7 +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); - 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)); 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?)