doc-links(php): PHPDoc @see references become MENTIONS edges, held until their audit

A PHP doc block names code with `@see Ref` at the start of a tag line (a
description may follow) and with inline `{@see Ref}`, both family `see`.
doclink_php.c harvests them (code spans and fenced blocks hold none; @link,
@uses and the other tags are not harvested; a URL is the core's href family)
and writes each file's scope: namespace blocks, use imports, types with
their supertypes already qualified in their own file, members, functions.
The persisted scope drops lines and imports: other files read names only.
Declarations inside a top-level conditional count as top level
(`if (!function_exists('f')) { function f() {} }`), never a function body.

doc_links_php.c resolves a reference as a FQSEN with PHP's own name rules:
- a leading backslash is taken as written; otherwise the first segment is
  replaced when it is the alias of a class `use` of the namespace block the
  reference is written in, else the block's namespace is put in front
  (phpDocumentor's resolver);
- 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/static/parent name the documented
  type and its 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; the reference does not say which);
- members are looked up on the type, its traits, its superclass and its
  interfaces (depth first); type, function and method names compare without
  case, properties and constants with it;
- properties, class constants and enum cases have no graph node: graph_gap;
  so has a type or function the graph folds into another one of the same
  name in its file (two namespace blocks), which the scope flags;
- a name the repository does not declare is external outside the
  repository's namespace roots (built-ins, Composer dependencies), else
  missing. Residual: a function a dependency declares in the current
  namespace is invisible, so the fallback would bind a global function of
  that name.
There is no scope_delta hook yet: a declaration change is a full run.

The family is held (ships=false) until its held-out audit passes; the suite
ships it through the test seam.

Proof (ship build, Apple M5 Pro / 48 GB, base 196ca71cda):
- koel 04ed8beaec34 vs the field test's prototype: 4 of its 5 pairs bound
  alike; the fifth sits in a class constant's doc block, which has no node.
- cakephp a75828db: 342 edge mentions + 39 unresolved rows = every one of
  the 381 non-URL references in function and type doc blocks; 36 more sit
  in property and constant doc blocks (no node, not harvested).
- symfony 9917e115: 452 edge mentions (434 edges) + 205 rows = all 657; 24
  in property, constant and other doc blocks.
- edges only through the global fallback: 1 (cakephp `@see debug()` in
  Cake\Core, the global debug() helper: correct); one more on symfony was
  wrong (`@see run()` meaning the class's own method bound a global script
  function) and the method-name rule above makes it ambiguous. Through
  self/static/parent: 20, all correct (12 + 5 self, 3 parent).
- a random sample of 24 edges (12 per corpus) read against their source
  lines: all correct.

Tests (doc_mentions_php): tokens with their lines; the scope blob, its
persisted form, namespace blocks and conditional declarations; every
resolution rule; the global fallback and self/static/parent each in a test
of its own; incremental equals full (a doc edit and a body edit repair one
file, a removed function is a full run); 60 modules give the same edges and
rows with one worker and several. Each rule red with it reverted (global
fallback, self/static, parent, trait members, the folded-name gap, use
aliases, method names without case, the bare-name method lookup, the
namespace roots, code spans, the method-name guard, conditional
declarations).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-10 16:48:44 +02:00
parent 196ca71cda
commit 7a321cac16
10 changed files with 2438 additions and 3 deletions
+3 -1
View File
@@ -391,6 +391,7 @@ EXTRACTION_SRCS = \
$(CBM_DIR)/doclink_rs.c \
$(CBM_DIR)/doclink_perl.c \
$(CBM_DIR)/doclink_ts.c \
$(CBM_DIR)/doclink_php.c \
$(CBM_DIR)/sql_values.c \
$(CBM_DIR)/helpers.c \
$(CBM_DIR)/cpp_branch_views.c \
@@ -522,6 +523,7 @@ PIPELINE_SRCS = \
src/pipeline/doc_links_rs.c \
src/pipeline/doc_links_perl.c \
src/pipeline/doc_links_ts.c \
src/pipeline/doc_links_php.c \
src/pipeline/doc_links_msbuild.c \
src/pipeline/pass_route_nodes.c \
src/pipeline/pass_enrichment.c \
@@ -784,7 +786,7 @@ TEST_DISCOVER_SRCS = \
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_pipeline_conventions.c tests/test_pipeline_frozen.c tests/test_importance.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_doclinks.c tests/test_doc_mentions.c tests/test_doc_links_md.c tests/test_doc_links_pdf.c tests/test_doc_links_rst.c tests/test_doc_links_adoc.c tests/test_doc_mentions_go.c tests/test_doc_mentions_jvm.c tests/test_doc_mentions_rs.c tests/test_doc_mentions_perl.c tests/test_doc_mentions_ts.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_pipeline_conventions.c tests/test_pipeline_frozen.c tests/test_importance.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_doclinks.c tests/test_doc_mentions.c tests/test_doc_links_md.c tests/test_doc_links_pdf.c tests/test_doc_links_rst.c tests/test_doc_links_adoc.c tests/test_doc_mentions_go.c tests/test_doc_mentions_jvm.c tests/test_doc_mentions_rs.c tests/test_doc_mentions_perl.c tests/test_doc_mentions_ts.c tests/test_doc_mentions_php.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
TEST_WATCHER_SRCS = tests/test_watcher.c
+5
View File
@@ -234,6 +234,11 @@ static const doclink_lang_t DOCLINK_LANGS[] = {
.scan_scope = cbm_doclink_ts_scan_scope,
.scope_tag = CBM_DOCLINK_TS_SCOPE_TAG,
.portable_scope = cbm_doclink_ts_portable_scope},
{.lang = CBM_LANG_PHP,
.parse_doc = cbm_doclink_php_parse_doc,
.scan_scope = cbm_doclink_php_scan_scope,
.scope_tag = CBM_DOCLINK_PHP_SCOPE_TAG,
.portable_scope = cbm_doclink_php_portable_scope},
};
static const doclink_lang_t *doclink_lang(CBMLanguage lang) {
+36
View File
@@ -104,6 +104,8 @@
X(CBM_DOCLINK_TS_LINK, CBM_LANG_TYPESCRIPT, "link", false, false, "MENTIONS") \
X(CBM_DOCLINK_TS_LINKCODE, CBM_LANG_TYPESCRIPT, "linkcode", false, false, "MENTIONS") \
X(CBM_DOCLINK_TS_SEE, CBM_LANG_TYPESCRIPT, "see", false, false, "MENTIONS") \
/* php (doclink_php.c): PHPDoc @see and {@see}; held until their audit passes */ \
X(CBM_DOCLINK_PHP_SEE, CBM_LANG_PHP, "see", false, false, "MENTIONS") \
/* markdown (doclink_md.c): what a document's text names explicitly */ \
X(CBM_DOCLINK_MD_LINK, CBM_LANG_MARKDOWN, "link", false, true, "MENTIONS") \
X(CBM_DOCLINK_MD_PATH, CBM_LANG_MARKDOWN, "path", false, true, "MENTIONS") \
@@ -555,6 +557,40 @@ char *cbm_doclink_ts_portable_scope(const char *scope);
* this length and the resolver reports it as unparseable. */
enum { CBM_DOCLINK_TS_REF_MAX = 512 };
/* ── PHP (doclink_php.c) ───────────────────────────────────────────── */
void cbm_doclink_php_parse_doc(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
uint32_t doc_line);
const char *cbm_doclink_php_scan_scope(CBMExtractCtx *ctx);
char *cbm_doclink_php_portable_scope(const char *scope);
#define CBM_DOCLINK_PHP_SCOPE_TAG "php1"
/* The longest reference a PHP token keeps; a longer one is cut just past this
* length and the resolver reports it as unparseable. */
enum { CBM_DOCLINK_PHP_REF_MAX = 512 };
/* One `use` import of a PHP namespace block: kind 'c' (a class or namespace),
* 'f' (a function), 'k' (a constant); the name without a leading backslash. */
typedef struct {
const char *alias;
size_t alias_len;
const char *name;
size_t name_len;
char kind;
} cbm_doclink_php_use_t;
/* The fully qualified name (no leading backslash) of a type name `ref` (n
* bytes) written in namespace `ns` (ns_len 0: the global one) under these
* imports, as PHP resolves it: a leading backslash is taken as written,
* `namespace\` stands for the current namespace, else the first segment is
* replaced when it is the alias of a class import (compared without case),
* else the namespace is put in front. Writes NUL-terminated into out (cap
* bytes); returns the length, 0 when it does not fit. */
size_t cbm_doclink_php_qualify(char *out, size_t cap, const char *ns, size_t ns_len,
const cbm_doclink_php_use_t *uses, int nuses, const char *ref,
size_t n);
/* ── Perl POD (doclink_perl.c) ─────────────────────────────────────── */
void cbm_doclink_perl_parse_doc(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
+950
View File
@@ -0,0 +1,950 @@
/*
* doclink_php.c — PHP doc-comment references (PHPDoc) and the PHP doc-link
* scope.
*
* Tokens. Only a block comment that opens with a slash and two stars is a
* doc. Its lines lose their leading white space, one `*` and one space. A
* reference is
* - `@see Ref` at the start of a tag line, a description may follow;
* - `{@see Ref}` or `{@see Ref description}` anywhere on a line;
* both family `see`. The reference is the text up to white space (or the
* closing brace), sentence punctuation at its end dropped. A URL is the core's
* href family. Text in a code span (backticks on one line) or in a fenced
* code block holds no reference. @link, @uses and the other tags are not
* harvested.
*
* Scope blob, one record per line after the tag line "php1" (tab-separated):
* N blk name first last a namespace block, numbered from 0 in file order,
* its name ("-": the global namespace) and lines; a
* file without a namespace declaration has none, and
* its one block is number 0
* U blk kind alias name a use import of block blk: kind c (a class or a
* namespace), f (a function), k (a constant); the
* alias as written, the name without its leading
* backslash
* T kind fqn flags first last a class (c), interface (i), trait (t) or enum
* (e) declared at the top level of a block, its fully
* qualified name and lines; flags `d` when the file
* declares another type or function of the same name
* (the graph keeps one node for both), else `-`
* B rel fqn a supertype of the T record before it: rel e
* (extends), i (implements), t (a used trait), the
* name qualified as PHP resolves it in this file
* K kind name a member of that type: m method, p property (no
* `$`; promoted constructor parameters too), c
* constant, e enum case
* F fqn flags a function declared at the top level of a block;
* flags as for T
* "At the top level" includes the branches of a top-level conditional
* (`if (!function_exists('f')) { function f() {} }`), never a function body.
* The persisted blob (cbm_doclink_php_portable_scope) drops the lines of N
* and T and the U records: other files read names only, and every name a
* supertype record holds is already qualified.
*/
#include "doclink.h"
#include "arena.h"
#include "helpers.h"
#include "foundation/constants.h"
#include "foundation/hash_table.h"
#include "foundation/mem_core.h"
#include "tree_sitter/api.h"
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum {
PHP_PREFIX_LEN = 3,
PHP_FENCE_LEN = 3,
PHP_SEE_LEN = 4,
PHP_INLINE_LEN = 5,
PHP_COND_DEPTH = 8, /* nested conditionals followed */
};
/* ── Names ───────────────────────────────────────────────────────── */
static bool php_eq_nocase(const char *a, size_t an, const char *b, size_t bn) {
if (an != bn) {
return false;
}
for (size_t i = 0; i < an; i++) {
if (tolower((unsigned char)a[i]) != tolower((unsigned char)b[i])) {
return false;
}
}
return true;
}
/* out[*w..] += s[0, n); false when it does not fit. */
static bool php_put(char *out, size_t cap, size_t *w, const char *s, size_t n) {
if (*w + n + SKIP_ONE > cap) {
return false;
}
memcpy(out + *w, s, n);
*w += n;
out[*w] = '\0';
return true;
}
size_t cbm_doclink_php_qualify(char *out, size_t cap, const char *ns, size_t ns_len,
const cbm_doclink_php_use_t *uses, int nuses, const char *ref,
size_t n) {
size_t w = 0;
if (cap == 0 || n == 0) {
return 0;
}
out[0] = '\0';
if (ref[0] == '\\') {
return php_put(out, cap, &w, ref + SKIP_ONE, n - SKIP_ONE) ? w : 0;
}
static const char rel[] = "namespace\\";
size_t rl = sizeof(rel) - SKIP_ONE;
if (n > rl && php_eq_nocase(ref, rl, rel, rl)) {
ref += rl; /* namespace\Foo: the current namespace's Foo */
n -= rl;
} else {
const char *bs = memchr(ref, '\\', n);
size_t first = bs ? (size_t)(bs - ref) : n;
for (int i = 0; i < nuses; i++) {
const cbm_doclink_php_use_t *u = &uses[i];
if (u->kind == 'c' && php_eq_nocase(u->alias, u->alias_len, ref, first)) {
bool ok = php_put(out, cap, &w, u->name, u->name_len) &&
php_put(out, cap, &w, ref + first, n - first);
return ok ? w : 0;
}
}
}
bool ok =
!ns_len || (php_put(out, cap, &w, ns, ns_len) && php_put(out, cap, &w, "\\", SKIP_ONE));
return ok && php_put(out, cap, &w, ref, n) ? w : 0;
}
/* ── Tokens ──────────────────────────────────────────────────────── */
typedef struct {
CBMExtractCtx *ctx;
const CBMDefinition *def;
} php_emit_t;
static bool php_is_url(const char *t, size_t n) {
size_t i = 0;
while (i < n && (isalnum((unsigned char)t[i]) || t[i] == '+' || t[i] == '.' || t[i] == '-')) {
i++;
}
if (i > 0 && i + CBM_SZ_3 <= n && memcmp(t + i, "://", CBM_SZ_3) == 0) {
return true;
}
return (n > CBM_SZ_4 && strncmp(t, "www.", CBM_SZ_4) == 0) ||
(n > CBM_SZ_7 && strncmp(t, "mailto:", CBM_SZ_7) == 0);
}
static void php_push(const php_emit_t *em, const char *ref, size_t n, uint32_t line) {
while (n > 0 && (ref[n - SKIP_ONE] == '.' || ref[n - SKIP_ONE] == ',' ||
ref[n - SKIP_ONE] == ';' || ref[n - SKIP_ONE] == ':')) {
n--; /* sentence punctuation */
}
if (n == 0) {
return;
}
if (n > CBM_DOCLINK_PHP_REF_MAX) {
n = CBM_DOCLINK_PHP_REF_MAX + SKIP_ONE; /* clipped: the resolver calls it unparseable */
}
int syntax = php_is_url(ref, n) ? CBM_DOCLINK_HREF : CBM_DOCLINK_PHP_SEE;
char *copy = cbm_arena_strndup(em->ctx->arena, ref, n);
if (!copy) {
return;
}
CBMDocLink link = {
.source_qn = em->def->qualified_name,
.raw = copy,
.line = line,
.def_line = em->def->start_line,
.syntax = (uint16_t)syntax,
};
cbm_doclinks_push(&em->ctx->result->doc_links, em->ctx->arena, link);
}
/* One doc line: its text after the leading `*`, and its source line. */
typedef struct {
const char *p;
size_t n;
uint32_t line;
} php_line_t;
/* The doc's lines, its opening and closing marks cut off. */
static int php_doc_lines(CBMArena *a, const char *doc, uint32_t doc_line, php_line_t **out) {
size_t dl = strlen(doc);
if (dl < PHP_PREFIX_LEN || strncmp(doc, "/**", PHP_PREFIX_LEN) != 0) {
return 0;
}
const char *b = doc + PHP_PREFIX_LEN;
const char *e = doc + dl;
if (e - b >= PAIR_LEN && e[-SKIP_ONE] == '/' && e[-PAIR_LEN] == '*') {
e -= PAIR_LEN;
}
int cap = SKIP_ONE;
for (const char *p = b; p < e; p++) {
cap += *p == '\n';
}
php_line_t *lines = (php_line_t *)cbm_arena_alloc(a, (size_t)cap * sizeof(php_line_t));
if (!lines) {
return 0;
}
int n = 0;
uint32_t line = doc_line;
for (const char *p = b; p <= e && n < cap;) {
const char *le = p;
while (le < e && *le != '\n') {
le++;
}
const char *s = p;
while (s < le && (*s == ' ' || *s == '\t')) {
s++;
}
if (s < le && *s == '*') {
s++;
if (s < le && *s == ' ') {
s++;
}
}
const char *t = le;
while (t > s && (t[-SKIP_ONE] == ' ' || t[-SKIP_ONE] == '\t' || t[-SKIP_ONE] == '\r')) {
t--;
}
lines[n++] = (php_line_t){s, (size_t)(t - s), line};
line++;
if (le >= e) {
break;
}
p = le + SKIP_ONE;
}
*out = lines;
return n;
}
/* Mark the bytes of a line that sit in a code span (backticks paired on the
* line). */
static void php_mask_spans(const php_line_t *ln, bool *mask) {
size_t i = 0;
while (i < ln->n) {
if (ln->p[i] != '`') {
i++;
continue;
}
size_t run = 0;
while (i + run < ln->n && ln->p[i + run] == '`') {
run++;
}
size_t j = i + run;
size_t close = SIZE_MAX;
while (j < ln->n) {
size_t r = 0;
while (j + r < ln->n && ln->p[j + r] == '`') {
r++;
}
if (r == run) {
close = j;
break;
}
j += r ? r : SKIP_ONE;
}
if (close == SIZE_MAX) {
i += run;
continue;
}
for (size_t k = i; k < close + run; k++) {
mask[k] = true;
}
i = close + run;
}
}
/* The first position >= j of p[0, n) that is no white space. */
static size_t php_skip_space(const char *p, size_t j, size_t n) {
while (j < n && isspace((unsigned char)p[j])) {
j++;
}
return j;
}
/* `{@see Ref ...}` anywhere on the line, outside code spans. */
static void php_line_inline(const php_emit_t *em, const php_line_t *ln, const bool *mask) {
for (size_t i = 0; i + PHP_INLINE_LEN <= ln->n; i++) {
if (mask[i] || memcmp(ln->p + i, "{@see", PHP_INLINE_LEN) != 0) {
continue;
}
size_t j = i + PHP_INLINE_LEN;
if (j < ln->n && ln->p[j] != '}' && !isspace((unsigned char)ln->p[j])) {
continue; /* {@seealso} or another word */
}
j = php_skip_space(ln->p, j, ln->n);
size_t k = j;
while (k < ln->n && ln->p[k] != '}' && !isspace((unsigned char)ln->p[k])) {
k++;
}
php_push(em, ln->p + j, k - j, ln->line);
i = k;
}
}
/* `@see Ref` at the start of a tag line. */
static void php_line_see(const php_emit_t *em, const php_line_t *ln) {
if (ln->n < PHP_SEE_LEN || memcmp(ln->p, "@see", PHP_SEE_LEN) != 0 ||
(ln->n > PHP_SEE_LEN && !isspace((unsigned char)ln->p[PHP_SEE_LEN]))) {
return;
}
size_t j = php_skip_space(ln->p, PHP_SEE_LEN, ln->n);
if (j < ln->n && ln->p[j] == '{') {
return; /* @see {@see Ref}: the inline tag is the reference */
}
size_t k = j;
while (k < ln->n && !isspace((unsigned char)ln->p[k])) {
k++;
}
php_push(em, ln->p + j, k - j, ln->line);
}
void cbm_doclink_php_parse_doc(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
uint32_t doc_line) {
if (!ctx || !ctx->result || !def || !doc || !def->qualified_name) {
return;
}
CBMArena *a = ctx->scratch ? ctx->scratch : ctx->arena;
php_line_t *lines = NULL;
int n = php_doc_lines(a, doc, doc_line, &lines);
php_emit_t em = {ctx, def};
bool fence = false;
for (int i = 0; i < n; i++) {
const php_line_t *ln = &lines[i];
if (ln->n >= PHP_FENCE_LEN && memcmp(ln->p, "```", PHP_FENCE_LEN) == 0) {
fence = !fence;
continue;
}
if (fence || ln->n == 0) {
continue;
}
bool *mask = (bool *)cbm_arena_calloc(a, ln->n + SKIP_ONE);
if (!mask) {
return;
}
php_mask_spans(ln, mask);
php_line_see(&em, ln);
php_line_inline(&em, ln, mask);
}
}
/* ── Scope blob ──────────────────────────────────────────────────── */
typedef struct {
CBMArena *a;
char *buf;
size_t len;
size_t cap;
bool oom;
} php_sb_t;
static void php_sb_put(php_sb_t *sb, const char *s, size_t n) {
if (sb->oom) {
return;
}
if (sb->len + n + SKIP_ONE > sb->cap) {
size_t ncap = sb->cap ? sb->cap * PAIR_LEN : CBM_SZ_256;
while (ncap < sb->len + n + SKIP_ONE) {
ncap *= PAIR_LEN;
}
char *grown = (char *)cbm_arena_alloc(sb->a, ncap);
if (!grown) {
sb->oom = true;
return;
}
if (sb->len) {
memcpy(grown, sb->buf, sb->len);
}
sb->buf = grown;
sb->cap = ncap;
}
memcpy(sb->buf + sb->len, s, n);
sb->len += n;
sb->buf[sb->len] = '\0';
}
static void php_sb_puts(php_sb_t *sb, const char *s) {
php_sb_put(sb, s, strlen(s));
}
/* A field: a tab, then the text (never empty: "-" stands for none). */
static void php_sb_field(php_sb_t *sb, const char *s, size_t n) {
php_sb_put(sb, "\t", SKIP_ONE);
php_sb_put(sb, n ? s : "-", n ? n : SKIP_ONE);
}
static void php_sb_num(php_sb_t *sb, uint32_t v) {
char num[CBM_SZ_32];
int n = snprintf(num, sizeof(num), "%u", v);
php_sb_field(sb, num, n > 0 ? (size_t)n : 0);
}
typedef struct {
CBMExtractCtx *ctx;
php_sb_t *sb;
CBMHashTable *names; /* short name of a top-level type or function -> count */
int blk;
const char *ns; /* the current block's namespace ("" for the global one) */
size_t ns_len;
cbm_doclink_php_use_t *uses; /* the current block's */
int nuses;
int cap_uses;
} php_walk_t;
static bool php_kind(TSNode n, const char *k) {
return !ts_node_is_null(n) && strcmp(ts_node_type(n), k) == 0;
}
/* A name node's text without white space (`A \ B` is `A\B`), in the arena. */
static const char *php_name_text(php_walk_t *w, TSNode n, size_t *len) {
uint32_t b = ts_node_start_byte(n);
uint32_t e = ts_node_end_byte(n);
char *out = (char *)cbm_arena_alloc(w->sb->a, (size_t)(e - b) + SKIP_ONE);
if (!out) {
w->sb->oom = true;
*len = 0;
return "";
}
size_t k = 0;
for (uint32_t i = b; i < e; i++) {
char c = w->ctx->source[i];
if (!isspace((unsigned char)c)) {
out[k++] = c;
}
}
out[k] = '\0';
*len = k;
return out;
}
/* All named children of a node, read with one cursor walk (indexing them is
* quadratic). */
static TSNode *php_children(php_walk_t *w, TSNode parent, int *count) {
*count = 0;
uint32_t nc = ts_node_named_child_count(parent);
if (nc == 0) {
return NULL;
}
TSNode *kids = (TSNode *)cbm_arena_alloc(w->sb->a, (size_t)nc * sizeof(TSNode));
if (!kids) {
w->sb->oom = true;
return NULL;
}
TSTreeCursor cur = ts_tree_cursor_new(parent);
for (bool more = ts_tree_cursor_goto_first_child(&cur); more && *count < (int)nc;
more = ts_tree_cursor_goto_next_sibling(&cur)) {
TSNode c = ts_tree_cursor_current_node(&cur);
if (ts_node_is_named(c)) {
kids[(*count)++] = c;
}
}
ts_tree_cursor_delete(&cur);
return kids;
}
/* `function` or `const` among a node's anonymous children: 'f', 'k', else
* `dflt`. */
static char php_use_kind(TSNode n, char dflt) {
uint32_t nc = ts_node_child_count(n);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_child(n, i);
if (ts_node_is_named(c)) {
continue;
}
if (php_kind(c, "function")) {
return 'f';
}
if (php_kind(c, "const")) {
return 'k';
}
}
return dflt;
}
static void php_add_use(php_walk_t *w, char kind, const char *alias, size_t alias_len,
const char *name, size_t name_len) {
if (name_len && name[0] == '\\') {
name++;
name_len--;
}
if (!alias_len || !name_len) {
return;
}
if (w->nuses == w->cap_uses) {
int ncap = w->cap_uses ? w->cap_uses * PAIR_LEN : CBM_SZ_8;
cbm_doclink_php_use_t *g = (cbm_doclink_php_use_t *)cbm_arena_alloc(
w->sb->a, (size_t)ncap * sizeof(cbm_doclink_php_use_t));
if (!g) {
w->sb->oom = true;
return;
}
if (w->nuses) {
memcpy(g, w->uses, (size_t)w->nuses * sizeof(cbm_doclink_php_use_t));
}
w->uses = g;
w->cap_uses = ncap;
}
w->uses[w->nuses++] = (cbm_doclink_php_use_t){alias, alias_len, name, name_len, kind};
php_sb_puts(w->sb, "\nU");
php_sb_num(w->sb, (uint32_t)w->blk);
php_sb_field(w->sb, &kind, SKIP_ONE);
php_sb_field(w->sb, alias, alias_len);
php_sb_field(w->sb, name, name_len);
}
/* One clause of a use declaration; `prefix` is a group's common prefix. */
static void php_use_clause(php_walk_t *w, TSNode clause, char kind, const char *prefix,
size_t prefix_len) {
TSNode alias = ts_node_child_by_field_name(clause, TS_FIELD("alias"));
TSNode name = ts_node_named_child(clause, 0);
if (!php_kind(name, "name") && !php_kind(name, "qualified_name")) {
return;
}
size_t nl = 0;
const char *nt = php_name_text(w, name, &nl);
const char *full = nt;
size_t fl = nl;
if (prefix_len) {
char *joined =
cbm_arena_sprintf(w->sb->a, "%.*s\\%.*s", (int)prefix_len, prefix, (int)nl, nt);
if (!joined) {
w->sb->oom = true;
return;
}
full = joined;
fl = strlen(joined);
}
const char *al = NULL;
size_t all = 0;
if (!ts_node_is_null(alias)) {
al = php_name_text(w, alias, &all);
} else {
const char *bs = full + fl;
while (bs > full && bs[-SKIP_ONE] != '\\') {
bs--;
}
al = bs;
all = (size_t)(full + fl - bs);
}
php_add_use(w, php_use_kind(clause, kind), al, all, full, fl);
}
static void php_use_decl(php_walk_t *w, TSNode decl) {
char kind = php_use_kind(decl, 'c');
TSNode body = ts_node_child_by_field_name(decl, TS_FIELD("body"));
const char *prefix = NULL;
size_t pl = 0;
if (!ts_node_is_null(body)) {
TSNode pre = ts_node_named_child(decl, 0);
if (!php_kind(pre, "namespace_name")) {
return;
}
prefix = php_name_text(w, pre, &pl);
if (pl && prefix[0] == '\\') {
prefix++;
pl--;
}
}
int n = 0;
TSNode *kids = php_children(w, ts_node_is_null(body) ? decl : body, &n);
for (int i = 0; i < n; i++) {
if (php_kind(kids[i], "namespace_use_clause")) {
php_use_clause(w, kids[i], kind, prefix, pl);
}
}
}
static bool php_is_type_decl(TSNode n) {
return php_kind(n, "class_declaration") || php_kind(n, "interface_declaration") ||
php_kind(n, "trait_declaration") || php_kind(n, "enum_declaration");
}
static char php_type_kind(TSNode n) {
if (php_kind(n, "interface_declaration")) {
return 'i';
}
if (php_kind(n, "trait_declaration")) {
return 't';
}
return php_kind(n, "enum_declaration") ? 'e' : 'c';
}
/* The name of a type or function declaration, NUL-terminated in the arena. */
static const char *php_decl_name(php_walk_t *w, TSNode decl, size_t *len) {
TSNode name = ts_node_child_by_field_name(decl, TS_FIELD("name"));
*len = 0;
return ts_node_is_null(name) ? NULL : php_name_text(w, name, len);
}
/* `fqn` of a declared short name in the current block. */
static const char *php_fqn(php_walk_t *w, const char *name, size_t len, size_t *out_len) {
if (!w->ns_len) {
*out_len = len;
return name;
}
char *s = cbm_arena_sprintf(w->sb->a, "%.*s\\%.*s", (int)w->ns_len, w->ns, (int)len, name);
if (!s) {
w->sb->oom = true;
*out_len = 0;
return "";
}
*out_len = strlen(s);
return s;
}
static char php_dup_flag(php_walk_t *w, const char *name) {
uintptr_t count = (uintptr_t)cbm_ht_get(w->names, name);
return count > SKIP_ONE ? 'd' : '-';
}
static void php_base(php_walk_t *w, char rel, TSNode name) {
size_t nl = 0;
const char *nt = php_name_text(w, name, &nl);
char buf[CBM_SZ_1K];
size_t fl =
cbm_doclink_php_qualify(buf, sizeof(buf), w->ns, w->ns_len, w->uses, w->nuses, nt, nl);
if (!fl) {
return;
}
php_sb_puts(w->sb, "\nB");
php_sb_field(w->sb, &rel, SKIP_ONE);
php_sb_field(w->sb, buf, fl);
}
/* The names a base clause, interface clause or trait use lists. */
static void php_bases(php_walk_t *w, char rel, TSNode clause) {
int n = 0;
TSNode *kids = php_children(w, clause, &n);
for (int i = 0; i < n; i++) {
if (php_kind(kids[i], "name") || php_kind(kids[i], "qualified_name")) {
php_base(w, rel, kids[i]);
}
}
}
static void php_member(php_walk_t *w, char kind, TSNode name) {
size_t nl = 0;
const char *nt = php_name_text(w, name, &nl);
if (nl && nt[0] == '$') {
nt++;
nl--;
}
if (!nl) {
return;
}
php_sb_puts(w->sb, "\nK");
php_sb_field(w->sb, &kind, SKIP_ONE);
php_sb_field(w->sb, nt, nl);
}
/* Promoted constructor parameters are properties. */
static void php_promoted(php_walk_t *w, TSNode method) {
TSNode params = ts_node_child_by_field_name(method, TS_FIELD("parameters"));
int n = 0;
TSNode *kids = ts_node_is_null(params) ? NULL : php_children(w, params, &n);
for (int i = 0; i < n; i++) {
if (php_kind(kids[i], "property_promotion_parameter")) {
TSNode name = ts_node_child_by_field_name(kids[i], TS_FIELD("name"));
if (!ts_node_is_null(name)) {
php_member(w, 'p', name);
}
}
}
}
/* The elements of a property or constant declaration. */
static void php_elements(php_walk_t *w, TSNode decl, const char *elem, char kind) {
int n = 0;
TSNode *kids = php_children(w, decl, &n);
for (int i = 0; i < n; i++) {
if (!php_kind(kids[i], elem)) {
continue;
}
TSNode name = ts_node_child_by_field_name(kids[i], TS_FIELD("name"));
if (ts_node_is_null(name)) {
name = ts_node_named_child(kids[i], 0);
}
if (php_kind(name, "name") || php_kind(name, "variable_name")) {
php_member(w, kind, name);
}
}
}
static void php_type_body(php_walk_t *w, TSNode body) {
int n = 0;
TSNode *kids = php_children(w, body, &n);
for (int i = 0; i < n; i++) {
TSNode c = kids[i];
if (php_kind(c, "use_declaration")) {
php_bases(w, 't', c);
}
}
for (int i = 0; i < n; i++) {
TSNode c = kids[i];
if (php_kind(c, "method_declaration")) {
TSNode name = ts_node_child_by_field_name(c, TS_FIELD("name"));
if (!ts_node_is_null(name)) {
php_member(w, 'm', name);
}
php_promoted(w, c);
} else if (php_kind(c, "property_declaration")) {
php_elements(w, c, "property_element", 'p');
} else if (php_kind(c, "const_declaration")) {
php_elements(w, c, "const_element", 'c');
} else if (php_kind(c, "enum_case")) {
TSNode name = ts_node_child_by_field_name(c, TS_FIELD("name"));
if (!ts_node_is_null(name)) {
php_member(w, 'e', name);
}
}
}
}
static void php_type(php_walk_t *w, TSNode decl) {
size_t nl = 0;
const char *name = php_decl_name(w, decl, &nl);
if (!name || !nl) {
return;
}
size_t fl = 0;
const char *fqn = php_fqn(w, name, nl, &fl);
char kind = php_type_kind(decl);
char flag = php_dup_flag(w, name);
php_sb_puts(w->sb, "\nT");
php_sb_field(w->sb, &kind, SKIP_ONE);
php_sb_field(w->sb, fqn, fl);
php_sb_field(w->sb, &flag, SKIP_ONE);
php_sb_num(w->sb, ts_node_start_point(decl).row + SKIP_ONE);
php_sb_num(w->sb, ts_node_end_point(decl).row + SKIP_ONE);
int n = 0;
TSNode *kids = php_children(w, decl, &n);
for (int i = 0; i < n; i++) {
if (php_kind(kids[i], "base_clause")) {
php_bases(w, kind == 'i' ? 'i' : 'e', kids[i]);
} else if (php_kind(kids[i], "class_interface_clause")) {
php_bases(w, 'i', kids[i]);
}
}
TSNode body = ts_node_child_by_field_name(decl, TS_FIELD("body"));
if (!ts_node_is_null(body)) {
php_type_body(w, body);
}
}
static void php_function(php_walk_t *w, TSNode decl) {
size_t nl = 0;
const char *name = php_decl_name(w, decl, &nl);
if (!name || !nl) {
return;
}
size_t fl = 0;
const char *fqn = php_fqn(w, name, nl, &fl);
char flag = php_dup_flag(w, name);
php_sb_puts(w->sb, "\nF");
php_sb_field(w->sb, fqn, fl);
php_sb_field(w->sb, &flag, SKIP_ONE);
}
static void php_count_name(php_walk_t *w, TSNode decl);
static bool php_is_branch(TSNode n) {
return php_kind(n, "if_statement") || php_kind(n, "compound_statement") ||
php_kind(n, "else_clause") || php_kind(n, "else_if_clause") ||
php_kind(n, "colon_block");
}
/* The types and functions a top-level conditional declares
* (`if (!function_exists('f')) { function f() {} }`): PHP declares them when
* the branch runs, and the graph has their nodes. Counted for the dup flags
* (count) or written. */
static void php_cond_decls(php_walk_t *w, TSNode n, int depth, bool count) {
if (depth > PHP_COND_DEPTH) {
return;
}
int k = 0;
TSNode *kids = php_children(w, n, &k);
for (int i = 0; i < k && !w->sb->oom; i++) {
TSNode c = kids[i];
bool type = php_is_type_decl(c);
if (type || php_kind(c, "function_definition")) {
if (count) {
php_count_name(w, c);
} else if (type) {
php_type(w, c);
} else {
php_function(w, c);
}
} else if (php_is_branch(c)) {
php_cond_decls(w, c, depth + SKIP_ONE, count);
}
}
}
/* One namespace block: kids[from, to) are its statements. Its use imports
* are read first: a supertype is qualified with all of them. */
static void php_block(php_walk_t *w, const TSNode *kids, int from, int to) {
w->nuses = 0;
for (int i = from; i < to; i++) {
if (php_kind(kids[i], "namespace_use_declaration")) {
php_use_decl(w, kids[i]);
}
}
for (int i = from; i < to && !w->sb->oom; i++) {
if (php_is_type_decl(kids[i])) {
php_type(w, kids[i]);
} else if (php_kind(kids[i], "function_definition")) {
php_function(w, kids[i]);
} else if (php_kind(kids[i], "if_statement")) {
php_cond_decls(w, kids[i], 0, false);
}
}
}
static void php_count_name(php_walk_t *w, TSNode decl) {
size_t nl = 0;
const char *name = php_decl_name(w, decl, &nl);
if (!name || !nl) {
return;
}
uintptr_t count = (uintptr_t)cbm_ht_get(w->names, name);
cbm_ht_set(w->names, name, (void *)(count + SKIP_ONE));
}
/* Count the short names of the top-level types and functions of kids[0, n)
* and of braced namespace bodies among them. */
static void php_count_names(php_walk_t *w, const TSNode *kids, int n) {
for (int i = 0; i < n; i++) {
TSNode c = kids[i];
if (php_is_type_decl(c) || php_kind(c, "function_definition")) {
php_count_name(w, c);
} else if (php_kind(c, "if_statement")) {
php_cond_decls(w, c, 0, true);
} else if (php_kind(c, "namespace_definition")) {
TSNode body = ts_node_child_by_field_name(c, TS_FIELD("body"));
int bn = 0;
TSNode *bk = ts_node_is_null(body) ? NULL : php_children(w, body, &bn);
php_count_names(w, bk, bn);
}
}
}
static void php_namespace_rec(php_walk_t *w, TSNode def, uint32_t first, uint32_t last) {
TSNode name = ts_node_child_by_field_name(def, TS_FIELD("name"));
w->ns = "";
w->ns_len = 0;
if (!ts_node_is_null(name)) {
w->ns = php_name_text(w, name, &w->ns_len);
if (w->ns_len && w->ns[0] == '\\') {
w->ns++;
w->ns_len--;
}
}
php_sb_puts(w->sb, "\nN");
php_sb_num(w->sb, (uint32_t)w->blk);
php_sb_field(w->sb, w->ns, w->ns_len);
php_sb_num(w->sb, first);
php_sb_num(w->sb, last);
}
static void php_walk_blocks(php_walk_t *w, const TSNode *kids, int n, uint32_t file_last) {
int first_ns = CBM_NOT_FOUND;
for (int i = 0; i < n && first_ns < 0; i++) {
first_ns = php_kind(kids[i], "namespace_definition") ? i : first_ns;
}
if (first_ns < 0) {
w->blk = 0;
php_block(w, kids, 0, n);
return;
}
w->blk = CBM_NOT_FOUND;
for (int i = first_ns; i < n && !w->sb->oom; i++) {
if (!php_kind(kids[i], "namespace_definition")) {
continue;
}
w->blk++;
TSNode def = kids[i];
TSNode body = ts_node_child_by_field_name(def, TS_FIELD("body"));
uint32_t first = ts_node_start_point(def).row + SKIP_ONE;
if (!ts_node_is_null(body)) {
php_namespace_rec(w, def, first, ts_node_end_point(def).row + SKIP_ONE);
int bn = 0;
TSNode *bk = php_children(w, body, &bn);
php_block(w, bk, 0, bn);
continue;
}
int next = i + SKIP_ONE;
while (next < n && !php_kind(kids[next], "namespace_definition")) {
next++;
}
uint32_t last = next < n ? ts_node_start_point(kids[next]).row : file_last;
php_namespace_rec(w, def, first, last);
php_block(w, kids, i + SKIP_ONE, next);
i = next - SKIP_ONE;
}
}
const char *cbm_doclink_php_scan_scope(CBMExtractCtx *ctx) {
if (!ctx || ts_node_is_null(ctx->root)) {
return NULL;
}
php_sb_t sb = {.a = ctx->arena};
php_sb_puts(&sb, CBM_DOCLINK_PHP_SCOPE_TAG);
php_walk_t w = {.ctx = ctx, .sb = &sb, .ns = ""};
w.names = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, CBM_SZ_32);
if (!w.names) {
return NULL;
}
int n = 0;
TSNode *kids = php_children(&w, ctx->root, &n);
php_count_names(&w, kids, n);
php_walk_blocks(&w, kids, n, ts_node_end_point(ctx->root).row + SKIP_ONE);
cbm_ht_free(w.names);
return sb.oom ? NULL : sb.buf;
}
/* The blob without lines and imports: "N blk name first last" -> "N blk
* name", "T kind fqn flags first last" -> "T kind fqn flags", U dropped. */
char *cbm_doclink_php_portable_scope(const char *scope) {
size_t n = strlen(scope);
char *out = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, n + SKIP_ONE);
if (!out) {
return NULL;
}
size_t w = 0;
for (const char *p = scope; *p;) {
const char *nl = strchr(p, '\n');
size_t len = nl ? (size_t)(nl - p) : strlen(p);
const char *next = nl ? nl + SKIP_ONE : p + len;
bool rec = len >= PAIR_LEN && p[SKIP_ONE] == '\t';
if (rec && p[0] == 'U') {
p = next;
continue;
}
size_t keep = len;
if (rec && (p[0] == 'N' || p[0] == 'T')) {
int tabs = 0;
for (size_t k = 0; k < len; k++) {
if (p[k] == '\t' && ++tabs == (p[0] == 'N' ? CBM_SZ_3 : CBM_SZ_4)) {
keep = k;
break;
}
}
}
memcpy(out + w, p, keep);
w += keep;
if (nl) {
out[w++] = '\n';
}
p = next;
}
out[w] = '\0';
return out;
}
+1 -1
View File
@@ -48,7 +48,7 @@ static const cbm_doclink_resolver_t *const DOCLINK_RESOLVERS[] = {
&cbm_doclink_cs_resolver, &cbm_doclink_md_resolver, &cbm_doclink_pdf_resolver,
&cbm_doclink_rst_resolver, &cbm_doclink_adoc_resolver, &cbm_doclink_go_resolver,
&cbm_doclink_jvm_resolver, &cbm_doclink_rs_resolver, &cbm_doclink_perl_resolver,
&cbm_doclink_ts_resolver,
&cbm_doclink_ts_resolver, &cbm_doclink_php_resolver,
};
enum { DOCLINK_RESOLVER_COUNT = sizeof(DOCLINK_RESOLVERS) / sizeof(DOCLINK_RESOLVERS[0]) };
+1
View File
@@ -300,6 +300,7 @@ extern const cbm_doclink_resolver_t cbm_doclink_jvm_resolver;
extern const cbm_doclink_resolver_t cbm_doclink_rs_resolver;
extern const cbm_doclink_resolver_t cbm_doclink_perl_resolver;
extern const cbm_doclink_resolver_t cbm_doclink_ts_resolver;
extern const cbm_doclink_resolver_t cbm_doclink_php_resolver;
/* True when `rel_path` is a scope input of some language (scope_input). */
bool cbm_doclinks_is_scope_input(const char *rel_path);
+975
View File
@@ -0,0 +1,975 @@
/*
* doc_links_php.c — resolving PHP doc references (doclink_php.c). A reference
* is a FQSEN as phpDocumentor reads it, resolved with PHP's own name rules.
*
* Forms: `\Ns\Type`, `Type`, `Type::method()`, `Type::$property`,
* `Type::CONSTANT` (an enum case too), `function()` and `\Ns\function()`;
* `self::`, `static::` and `parent::` with a member. A member written without
* `()` and without `$` is a constant or enum case, else a method.
*
* Names. A leading backslash is taken as written. Otherwise the first segment
* of a type name is replaced when it is the alias of a class `use` of the
* namespace block the reference is written in (aliases compare without case),
* else the block's namespace is put in front: phpDocumentor's rule. Two rules
* 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
* 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
* name (a residual).
*
* Members are looked up on the type, then on the traits it uses, its
* superclass and its interfaces, each of them the same way (depth first; the
* first declaration found wins). Nothing found where a supertype is outside
* the repository is external; nothing found with every supertype in the
* repository is missing.
*
* Outcomes. A type, function or method binds to its graph node. A property,
* constant or enum case has none (a graph gap), nor has a type or function
* the graph folds into another one of the same name in its file (the scope
* flags those). A type or qualified function the repository does not declare
* is external when the first segment of its name is no namespace the
* repository declares (PHP's built-ins, Composer dependencies), else missing;
* an unqualified function nobody declares is external (PHP's built-in
* functions). Two declarations of one name are ambiguous unless one is in the
* reference's own file. Test code binds only from test code. Edges are exact
* for a name written fully qualified or bound by a use import, unique
* otherwise.
*/
#include "pipeline/doc_links.h"
#include "cbm.h"
#include "doclink.h"
#include "helpers.h" /* cbm_fqn_module_source_lang, cbm_is_test_file */
#include "foundation/arena.h"
#include "foundation/constants.h"
#include "foundation/hash_table.h"
#include "foundation/log.h"
#include "foundation/mem_core.h"
#include "graph_buffer/graph_buffer.h"
#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum {
PR_QN_MAX = 2048,
PR_NAME_MAX = 1024,
PR_FIELDS = 6,
PR_MAX_DEPTH = 16,
PR_VISIT_MAX = 64,
PR_USE_FIELDS = 5, /* U blk kind alias name */
PR_LINE_FIELDS = 4, /* N blk name first last / T kind fqn flags first last: lines from here */
};
/* ── The index ───────────────────────────────────────────────────── */
typedef struct {
char kind; /* m p c e */
const char *name;
} pr_member_t;
typedef struct {
char rel; /* e i t */
const char *fqn;
} pr_base_t;
typedef struct {
int file;
char kind; /* c i t e */
bool dup; /* folded into another declaration of its file */
const char *fqn;
const char *name; /* the tail of fqn */
uint32_t first; /* lines: only in a fresh blob */
uint32_t last;
pr_base_t *bases;
int nbases;
int cap_bases;
pr_member_t *members;
int nmembers;
int cap_members;
int next; /* the next type of the same name (without case), or -1 */
} pr_type_t;
typedef struct {
int file;
bool dup;
const char *fqn;
const char *name;
int next;
} pr_func_t;
typedef struct {
int blk;
const char *name;
uint32_t first;
uint32_t last;
} pr_ns_t;
typedef struct {
const char *path;
const char *module_qn;
bool test;
pr_ns_t *nss;
int nnss;
int cap_nss;
cbm_doclink_php_use_t *uses; /* only in a fresh blob; a block's are adjacent */
int *use_blk;
int nuses;
int cap_uses;
int cap_use_blk;
int type_first; /* its types: types[type_first, type_first + type_count) */
int type_count;
} pr_file_t;
typedef struct {
CBMArena arena;
const char *project;
pr_file_t *files;
int nfiles;
int *run_to_file;
int run_count;
pr_type_t *types;
int ntypes;
int cap_types;
pr_func_t *funcs;
int nfuncs;
int cap_funcs;
CBMHashTable *type_at; /* lowercase fqn -> the first type's index + 1 */
CBMHashTable *func_at; /* lowercase fqn -> the first function's index + 1 */
CBMHashTable *roots; /* lowercase first segment of a declared namespace */
CBMHashTable *by_path; /* rel_path -> file index + 1 */
bool oom;
} pr_index_t;
static const char *pr_copy(pr_index_t *ix, const char *s, size_t n) {
char *c = cbm_arena_strndup(&ix->arena, s, n);
ix->oom = ix->oom || !c;
return c;
}
static const char *pr_lower(pr_index_t *ix, const char *s, size_t n) {
char *c = cbm_arena_strndup(&ix->arena, s, n);
if (!c) {
ix->oom = true;
return NULL;
}
for (size_t i = 0; i < n; i++) {
c[i] = (char)tolower((unsigned char)c[i]);
}
return c;
}
/* Room for one more item in a growing arena array. */
static bool pr_grow(pr_index_t *ix, void **items, int count, int *cap, size_t size) {
if (count < *cap) {
return true;
}
int ncap = *cap ? *cap * PAIR_LEN : CBM_SZ_8;
void *g = cbm_arena_alloc(&ix->arena, (size_t)ncap * size);
if (!g) {
ix->oom = true;
return false;
}
if (count) {
memcpy(g, *items, (size_t)count * size);
}
*items = g;
*cap = ncap;
return true;
}
static bool pr_dash(const char *s, size_t n) {
return n == SKIP_ONE && s[0] == '-';
}
static uint32_t pr_num(const char *s) {
return (uint32_t)strtoul(s, NULL, CBM_DECIMAL_BASE);
}
static pr_type_t *pr_last_type(pr_index_t *ix, const pr_file_t *f) {
return f->type_count ? &ix->types[f->type_first + f->type_count - SKIP_ONE] : NULL;
}
static void pr_read_type(pr_index_t *ix, pr_file_t *f, int fi, const char **fl, const size_t *ln,
int n) {
if (!pr_grow(ix, (void **)&ix->types, ix->ntypes, &ix->cap_types, sizeof(pr_type_t))) {
return;
}
if (!f->type_count) {
f->type_first = ix->ntypes;
}
pr_type_t *t = &ix->types[ix->ntypes++];
f->type_count++;
memset(t, 0, sizeof(*t));
t->file = fi;
t->kind = fl[1][0];
t->fqn = pr_copy(ix, fl[PAIR_LEN], ln[PAIR_LEN]);
t->dup = fl[CBM_SZ_3][0] == 'd';
t->next = CBM_NOT_FOUND;
if (n > PR_LINE_FIELDS) {
t->first = pr_num(fl[PR_LINE_FIELDS]);
t->last = n > PR_LINE_FIELDS + SKIP_ONE ? pr_num(fl[PR_LINE_FIELDS + SKIP_ONE]) : 0;
}
if (t->fqn) {
const char *bs = strrchr(t->fqn, '\\');
t->name = bs ? bs + SKIP_ONE : t->fqn;
}
}
static void pr_read_record(pr_index_t *ix, pr_file_t *f, int fi, const char **fl, const size_t *ln,
int n) {
char tag = fl[0][0];
pr_type_t *t = NULL;
if (tag == 'N' && n >= CBM_SZ_3 &&
pr_grow(ix, (void **)&f->nss, f->nnss, &f->cap_nss, sizeof(pr_ns_t))) {
pr_ns_t *s = &f->nss[f->nnss++];
s->blk = (int)pr_num(fl[1]);
s->name =
pr_dash(fl[PAIR_LEN], ln[PAIR_LEN]) ? "" : pr_copy(ix, fl[PAIR_LEN], ln[PAIR_LEN]);
s->first = n > CBM_SZ_3 ? pr_num(fl[CBM_SZ_3]) : 0;
s->last = n > CBM_SZ_4 ? pr_num(fl[CBM_SZ_4]) : 0;
} else if (tag == 'U' && n >= PR_USE_FIELDS &&
pr_grow(ix, (void **)&f->uses, f->nuses, &f->cap_uses,
sizeof(cbm_doclink_php_use_t)) &&
pr_grow(ix, (void **)&f->use_blk, f->nuses, &f->cap_use_blk, sizeof(int))) {
cbm_doclink_php_use_t *u = &f->uses[f->nuses];
f->use_blk[f->nuses++] = (int)pr_num(fl[1]);
u->kind = fl[PAIR_LEN][0];
u->alias = pr_copy(ix, fl[CBM_SZ_3], ln[CBM_SZ_3]);
u->alias_len = ln[CBM_SZ_3];
u->name = pr_copy(ix, fl[CBM_SZ_4], ln[CBM_SZ_4]);
u->name_len = ln[CBM_SZ_4];
} else if (tag == 'T' && n >= CBM_SZ_4) {
pr_read_type(ix, f, fi, fl, ln, n);
} else if (tag == 'B' && n >= CBM_SZ_3 && (t = pr_last_type(ix, f)) &&
pr_grow(ix, (void **)&t->bases, t->nbases, &t->cap_bases, sizeof(pr_base_t))) {
t->bases[t->nbases++] = (pr_base_t){fl[1][0], pr_copy(ix, fl[PAIR_LEN], ln[PAIR_LEN])};
} else if (tag == 'K' && n >= CBM_SZ_3 && (t = pr_last_type(ix, f)) &&
pr_grow(ix, (void **)&t->members, t->nmembers, &t->cap_members,
sizeof(pr_member_t))) {
t->members[t->nmembers++] =
(pr_member_t){fl[1][0], pr_copy(ix, fl[PAIR_LEN], ln[PAIR_LEN])};
} else if (tag == 'F' && n >= CBM_SZ_3 &&
pr_grow(ix, (void **)&ix->funcs, ix->nfuncs, &ix->cap_funcs, sizeof(pr_func_t))) {
pr_func_t *fn = &ix->funcs[ix->nfuncs++];
fn->file = fi;
fn->fqn = pr_copy(ix, fl[1], ln[1]);
fn->dup = fl[PAIR_LEN][0] == 'd';
fn->next = CBM_NOT_FOUND;
const char *bs = fn->fqn ? strrchr(fn->fqn, '\\') : NULL;
fn->name = bs ? bs + SKIP_ONE : fn->fqn;
}
}
static void pr_read_blob(pr_index_t *ix, pr_file_t *f, int fi, const char *blob) {
const char *line = strchr(blob, '\n'); /* the tag line */
while (line && !ix->oom) {
const char *b = line + SKIP_ONE;
const char *e = strchr(b, '\n');
if (!e) {
e = b + strlen(b);
}
const char *fl[PR_FIELDS];
size_t ln[PR_FIELDS];
int n = 0;
const char *s = b;
for (const char *p = b; p <= e && n < PR_FIELDS; p++) {
if (p == e || *p == '\t') {
fl[n] = s;
ln[n] = (size_t)(p - s);
n++;
s = p + SKIP_ONE;
}
}
if (n > 0 && ln[0] == SKIP_ONE) {
pr_read_record(ix, f, fi, fl, ln, n);
}
line = *e ? e : NULL;
}
}
static void pr_destroy(void *index) {
pr_index_t *ix = (pr_index_t *)index;
if (!ix) {
return;
}
cbm_ht_free(ix->type_at);
cbm_ht_free(ix->func_at);
cbm_ht_free(ix->roots);
cbm_ht_free(ix->by_path);
cbm_free(CBM_MEM_CLASS_OTHER, ix->run_to_file);
cbm_arena_destroy(&ix->arena);
cbm_free(CBM_MEM_CLASS_OTHER, ix);
}
/* The namespace root of a qualified name ("" for a global one). */
static void pr_add_root(pr_index_t *ix, const char *name) {
const char *bs = name ? strchr(name, '\\') : NULL;
size_t n = bs ? (size_t)(bs - name) : (name ? strlen(name) : 0);
if (!n) {
return;
}
const char *key = pr_lower(ix, name, n);
if (key && !cbm_ht_get(ix->roots, key)) {
cbm_ht_set(ix->roots, key, (void *)(uintptr_t)SKIP_ONE);
}
}
/* Chain every type and function into its name's list, and collect the
* namespace roots. */
static bool pr_link(pr_index_t *ix) {
ix->type_at = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, (uint32_t)(ix->ntypes + SKIP_ONE));
ix->func_at = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, (uint32_t)(ix->nfuncs + SKIP_ONE));
ix->roots = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, CBM_SZ_64);
if (!ix->type_at || !ix->func_at || !ix->roots) {
return false;
}
for (int i = 0; i < ix->ntypes && !ix->oom; i++) {
pr_type_t *t = &ix->types[i];
const char *key = t->fqn ? pr_lower(ix, t->fqn, strlen(t->fqn)) : NULL;
if (!key) {
continue;
}
void *prev = cbm_ht_get(ix->type_at, key);
t->next = prev ? (int)((uintptr_t)prev - SKIP_ONE) : CBM_NOT_FOUND;
cbm_ht_set(ix->type_at, key, (void *)(uintptr_t)(i + SKIP_ONE));
if (strchr(t->fqn, '\\')) {
pr_add_root(ix, t->fqn);
}
}
for (int i = 0; i < ix->nfuncs && !ix->oom; i++) {
pr_func_t *fn = &ix->funcs[i];
const char *key = fn->fqn ? pr_lower(ix, fn->fqn, strlen(fn->fqn)) : NULL;
if (!key) {
continue;
}
void *prev = cbm_ht_get(ix->func_at, key);
fn->next = prev ? (int)((uintptr_t)prev - SKIP_ONE) : CBM_NOT_FOUND;
cbm_ht_set(ix->func_at, key, (void *)(uintptr_t)(i + SKIP_ONE));
if (strchr(fn->fqn, '\\')) {
pr_add_root(ix, fn->fqn);
}
}
for (int i = 0; i < ix->nfiles && !ix->oom; i++) {
for (int k = 0; k < ix->files[i].nnss; k++) {
pr_add_root(ix, ix->files[i].nss[k].name);
}
}
return !ix->oom;
}
static void *pr_build(const cbm_doclink_build_in_t *in) {
pr_index_t *ix = (pr_index_t *)cbm_calloc(CBM_MEM_CLASS_OTHER, sizeof(*ix));
if (!ix) {
return NULL;
}
cbm_arena_init(&ix->arena);
ix->project = in->ctx->project_name;
ix->nfiles = in->file_count;
ix->run_count = in->run_file_count;
ix->files =
in->file_count
? (pr_file_t *)cbm_arena_calloc(&ix->arena, (size_t)in->file_count * sizeof(pr_file_t))
: NULL;
ix->run_to_file = (int *)cbm_calloc(CBM_MEM_CLASS_OTHER,
(size_t)(in->run_file_count + SKIP_ONE) * sizeof(int));
ix->by_path = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, (uint32_t)(in->file_count + SKIP_ONE));
bool ok = (!in->file_count || ix->files) && ix->run_to_file && ix->by_path;
for (int r = 0; ok && r < in->run_file_count; r++) {
ix->run_to_file[r] = CBM_NOT_FOUND;
}
for (int i = 0; ok && i < in->file_count && !ix->oom; i++) {
const cbm_doclink_file_t *src = &in->files[i];
pr_file_t *f = &ix->files[i];
f->path = src->rel_path;
f->module_qn =
cbm_fqn_module_source_lang(&ix->arena, ix->project, src->rel_path, CBM_LANG_PHP);
f->test = cbm_is_test_file(src->rel_path, CBM_LANG_PHP);
ix->oom = ix->oom || !f->module_qn;
cbm_ht_set(ix->by_path, src->rel_path, (void *)(uintptr_t)(i + SKIP_ONE));
if (src->run_file >= 0 && src->run_file < in->run_file_count) {
ix->run_to_file[src->run_file] = i;
}
if (src->scope) {
pr_read_blob(ix, f, i, src->scope);
}
}
ok = ok && !ix->oom && pr_link(ix);
if (!ok) {
cbm_log_error("doc_links.php.error", "step", "index", "reason", "alloc");
pr_destroy(ix);
return NULL;
}
char b[CBM_SZ_3][CBM_SZ_32];
snprintf(b[0], sizeof(b[0]), "%d", ix->nfiles);
snprintf(b[SKIP_ONE], sizeof(b[SKIP_ONE]), "%d", ix->ntypes);
snprintf(b[PAIR_LEN], sizeof(b[PAIR_LEN]), "%d", ix->nfuncs);
cbm_log_info("doc_links.php.index", "files", b[0], "types", b[SKIP_ONE], "functions",
b[PAIR_LEN]);
return ix;
}
/* ── References ──────────────────────────────────────────────────── */
typedef struct {
const char *cls; /* a type or function name, NULL for none */
size_t cls_len;
const char *mem; /* a member name (no `$`), NULL for none */
size_t mem_len;
bool prop; /* `::$name` */
bool call; /* `()` at the end */
} pr_ref_t;
static bool pr_ident_start(char c) {
return isalpha((unsigned char)c) || c == '_' || (unsigned char)c >= 0x80;
}
static bool pr_ident_char(char c) {
return isalnum((unsigned char)c) || c == '_' || (unsigned char)c >= 0x80;
}
/* The end of an identifier at s[i] (i when there is none). */
static size_t pr_ident_end(const char *s, size_t i, size_t n) {
if (i >= n || !pr_ident_start(s[i])) {
return i;
}
while (i < n && pr_ident_char(s[i])) {
i++;
}
return i;
}
/* The end of a name (`\`-joined identifiers, a leading `\` allowed) at s[0]. */
static size_t pr_name_end(const char *s, size_t n) {
size_t i = (n > 0 && s[0] == '\\') ? SKIP_ONE : 0;
size_t e = pr_ident_end(s, i, n);
if (e == i) {
return 0;
}
while (e + SKIP_ONE < n && s[e] == '\\' && pr_ident_start(s[e + SKIP_ONE])) {
e = pr_ident_end(s, e + SKIP_ONE, n);
}
return e;
}
static bool pr_parse(const char *s, size_t n, pr_ref_t *r) {
memset(r, 0, sizeof(*r));
size_t i = pr_name_end(s, n);
if (i == 0) {
return false; /* a member needs its type: `::name` is no FQSEN */
}
r->cls = s;
r->cls_len = i;
if (i + PAIR_LEN <= n && s[i] == ':' && s[i + SKIP_ONE] == ':') {
i += PAIR_LEN;
if (i < n && s[i] == '$') {
r->prop = true;
i++;
}
size_t e = pr_ident_end(s, i, n);
if (e == i) {
return false;
}
r->mem = s + i;
r->mem_len = e - i;
i = e;
}
if (!r->prop && i + PAIR_LEN == n && s[i] == '(' && s[i + SKIP_ONE] == ')') {
r->call = true;
i = n;
}
return i == n;
}
/* ── Resolution ──────────────────────────────────────────────────── */
typedef enum {
PR_NONE = 0, /* nothing found */
PR_TYPE, /* a type of the index (`type`) */
PR_NODE,
PR_GAP,
PR_EXTERNAL,
PR_MISSING,
PR_AMBIGUOUS,
} pr_kind_t;
typedef struct {
pr_kind_t kind;
int type;
const cbm_gbuf_node_t *node;
bool exact;
} pr_res_t;
typedef struct {
const pr_index_t *ix;
const cbm_gbuf_t *g;
int src;
const char *ns; /* the reference's namespace block */
size_t ns_len;
const cbm_doclink_php_use_t *uses; /* that block's imports */
int nuses;
int cur; /* the type the doc sits in, or -1 */
} pr_ctx_t;
static pr_res_t pr_mk(pr_kind_t k) {
return (pr_res_t){k, CBM_NOT_FOUND, NULL, false};
}
static bool pr_eq_nocase(const char *a, size_t an, const char *b, size_t bn) {
if (an != bn) {
return false;
}
for (size_t i = 0; i < an; i++) {
if (tolower((unsigned char)a[i]) != tolower((unsigned char)b[i])) {
return false;
}
}
return true;
}
static void pr_lower_into(char *out, const char *s, size_t n) {
for (size_t i = 0; i < n; i++) {
out[i] = (char)tolower((unsigned char)s[i]);
}
out[n] = '\0';
}
/* The namespace block of `line` in file f and its imports. */
static void pr_block_at(pr_ctx_t *c, const pr_file_t *f, uint32_t line) {
int blk = f->nnss ? CBM_NOT_FOUND : 0;
c->ns = "";
c->ns_len = 0;
for (int i = 0; i < f->nnss; i++) {
if (line >= f->nss[i].first && line <= f->nss[i].last) {
blk = f->nss[i].blk;
c->ns = f->nss[i].name;
c->ns_len = strlen(c->ns);
break;
}
}
c->uses = NULL;
c->nuses = 0;
for (int i = 0; i < f->nuses; i++) {
if (f->use_blk[i] != blk) {
continue;
}
if (!c->uses) {
c->uses = &f->uses[i];
}
c->nuses++;
}
}
/* The type of file f whose lines hold `line`, or -1. */
static int pr_type_at(const pr_index_t *ix, const pr_file_t *f, uint32_t line) {
for (int i = 0; i < f->type_count; i++) {
const pr_type_t *t = &ix->types[f->type_first + i];
if (line >= t->first && line <= t->last) {
return f->type_first + i;
}
}
return CBM_NOT_FOUND;
}
/* The node at `qn` in file `path`, or NULL. */
static const cbm_gbuf_node_t *pr_node(const pr_ctx_t *c, const char *qn, const char *path) {
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(c->g, qn);
if (!n || !n->label || !n->file_path || strcmp(n->file_path, path) != 0) {
return NULL;
}
return n;
}
/* A declared type's own node: a gap when the graph folded it or has none. */
static pr_res_t pr_type_node(const pr_ctx_t *c, int ti) {
const pr_type_t *t = &c->ix->types[ti];
const pr_file_t *f = &c->ix->files[t->file];
char qn[PR_QN_MAX];
int n = snprintf(qn, sizeof(qn), "%s.%s", f->module_qn, t->name);
const cbm_gbuf_node_t *node =
(!t->dup && n > 0 && (size_t)n < sizeof(qn)) ? pr_node(c, qn, f->path) : NULL;
pr_res_t r = pr_mk(node ? PR_NODE : PR_GAP);
r.node = node;
return r;
}
/* The repository's type named `fqn`: one, several (ambiguous, unless one is
* in the reference's file) or none. */
static pr_res_t pr_find_type(const pr_ctx_t *c, const char *fqn, size_t len) {
char key[PR_NAME_MAX];
if (len >= sizeof(key)) {
return pr_mk(PR_NONE);
}
pr_lower_into(key, fqn, len);
void *v = cbm_ht_get(c->ix->type_at, key);
int first = v ? (int)((uintptr_t)v - SKIP_ONE) : CBM_NOT_FOUND;
int count = 0;
int own = CBM_NOT_FOUND;
for (int i = first; i >= 0; i = c->ix->types[i].next) {
count++;
own = c->ix->types[i].file == c->src ? i : own;
}
if (count == 0) {
return pr_mk(PR_NONE);
}
pr_res_t r = pr_mk(count == SKIP_ONE || own >= 0 ? PR_TYPE : PR_AMBIGUOUS);
r.type = count == SKIP_ONE ? first : own;
return r;
}
/* A name the repository does not declare: missing inside one of its
* namespaces, else external. */
static pr_res_t pr_absent(const pr_ctx_t *c, const char *fqn, size_t len) {
const char *bs = memchr(fqn, '\\', len);
char key[PR_NAME_MAX];
size_t rl = bs ? (size_t)(bs - fqn) : 0;
if (!rl || rl >= sizeof(key)) {
return pr_mk(PR_EXTERNAL);
}
pr_lower_into(key, fqn, rl);
return pr_mk(cbm_ht_get(c->ix->roots, key) ? PR_MISSING : PR_EXTERNAL);
}
/* True when the first segment of `name` is a class-use alias. */
static bool pr_aliased(const pr_ctx_t *c, const char *name, size_t len) {
const char *bs = memchr(name, '\\', len);
size_t first = bs ? (size_t)(bs - name) : len;
for (int i = 0; i < c->nuses; i++) {
if (c->uses[i].kind == 'c' &&
pr_eq_nocase(c->uses[i].alias, c->uses[i].alias_len, name, first)) {
return true;
}
}
return false;
}
static bool pr_is(const char *s, size_t n, const char *word) {
return pr_eq_nocase(s, n, word, strlen(word));
}
/* The superclass of type ti, as a type of the index. */
static pr_res_t pr_parent(const pr_ctx_t *c, int ti) {
const pr_type_t *t = &c->ix->types[ti];
for (int i = 0; i < t->nbases; i++) {
if (t->bases[i].rel == 'e' && t->bases[i].fqn) {
pr_res_t r = pr_find_type(c, t->bases[i].fqn, strlen(t->bases[i].fqn));
return r.kind == PR_NONE ? pr_mk(PR_EXTERNAL) : r;
}
}
return pr_mk(PR_MISSING);
}
/* The type a reference's type part names. */
static pr_res_t pr_type_of(const pr_ctx_t *c, const pr_ref_t *r) {
if (pr_is(r->cls, r->cls_len, "self") || pr_is(r->cls, r->cls_len, "static")) {
pr_res_t res = pr_mk(c->cur >= 0 ? PR_TYPE : PR_MISSING);
res.type = c->cur;
return res;
}
if (pr_is(r->cls, r->cls_len, "parent")) {
return c->cur >= 0 ? pr_parent(c, c->cur) : pr_mk(PR_MISSING);
}
char fqn[PR_NAME_MAX];
size_t fl = cbm_doclink_php_qualify(fqn, sizeof(fqn), c->ns, c->ns_len, c->uses, c->nuses,
r->cls, r->cls_len);
if (!fl) {
return pr_mk(PR_MISSING);
}
pr_res_t res = pr_find_type(c, fqn, fl);
if (res.kind == PR_NONE) {
res = pr_absent(c, fqn, fl);
}
res.exact = r->cls[0] == '\\' || pr_aliased(c, r->cls, r->cls_len);
return res;
}
/* ── Members across supertypes ───────────────────────────────────── */
typedef struct {
const pr_ctx_t *c;
char want; /* 'm' a method, 'p' a property, 'c' a constant or enum case */
const char *name;
size_t len;
int visited[PR_VISIT_MAX];
int nvisited;
bool open; /* a supertype outside the repository */
bool amb; /* a supertype with several declarations */
int owner; /* found: the declaring type and member */
const pr_member_t *found;
} pr_search_t;
static bool pr_member_is(const pr_search_t *s, const pr_member_t *m) {
size_t ml = strlen(m->name);
if (s->want == 'm') {
return m->kind == 'm' && pr_eq_nocase(m->name, ml, s->name, s->len);
}
bool kind = s->want == 'p' ? m->kind == 'p' : (m->kind == 'c' || m->kind == 'e');
return kind && ml == s->len && memcmp(m->name, s->name, ml) == 0;
}
static bool pr_seen(pr_search_t *s, int ti) {
for (int i = 0; i < s->nvisited; i++) {
if (s->visited[i] == ti) {
return true;
}
}
if (s->nvisited == PR_VISIT_MAX) {
s->open = true; /* too wide to follow: not every supertype was read */
return true;
}
s->visited[s->nvisited++] = ti;
return false;
}
static bool pr_search(pr_search_t *s, int ti, int depth) {
if (depth > PR_MAX_DEPTH) {
s->open = true;
return false;
}
if (pr_seen(s, ti)) {
return false;
}
const pr_type_t *t = &s->c->ix->types[ti];
for (int i = 0; i < t->nmembers; i++) {
if (pr_member_is(s, &t->members[i])) {
s->owner = ti;
s->found = &t->members[i];
return true;
}
}
static const char order[] = {'t', 'e', 'i'}; /* traits, superclass, interfaces */
for (size_t o = 0; o < sizeof(order); o++) {
for (int i = 0; i < t->nbases; i++) {
if (t->bases[i].rel != order[o] || !t->bases[i].fqn) {
continue;
}
pr_res_t r = pr_find_type(s->c, t->bases[i].fqn, strlen(t->bases[i].fqn));
if (r.kind == PR_TYPE && pr_search(s, r.type, depth + SKIP_ONE)) {
return true;
}
s->amb = s->amb || r.kind == PR_AMBIGUOUS;
s->open = s->open || r.kind == PR_NONE;
}
}
return false;
}
/* The member's outcome: a method's node, else a graph gap. */
static pr_res_t pr_member_node(const pr_ctx_t *c, const pr_search_t *s) {
const pr_type_t *t = &c->ix->types[s->owner];
if (s->found->kind != 'm' || t->dup) {
return pr_mk(PR_GAP);
}
const pr_file_t *f = &c->ix->files[t->file];
char qn[PR_QN_MAX];
int n = snprintf(qn, sizeof(qn), "%s.%s.%s", f->module_qn, t->name, s->found->name);
const cbm_gbuf_node_t *node =
(n > 0 && (size_t)n < sizeof(qn)) ? pr_node(c, qn, f->path) : NULL;
pr_res_t r = pr_mk(node ? PR_NODE : PR_GAP);
r.node = node;
return r;
}
static pr_res_t pr_member(const pr_ctx_t *c, int ti, const pr_ref_t *r) {
pr_search_t s = {.c = c, .name = r->mem, .len = r->mem_len, .owner = CBM_NOT_FOUND};
s.want = r->prop ? 'p' : r->call ? 'm' : 'c';
bool hit = pr_search(&s, ti, 0);
if (!hit && s.want == 'c') {
/* `Type::name` without parentheses names a method as often as a
* constant: no constant or case of that name, then a method */
bool open = s.open;
bool amb = s.amb;
s.want = 'm';
s.nvisited = 0;
hit = pr_search(&s, ti, 0);
s.open = s.open || open;
s.amb = s.amb || amb;
}
if (hit) {
return pr_member_node(c, &s);
}
return pr_mk(s.amb ? PR_AMBIGUOUS : s.open ? PR_EXTERNAL : PR_MISSING);
}
/* ── Functions ───────────────────────────────────────────────────── */
static pr_res_t pr_find_func(const pr_ctx_t *c, const char *fqn, size_t len) {
char key[PR_NAME_MAX];
if (len >= sizeof(key)) {
return pr_mk(PR_NONE);
}
pr_lower_into(key, fqn, len);
void *v = cbm_ht_get(c->ix->func_at, key);
int first = v ? (int)((uintptr_t)v - SKIP_ONE) : CBM_NOT_FOUND;
int count = 0;
int own = CBM_NOT_FOUND;
for (int i = first; i >= 0; i = c->ix->funcs[i].next) {
count++;
own = c->ix->funcs[i].file == c->src ? i : own;
}
if (count == 0) {
return pr_mk(PR_NONE);
}
if (count > SKIP_ONE && own < 0) {
return pr_mk(PR_AMBIGUOUS);
}
const pr_func_t *fn = &c->ix->funcs[count == SKIP_ONE ? first : own];
const pr_file_t *f = &c->ix->files[fn->file];
char qn[PR_QN_MAX];
int n = snprintf(qn, sizeof(qn), "%s.%s", f->module_qn, fn->name);
const cbm_gbuf_node_t *node =
(!fn->dup && n > 0 && (size_t)n < sizeof(qn)) ? pr_node(c, qn, f->path) : NULL;
pr_res_t r = pr_mk(node ? PR_NODE : PR_GAP);
r.node = node;
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)) {
/* qualified: the type rules (a namespace alias may start it) */
size_t fl = cbm_doclink_php_qualify(fqn, sizeof(fqn), c->ns, c->ns_len, c->uses, c->nuses,
r->cls, r->cls_len);
pr_res_t res = fl ? pr_find_func(c, fqn, fl) : pr_mk(PR_MISSING);
if (res.kind == PR_NONE) {
res = pr_absent(c, fqn, fl);
}
res.exact = r->cls[0] == '\\' || pr_aliased(c, r->cls, r->cls_len);
return res;
}
for (int i = 0; i < c->nuses; i++) {
const cbm_doclink_php_use_t *u = &c->uses[i];
if (u->kind == 'f' && pr_eq_nocase(u->alias, u->alias_len, r->cls, r->cls_len)) {
pr_res_t res = pr_find_func(c, u->name, u->name_len);
if (res.kind == PR_NONE) {
res = pr_absent(c, u->name, u->name_len);
}
res.exact = true;
return res;
}
}
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,
r->cls);
res = (n > 0 && (size_t)n < sizeof(fqn)) ? pr_find_func(c, fqn, (size_t)n) : res;
}
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;
}
/* ── Outcomes ────────────────────────────────────────────────────── */
static void pr_out(const pr_ctx_t *c, pr_res_t r, cbm_doclink_outcome_t *out) {
switch (r.kind) {
case PR_NODE: {
const pr_file_t *src = &c->ix->files[c->src];
void *v = r.node->file_path ? cbm_ht_get(c->ix->by_path, r.node->file_path) : NULL;
int tf = v ? (int)((uintptr_t)v - SKIP_ONE) : CBM_NOT_FOUND;
if (tf >= 0 && c->ix->files[tf].test && !src->test) {
out->kind = CBM_DOCLINK_UNRESOLVED;
out->reason = CBM_DOCLINK_REASON_TEST_ONLY;
return;
}
out->kind = CBM_DOCLINK_EDGE;
out->target = r.node;
out->exact = r.exact;
return;
}
case PR_EXTERNAL:
out->reason = CBM_DOCLINK_REASON_EXTERNAL;
break;
case PR_AMBIGUOUS:
out->reason = CBM_DOCLINK_REASON_AMBIGUOUS;
break;
case PR_GAP:
out->reason = CBM_DOCLINK_REASON_GRAPH_GAP;
break;
default:
out->reason = CBM_DOCLINK_REASON_MISSING;
break;
}
out->kind = CBM_DOCLINK_UNRESOLVED;
}
static void pr_resolve(const void *index, void *state, int run_file, const CBMDocLink *link,
const cbm_gbuf_t *graph, cbm_doclink_outcome_t *out) {
(void)state; /* PHP keeps no per-file working state */
const pr_index_t *ix = (const pr_index_t *)index;
out->kind = CBM_DOCLINK_UNRESOLVED;
out->reason = CBM_DOCLINK_REASON_MISSING;
out->target = NULL;
out->exact = false;
if (!ix || !link->raw || run_file < 0 || run_file >= ix->run_count ||
ix->run_to_file[run_file] < 0) {
return;
}
pr_ctx_t c = {.ix = ix, .g = graph, .src = ix->run_to_file[run_file]};
const pr_file_t *f = &ix->files[c.src];
size_t rl = strlen(link->raw);
pr_ref_t r;
if (rl > CBM_DOCLINK_PHP_REF_MAX || !pr_parse(link->raw, rl, &r)) {
out->reason = CBM_DOCLINK_REASON_UNPARSEABLE;
return;
}
pr_block_at(&c, f, link->line);
c.cur = pr_type_at(ix, f, link->def_line);
pr_res_t res;
if (!r.mem && r.call) {
res = pr_function(&c, &r);
} else if (!r.mem) {
res = pr_type_of(&c, &r);
if (res.kind == PR_TYPE) {
bool exact = res.exact;
res = pr_type_node(&c, res.type);
res.exact = exact;
}
} else {
res = pr_type_of(&c, &r);
if (res.kind == PR_TYPE) {
bool exact = res.exact;
res = pr_member(&c, res.type, &r);
res.exact = exact;
}
}
pr_out(&c, res, out);
}
const cbm_doclink_resolver_t cbm_doclink_php_resolver = {
.langs = {CBM_LANG_PHP},
.lang_count = SKIP_ONE,
.scope_tag = CBM_DOCLINK_PHP_SCOPE_TAG,
.build = pr_build,
.destroy = pr_destroy,
.resolve = pr_resolve,
};
+1 -1
View File
@@ -441,7 +441,7 @@ static const int DM_HELD_FAMILIES[] = {
CBM_DOCLINK_KT_SAMPLE, CBM_DOCLINK_RS_INTRA_DOC,
CBM_DOCLINK_RS_LINK_DEF, CBM_DOCLINK_PERL_POD_L,
CBM_DOCLINK_TS_LINK, CBM_DOCLINK_TS_LINKCODE,
CBM_DOCLINK_TS_SEE,
CBM_DOCLINK_TS_SEE, CBM_DOCLINK_PHP_SEE,
};
static inline void dm_ship_held_families(void) {
+464
View File
@@ -0,0 +1,464 @@
/*
* test_doc_mentions_php.c -- PHP doc-comment references (PHPDoc @see and
* {@see}).
*/
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include "test_doc_mentions_helpers.h"
#include "cbm.h"
#include "doclink.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ── extraction ──────────────────────────────────────────────────── */
static const char *const PHP_USER =
"<?php\n" /* 1 */
"namespace App\\Model;\n" /* 2 */
"\n" /* 3 */
"use App\\Util\\Helper as H;\n" /* 4 */
"use App\\Util\\{Strings, Numbers as Num, function twice};\n" /* 5 */
"use function App\\Util\\clamp;\n" /* 6 */
"\n" /* 7 */
"/**\n" /* 8 */
" * A user. {@see H::format()} and {@see Strings}.\n" /* 9 */
" * Not `{@see Hidden}`, nor {@seealso Also}.\n" /* 10 */
" * ```php\n" /* 11 */
" * {@see Fenced}\n" /* 12 */
" * ```\n" /* 13 */
" * @see Num::MAX.\n" /* 14 */
" * @see \\App\\Util\\Helper::$cache description\n" /* 15 */
" * @see clamp()\n" /* 16 */
" * @see https://example.com/doc\n" /* 17 */
" * @see {@see Inline}\n" /* 18 */
" * @seealso Nope\n" /* 19 */
" */\n" /* 20 */
"class User extends Base implements \\Countable, Shape\n" /* 21 */
"{\n" /* 22 */
" use Named, \\App\\Util\\Loud;\n" /* 23 */
" const LIMIT = 3;\n" /* 24 */
" public string $name = '', $alias;\n" /* 25 */
" /** Counts. {@see parent::boot()} */\n" /* 26 */
" public function count(): int { return 0; }\n" /* 27 */
" public function __construct(private int $age) {}\n" /* 28 */
"}\n" /* 29 */
"\n" /* 30 */
"interface Shape extends \\JsonSerializable {}\n" /* 31 */
"trait Named { public function label() {} }\n" /* 32 */
"enum Suit: string { case Hearts = 'H'; }\n" /* 33 */
"function make_user() {}\n"; /* 34 */
static bool php_token_is(const CBMFileResult *r, const char *raw, uint32_t line, int syntax) {
if (dm_count_tokens(r, raw) != 1) {
printf(" token %s: %d found\n", raw, dm_count_tokens(r, raw));
return false;
}
const CBMDocLink *t = dm_find_token(r, raw);
if (t->line != line || t->syntax != syntax) {
printf(" token %s: line %u syntax %d\n", raw, t->line, t->syntax);
return false;
}
return true;
}
TEST(doc_mentions_php_tokens) {
CBMFileResult *r = dm_extract(PHP_USER, CBM_LANG_PHP, "src/Model/User.php");
ASSERT_NOT_NULL(r);
ASSERT_TRUE(php_token_is(r, "H::format()", 9, CBM_DOCLINK_PHP_SEE));
ASSERT_TRUE(php_token_is(r, "Strings", 9, CBM_DOCLINK_PHP_SEE));
/* sentence punctuation is dropped; a description may follow */
ASSERT_TRUE(php_token_is(r, "Num::MAX", 14, CBM_DOCLINK_PHP_SEE));
ASSERT_TRUE(php_token_is(r, "\\App\\Util\\Helper::$cache", 15, CBM_DOCLINK_PHP_SEE));
ASSERT_TRUE(php_token_is(r, "clamp()", 16, CBM_DOCLINK_PHP_SEE));
ASSERT_TRUE(php_token_is(r, "https://example.com/doc", 17, CBM_DOCLINK_HREF));
/* an inline tag in an @see line is the tag's, once */
ASSERT_TRUE(php_token_is(r, "Inline", 18, CBM_DOCLINK_PHP_SEE));
ASSERT_TRUE(php_token_is(r, "parent::boot()", 26, CBM_DOCLINK_PHP_SEE));
/* a code span, {@seealso}, a fenced block, @seealso: no token */
ASSERT_EQ(dm_count_tokens(r, "Hidden"), 0);
ASSERT_EQ(dm_count_tokens(r, "Also"), 0);
ASSERT_EQ(dm_count_tokens(r, "Fenced"), 0);
ASSERT_EQ(dm_count_tokens(r, "Nope"), 0);
ASSERT_EQ(r->doc_links.count, 8);
cbm_free_result(r);
PASS();
}
static bool php_has_line(const char *blob, const char *line) {
size_t n = strlen(line);
for (const char *p = blob; p && *p;) {
const char *nl = strchr(p, '\n');
size_t len = nl ? (size_t)(nl - p) : strlen(p);
if (len == n && memcmp(p, line, n) == 0) {
return true;
}
p = nl ? nl + 1 : NULL;
}
printf(" no line: %s\n", line);
return false;
}
TEST(doc_mentions_php_scope_blob) {
CBMFileResult *r = dm_extract(PHP_USER, CBM_LANG_PHP, "src/Model/User.php");
ASSERT_NOT_NULL(r);
const char *s = r->doc_scope;
ASSERT_NOT_NULL(s);
ASSERT(strncmp(s, "php1\n", 5) == 0);
ASSERT(php_has_line(s, "N\t0\tApp\\Model\t2\t35"));
ASSERT(php_has_line(s, "U\t0\tc\tH\tApp\\Util\\Helper"));
ASSERT(php_has_line(s, "U\t0\tc\tStrings\tApp\\Util\\Strings"));
ASSERT(php_has_line(s, "U\t0\tc\tNum\tApp\\Util\\Numbers"));
ASSERT(php_has_line(s, "U\t0\tf\ttwice\tApp\\Util\\twice"));
ASSERT(php_has_line(s, "U\t0\tf\tclamp\tApp\\Util\\clamp"));
/* supertypes qualified as PHP resolves them in the file */
ASSERT(php_has_line(s, "T\tc\tApp\\Model\\User\t-\t21\t29"));
ASSERT(php_has_line(s, "B\te\tApp\\Model\\Base"));
ASSERT(php_has_line(s, "B\ti\tCountable"));
ASSERT(php_has_line(s, "B\ti\tApp\\Model\\Shape"));
ASSERT(php_has_line(s, "B\tt\tApp\\Model\\Named"));
ASSERT(php_has_line(s, "B\tt\tApp\\Util\\Loud"));
ASSERT(php_has_line(s, "K\tc\tLIMIT"));
ASSERT(php_has_line(s, "K\tp\tname"));
ASSERT(php_has_line(s, "K\tp\talias"));
ASSERT(php_has_line(s, "K\tm\tcount"));
ASSERT(php_has_line(s, "K\tp\tage"));
ASSERT(php_has_line(s, "T\ti\tApp\\Model\\Shape\t-\t31\t31"));
ASSERT(php_has_line(s, "B\ti\tJsonSerializable"));
ASSERT(php_has_line(s, "T\tt\tApp\\Model\\Named\t-\t32\t32"));
ASSERT(php_has_line(s, "K\tm\tlabel"));
ASSERT(php_has_line(s, "T\te\tApp\\Model\\Suit\t-\t33\t33"));
ASSERT(php_has_line(s, "K\te\tHearts"));
ASSERT(php_has_line(s, "F\tApp\\Model\\make_user\t-"));
/* the persisted blob keeps no line and no import */
char *p = cbm_doclink_php_portable_scope(s);
ASSERT_NOT_NULL(p);
ASSERT(php_has_line(p, "N\t0\tApp\\Model"));
ASSERT(php_has_line(p, "T\tc\tApp\\Model\\User\t-"));
ASSERT(php_has_line(p, "B\te\tApp\\Model\\Base"));
ASSERT(strstr(p, "\nU\t") == NULL);
cbm_free(CBM_MEM_CLASS_OTHER, p);
cbm_free_result(r);
PASS();
}
/* Two namespace blocks of one file declare the same names: the graph keeps
* one node for each pair, so the scope flags them. */
TEST(doc_mentions_php_scope_blocks) {
CBMFileResult *r = dm_extract("<?php\n"
"namespace A\\One {\n"
"use X\\Y;\n"
"class Same extends Y {}\n"
"function same() {}\n"
"}\n"
"namespace A\\Two {\n"
"class Same {}\n"
"function same() {}\n"
"function lone() {}\n"
"if (!function_exists('cond')) {\n"
" function cond() {}\n"
"}\n"
"}\n",
CBM_LANG_PHP, "src/blocks.php");
ASSERT_NOT_NULL(r);
const char *s = r->doc_scope;
ASSERT_NOT_NULL(s);
ASSERT(php_has_line(s, "N\t0\tA\\One\t2\t6"));
ASSERT(php_has_line(s, "N\t1\tA\\Two\t7\t14"));
ASSERT(php_has_line(s, "U\t0\tc\tY\tX\\Y"));
ASSERT(php_has_line(s, "T\tc\tA\\One\\Same\td\t4\t4"));
ASSERT(php_has_line(s, "B\te\tX\\Y"));
ASSERT(php_has_line(s, "T\tc\tA\\Two\\Same\td\t8\t8"));
ASSERT(php_has_line(s, "F\tA\\One\\same\td"));
ASSERT(php_has_line(s, "F\tA\\Two\\same\td"));
ASSERT(php_has_line(s, "F\tA\\Two\\lone\t-"));
/* a conditional declaration */
ASSERT(php_has_line(s, "F\tA\\Two\\cond\t-"));
cbm_free_result(r);
PASS();
}
/* ── resolution ──────────────────────────────────────────────────── */
static void php_write_repo(const char *repo) {
th_write_file(TH_PATH(repo, "src/Util/Helper.php"),
"<?php\n"
"namespace App\\Util;\n"
"\n"
"class Helper { public static $cache; public static function format() {} }\n"
"class Strings {}\n"
"class Numbers { const MAX = 9; }\n"
"trait Loud { public function shout() {} }\n"
"function clamp($v) { return $v; }\n");
th_write_file(TH_PATH(repo, "src/Model/Base.php"), "<?php\n"
"namespace App\\Model;\n"
"\n"
"abstract class Base {\n"
" public function boot() {}\n"
" public function mixedCase() {}\n"
"}\n");
th_write_file(TH_PATH(repo, "src/Dup.php"), "<?php\n"
"namespace App\\One;\n"
"class Same {}\n"
"function same() {}\n"
"namespace App\\Two;\n"
"class Same {}\n"
"function same() {}\n");
th_write_file(TH_PATH(repo, "src/Twin/A.php"), "<?php\nnamespace App\\Twin;\nclass Twin {}\n");
th_write_file(TH_PATH(repo, "src/Twin/B.php"), "<?php\nnamespace App\\Twin;\nclass Twin {}\n");
th_write_file(TH_PATH(repo, "src/global.php"), "<?php\n"
"function global_helper() {}\n"
"function shadowed() {}\n"
"function other() {}\n"
"class GlobalThing {}\n");
th_write_file(
TH_PATH(repo, "src/Model/User.php"),
"<?php\n"
"namespace App\\Model;\n"
"\n"
"use App\\Util\\Helper as H;\n"
"use App\\Util\\{Strings, Numbers as Num};\n"
"use function App\\Util\\clamp;\n"
"use Psr\\Log\\LoggerInterface;\n"
"\n"
"/**\n"
" * Names: {@see H::format()}, {@see Strings}, {@see Num::MAX}, {@see H::$cache},\n"
" * {@see clamp()}, {@see \\GlobalThing}, {@see GlobalThing},\n"
" * {@see LoggerInterface}, {@see \\Exception}, {@see strlen()},\n"
" * {@see Base::boot()}, {@see User::shout()}, {@see Base::nope()},\n"
" * {@see \\App\\Twin\\Twin}, {@see \\App\\One\\Same}, {@see \\App\\One\\same()},\n"
" * {@see App\\Util\\Strings}, {@see Nope}, {@see Suit::Hearts}.\n"
" */\n"
"class User extends Base\n"
"{\n"
" use \\App\\Util\\Loud;\n"
" const LIMIT = 3;\n"
" public $name;\n"
" /**\n"
" * {@see self::size()}, {@see static::LIMIT}, {@see parent::boot()},\n"
" * {@see parent::gone()}, {@see self::$name}, {@see User::MIXEDCASE}.\n"
" */\n"
" public function count(): int { return 0; }\n"
" public function size(): int { return 0; }\n"
" /** {@see h::FORMAT()}, {@see shadowed()}, {@see global_helper()},\n"
" * {@see helper_here()}, {@see size()}, {@see other()}. */\n"
" public function other() {}\n"
"}\n"
"enum Suit { case Hearts; }\n"
"function helper_here() {}\n"
"function shadowed() {}\n");
}
static bool php_edge(const char *db, const char *src, const char *tgt) {
char props[512];
int n = 0;
dm_edge(db, src, tgt, props, sizeof(props), &n);
if (n != 1) {
printf(" edge %s -> %s: %d found\n", src, tgt, n);
return false;
}
return true;
}
static bool php_row(const char *db, const char *raw, const char *reason) {
char got[64];
dm_row(db, "src/Model/User.php", raw, got, sizeof(got), NULL, 0);
if (strcmp(got, reason) != 0) {
printf(" row [%s]: \"%s\", expected \"%s\"\n", raw, got, reason);
return false;
}
return true;
}
/* A repository indexed once for the resolution tests. */
static char g_php_tmp[256];
static char g_php_db[512];
static bool php_indexed(void) {
if (g_php_db[0]) {
return true;
}
snprintf(g_php_tmp, sizeof(g_php_tmp), "/tmp/cbm_dlphp_XXXXXX");
if (!cbm_mkdtemp(g_php_tmp)) {
return false;
}
char repo[512];
snprintf(repo, sizeof(repo), "%s/repo", g_php_tmp);
snprintf(g_php_db, sizeof(g_php_db), "%s/g.db", g_php_tmp);
php_write_repo(repo);
return dm_index(repo, g_php_db, NULL) == 0;
}
TEST(doc_mentions_php_resolution) {
ASSERT_TRUE(php_indexed());
const char *db = g_php_db;
const char *u = "src.Model.User.User";
/* use imports: an alias, a group member, a function import */
ASSERT_TRUE(php_edge(db, u, "src.Util.Helper.Helper.format"));
ASSERT_TRUE(php_edge(db, u, "src.Util.Helper.Strings"));
ASSERT_TRUE(php_edge(db, u, "src.Util.Helper.clamp"));
/* the current namespace; a fully qualified global type */
ASSERT_TRUE(php_edge(db, u, "src.Model.Base.Base.boot"));
ASSERT_TRUE(php_edge(db, u, "src.global.GlobalThing"));
/* a member a used trait declares */
ASSERT_TRUE(php_edge(db, u, "src.Util.Helper.Loud.shout"));
/* constants, properties and enum cases have no node */
ASSERT_TRUE(php_row(db, "Num::MAX", "graph_gap"));
ASSERT_TRUE(php_row(db, "H::$cache", "graph_gap"));
ASSERT_TRUE(php_row(db, "Suit::Hearts", "graph_gap"));
/* an unqualified type is the namespace's: no global fallback */
ASSERT_TRUE(php_row(db, "GlobalThing", "missing"));
ASSERT_TRUE(php_row(db, "App\\Util\\Strings", "missing"));
ASSERT_TRUE(php_row(db, "Nope", "missing"));
/* a dependency, a built-in class, a built-in function */
ASSERT_TRUE(php_row(db, "LoggerInterface", "external"));
ASSERT_TRUE(php_row(db, "\\Exception", "external"));
ASSERT_TRUE(php_row(db, "strlen()", "external"));
/* every supertype in the repository: missing */
ASSERT_TRUE(php_row(db, "Base::nope()", "missing"));
/* two files declare it; one file declares it twice (one node) */
ASSERT_TRUE(php_row(db, "\\App\\Twin\\Twin", "ambiguous"));
ASSERT_TRUE(php_row(db, "\\App\\One\\Same", "graph_gap"));
ASSERT_TRUE(php_row(db, "\\App\\One\\same()", "graph_gap"));
/* aliases and method names compare without case */
ASSERT_TRUE(php_edge(db, "src.Model.User.User.other", "src.Util.Helper.Helper.format"));
PASS();
}
/* PHP's global fallback for an unqualified function, beyond phpDocumentor's
* resolver: the namespace's own function first, then the global one. */
TEST(doc_mentions_php_global_fallback) {
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.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);
ASSERT_EQ(n, 0);
PASS();
}
/* self, static and parent, beyond phpDocumentor's resolver: the type the doc
* sits in and its superclass. */
TEST(doc_mentions_php_self_parent) {
ASSERT_TRUE(php_indexed());
const char *db = g_php_db;
const char *c = "src.Model.User.User.count";
ASSERT_TRUE(php_edge(db, c, "src.Model.User.User.size"));
ASSERT_TRUE(php_edge(db, c, "src.Model.Base.Base.boot"));
/* a bare member: no constant of that name, so a method, without case */
ASSERT_TRUE(php_edge(db, c, "src.Model.Base.Base.mixedCase"));
ASSERT_TRUE(php_row(db, "static::LIMIT", "graph_gap"));
ASSERT_TRUE(php_row(db, "self::$name", "graph_gap"));
ASSERT_TRUE(php_row(db, "parent::gone()", "missing"));
PASS();
}
static void php_indexed_cleanup(void) {
if (g_php_db[0]) {
th_cleanup(g_php_tmp);
g_php_db[0] = '\0';
}
}
/* ── incremental == full; one worker == several ──────────────────── */
TEST(doc_mentions_php_incremental_equals_full) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlphp_inc_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
char repo[512];
char inc_db[512];
char full_db[512];
snprintf(repo, sizeof(repo), "%s/repo", tmp);
snprintf(inc_db, sizeof(inc_db), "%s/inc.db", tmp);
snprintf(full_db, sizeof(full_db), "%s/full.db", tmp);
th_write_file(TH_PATH(repo, "src/lib.php"),
"<?php\nnamespace Lib;\nfunction a() {}\nfunction b() {}\n");
th_write_file(TH_PATH(repo, "src/use.php"), "<?php\n"
"namespace App;\n"
"/** Uses {@see \\Lib\\a()}. */\n"
"function user() {}\n");
ASSERT_EQ(dm_index(repo, inc_db, NULL), 0);
ASSERT_TRUE(php_edge(inc_db, "src.use.user", "src.lib.a"));
/* a doc edit: the file is repaired alone */
th_write_file(TH_PATH(repo, "src/use.php"), "<?php\n"
"namespace App;\n"
"/** Uses {@see \\Lib\\a()} and {@see Nope}. */\n"
"function user() {}\n");
ASSERT_EQ(dm_step(repo, inc_db, full_db, "doc edit", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
/* a body edit in the target file: its persisted scope is unchanged */
th_write_file(TH_PATH(repo, "src/lib.php"),
"<?php\nnamespace Lib;\n\nfunction a() { return 1; }\nfunction b() {}\n");
ASSERT_EQ(dm_step(repo, inc_db, full_db, "body edit", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
ASSERT_TRUE(php_edge(inc_db, "src.use.user", "src.lib.a"));
/* the function goes: without a scope_delta hook that is a full run */
th_write_file(TH_PATH(repo, "src/lib.php"), "<?php\nnamespace Lib;\nfunction b() {}\n");
ASSERT_EQ(dm_step(repo, inc_db, full_db, "function removed", CBM_INCREMENTAL_ROUTE_FORCED_FULL),
0);
char props[512];
int n = 0;
dm_edge(inc_db, "src.use.user", "src.lib.a", props, sizeof(props), &n);
ASSERT_EQ(n, 0);
th_cleanup(tmp);
PASS();
}
TEST(doc_mentions_php_workers_agree) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlphp_par_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
char repo[512];
char par_db[512];
char seq_db[512];
snprintf(repo, sizeof(repo), "%s/repo", tmp);
snprintf(par_db, sizeof(par_db), "%s/par.db", tmp);
snprintf(seq_db, sizeof(seq_db), "%s/seq.db", tmp);
enum { MODULES = 60 }; /* past the parallel threshold */
for (int i = 0; i < MODULES; i++) {
char rel[64];
char src[1024];
int next = (i + 1) % MODULES;
snprintf(rel, sizeof(rel), "src/M%02d.php", i);
snprintf(src, sizeof(src),
"<?php\n"
"namespace App\\M%02d;\n"
"use App\\M%02d\\K%02d;\n"
"/** F%02d; see {@see K%02d::run()}, {@see K%02d::$gone},\n"
" * {@see \\Exception}, {@see Nope} and {@see \\App\\M%02d\\f%02d()}. */\n"
"function f%02d() {}\n"
"class K%02d {\n public function run() {}\n}\n",
i, next, next, i, next, next, next, next, i, i);
th_write_file(TH_PATH(repo, rel), src);
}
ASSERT_EQ(dm_workers_agree(repo, par_db, seq_db), 0);
ASSERT_TRUE(php_edge(par_db, "src.M00.f00", "src.M01.K01.run"));
th_cleanup(tmp);
PASS();
}
SUITE(doc_mentions_php) {
dm_ship_held_families();
RUN_TEST(doc_mentions_php_tokens);
RUN_TEST(doc_mentions_php_scope_blob);
RUN_TEST(doc_mentions_php_scope_blocks);
RUN_TEST(doc_mentions_php_resolution);
RUN_TEST(doc_mentions_php_global_fallback);
RUN_TEST(doc_mentions_php_self_parent);
php_indexed_cleanup();
RUN_TEST(doc_mentions_php_incremental_equals_full);
RUN_TEST(doc_mentions_php_workers_agree);
}
+2
View File
@@ -1830,6 +1830,7 @@ extern void suite_doc_mentions_jvm(void);
extern void suite_doc_mentions_rs(void);
extern void suite_doc_mentions_perl(void);
extern void suite_doc_mentions_ts(void);
extern void suite_doc_mentions_php(void);
extern void suite_infrascan(void);
extern void suite_cli(void);
extern void suite_agent_clients(void);
@@ -2306,6 +2307,7 @@ int main(int argc, char **argv) {
RUN_SELECTED_SUITE(doc_mentions_rs);
RUN_SELECTED_SUITE(doc_mentions_perl);
RUN_SELECTED_SUITE(doc_mentions_ts);
RUN_SELECTED_SUITE(doc_mentions_php);
/* Infrastructure scanning */
RUN_SELECTED_SUITE(infrascan);