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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Zend/tests/bug69315.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion Zend/tests/exceptions/bug31102.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Caught: Test1::__construct
Caught: {closure:%s:%d}
{closure:%s:%d}(Test3,3)

Fatal error: Uncaught Error: Class "Test3" not found in %s:%d
Fatal error: Uncaught Error: Class "Test3" not found (did you mean Test1?) in %s:%d
Stack trace:
#0 %s(%d): eval()
#1 {main}
Expand Down
35 changes: 35 additions & 0 deletions Zend/tests/levenshtein_suggest_class.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--TEST--
Levenshtein suggestion for undefined class, interface and trait lookups
--FILE--
<?php
// One edit away — should suggest
try { new ArrayObjekt(); } catch (Error $e) { echo $e->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
26 changes: 26 additions & 0 deletions Zend/tests/levenshtein_suggest_function.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Levenshtein suggestion for undefined function calls
--FILE--
<?php
// One edit away — should suggest
try { strlenn("x"); } catch (Error $e) { echo $e->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()?)
29 changes: 29 additions & 0 deletions Zend/tests/levenshtein_suggest_method.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
Levenshtein suggestion for undefined method calls
--FILE--
<?php
class Calculator {
public function add(int $a, int $b): int { return $a + $b; }
public function subtract(int $a, int $b): int { return $a - $b; }
public function multiply(int $a, int $b): int { return $a * $b; }
}

$calc = new Calculator();

// One edit away — should suggest
try { $calc->addd(1, 2); } catch (Error $e) { echo $e->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()?)
4 changes: 2 additions & 2 deletions Zend/tests/nullsafe_operator/003.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ string(3) "bar"
Warning: Undefined property: Foo::$baz in %s.php on line 20
NULL
string(3) "qux"
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()?)"
2 changes: 1 addition & 1 deletion Zend/tests/nullsafe_operator/033.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ string(3) "bar"
Warning: Undefined property: Foo::$baz in %s.php on line 20
string(0) ""
string(3) "qux"
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
Expand Down
73 changes: 71 additions & 2 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions Zend/zend_execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
48 changes: 43 additions & 5 deletions Zend/zend_execute_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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));
}
}

Expand Down
37 changes: 37 additions & 0 deletions Zend/zend_string.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 2 additions & 0 deletions Zend/zend_string.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
4 changes: 2 additions & 2 deletions Zend/zend_vm_execute.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 3 additions & 11 deletions ext/opcache/jit/zend_jit_ir.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
Loading
Loading