doc-links(php): a bare name() in a type's doc is the type's method

phpDocumentor reads `@see name()` as a function. Authors write it for the
documented class's own method: symfony has 97 such references that ended as
external rows (Command `@see setCode()`, ProgressBar `{@see resumeAll()}`),
and one that bound a global build-script function (ProcessHelper `@see
run()`, meaning its own run()). In the doc of a type or of one of its
members, a bare name() is now a method the type declares or inherits; a
`use function` import of the name still wins, and outside a type the
function rules stay. The ambiguity guard this replaces is gone.

Proof (ship build, Apple M5 Pro / 48 GB): symfony 9917e115 +91 edges
(external bare name() rows 97 -> 6), cakephp a75828db +6 (7 -> 1); a sample
of 14 new edges read against their lines: all correct, inherited methods
included (UploadedFile -> File::guessExtension).

Test: doc_mentions_php_current_class (the method wins over a global function
of the same name; the documented method itself is a self-mention), red with
the rule reverted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-10 19:55:29 +02:00
1 parent 2fdcbfe288
commit c5c400a1cb
2 files changed
+33 -28

No files matched your search

+13 -24
View File
@@ -14,10 +14,10 @@
* go beyond that tool, as PHP itself has them: an unqualified function name
* is taken from a `use function` import, else from the current namespace,
* else from the global namespace; and `self` and `static` name the type the
* doc sits in, `parent` that type's superclass. A bare `name()` that would
* bind a function but is written in a type that declares or inherits a
* method of that name is ambiguous: the method or the function, and the
* reference does not say which. Type, function
* doc sits in, `parent` that type's superclass. Beyond both, as authors
* write it: a bare `name()` in the doc of a type (or of its members) that
* declares or inherits a method of that name is that method; a `use
* function` import of the name still wins. Type, function
* and method names compare without case, properties and constants with it.
* The global fallback cannot see a function a dependency declares in the
* current namespace: a reference to one binds the global function of that
@@ -833,17 +833,6 @@ static pr_res_t pr_find_func(const pr_ctx_t *c, const char *fqn, size_t len) {
return r;
}
/* True when the type the doc sits in declares or inherits a method named
* like the reference. */
static bool pr_type_has_method(const pr_ctx_t *c, const pr_ref_t *r) {
if (c->cur < 0) {
return false;
}
pr_search_t s = {
.c = c, .want = 'm', .name = r->cls, .len = r->cls_len, .owner = CBM_NOT_FOUND};
return pr_search(&s, c->cur, 0);
}
static pr_res_t pr_function(const pr_ctx_t *c, const pr_ref_t *r) {
char fqn[PR_NAME_MAX];
if (r->cls[0] == '\\' || memchr(r->cls, '\\', r->cls_len)) {
@@ -868,6 +857,14 @@ static pr_res_t pr_function(const pr_ctx_t *c, const pr_ref_t *r) {
return res;
}
}
if (c->cur >= 0) {
/* a method the documented type declares or inherits */
pr_search_t s = {
.c = c, .want = 'm', .name = r->cls, .len = r->cls_len, .owner = CBM_NOT_FOUND};
if (pr_search(&s, c->cur, 0)) {
return pr_member_node(c, &s);
}
}
pr_res_t res = pr_mk(PR_NONE);
if (c->ns_len) {
int n = snprintf(fqn, sizeof(fqn), "%.*s\\%.*s", (int)c->ns_len, c->ns, (int)r->cls_len,
@@ -877,15 +874,7 @@ static pr_res_t pr_function(const pr_ctx_t *c, const pr_ref_t *r) {
if (res.kind == PR_NONE) {
res = pr_find_func(c, r->cls, r->cls_len); /* PHP's global fallback */
}
if (res.kind == PR_NONE) {
return pr_mk(PR_EXTERNAL); /* a built-in function */
}
if ((res.kind == PR_NODE || res.kind == PR_GAP) && pr_type_has_method(c, r)) {
/* written in a type that has a method of that name: the method or
* this function, and the reference does not pick one */
return pr_mk(PR_AMBIGUOUS);
}
return res;
return res.kind == PR_NONE ? pr_mk(PR_EXTERNAL) : res; /* none: a built-in */
}
/* ── Outcomes ────────────────────────────────────────────────────── */
+20 -4
View File
@@ -216,6 +216,7 @@ static void php_write_repo(const char *repo) {
"function global_helper() {}\n"
"function shadowed() {}\n"
"function other() {}\n"
"function size() {}\n"
"class GlobalThing {}\n");
th_write_file(
TH_PATH(repo, "src/Model/User.php"),
@@ -342,10 +343,6 @@ TEST(doc_mentions_php_global_fallback) {
ASSERT_TRUE(php_edge(db, o, "src.Model.User.shadowed"));
ASSERT_TRUE(php_edge(db, o, "src.global.global_helper"));
ASSERT_TRUE(php_edge(db, o, "src.Model.User.helper_here"));
/* a function named like a method of the documented type: two readings;
* no such function: PHP's built-ins */
ASSERT_TRUE(php_row(db, "other()", "ambiguous"));
ASSERT_TRUE(php_row(db, "size()", "external"));
char props[512];
int n = 0;
dm_edge(db, o, "src.global.shadowed", props, sizeof(props), &n);
@@ -369,6 +366,24 @@ TEST(doc_mentions_php_self_parent) {
PASS();
}
/* A bare name() in a type's doc: the type's own method first, though a
* global function has the name too. */
TEST(doc_mentions_php_current_class) {
ASSERT_TRUE(php_indexed());
const char *db = g_php_db;
const char *o = "src.Model.User.User.other";
ASSERT_TRUE(php_edge(db, o, "src.Model.User.User.size"));
char props[512];
int n = 0;
dm_edge(db, o, "src.global.size", props, sizeof(props), &n);
ASSERT_EQ(n, 0);
/* the documented method itself: a self-mention, neither edge nor row */
ASSERT_TRUE(php_row(db, "other()", ""));
dm_edge(db, o, "src.global.other", props, sizeof(props), &n);
ASSERT_EQ(n, 0);
PASS();
}
static void php_indexed_cleanup(void) {
if (g_php_db[0]) {
th_cleanup(g_php_tmp);
@@ -463,6 +478,7 @@ SUITE(doc_mentions_php) {
RUN_TEST(doc_mentions_php_resolution);
RUN_TEST(doc_mentions_php_global_fallback);
RUN_TEST(doc_mentions_php_self_parent);
RUN_TEST(doc_mentions_php_current_class);
php_indexed_cleanup();
RUN_TEST(doc_mentions_php_incremental_equals_full);
RUN_TEST(doc_mentions_php_workers_agree);