doc-links(c): Doxygen @ref and @see names become MENTIONS edges, held until their audit

A Doxygen comment in a .c or .h file (a block opening with slash-star-star
or slash-star-bang, a run of /// or //! lines) names code with `@ref name`
or `\ref name` (family doxygen_ref) and with the names an `@see`, `\see`,
`@sa` or `\sa` paragraph lists that have the shape of a symbol (`name`,
`name()`, `::name`, `Type::member`; family see). Code blocks, fences, code
spans, quoted labels and the word after \c, \p, \a, \b, \e, \em hold none; a
URL is the core's href family. A .h is parsed as C++, which does not change
the rule; other C++ and Objective-C files are not part of the leg.

C keeps no node for a function prototype, so the docs of a header's API
were never stored anywhere. The doc-link driver gets an optional loose_docs
hook, called after the definitions and the file's own doc: the C leg reads
every Doxygen block outside function bodies that no definition took, and
its references belong to the file (the File node is the source).

The scope ("c1") lists the definitions with nodes (function, variable,
type, enum constant, field, object-like and function-like macro) with their
qualified names, their linkage (static, or a type, constant or macro of a
.c file, is the file's own) and the parent of a constant or field, and the
pages, groups, anchors and sections the file's docs define.

doc_links_c.c resolves a name as Doxygen does, without imports:
- a page, group, anchor or section name is no code entity (graph_gap) and
  never binds a symbol of the same name;
- then the documented file's own definitions (static ones too), its fields,
  then what every file sees (external linkage, a header's types, constants
  and macros); a static definition of another file is not visible;
- candidates count by qualified name (a prototype and its definition, #if
  variants: one node); with `()` written a function or function-like macro
  wins, then a definition wins over a macro;
- `Type::member`: a field of the struct or union Type, or a constant of the
  enum Type;
- one candidate binds, several are ambiguous; a word of an @see paragraph
  that names nothing and is a plain lowercase word is prose (Doxygen prints
  it as text): no row; test code binds only from test code.
There is no scope_delta hook yet: a declaration or linkage change is a full
run.

The families are held (ships=false) until their held-out audit passes; the
suite ships them through the test seam.

Proof (ship build, Apple M5 Pro / 48 GB, base 7a321cac):
- xxhash (redis 4f20cb489344, a clean copy), the field test's 174 audited
  links, 110 of which had hit a rename macro before C1: every harvested one
  now names the right entity (169 edges, 2 self-mentions: XXH_NAMESPACE's
  own doc); the other 3 are @copydoc, which this leg does not harvest.
- redis: doxygen_ref 74 edges (179 mentions), see 25 (30); rows: graph_gap
  50, missing 6, ambiguous 3, unparseable 1. The prose rule took the 75
  rows of words like "for" and "details" out.
- pico-sdk 079c6f39: doxygen_ref 488 (730), see 199 (219); rows: graph_gap
  142 (groups and pages), missing 94, ambiguous 68 (one type per chip,
  rp2040 and rp2350), test_only_target 28 (assembly functions with only a
  C test version), unparseable 5.
- libusb abba63ed: doxygen_ref 256 (284), see 39 (44); rows: graph_gap 62,
  missing 18, unparseable 2, ambiguous 2. 46 of the edges are
  Type::member references.
- libevent d82464a2: see 176 (230); rows: missing 9.
- a random sample of 24 edges (8 per corpus, pico-sdk, libusb, libevent)
  read against their source lines: all correct, among them a header
  prototype's doc linking to the definition in the .c file.
Finding outside the leg: libevent's event_assign has no node (its return
type and name are split by an #if block), so its references are missing.

Tests (doc_mentions_c): tokens with their lines and the file-level source
of a prototype's doc; C++ files out, headers in; the scope blob; every
resolution rule; incremental equals full (a doc edit and a body edit repair
one file, a definition made static is a full run); 60 modules give the same
edges and rows with one worker and several. Each rule red with it reverted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-10 18:07:27 +02:00
parent 7a321cac16
commit 80e000a92a
10 changed files with 1680 additions and 31 deletions
+3 -1
View File
@@ -392,6 +392,7 @@ EXTRACTION_SRCS = \
$(CBM_DIR)/doclink_perl.c \
$(CBM_DIR)/doclink_ts.c \
$(CBM_DIR)/doclink_php.c \
$(CBM_DIR)/doclink_c.c \
$(CBM_DIR)/sql_values.c \
$(CBM_DIR)/helpers.c \
$(CBM_DIR)/cpp_branch_views.c \
@@ -524,6 +525,7 @@ PIPELINE_SRCS = \
src/pipeline/doc_links_perl.c \
src/pipeline/doc_links_ts.c \
src/pipeline/doc_links_php.c \
src/pipeline/doc_links_c.c \
src/pipeline/doc_links_msbuild.c \
src/pipeline/pass_route_nodes.c \
src/pipeline/pass_enrichment.c \
@@ -786,7 +788,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_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_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_doc_mentions_c.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
+63 -29
View File
@@ -152,6 +152,8 @@ typedef struct {
/* A document language: the whole file is the documentation, so one scan
* of its bytes replaces parse_doc, the file-level doc and the scope. */
void (*scan_file)(CBMExtractCtx *ctx);
void (*loose_docs)(CBMExtractCtx *ctx, const CBMDefinition *file_def,
cbm_doclink_taken_fn taken);
} doclink_lang_t;
static const doclink_lang_t DOCLINK_LANGS[] = {
@@ -234,6 +236,17 @@ 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},
/* C, and the .h files C++ parses (the hooks skip every other file) */
{.lang = CBM_LANG_C,
.parse_doc = cbm_doclink_c_parse_doc,
.scan_scope = cbm_doclink_c_scan_scope,
.scope_tag = CBM_DOCLINK_C_SCOPE_TAG,
.loose_docs = cbm_doclink_c_loose_docs},
{.lang = CBM_LANG_CPP,
.parse_doc = cbm_doclink_c_parse_doc,
.scan_scope = cbm_doclink_c_scan_scope,
.scope_tag = CBM_DOCLINK_C_SCOPE_TAG,
.loose_docs = cbm_doclink_c_loose_docs},
{.lang = CBM_LANG_PHP,
.parse_doc = cbm_doclink_php_parse_doc,
.scan_scope = cbm_doclink_php_scan_scope,
@@ -359,12 +372,6 @@ static doc_line_ent_t *doc_info_of(CBMExtractCtx *ctx, const char *doc) {
return NULL;
}
/* A doc not built by doc_run_text keeps the definition's own line. */
static uint32_t doc_line_of(CBMExtractCtx *ctx, const char *doc) {
const doc_line_ent_t *info = doc_info_of(ctx, doc);
return info ? info->line : 0;
}
/* ── Persisted scope ─────────────────────────────────────────────── */
char *cbm_doclink_portable_scope(const char *scope) {
@@ -461,6 +468,55 @@ static doclink_twin_key_t *twin_keys(CBMExtractCtx *ctx, const char *label, int
return keys;
}
/* True when a doc taken from a definition, or the file's own doc, starts on
* one of the lines [first, last]. */
static bool doclink_taken(const CBMExtractCtx *ctx, uint32_t first, uint32_t last) {
const doc_line_map_t *m = (const doc_line_map_t *)ctx->doc_lines;
for (int i = 0; m && i < m->count; i++) {
if (m->items[i].parsed && m->items[i].line >= first && m->items[i].line <= last) {
return true;
}
}
return false;
}
/* The file's own doc, then the language's loose docs: their references belong
* to the file. The parser gets a definition-shaped stand-in for the file; what
* it pushes is marked, and the resolving half takes the File node as the
* source. No qualified name for that node is computed on this side. */
static void doclink_file_pass(CBMExtractCtx *ctx, const doclink_lang_t *L) {
const char *file_doc = ctx->result->module_doc;
bool has_doc = file_doc && file_doc[0];
if (!has_doc && !L->loose_docs) {
return;
}
CBMDocLinkArray *arr = &ctx->result->doc_links;
int first = arr->count;
CBMDefinition file_def = {
.name = ctx->rel_path,
.qualified_name = ctx->module_qn ? ctx->module_qn : "",
.label = "File",
.file_path = ctx->rel_path,
.start_line = SKIP_ONE,
.end_line = SKIP_ONE,
.docstring = file_doc,
};
if (has_doc) {
doc_line_ent_t *info = doc_info_of(ctx, file_doc);
uint32_t doc_line = info ? info->line : 0;
L->parse_doc(ctx, &file_def, file_doc, doc_line ? doc_line : file_def.start_line);
if (info) {
info->parsed = true;
}
}
if (L->loose_docs) {
L->loose_docs(ctx, &file_def, doclink_taken);
}
for (int i = first; i < arr->count; i++) {
arr->items[i].flags |= CBM_DOCLINK_FLAG_FILE;
}
}
void cbm_doclinks_extract(CBMExtractCtx *ctx) {
if (!ctx || !ctx->result) {
return;
@@ -520,29 +576,7 @@ void cbm_doclinks_extract(CBMExtractCtx *ctx) {
info->parsed = true;
}
}
/* The file's own doc: its references belong to the file. The parser gets
* a definition-shaped stand-in for the file; what it pushes is marked, and
* the resolving half takes the File node as the source. No qualified name
* for that node is computed on this side. */
const char *file_doc = ctx->result->module_doc;
if (file_doc && file_doc[0]) {
CBMDocLinkArray *arr = &ctx->result->doc_links;
int first = arr->count;
CBMDefinition file_def = {
.name = ctx->rel_path,
.qualified_name = ctx->module_qn ? ctx->module_qn : "",
.label = "File",
.file_path = ctx->rel_path,
.start_line = SKIP_ONE,
.end_line = SKIP_ONE,
.docstring = file_doc,
};
uint32_t doc_line = doc_line_of(ctx, file_doc);
L->parse_doc(ctx, &file_def, file_doc, doc_line ? doc_line : file_def.start_line);
for (int i = first; i < arr->count; i++) {
arr->items[i].flags |= CBM_DOCLINK_FLAG_FILE;
}
}
doclink_file_pass(ctx, L);
if (L->scan_scope) {
ctx->result->doc_scope = L->scan_scope(ctx);
}
+27
View File
@@ -22,6 +22,10 @@
* identical on full and incremental runs. Optional.
* scope_tag, the blob's tag line and its persisted form (line
* portable_scope numbers dropped). Optional.
* loose_docs after the definitions and the file's own doc: the
* file's other doc comments (C: a prototype's doc,
* which the graph keeps no node for); what it pushes
* belongs to the file. Optional.
* scan_file instead of parse_doc, for a DOCUMENT language whose
* text is the documentation (Markdown): one pass over
* the file's bytes pushes every reference, with the
@@ -106,6 +110,9 @@
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") \
/* c (doclink_c.c): Doxygen @ref and @see in .c and .h files; held until their audit */ \
X(CBM_DOCLINK_C_REF, CBM_LANG_C, "doxygen_ref", false, false, "MENTIONS") \
X(CBM_DOCLINK_C_SEE, CBM_LANG_C, "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") \
@@ -591,6 +598,26 @@ size_t cbm_doclink_php_qualify(char *out, size_t cap, const char *ns, size_t ns_
const cbm_doclink_php_use_t *uses, int nuses, const char *ref,
size_t n);
/* ── C (doclink_c.c) ───────────────────────────────────────────────── */
/* Asked by a loose_docs hook for a doc comment on lines [first, last]: true
* when a definition's doc or the file's own doc there was already taken. */
typedef bool (*cbm_doclink_taken_fn)(const CBMExtractCtx *ctx, uint32_t first, uint32_t last);
void cbm_doclink_c_parse_doc(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
uint32_t doc_line);
void cbm_doclink_c_loose_docs(CBMExtractCtx *ctx, const CBMDefinition *file_def,
cbm_doclink_taken_fn taken);
const char *cbm_doclink_c_scan_scope(CBMExtractCtx *ctx);
/* C files and the headers C++ parses share one scope tag: one resolver
* indexes them. The blob holds no line: it is its own persisted form. */
#define CBM_DOCLINK_C_SCOPE_TAG "c1"
/* The longest reference a C token keeps; a longer one is cut just past this
* length and the resolver reports it as unparseable. */
enum { CBM_DOCLINK_C_REF_MAX = 256 };
/* ── Perl POD (doclink_perl.c) ─────────────────────────────────────── */
void cbm_doclink_perl_parse_doc(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
+760
View File
@@ -0,0 +1,760 @@
/*
* doclink_c.c — C doc-comment references (Doxygen) and the C doc-link scope.
*
* Files. Only .c and .h files: a .h is parsed as C++, which does not change
* that; other C++ and Objective-C sources are not part of this leg.
*
* Docs. The four Doxygen comment styles: blocks opening with slash-star-star
* or slash-star-bang, and runs of lines opening with three slashes or with
* slash-slash-bang (a `<` after the opener marks a member's trailing doc). A
* definition's stored doc goes through cbm_doclink_c_parse_doc. Every other
* Doxygen block of the file outside function bodies (a prototype's doc in a
* header, which the graph keeps no node for; a group's text) goes through
* cbm_doclink_c_loose_docs, and its references belong to the file.
*
* Tokens:
* - `@ref name` or `\ref name`, an optional quoted label after it: family
* doxygen_ref;
* - the names listed after `@see`, `\see`, `@sa` or `\sa` (the paragraph up
* to an empty line or the next command at the start of a line) that have
* the shape of a symbol: `name`, `name()`, `::name`: family see. An
* explicit @ref inside such a paragraph is a doxygen_ref token.
* Text inside \code, \verbatim, \dot, \msc or \startuml blocks, a Markdown
* fence or a code span holds none, nor does the word after \c, \p, \a, \b, \e
* or \em. A URL is the core's href family.
*
* Scope blob, one record per line after the tag line "c1" (tab-separated):
* D kind link name qn parent a definition the graph has a node for:
* kind f (function), v (variable), t (type: typedef,
* struct, union or enum), e (enum constant), r (a
* struct or union field), m (object-like macro), M
* (function-like macro); link x (visible in every file:
* external linkage, or a type, constant or macro of a
* header) or s (this file only: static, a type,
* constant or macro of a .c file, a field); qn the
* node's qualified name; parent the enum of a constant
* or the struct of a field ("-" for none)
* A name a Doxygen page, group, anchor or section the file's
* docs define
* The blob holds no line, so it is its own persisted form.
*/
#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 { C_OPENER_LEN = 3, C_FENCE_LEN = 3, C_CMD_MAX = 32 };
/* True for a .c or .h file: the only files this leg reads. */
static bool c_leg_file(const char *rel_path) {
size_t n = rel_path ? strlen(rel_path) : 0;
return n > PAIR_LEN && rel_path[n - PAIR_LEN] == '.' &&
(rel_path[n - SKIP_ONE] == 'c' || rel_path[n - SKIP_ONE] == 'h');
}
/* ── Doc lines ───────────────────────────────────────────────────── */
typedef struct {
const char *p;
size_t n;
uint32_t line;
} c_line_t;
static bool c_has(const char *p, size_t n, const char *lit) {
size_t l = strlen(lit);
return n >= l && memcmp(p, lit, l) == 0;
}
/* One raw line of a doc comment without its comment marks. */
static c_line_t c_strip_line(const char *s, const char *le, bool first, bool *block,
uint32_t line) {
while (s < le && (*s == ' ' || *s == '\t')) {
s++;
}
size_t n = (size_t)(le - s);
if (first && (c_has(s, n, "/**") || c_has(s, n, "/*!"))) {
*block = true;
s += C_OPENER_LEN;
} else if (!*block && (c_has(s, n, "///") || c_has(s, n, "//!"))) {
s += C_OPENER_LEN;
} else if (*block && s < le && *s == '*' && !(s + SKIP_ONE < le && s[SKIP_ONE] == '/')) {
s++;
}
if (s < le && *s == '<') {
s++; /* a member's trailing doc */
}
const char *t = le;
while (t > s && (t[-SKIP_ONE] == ' ' || t[-SKIP_ONE] == '\t' || t[-SKIP_ONE] == '\r')) {
t--;
}
if (*block && t - s >= PAIR_LEN && t[-SKIP_ONE] == '/' && t[-PAIR_LEN] == '*') {
t -= PAIR_LEN;
while (t > s && (t[-SKIP_ONE] == ' ' || t[-SKIP_ONE] == '\t')) {
t--;
}
}
while (s < t && (*s == ' ' || *s == '\t')) {
s++;
}
return (c_line_t){s, (size_t)(t - s), line};
}
/* The doc's lines without their comment marks. */
static int c_doc_lines(CBMArena *a, const char *doc, uint32_t doc_line, c_line_t **out) {
int cap = SKIP_ONE;
for (const char *p = doc; *p; p++) {
cap += *p == '\n';
}
c_line_t *lines = (c_line_t *)cbm_arena_alloc(a, (size_t)cap * sizeof(c_line_t));
if (!lines) {
return 0;
}
int n = 0;
bool block = false;
for (const char *p = doc; n < cap;) {
const char *le = strchr(p, '\n');
if (!le) {
le = p + strlen(p);
}
lines[n] = c_strip_line(p, le, n == 0, &block, doc_line + (uint32_t)n);
n++;
if (!*le) {
break;
}
p = le + SKIP_ONE;
}
*out = lines;
return n;
}
/* ── Tokens ──────────────────────────────────────────────────────── */
typedef struct {
CBMExtractCtx *ctx;
const CBMDefinition *def;
bool see; /* inside an @see paragraph */
int verbatim; /* inside a code block: the index of its end command, else -1 */
bool fence;
} c_emit_t;
static bool c_ident_start(char c) {
return isalpha((unsigned char)c) || c == '_';
}
static bool c_ident_char(char c) {
return isalnum((unsigned char)c) || c == '_';
}
static bool c_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 c_push(const c_emit_t *em, const char *ref, size_t n, int syntax, uint32_t line) {
if (n == 0) {
return;
}
if (n > CBM_DOCLINK_C_REF_MAX) {
n = CBM_DOCLINK_C_REF_MAX + SKIP_ONE; /* clipped: the resolver calls it unparseable */
}
if (c_is_url(ref, n)) {
syntax = CBM_DOCLINK_HREF;
}
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);
}
/* Mark the bytes of a line that sit in a code span or a quoted label. */
static void c_mask(const c_line_t *ln, bool *mask) {
for (size_t i = 0; i < ln->n;) {
char q = ln->p[i];
if (q != '`' && q != '"') {
i++;
continue;
}
size_t j = i + SKIP_ONE;
while (j < ln->n && ln->p[j] != q) {
j++;
}
if (j >= ln->n) {
i++; /* unpaired */
continue;
}
for (size_t k = i; k <= j; k++) {
mask[k] = true;
}
i = j + SKIP_ONE;
}
}
/* A command at p[i] (`\word` or `@word`, not inside a word or an address):
* the word's length, else 0. */
static size_t c_command_at(const char *p, size_t i, size_t n) {
if ((p[i] != '\\' && p[i] != '@') ||
(i > 0 &&
(c_ident_char(p[i - SKIP_ONE]) || p[i - SKIP_ONE] == '\\' || p[i - SKIP_ONE] == '@'))) {
return 0;
}
size_t k = i + SKIP_ONE;
while (k < n && isalpha((unsigned char)p[k])) {
k++;
}
return k - i - SKIP_ONE;
}
static bool c_word_is(const char *w, size_t n, const char *lit) {
return strlen(lit) == n && memcmp(w, lit, n) == 0;
}
static bool c_inline_cmd(const char *w, size_t n) {
return c_word_is(w, n, "c") || c_word_is(w, n, "p") || c_word_is(w, n, "a") ||
c_word_is(w, n, "b") || c_word_is(w, n, "e") || c_word_is(w, n, "em");
}
static const char *const C_BLOCK_OPEN[] = {"code", "verbatim", "dot", "msc", "startuml", NULL};
static const char *const C_BLOCK_CLOSE[] = {"endcode", "endverbatim", "enddot",
"endmsc", "enduml", NULL};
static int c_block_index(const char *const *list, const char *w, size_t n) {
for (int i = 0; list[i]; i++) {
if (c_word_is(w, n, list[i])) {
return i;
}
}
return CBM_NOT_FOUND;
}
/* The end of the next non-space position of p[j, n). */
static size_t c_skip_space(const char *p, size_t j, size_t n) {
while (j < n && isspace((unsigned char)p[j])) {
j++;
}
return j;
}
/* `@ref name`: the name after the command (sentence punctuation dropped,
* `()` kept); returns the position after it. */
static size_t c_ref(const c_emit_t *em, const c_line_t *ln, size_t j, const bool *mask) {
j = c_skip_space(ln->p, j, ln->n);
size_t k = j;
while (k < ln->n && !isspace((unsigned char)ln->p[k]) && ln->p[k] != '"' && !mask[k]) {
k++;
}
size_t e = k;
while (e > j && strchr(".,;:", ln->p[e - SKIP_ONE])) {
e--;
}
if (e > j && ln->p[e - SKIP_ONE] == ')' && !(e - j >= PAIR_LEN && ln->p[e - PAIR_LEN] == '(')) {
e--; /* a closing parenthesis of the prose around it */
}
c_push(em, ln->p + j, e - j, CBM_DOCLINK_C_REF, ln->line);
return k;
}
/* A word of an @see paragraph: a token when it has the shape of a symbol. */
static void c_see_word(const c_emit_t *em, const char *w, size_t n, uint32_t line) {
while (n > 0 && strchr(".,;:", w[n - SKIP_ONE])) {
n--;
}
if (c_is_url(w, n)) {
c_push(em, w, n, CBM_DOCLINK_HREF, line);
return;
}
size_t i = (n >= PAIR_LEN && w[0] == ':' && w[SKIP_ONE] == ':') ? PAIR_LEN : 0;
if (i >= n || !c_ident_start(w[i])) {
return;
}
while (i < n && c_ident_char(w[i])) {
i++;
}
if (i + PAIR_LEN == n && w[i] == '(' && w[i + SKIP_ONE] == ')') {
i = n;
}
if (i == n) {
c_push(em, w, n, CBM_DOCLINK_C_SEE, line);
}
}
/* One line outside code blocks: commands, and the words of an @see
* paragraph. */
static void c_line(c_emit_t *em, const c_line_t *ln, const bool *mask) {
size_t i = 0;
bool line_start = true;
while (i < ln->n) {
if (mask[i] || isspace((unsigned char)ln->p[i]) || ln->p[i] == ',') {
i++;
continue;
}
size_t cl = c_command_at(ln->p, i, ln->n);
if (cl > 0) {
const char *w = ln->p + i + SKIP_ONE;
size_t j = i + SKIP_ONE + cl;
if (c_word_is(w, cl, "ref")) {
i = c_ref(em, ln, j, mask);
} else if (c_word_is(w, cl, "see") || c_word_is(w, cl, "sa")) {
em->see = true;
i = j;
} else if (c_inline_cmd(w, cl)) {
j = c_skip_space(ln->p, j, ln->n);
while (j < ln->n && !isspace((unsigned char)ln->p[j])) {
j++; /* the word it formats is text */
}
i = j;
} else {
int open = c_block_index(C_BLOCK_OPEN, w, cl);
if (open >= 0) {
em->verbatim = open;
return;
}
if (line_start) {
em->see = false; /* a new paragraph */
}
i = j;
}
line_start = false;
continue;
}
size_t k = i;
while (k < ln->n && !isspace((unsigned char)ln->p[k]) && ln->p[k] != ',' && !mask[k] &&
c_command_at(ln->p, k, ln->n) == 0) {
k++;
}
if (k == i) {
k = i + SKIP_ONE;
} else if (em->see) {
c_see_word(em, ln->p + i, k - i, ln->line);
}
line_start = false;
i = k;
}
}
/* The end command of the open code block on this line, if any. */
static bool c_closes_block(const c_line_t *ln, int open) {
for (size_t i = 0; i < ln->n; i++) {
size_t cl = c_command_at(ln->p, i, ln->n);
if (cl && c_word_is(ln->p + i + SKIP_ONE, cl, C_BLOCK_CLOSE[open])) {
return true;
}
}
return false;
}
static void c_harvest(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
uint32_t doc_line) {
CBMArena *a = ctx->scratch ? ctx->scratch : ctx->arena;
c_line_t *lines = NULL;
int n = c_doc_lines(a, doc, doc_line, &lines);
c_emit_t em = {ctx, def, false, CBM_NOT_FOUND, false};
for (int i = 0; i < n; i++) {
const c_line_t *ln = &lines[i];
if (em.verbatim >= 0) {
if (c_closes_block(ln, em.verbatim)) {
em.verbatim = CBM_NOT_FOUND;
}
continue;
}
if (ln->n >= C_FENCE_LEN &&
(memcmp(ln->p, "```", C_FENCE_LEN) == 0 || memcmp(ln->p, "~~~", C_FENCE_LEN) == 0)) {
em.fence = !em.fence;
continue;
}
if (em.fence) {
continue;
}
if (ln->n == 0) {
em.see = false; /* an empty line ends a paragraph */
continue;
}
bool *mask = (bool *)cbm_arena_calloc(a, ln->n + SKIP_ONE);
if (!mask) {
return;
}
c_mask(ln, mask);
c_line(&em, ln, mask);
}
}
void cbm_doclink_c_parse_doc(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
uint32_t doc_line) {
if (!ctx || !ctx->result || !def || !doc || !def->qualified_name ||
!c_leg_file(ctx->rel_path)) {
return;
}
c_harvest(ctx, def, doc, doc_line);
}
/* ── Loose docs ──────────────────────────────────────────────────── */
static bool c_kind(TSNode n, const char *k) {
return !ts_node_is_null(n) && strcmp(ts_node_type(n), k) == 0;
}
/* A comment node's Doxygen style: 'b' a block, 'l' a line, 0 none. */
static char c_doc_style(const char *src, TSNode n) {
uint32_t b = ts_node_start_byte(n);
uint32_t e = ts_node_end_byte(n);
const char *p = src + b;
size_t len = e - b;
if (c_has(p, len, "/**/") || c_has(p, len, "/***")) {
return 0;
}
if (c_has(p, len, "/**") || c_has(p, len, "/*!")) {
return 'b';
}
if (c_has(p, len, "////")) {
return 0;
}
return (c_has(p, len, "///") || c_has(p, len, "//!")) ? 'l' : 0;
}
typedef struct {
CBMExtractCtx *ctx;
cbm_doclink_taken_fn taken;
const CBMDefinition *file_def;
uint32_t start_byte; /* a pending run of line comments */
uint32_t end_byte;
uint32_t first;
uint32_t last;
bool pending;
} c_loose_t;
static void c_loose_emit(c_loose_t *lw, uint32_t sb, uint32_t eb, uint32_t first, uint32_t last) {
if (lw->taken(lw->ctx, first, last)) {
return; /* a definition's doc: harvested with the definition */
}
CBMArena *a = lw->ctx->scratch ? lw->ctx->scratch : lw->ctx->arena;
char *doc = cbm_arena_strndup(a, lw->ctx->source + sb, eb - sb);
if (doc) {
c_harvest(lw->ctx, lw->file_def, doc, first);
}
}
static void c_loose_flush(c_loose_t *lw) {
if (lw->pending) {
c_loose_emit(lw, lw->start_byte, lw->end_byte, lw->first, lw->last);
lw->pending = false;
}
}
static void c_loose_comment(c_loose_t *lw, TSNode n) {
char style = c_doc_style(lw->ctx->source, n);
uint32_t row = ts_node_start_point(n).row + SKIP_ONE;
if (style == 'l' && lw->pending && row == lw->last + SKIP_ONE) {
lw->end_byte = ts_node_end_byte(n); /* the run goes on */
lw->last = row;
return;
}
c_loose_flush(lw);
if (style == 'l') {
lw->pending = true;
lw->start_byte = ts_node_start_byte(n);
lw->end_byte = ts_node_end_byte(n);
lw->first = row;
lw->last = row;
} else if (style == 'b') {
c_loose_emit(lw, ts_node_start_byte(n), ts_node_end_byte(n), row,
ts_node_end_point(n).row + SKIP_ONE);
}
}
void cbm_doclink_c_loose_docs(CBMExtractCtx *ctx, const CBMDefinition *file_def,
cbm_doclink_taken_fn taken) {
if (!ctx || !ctx->result || !file_def || !taken || ts_node_is_null(ctx->root) ||
!c_leg_file(ctx->rel_path)) {
return;
}
c_loose_t lw = {.ctx = ctx, .taken = taken, .file_def = file_def};
TSTreeCursor cur = ts_tree_cursor_new(ctx->root);
for (bool more = ts_tree_cursor_goto_first_child(&cur); more;) {
TSNode n = ts_tree_cursor_current_node(&cur);
if (c_kind(n, "comment")) {
c_loose_comment(&lw, n);
} else if (ts_node_is_named(n)) {
c_loose_flush(&lw); /* code between: the run ends */
}
/* Doxygen reads no comment inside a function body */
if (!c_kind(n, "compound_statement") && ts_tree_cursor_goto_first_child(&cur)) {
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cur)) {
if (!ts_tree_cursor_goto_parent(&cur)) {
more = false;
break;
}
}
}
ts_tree_cursor_delete(&cur);
c_loose_flush(&lw);
}
/* ── Scope blob ──────────────────────────────────────────────────── */
typedef struct {
CBMArena *a;
char *buf;
size_t len;
size_t cap;
bool oom;
} c_sb_t;
static void c_sb_put(c_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 c_sb_field(c_sb_t *sb, const char *s, size_t n) {
c_sb_put(sb, "\t", SKIP_ONE);
for (size_t i = 0; i < n; i++) {
char c = (s[i] == '\t' || s[i] == '\n' || s[i] == '\r') ? ' ' : s[i];
c_sb_put(sb, &c, SKIP_ONE);
}
}
/* The name a declarator declares (through pointers, arrays, functions and
* parentheses). */
static TSNode c_declarator_name(TSNode d) {
for (int guard = 0; guard < CBM_SZ_32 && !ts_node_is_null(d); guard++) {
if (c_kind(d, "identifier") || c_kind(d, "type_identifier")) {
return d;
}
TSNode inner = ts_node_child_by_field_name(d, TS_FIELD("declarator"));
if (ts_node_is_null(inner)) {
inner = ts_node_named_child(d, 0); /* a parenthesized declarator */
}
d = inner;
}
return (TSNode){0};
}
static bool c_is_static(const char *src, TSNode decl) {
uint32_t nc = ts_node_named_child_count(decl);
for (uint32_t i = 0; i < nc; i++) {
TSNode c = ts_node_named_child(decl, i);
if (!c_kind(c, "storage_class_specifier")) {
continue;
}
uint32_t b = ts_node_start_byte(c);
uint32_t e = ts_node_end_byte(c);
if (e - b == CBM_SZ_6 && memcmp(src + b, "static", CBM_SZ_6) == 0) {
return true;
}
}
return false;
}
/* The names a static declaration or definition declares. */
static void c_static_names(CBMExtractCtx *ctx, CBMHashTable *names, TSNode decl) {
if (!c_is_static(ctx->source, decl)) {
return;
}
TSTreeCursor cur = ts_tree_cursor_new(decl);
for (bool more = ts_tree_cursor_goto_first_child(&cur); more;
more = ts_tree_cursor_goto_next_sibling(&cur)) {
const char *field = ts_tree_cursor_current_field_name(&cur);
if (!field || strcmp(field, "declarator") != 0) {
continue;
}
TSNode name = c_declarator_name(ts_tree_cursor_current_node(&cur));
if (ts_node_is_null(name)) {
continue;
}
uint32_t b = ts_node_start_byte(name);
char *key = cbm_arena_strndup(ctx->arena, ctx->source + b, ts_node_end_byte(name) - b);
if (key) {
cbm_ht_set(names, key, (void *)(uintptr_t)SKIP_ONE);
}
}
ts_tree_cursor_delete(&cur);
}
/* The static names of the file: top-level declarations, also inside
* preprocessor conditionals and extern "C" blocks. */
static void c_collect_static(CBMExtractCtx *ctx, CBMHashTable *names) {
TSTreeCursor cur = ts_tree_cursor_new(ctx->root);
for (bool more = ts_tree_cursor_goto_first_child(&cur); more;) {
TSNode n = ts_tree_cursor_current_node(&cur);
bool container = c_kind(n, "preproc_if") || c_kind(n, "preproc_ifdef") ||
c_kind(n, "preproc_else") || c_kind(n, "preproc_elif") ||
c_kind(n, "preproc_elifdef") || c_kind(n, "linkage_specification") ||
c_kind(n, "declaration_list");
if (c_kind(n, "function_definition") || c_kind(n, "declaration")) {
c_static_names(ctx, names, n);
}
if (container && ts_tree_cursor_goto_first_child(&cur)) {
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cur)) {
if (!ts_tree_cursor_goto_parent(&cur)) {
more = false;
break;
}
}
}
ts_tree_cursor_delete(&cur);
}
static char c_def_kind(const CBMDefinition *d) {
const char *l = d->label;
if (strcmp(l, "Function") == 0) {
return 'f';
}
if (strcmp(l, "Variable") == 0) {
return d->parent_class ? 'e' : 'v';
}
if (strcmp(l, "Field") == 0) {
return 'r';
}
if (strcmp(l, "Macro") == 0) {
return d->signature ? 'M' : 'm';
}
if (strcmp(l, "Type") == 0 || strcmp(l, "Class") == 0 || strcmp(l, "Enum") == 0) {
return 't';
}
return 0;
}
/* `\page name`, `\defgroup name` ... in one doc text: A records. */
static void c_anchors(c_sb_t *sb, const char *doc) {
static const char *const cmds[] = {"page", "defgroup", "addtogroup", "anchor",
"section", "subsection", "subsubsection", NULL};
size_t n = strlen(doc);
for (size_t i = 0; i < n; i++) {
size_t cl = c_command_at(doc, i, n);
if (!cl) {
continue;
}
bool hit = false;
for (int k = 0; cmds[k] && !hit; k++) {
hit = c_word_is(doc + i + SKIP_ONE, cl, cmds[k]);
}
size_t j = i + SKIP_ONE + cl;
if (!hit || j >= n || (doc[j] != ' ' && doc[j] != '\t')) {
continue;
}
j = c_skip_space(doc, j, n);
size_t k = j;
while (k < n && (c_ident_char(doc[k]) || doc[k] == '.' || doc[k] == '-')) {
k++;
}
if (k > j) {
c_sb_put(sb, "\nA", PAIR_LEN);
c_sb_field(sb, doc + j, k - j);
}
i = k;
}
}
/* The anchors every Doxygen comment of the file defines. */
static void c_scope_anchors(CBMExtractCtx *ctx, c_sb_t *sb) {
TSTreeCursor cur = ts_tree_cursor_new(ctx->root);
for (bool more = ts_tree_cursor_goto_first_child(&cur); more;) {
TSNode n = ts_tree_cursor_current_node(&cur);
if (c_kind(n, "comment") && c_doc_style(ctx->source, n)) {
uint32_t b = ts_node_start_byte(n);
char *text = cbm_arena_strndup(sb->a, ctx->source + b, ts_node_end_byte(n) - b);
if (text) {
c_anchors(sb, text);
}
}
if (!c_kind(n, "compound_statement") && ts_tree_cursor_goto_first_child(&cur)) {
continue;
}
while (!ts_tree_cursor_goto_next_sibling(&cur)) {
if (!ts_tree_cursor_goto_parent(&cur)) {
more = false;
break;
}
}
}
ts_tree_cursor_delete(&cur);
}
const char *cbm_doclink_c_scan_scope(CBMExtractCtx *ctx) {
if (!ctx || !ctx->result || ts_node_is_null(ctx->root) || !c_leg_file(ctx->rel_path)) {
return NULL;
}
c_sb_t sb = {.a = ctx->arena};
c_sb_put(&sb, CBM_DOCLINK_C_SCOPE_TAG, strlen(CBM_DOCLINK_C_SCOPE_TAG));
CBMHashTable *statics = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, CBM_SZ_32);
CBMHashTable *seen = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, CBM_SZ_64);
if (!statics || !seen) {
cbm_ht_free(statics);
cbm_ht_free(seen);
return NULL;
}
c_collect_static(ctx, statics);
size_t pl = strlen(ctx->rel_path);
bool header = ctx->rel_path[pl - SKIP_ONE] == 'h';
const CBMDefArray *defs = &ctx->result->defs;
for (int i = 0; i < defs->count && !sb.oom; i++) {
const CBMDefinition *d = &defs->items[i];
char kind = (d->label && d->name && d->qualified_name) ? c_def_kind(d) : 0;
if (!kind || cbm_ht_get(seen, d->qualified_name)) {
continue; /* one node per qualified name (#if variants) */
}
cbm_ht_set(seen, d->qualified_name, (void *)(uintptr_t)SKIP_ONE);
bool object = kind == 'f' || kind == 'v';
char link = object ? (cbm_ht_get(statics, d->name) ? 's' : 'x')
: (header && kind != 'r' ? 'x' : 's');
c_sb_put(&sb, "\nD", PAIR_LEN);
c_sb_field(&sb, &kind, SKIP_ONE);
c_sb_field(&sb, &link, SKIP_ONE);
c_sb_field(&sb, d->name, strlen(d->name));
c_sb_field(&sb, d->qualified_name, strlen(d->qualified_name));
const char *parent =
(kind == 'e' || kind == 'r') && d->parent_class ? d->parent_class : "-";
c_sb_field(&sb, parent, strlen(parent));
}
cbm_ht_free(statics);
cbm_ht_free(seen);
c_scope_anchors(ctx, &sb);
return sb.oom ? NULL : sb.buf;
}
+1 -1
View File
@@ -48,7 +48,7 @@ static const cbm_doclink_resolver_t *const DOCLINK_RESOLVERS[] = {
&cbm_doclink_cs_resolver, &cbm_doclink_md_resolver, &cbm_doclink_pdf_resolver,
&cbm_doclink_rst_resolver, &cbm_doclink_adoc_resolver, &cbm_doclink_go_resolver,
&cbm_doclink_jvm_resolver, &cbm_doclink_rs_resolver, &cbm_doclink_perl_resolver,
&cbm_doclink_ts_resolver, &cbm_doclink_php_resolver,
&cbm_doclink_ts_resolver, &cbm_doclink_php_resolver, &cbm_doclink_c_resolver,
};
enum { DOCLINK_RESOLVER_COUNT = sizeof(DOCLINK_RESOLVERS) / sizeof(DOCLINK_RESOLVERS[0]) };
+1
View File
@@ -301,6 +301,7 @@ 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;
extern const cbm_doclink_resolver_t cbm_doclink_c_resolver;
/* True when `rel_path` is a scope input of some language (scope_input). */
bool cbm_doclinks_is_scope_input(const char *rel_path);
+481
View File
@@ -0,0 +1,481 @@
/*
* doc_links_c.c — resolving C doc references (doclink_c.c): Doxygen @ref and
* @see names, with the lookup Doxygen itself has, which knows no imports.
*
* A name (`name`, `name()`, `::name`) is looked up
* 1. among the Doxygen pages, groups, anchors and sections of the
* repository's docs: such a name is no code entity (a graph gap), and
* never binds to a symbol of the same name;
* 2. among the definitions of the documented file, static ones included;
* 3. among the definitions every file sees: external linkage, and the types,
* enum constants and macros of headers. A static definition of another
* file is not visible.
* A field of the documented file (`@ref field`) comes after its other
* definitions and before what every file sees. `Type::member` names a field
* of the struct or union Type, or a constant of the enum Type, Type found by
* the same levels.
* The first level with a candidate decides. Its candidates are counted by
* qualified name: a prototype and its definition, and the #if variants of
* one definition, are one node, the one the graph keeps for that name. A
* definition (function, variable, type, enum constant) wins over a macro of
* the same name; with `()` written, a function or function-like macro wins
* over the others. One candidate binds (exact in the documented file,
* unique elsewhere), several are ambiguous, none is missing; but a word of an
* @see paragraph that names nothing and is a plain lowercase word (no `_`,
* digit, capital, `()` or `::`) is prose, which Doxygen prints as text: no
* row. Test code binds only from test code.
*/
#include "pipeline/doc_links.h"
#include "cbm.h"
#include "doclink.h"
#include "helpers.h" /* 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 { CR_FIELDS = 6, CR_NAME_MAX = 256, CR_CANDS = 64, CR_QN_MAX = 2048 };
/* ── The index ───────────────────────────────────────────────────── */
typedef struct {
int file;
char kind; /* f v t e r m M */
bool global;
const char *name;
const char *qn;
const char *parent; /* the enum of a constant, the struct of a field; NULL */
int next; /* the next definition of the same name, or -1 */
} cr_def_t;
typedef struct {
const char *path;
bool test;
} cr_file_t;
typedef struct {
CBMArena arena;
cr_file_t *files;
int nfiles;
int *run_to_file;
int run_count;
cr_def_t *defs;
int ndefs;
int cap_defs;
CBMHashTable *by_name; /* name -> the first definition's index + 1 */
CBMHashTable *anchors; /* a Doxygen page, group, anchor or section name */
CBMHashTable *by_path; /* rel_path -> file index + 1 */
bool oom;
} cr_index_t;
static const char *cr_copy(cr_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 bool cr_grow(cr_index_t *ix) {
if (ix->ndefs < ix->cap_defs) {
return true;
}
int ncap = ix->cap_defs ? ix->cap_defs * PAIR_LEN : CBM_SZ_64;
cr_def_t *g = (cr_def_t *)cbm_arena_alloc(&ix->arena, (size_t)ncap * sizeof(cr_def_t));
if (!g) {
ix->oom = true;
return false;
}
if (ix->ndefs) {
memcpy(g, ix->defs, (size_t)ix->ndefs * sizeof(cr_def_t));
}
ix->defs = g;
ix->cap_defs = ncap;
return true;
}
static void cr_read_record(cr_index_t *ix, int fi, const char **fl, const size_t *ln, int n) {
if (fl[0][0] == 'D' && n >= CR_FIELDS - SKIP_ONE && cr_grow(ix)) { /* parent optional */
cr_def_t *d = &ix->defs[ix->ndefs++];
d->file = fi;
d->kind = fl[1][0];
d->global = fl[PAIR_LEN][0] == 'x';
d->name = cr_copy(ix, fl[CBM_SZ_3], ln[CBM_SZ_3]);
d->qn = cr_copy(ix, fl[CBM_SZ_4], ln[CBM_SZ_4]);
bool none = n <= CBM_SZ_5 || (ln[CBM_SZ_5] == SKIP_ONE && fl[CBM_SZ_5][0] == '-');
d->parent = none ? NULL : cr_copy(ix, fl[CBM_SZ_5], ln[CBM_SZ_5]);
d->next = CBM_NOT_FOUND;
} else if (fl[0][0] == 'A' && n >= PAIR_LEN) {
const char *name = cr_copy(ix, fl[1], ln[1]);
if (name && !cbm_ht_get(ix->anchors, name)) {
cbm_ht_set(ix->anchors, name, (void *)(uintptr_t)SKIP_ONE);
}
}
}
static void cr_read_blob(cr_index_t *ix, 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[CR_FIELDS];
size_t ln[CR_FIELDS];
int n = 0;
const char *s = b;
for (const char *p = b; p <= e && n < CR_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) {
cr_read_record(ix, fi, fl, ln, n);
}
line = *e ? e : NULL;
}
}
static void cr_destroy(void *index) {
cr_index_t *ix = (cr_index_t *)index;
if (!ix) {
return;
}
cbm_ht_free(ix->by_name);
cbm_ht_free(ix->anchors);
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);
}
/* Chain every definition into its name's list, in index order. */
static bool cr_link(cr_index_t *ix) {
ix->by_name = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, (uint32_t)(ix->ndefs + SKIP_ONE));
if (!ix->by_name) {
return false;
}
for (int i = ix->ndefs - SKIP_ONE; i >= 0; i--) {
cr_def_t *d = &ix->defs[i];
if (!d->name || !d->qn) {
continue;
}
void *prev = cbm_ht_get(ix->by_name, d->name);
d->next = prev ? (int)((uintptr_t)prev - SKIP_ONE) : CBM_NOT_FOUND;
cbm_ht_set(ix->by_name, d->name, (void *)(uintptr_t)(i + SKIP_ONE));
}
return true;
}
static void *cr_build(const cbm_doclink_build_in_t *in) {
cr_index_t *ix = (cr_index_t *)cbm_calloc(CBM_MEM_CLASS_OTHER, sizeof(*ix));
if (!ix) {
return NULL;
}
cbm_arena_init(&ix->arena);
ix->nfiles = in->file_count;
ix->run_count = in->run_file_count;
ix->files =
in->file_count
? (cr_file_t *)cbm_arena_calloc(&ix->arena, (size_t)in->file_count * sizeof(cr_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));
ix->anchors = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, CBM_SZ_64);
bool ok = (!in->file_count || ix->files) && ix->run_to_file && ix->by_path && ix->anchors;
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];
cr_file_t *f = &ix->files[i];
f->path = src->rel_path;
f->test = cbm_is_test_file(src->rel_path, CBM_LANG_C);
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) {
cr_read_blob(ix, i, src->scope);
}
}
ok = ok && !ix->oom && cr_link(ix);
if (!ok) {
cbm_log_error("doc_links.c.error", "step", "index", "reason", "alloc");
cr_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->ndefs);
snprintf(b[PAIR_LEN], sizeof(b[PAIR_LEN]), "%u", cbm_ht_count(ix->anchors));
cbm_log_info("doc_links.c.index", "files", b[0], "definitions", b[SKIP_ONE], "anchors",
b[PAIR_LEN]);
return ix;
}
/* ── Resolution ──────────────────────────────────────────────────── */
/* The candidates of one level, one per qualified name. */
typedef struct {
const cr_def_t *items[CR_CANDS];
int n;
bool full; /* more than CR_CANDS: ambiguous */
} cr_cands_t;
static void cr_add(cr_cands_t *c, const cr_def_t *d) {
for (int i = 0; i < c->n; i++) {
if (strcmp(c->items[i]->qn, d->qn) == 0) {
return;
}
}
if (c->n == CR_CANDS) {
c->full = true;
return;
}
c->items[c->n++] = d;
}
static bool cr_macro(const cr_def_t *d) {
return d->kind == 'm' || d->kind == 'M';
}
static bool cr_callable(const cr_def_t *d) {
return d->kind == 'f' || d->kind == 'M';
}
/* Keep the candidates `keep` accepts, when there is one. */
static void cr_prefer(cr_cands_t *c, bool (*keep)(const cr_def_t *), bool want) {
int k = 0;
for (int i = 0; i < c->n; i++) {
k += keep(c->items[i]) == want;
}
if (k == 0 || k == c->n) {
return;
}
int w = 0;
for (int i = 0; i < c->n; i++) {
if (keep(c->items[i]) == want) {
c->items[w++] = c->items[i];
}
}
c->n = w;
}
enum { CR_LEVEL_OWN = 0, CR_LEVEL_FIELD = 1, CR_LEVEL_GLOBAL = 2, CR_LEVELS = 3 };
/* The definitions of `name` one level sees: the documented file's own (any
* linkage, fields apart), its fields, or those every file sees. */
static void cr_level(const cr_index_t *ix, const char *name, int src, int level, cr_cands_t *c) {
void *v = cbm_ht_get(ix->by_name, name);
for (int i = v ? (int)((uintptr_t)v - SKIP_ONE) : CBM_NOT_FOUND; i >= 0; i = ix->defs[i].next) {
const cr_def_t *d = &ix->defs[i];
bool field = d->kind == 'r';
bool in = level == CR_LEVEL_GLOBAL ? d->global
: d->file == src && field == (level == CR_LEVEL_FIELD);
if (in) {
cr_add(c, d);
}
}
}
static void cr_out_node(const cr_index_t *ix, int src, const cr_def_t *d, bool exact,
const cbm_gbuf_t *graph, cbm_doclink_outcome_t *out) {
const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(graph, d->qn);
if (!node || !node->label) {
out->reason = CBM_DOCLINK_REASON_GRAPH_GAP;
return;
}
if (ix->files[d->file].test && !ix->files[src].test) {
out->reason = CBM_DOCLINK_REASON_TEST_ONLY;
return;
}
out->kind = CBM_DOCLINK_EDGE;
out->target = node;
out->exact = exact;
}
static bool cr_ident(const char *s, size_t n) {
if (n == 0 || !(isalpha((unsigned char)s[0]) || s[0] == '_')) {
return false;
}
for (size_t i = 0; i < n; i++) {
if (!isalnum((unsigned char)s[i]) && s[i] != '_') {
return false;
}
}
return true;
}
/* The name of a reference, `::` in front and `()` behind dropped, and the
* type of `Type::member` (scope, "" for none); false when it is no C name. */
static bool cr_name(const char *raw, char *out, char *scope, size_t cap, bool *call) {
size_t n = strlen(raw);
if (n >= PAIR_LEN && raw[0] == ':' && raw[SKIP_ONE] == ':') {
raw += PAIR_LEN;
n -= PAIR_LEN;
}
*call = n >= PAIR_LEN && raw[n - PAIR_LEN] == '(' && raw[n - SKIP_ONE] == ')';
if (*call) {
n -= PAIR_LEN;
}
scope[0] = '\0';
const char *sep = NULL;
for (size_t i = 0; i + SKIP_ONE < n && !sep; i++) {
sep = raw[i] == ':' && raw[i + SKIP_ONE] == ':' ? raw + i : NULL;
}
if (sep) {
size_t sl = (size_t)(sep - raw);
if (sl >= cap || !cr_ident(raw, sl)) {
return false;
}
memcpy(scope, raw, sl);
scope[sl] = '\0';
n -= sl + PAIR_LEN;
raw = sep + PAIR_LEN;
}
if (n >= cap || !cr_ident(raw, n)) {
return false;
}
memcpy(out, raw, n);
out[n] = '\0';
return true;
}
static bool cr_type(const cr_def_t *d) {
return d->kind == 't';
}
/* `Type::member`: the type by the usual levels, then its field (a node at
* the type's qualified name + `.` + member) or its enum constant. */
static void cr_member(const cr_index_t *ix, int src, const char *scope, const char *name,
const cbm_gbuf_t *graph, cbm_doclink_outcome_t *out) {
const cr_def_t *type = NULL;
for (int level = 0; level < CR_LEVELS && !type; level++) {
cr_cands_t c = {.n = 0};
cr_level(ix, scope, src, level, &c);
cr_prefer(&c, cr_type, true);
if (c.n == 0 || !cr_type(c.items[0])) {
continue;
}
if (c.full || c.n > SKIP_ONE) {
out->reason = CBM_DOCLINK_REASON_AMBIGUOUS;
return;
}
type = c.items[0];
}
if (!type) {
return; /* missing */
}
char qn[CR_QN_MAX];
int n = snprintf(qn, sizeof(qn), "%s.%s", type->qn, name);
cr_def_t field = {.file = type->file, .kind = 'r', .name = name, .qn = qn};
if (n > 0 && (size_t)n < sizeof(qn) && cbm_gbuf_find_by_qn(graph, qn)) {
cr_out_node(ix, src, &field, true, graph, out);
return;
}
void *v = cbm_ht_get(ix->by_name, name);
for (int i = v ? (int)((uintptr_t)v - SKIP_ONE) : CBM_NOT_FOUND; i >= 0; i = ix->defs[i].next) {
const cr_def_t *d = &ix->defs[i];
if (d->kind == 'e' && d->parent && strcmp(d->parent, type->qn) == 0) {
cr_out_node(ix, src, d, true, graph, out);
return;
}
}
}
/* A plain lowercase word: what an @see paragraph's prose is made of. */
static bool cr_plain_word(const char *s) {
for (; *s; s++) {
if (*s < 'a' || *s > 'z') {
return false;
}
}
return true;
}
static void cr_resolve_name(const cr_index_t *ix, int src, const CBMDocLink *link,
const cbm_gbuf_t *graph, cbm_doclink_outcome_t *out);
static void cr_resolve(const void *index, void *state, int run_file, const CBMDocLink *link,
const cbm_gbuf_t *graph, cbm_doclink_outcome_t *out) {
(void)state; /* C keeps no per-file working state */
const cr_index_t *ix = (const cr_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;
}
int src = ix->run_to_file[run_file];
cr_resolve_name(ix, src, link, graph, out);
if (link->syntax == CBM_DOCLINK_C_SEE && out->kind == CBM_DOCLINK_UNRESOLVED &&
out->reason == CBM_DOCLINK_REASON_MISSING && cr_plain_word(link->raw)) {
out->kind = CBM_DOCLINK_LOCAL; /* prose: Doxygen prints it as text */
}
}
static void cr_resolve_name(const cr_index_t *ix, int src, const CBMDocLink *link,
const cbm_gbuf_t *graph, cbm_doclink_outcome_t *out) {
if (cbm_ht_get(ix->anchors, link->raw)) {
out->reason = CBM_DOCLINK_REASON_GRAPH_GAP; /* a page, group or anchor */
return;
}
char name[CR_NAME_MAX];
char scope[CR_NAME_MAX];
bool call = false;
if (strlen(link->raw) > CBM_DOCLINK_C_REF_MAX ||
!cr_name(link->raw, name, scope, sizeof(name), &call)) {
out->reason = CBM_DOCLINK_REASON_UNPARSEABLE;
return;
}
if (scope[0]) {
cr_member(ix, src, scope, name, graph, out);
return;
}
if (cbm_ht_get(ix->anchors, name)) {
out->reason = CBM_DOCLINK_REASON_GRAPH_GAP;
return;
}
for (int level = 0; level < CR_LEVELS; level++) {
cr_cands_t c = {.n = 0};
cr_level(ix, name, src, level, &c);
if (c.n == 0) {
continue;
}
if (call) {
cr_prefer(&c, cr_callable, true); /* `()` written: what can be called */
}
cr_prefer(&c, cr_macro, false); /* then a definition before a macro */
if (c.full || c.n > SKIP_ONE) {
out->reason = CBM_DOCLINK_REASON_AMBIGUOUS;
return;
}
cr_out_node(ix, src, c.items[0], level != CR_LEVEL_GLOBAL, graph, out);
return;
}
}
const cbm_doclink_resolver_t cbm_doclink_c_resolver = {
.langs = {CBM_LANG_C, CBM_LANG_CPP},
.lang_count = PAIR_LEN,
.scope_tag = CBM_DOCLINK_C_SCOPE_TAG,
.build = cr_build,
.destroy = cr_destroy,
.resolve = cr_resolve,
};
+341
View File
@@ -0,0 +1,341 @@
/*
* test_doc_mentions_c.c -- C doc-comment references (Doxygen @ref, @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 C_WIDGET =
"#include \"api.h\"\n" /* 1 */
"\n" /* 2 */
"/**\n" /* 3 */
" * Makes widgets. @ref widget_free \"free it\" and \\ref api_counter.\n" /* 4 */
" * Not `@ref Hidden` nor \"@ref Quoted\".\n" /* 5 */
" * @code\n" /* 6 */
" * @ref InCode\n" /* 7 */
" * @endcode\n" /* 8 */
" * ```\n" /* 9 */
" * @ref Fenced\n" /* 10 */
" * ```\n" /* 11 */
" * @see widget_free(), helper, ::api_counter, https://example.com/w\n" /* 12 */
" * also_this\n" /* 13 */
" *\n" /* 14 */
" * after_blank\n" /* 15 */
" * \\sa \\c not_this, Other_thing.\n" /* 16 */
" * @param n not_a_see\n" /* 17 */
" */\n" /* 18 */
"int widget_new(int n) { return n; }\n" /* 19 */
"\n" /* 20 */
"/// Line doc. @ref API_MAX\n" /* 21 */
"/// @sa widget_new()\n" /* 22 */
"static int helper(int x) { return x; }\n" /* 23 */
"\n" /* 24 */
"/** @defgroup grp The group */\n" /* 25 */
"\n" /* 26 */
"/** A prototype's doc: @ref grp and @ref widget_new. */\n" /* 27 */
"int f2(void);\n" /* 28 */
"#define API_MAX 3\n" /* 29 */
"#define TWICE(x) ((x) * 2)\n" /* 30 */
"static int counter;\n" /* 31 */
"int api_counter = 0;\n" /* 32 */
"enum mode { MODE_A };\n"; /* 33 */
static bool c_token_is(const CBMFileResult *r, const char *raw, uint32_t line, int syntax,
bool file) {
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);
bool is_file = (t->flags & CBM_DOCLINK_FLAG_FILE) != 0;
if (t->line != line || t->syntax != syntax || is_file != file) {
printf(" token %s: line %u syntax %d file %d\n", raw, t->line, t->syntax, is_file);
return false;
}
return true;
}
TEST(doc_mentions_c_tokens) {
CBMFileResult *r = dm_extract(C_WIDGET, CBM_LANG_C, "src/widget.c");
ASSERT_NOT_NULL(r);
ASSERT_TRUE(c_token_is(r, "widget_free", 4, CBM_DOCLINK_C_REF, false));
ASSERT_TRUE(c_token_is(r, "api_counter", 4, CBM_DOCLINK_C_REF, false));
/* the names an @see paragraph lists, to its empty line */
ASSERT_TRUE(c_token_is(r, "widget_free()", 12, CBM_DOCLINK_C_SEE, false));
ASSERT_TRUE(c_token_is(r, "helper", 12, CBM_DOCLINK_C_SEE, false));
ASSERT_TRUE(c_token_is(r, "::api_counter", 12, CBM_DOCLINK_C_SEE, false));
ASSERT_TRUE(c_token_is(r, "https://example.com/w", 12, CBM_DOCLINK_HREF, false));
ASSERT_TRUE(c_token_is(r, "also_this", 13, CBM_DOCLINK_C_SEE, false));
ASSERT_TRUE(c_token_is(r, "Other_thing", 16, CBM_DOCLINK_C_SEE, false));
/* a /// run */
ASSERT_TRUE(c_token_is(r, "API_MAX", 21, CBM_DOCLINK_C_REF, false));
ASSERT_TRUE(c_token_is(r, "widget_new()", 22, CBM_DOCLINK_C_SEE, false));
/* a prototype has no node: its doc belongs to the file */
ASSERT_TRUE(c_token_is(r, "grp", 27, CBM_DOCLINK_C_REF, true));
ASSERT_TRUE(c_token_is(r, "widget_new", 27, CBM_DOCLINK_C_REF, true));
/* code spans, quotes, code blocks, fences, a new paragraph, \c */
ASSERT_EQ(dm_count_tokens(r, "Hidden"), 0);
ASSERT_EQ(dm_count_tokens(r, "Quoted"), 0);
ASSERT_EQ(dm_count_tokens(r, "InCode"), 0);
ASSERT_EQ(dm_count_tokens(r, "Fenced"), 0);
ASSERT_EQ(dm_count_tokens(r, "after_blank"), 0);
ASSERT_EQ(dm_count_tokens(r, "not_this"), 0);
ASSERT_EQ(dm_count_tokens(r, "not_a_see"), 0);
ASSERT_EQ(r->doc_links.count, 12);
cbm_free_result(r);
PASS();
}
/* A C++ file is not part of the leg; a header is. */
TEST(doc_mentions_c_files) {
const char *src = "/** See @ref other_fn. */\nint fn(void) { return 0; }\n";
CBMFileResult *r = dm_extract(src, CBM_LANG_CPP, "src/x.cpp");
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->doc_links.count, 0);
ASSERT_NULL(r->doc_scope);
cbm_free_result(r);
r = dm_extract(src, CBM_LANG_CPP, "include/x.h");
ASSERT_NOT_NULL(r);
ASSERT_EQ(dm_count_tokens(r, "other_fn"), 1);
cbm_free_result(r);
PASS();
}
static bool c_has_prefix_line(const char *blob, const char *prefix) {
size_t n = strlen(prefix);
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, prefix, n) == 0) {
return true;
}
p = nl ? nl + 1 : NULL;
}
printf(" no line: %s...\n", prefix);
return false;
}
TEST(doc_mentions_c_scope_blob) {
CBMFileResult *r = dm_extract(C_WIDGET, CBM_LANG_C, "src/widget.c");
ASSERT_NOT_NULL(r);
const char *s = r->doc_scope;
ASSERT_NOT_NULL(s);
ASSERT(strncmp(s, "c1\n", 3) == 0);
ASSERT(c_has_prefix_line(s, "D\tf\tx\twidget_new\t"));
ASSERT(c_has_prefix_line(s, "D\tf\ts\thelper\t"));
ASSERT(c_has_prefix_line(s, "D\tv\ts\tcounter\t"));
ASSERT(c_has_prefix_line(s, "D\tv\tx\tapi_counter\t"));
/* types, constants and macros of a .c file are its own */
ASSERT(c_has_prefix_line(s, "D\tm\ts\tAPI_MAX\t"));
ASSERT(c_has_prefix_line(s, "D\tM\ts\tTWICE\t"));
ASSERT(c_has_prefix_line(s, "D\tt\ts\tmode\t"));
ASSERT(c_has_prefix_line(s, "D\te\ts\tMODE_A\t"));
ASSERT(c_has_prefix_line(s, "A\tgrp"));
cbm_free_result(r);
PASS();
}
/* ── resolution ──────────────────────────────────────────────────── */
static void c_write_repo(const char *repo) {
th_write_file(TH_PATH(repo, "include/api.h"), "#ifndef API_H\n"
"#define API_H\n"
"/** @defgroup widgets Widgets */\n"
"/** Makes one, see @ref widget_free. */\n"
"int widget_new(int n);\n"
"void widget_free(int w);\n"
"#define API_MAX 16\n"
"#define api_twice(x) ((x) * 2)\n"
"int dual(int x);\n"
"#define dual(x) ((x) + 1)\n"
"#define level(x) (x)\n"
"enum color { RED, GREEN };\n"
"struct st { int fld; };\n"
"typedef struct widget { int n; } widget_t;\n"
"extern int api_counter;\n"
"#endif\n");
th_write_file(TH_PATH(repo, "src/widget.c"),
"#include \"api.h\"\n"
"int api_counter;\n"
"int level;\n"
"static int helper(int x) { return x; }\n"
"int widget_new(int n) { return n; }\n"
"void widget_free(int w) { (void)w; }\n"
"int (dual)(int x) { return x; }\n"
"/**\n"
" * The core. @ref helper, @ref api_counter, @ref API_MAX, @ref dual,\n"
" * @ref RED, @ref widgets, @ref other_static, @ref twin, @ref nope,\n"
" * @ref widget_t, @ref test_only_fn, @ref st::fld, @ref color::GREEN,\n"
" * @ref lfield, @ref st::nofield.\n"
" * @see api_twice() for details, unknown_fn()\n"
" */\n"
"int core(void) { return helper(0); }\n"
"struct local { int lfield; };\n");
th_write_file(TH_PATH(repo, "src/other.c"), "#include \"api.h\"\n"
"static int other_static(void) { return 0; }\n"
"int twin(void) { return 1; }\n"
"/** Other: @ref dual, @ref level and\n"
" * @ref level(). */\n"
"int other(void) { return other_static(); }\n");
th_write_file(TH_PATH(repo, "src/third.c"), "int twin(void) { return 2; }\n");
th_write_file(TH_PATH(repo, "tests/test_widget.c"), "int test_only_fn(void) { return 0; }\n");
}
static bool c_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 c_row(const char *db, const char *raw, const char *reason) {
char got[64];
dm_row(db, "src/widget.c", 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;
}
TEST(doc_mentions_c_resolution) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlc_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
char repo[512];
char db[512];
snprintf(repo, sizeof(repo), "%s/repo", tmp);
snprintf(db, sizeof(db), "%s/g.db", tmp);
c_write_repo(repo);
ASSERT_EQ(dm_index(repo, db, NULL), 0);
const char *core = "src.widget.core";
/* the documented file's own definitions, a static one too */
ASSERT_TRUE(c_edge(db, core, "src.widget.helper"));
ASSERT_TRUE(c_edge(db, core, "src.widget.api_counter"));
ASSERT_TRUE(c_edge(db, core, "src.widget.dual"));
/* what every file sees: a header's macros, constants and types */
ASSERT_TRUE(c_edge(db, core, "include.api.API_MAX#macro"));
ASSERT_TRUE(c_edge(db, core, "include.api.RED"));
ASSERT_TRUE(c_edge(db, core, "include.api.widget_t"));
ASSERT_TRUE(c_edge(db, core, "include.api.api_twice#macro"));
/* a group is no code entity; another file's static is not visible */
ASSERT_TRUE(c_row(db, "widgets", "graph_gap"));
ASSERT_TRUE(c_row(db, "other_static", "missing"));
ASSERT_TRUE(c_row(db, "nope", "missing"));
ASSERT_TRUE(c_row(db, "twin", "ambiguous"));
ASSERT_TRUE(c_row(db, "test_only_fn", "test_only_target"));
/* Type::member: a struct field, an enum constant; a field of the file */
ASSERT_TRUE(c_edge(db, core, "include.api.st.fld"));
ASSERT_TRUE(c_edge(db, core, "include.api.GREEN"));
ASSERT_TRUE(c_edge(db, core, "src.widget.local.lfield"));
ASSERT_TRUE(c_row(db, "st::nofield", "missing"));
/* prose in an @see paragraph is text; a code-shaped name is missing */
ASSERT_TRUE(c_row(db, "for", ""));
ASSERT_TRUE(c_row(db, "details", ""));
ASSERT_TRUE(c_row(db, "unknown_fn()", "missing"));
/* a definition before a macro; with (), a function-like macro first */
ASSERT_TRUE(c_edge(db, "src.other.other", "src.widget.dual"));
ASSERT_TRUE(c_edge(db, "src.other.other", "src.widget.level"));
ASSERT_TRUE(c_edge(db, "src.other.other", "include.api.level#macro"));
/* a header prototype's doc: the header file is its source */
ASSERT_TRUE(c_edge(db, "include.api.h.__file__", "src.widget.widget_free"));
th_cleanup(tmp);
PASS();
}
/* ── incremental == full; one worker == several ──────────────────── */
TEST(doc_mentions_c_incremental_equals_full) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlc_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.c"),
"int a(void) { return 0; }\nint b(void) { return 1; }\n");
th_write_file(TH_PATH(repo, "src/use.c"),
"/** Uses @ref a. */\nint user(void) { return 0; }\n");
ASSERT_EQ(dm_index(repo, inc_db, NULL), 0);
ASSERT_TRUE(c_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.c"),
"/** Uses @ref a and @ref nope. */\nint user(void) { return 0; }\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 scope is unchanged */
th_write_file(TH_PATH(repo, "src/lib.c"),
"int a(void) { return 2; }\nint b(void) { return 1; }\n");
ASSERT_EQ(dm_step(repo, inc_db, full_db, "body edit", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
ASSERT_TRUE(c_edge(inc_db, "src.use.user", "src.lib.a"));
/* a becomes static: without a scope_delta hook that is a full run */
th_write_file(TH_PATH(repo, "src/lib.c"),
"static int a(void) { return 0; }\nint b(void) { return 1; }\n");
ASSERT_EQ(dm_step(repo, inc_db, full_db, "made static", 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_c_workers_agree) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlc_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.c", i);
snprintf(src, sizeof(src),
"/** F%02d; see @ref f%02d, @ref K%02d, @ref nope.\n"
" * @see g%02d() */\n"
"int f%02d(void) { return 0; }\n"
"int g%02d(void) { return 1; }\n"
"#define K%02d 1\n",
i, next, next, next, i, i, i);
th_write_file(TH_PATH(repo, rel), src);
}
ASSERT_EQ(dm_workers_agree(repo, par_db, seq_db), 0);
ASSERT_TRUE(c_edge(par_db, "src.m00.f00", "src.m01.f01"));
th_cleanup(tmp);
PASS();
}
SUITE(doc_mentions_c) {
dm_ship_held_families();
RUN_TEST(doc_mentions_c_tokens);
RUN_TEST(doc_mentions_c_files);
RUN_TEST(doc_mentions_c_scope_blob);
RUN_TEST(doc_mentions_c_resolution);
RUN_TEST(doc_mentions_c_incremental_equals_full);
RUN_TEST(doc_mentions_c_workers_agree);
}
+1
View File
@@ -442,6 +442,7 @@ static const int DM_HELD_FAMILIES[] = {
CBM_DOCLINK_RS_LINK_DEF, CBM_DOCLINK_PERL_POD_L,
CBM_DOCLINK_TS_LINK, CBM_DOCLINK_TS_LINKCODE,
CBM_DOCLINK_TS_SEE, CBM_DOCLINK_PHP_SEE,
CBM_DOCLINK_C_REF, CBM_DOCLINK_C_SEE,
};
static inline void dm_ship_held_families(void) {
+2
View File
@@ -1831,6 +1831,7 @@ 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_doc_mentions_c(void);
extern void suite_infrascan(void);
extern void suite_cli(void);
extern void suite_agent_clients(void);
@@ -2308,6 +2309,7 @@ int main(int argc, char **argv) {
RUN_SELECTED_SUITE(doc_mentions_perl);
RUN_SELECTED_SUITE(doc_mentions_ts);
RUN_SELECTED_SUITE(doc_mentions_php);
RUN_SELECTED_SUITE(doc_mentions_c);
/* Infrastructure scanning */
RUN_SELECTED_SUITE(infrascan);