mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-26 06:43:15 +08:00
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:
+3
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]) };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user