Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
FCC in initializer suggests a similar function name for a missing function.
--FILE--
<?php

const Closure = strlenn(...);

var_dump(Closure);

?>
--EXPECTF--
Fatal error: Uncaught Error: Call to undefined function strlenn() (did you mean strlen?) in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
FCC in default argument suggests a similar function name for a missing function.
--FILE--
<?php

function test(Closure $name = array_pussh(...)) {
var_dump($name);
}

try {
test();
} catch (Error $e) {
echo $e->getMessage(), "\n";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
echo $e->getMessage(), "\n";
echo $e::class, ': ', $e->getMessage(), "\n";

}

?>
--EXPECT--
Call to undefined function array_pussh() (did you mean array_push?)
Comment thread
jorgsowa marked this conversation as resolved.
35 changes: 35 additions & 0 deletions Zend/tests/name_suggestion/suggestion_undefined_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::class, ': ', $e->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
28 changes: 28 additions & 0 deletions Zend/tests/name_suggestion/suggestion_undefined_fcc.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Levenshtein suggestion for undefined function in FCC context
--FILE--
<?php
// Test 1: In default argument position

function test(Closure $name = array_pussh(...)) {
var_dump($name);
}

try {
test();
} catch (Error $e) {
echo $e::class, ': ', $e->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?)
26 changes: 26 additions & 0 deletions Zend/tests/name_suggestion/suggestion_undefined_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::class, ': ', $e->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?)
29 changes: 29 additions & 0 deletions Zend/tests/name_suggestion/suggestion_undefined_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::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?)
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(56) "Call to undefined method Foo::quux() (did you mean qux?)"
string(3) "bar"

Warning: Undefined property: Foo::$baz in %s.php on line 29
NULL
string(3) "qux"
string(36) "Call to undefined method Foo::quux()"
string(56) "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(56) "Call to undefined method Foo::quux() (did you mean qux?)"
string(3) "bar"

Warning: Undefined property: Foo::$baz in %s.php on line 29
Expand Down
10 changes: 9 additions & 1 deletion Zend/zend_ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1252,7 +1253,14 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
}

if (!fptr) {
zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(function_name));
zend_string *lc_name = zend_string_tolower(function_name);
zend_string *suggestion = zend_find_similar_function(ZSTR_VAL(lc_name), ZSTR_LEN(lc_name));
if (suggestion) {
zend_throw_error(NULL, "Call to undefined function %s() (did you mean %s?)", ZSTR_VAL(function_name), ZSTR_VAL(suggestion));
} else {
zend_throw_error(NULL, "Call to undefined function %s()", ZSTR_VAL(function_name));
}
zend_string_release(lc_name);
return FAILURE;
}

Expand Down
73 changes: 71 additions & 2 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -2558,9 +2558,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;
}

ZEND_API 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 @@ -5167,7 +5231,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
2 changes: 2 additions & 0 deletions Zend/zend_execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@ 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_string *zend_find_similar_function(const char *lcname, size_t lcname_len);
ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_non_static_method_call(const zend_function *fbc);

ZEND_API void zend_frameless_observed_call(zend_execute_data *execute_data);
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 @@ -1699,6 +1699,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 @@ -1712,12 +1747,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
Loading
Loading