mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
doc-links(perl): POD L<> codes become MENTIONS edges, held until their audit
A sub's POD section and the module's own POD (the File node's doc) name code with L<> codes: L<Module::Name>, L<Module::Name/section>, L</section>, with a `text|` prefix or not. doclink_perl.c reads them the way perlpodspec and Pod::Simple do (verbatim paragraphs and data regions hold none, an L inside an L is no link, a URL is the core's href family) and writes each file's scope: its packages, its subs and its headings. Lines are exact: a sub's doc is matched against the file's sections, and the module doc's runs (extract_defs.c) are found again in the source in order; a module doc is POD from its start, so a run that begins with text is read too. doc_links_perl.c resolves them: - a module is the file that declares its package; of several, the one the library layout names (.../Name/Space.pm), else ambiguous. The target is the file's File node, as the field test's audited prototype has it (a Module node is missing where a same-named Folder holds its qualified name, lib/Mojo.pm next to lib/Mojo/). - L<Module/section>: the sub its heading names when the module's file defines that sub; any other section is a place in the module's documentation, the module. - L</section>: a sub the heading names; a heading that names no sub is the page itself (local, neither edge nor row); a section the page has no heading for is missing. - a module the repository does not declare (CPAN, the core, a .pod page: discovery keeps no .pod file) and a man page are external; test code (t/, xt/, *.t, and cbm_is_test_file, which has no Perl rule) binds only from test code. There is no scope_delta hook yet: a change to a file's packages, subs or headings re-resolves every Perl reference. The family is held (ships=false) until its held-out audit passes; the suite ships it through the test seam. Tests (doc_mentions_perl): tokens with their lines, file-level and sub-level, each read once; every resolution rule; incremental equals full (a module doc edit and an added module-level reference repair the file, a heading change is a full run); 60 modules give the same edges and rows with one worker and several. Each rule red with it reverted (the module doc's line mapping, a module run read from its start, the missing section, the library layout). Mojolicious at 93f591742a7d (the family shipped, a measurement build) against the field test's prototype records of its .pm/.pl files: 950 (source, target) pairs both have; the 197 only the prototype has are all references within one file (570), which the core makes no edge of; 8 only here come from the extensionless scripts the prototype did not read. Every prototype row has its row here: external 102, missing 1, and the 172 targets in .pod pages external by the ruling for .pod pages. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
ef5359e3ee
commit
f4277ae783
10 files changed
+2516
-3
No files matched your search
+3
-1
@@ -389,6 +389,7 @@ EXTRACTION_SRCS = \
|
||||
$(CBM_DIR)/doclink_go.c \
|
||||
$(CBM_DIR)/doclink_jvm.c \
|
||||
$(CBM_DIR)/doclink_rs.c \
|
||||
$(CBM_DIR)/doclink_perl.c \
|
||||
$(CBM_DIR)/sql_values.c \
|
||||
$(CBM_DIR)/helpers.c \
|
||||
$(CBM_DIR)/cpp_branch_views.c \
|
||||
@@ -518,6 +519,7 @@ PIPELINE_SRCS = \
|
||||
src/pipeline/doc_links_go.c \
|
||||
src/pipeline/doc_links_jvm.c \
|
||||
src/pipeline/doc_links_rs.c \
|
||||
src/pipeline/doc_links_perl.c \
|
||||
src/pipeline/doc_links_msbuild.c \
|
||||
src/pipeline/pass_route_nodes.c \
|
||||
src/pipeline/pass_enrichment.c \
|
||||
@@ -780,7 +782,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_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_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
|
||||
|
||||
|
||||
@@ -211,6 +211,12 @@ static const doclink_lang_t DOCLINK_LANGS[] = {
|
||||
.scan_scope = cbm_doclink_rs_manifest_scan_scope,
|
||||
.scope_tag = CBM_DOCLINK_RS_SCOPE_TAG,
|
||||
.portable_scope = cbm_doclink_rs_portable_scope},
|
||||
/* Perl: POD L<> codes; the scope blob carries no lines, so it persists as
|
||||
* it is */
|
||||
{.lang = CBM_LANG_PERL,
|
||||
.parse_doc = cbm_doclink_perl_parse_doc,
|
||||
.scan_scope = cbm_doclink_perl_scan_scope,
|
||||
.scope_tag = CBM_DOCLINK_PERL_SCOPE_TAG},
|
||||
};
|
||||
|
||||
static const doclink_lang_t *doclink_lang(CBMLanguage lang) {
|
||||
|
||||
@@ -98,6 +98,8 @@
|
||||
/* rust (doclink_rs.c): intra-doc links; held until their audit passes */ \
|
||||
X(CBM_DOCLINK_RS_INTRA_DOC, CBM_LANG_RUST, "intra_doc", false, false, "MENTIONS") \
|
||||
X(CBM_DOCLINK_RS_LINK_DEF, CBM_LANG_RUST, "link_def", false, false, "MENTIONS") \
|
||||
/* perl (doclink_perl.c): POD L<> codes; held until their audit passes */ \
|
||||
X(CBM_DOCLINK_PERL_POD_L, CBM_LANG_PERL, "pod_L", 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") \
|
||||
@@ -535,5 +537,47 @@ typedef enum {
|
||||
* are read. */
|
||||
CBMDocLinkRsPathStatus cbm_doclink_rs_parse_path(const char *raw, size_t len, bool lenient,
|
||||
CBMDocLinkRsPath *out);
|
||||
/* ── Perl POD (doclink_perl.c) ─────────────────────────────────────── */
|
||||
|
||||
void cbm_doclink_perl_parse_doc(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
|
||||
uint32_t doc_line);
|
||||
const char *cbm_doclink_perl_scan_scope(CBMExtractCtx *ctx);
|
||||
|
||||
/* The Perl scope blob starts with this tag line. */
|
||||
#define CBM_DOCLINK_PERL_SCOPE_TAG "perl1"
|
||||
|
||||
enum {
|
||||
/* Longest L<> content kept as a token's raw text; a longer one is cut and
|
||||
* marked malformed (its raw text then starts with `L<`). */
|
||||
CBM_DOCLINK_PERL_RAW_MAX = 512,
|
||||
/* Buffer for a page name, a section text or a heading key. */
|
||||
CBM_DOCLINK_PERL_KEY_MAX = 256,
|
||||
};
|
||||
|
||||
/* A POD text as it renders: formatting codes replaced by their content
|
||||
* (X<> and Z<> by nothing, E<> by its character), white-space runs as one
|
||||
* space, none at the ends. Headings and the section part of a link are
|
||||
* compared in this form. Writes a NUL-terminated string into `out` (cut at
|
||||
* cap - 1, never inside a UTF-8 character) and returns its length. */
|
||||
size_t cbm_doclink_perl_plain_text(const char *in, size_t len, char *out, size_t cap);
|
||||
|
||||
typedef enum {
|
||||
CBM_PERL_LINK_UNPARSEABLE = 0, /* malformed, nested L<>, or no page name */
|
||||
CBM_PERL_LINK_PAGE, /* L<Module::Name> or L<Module::Name/section> */
|
||||
CBM_PERL_LINK_SECTION, /* L</section>, L<"section">: the same page */
|
||||
CBM_PERL_LINK_MAN, /* L<name(3)>: a man page */
|
||||
} CBMPerlLinkKind;
|
||||
|
||||
/* One pod_L token's raw text, split the way perlpodspec splits it. */
|
||||
typedef struct {
|
||||
CBMPerlLinkKind kind;
|
||||
char page[CBM_DOCLINK_PERL_KEY_MAX]; /* "" for a section of the same page */
|
||||
char section[CBM_DOCLINK_PERL_KEY_MAX]; /* "" when only a page is named */
|
||||
} CBMPerlLink;
|
||||
|
||||
/* Split a pod_L token's raw text: the first top-level `|` ends the link text,
|
||||
* the first top-level `/` after it separates page and section, quotes around
|
||||
* a section are dropped. False (kind UNPARSEABLE) for anything else. */
|
||||
bool cbm_doclink_perl_parse_link(const char *raw, CBMPerlLink *out);
|
||||
|
||||
#endif /* CBM_DOCLINK_H */
|
||||
File diff suppressed because it is too large.
Load diff
@@ -47,7 +47,7 @@ const char *cbm_doclink_reason_name(int reason) {
|
||||
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_jvm_resolver, &cbm_doclink_rs_resolver, &cbm_doclink_perl_resolver,
|
||||
};
|
||||
|
||||
enum { DOCLINK_RESOLVER_COUNT = sizeof(DOCLINK_RESOLVERS) / sizeof(DOCLINK_RESOLVERS[0]) };
|
||||
|
||||
@@ -298,6 +298,7 @@ bool cbm_doclinks_binds_lines(CBMLanguage lang);
|
||||
extern const cbm_doclink_resolver_t cbm_doclink_go_resolver;
|
||||
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;
|
||||
|
||||
/* 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,489 @@
|
||||
/*
|
||||
* doc_links_perl.c — resolving Perl POD L<> references (doclink_perl.c).
|
||||
*
|
||||
* A page is a module. L<Mojo::Base> binds the module whose file declares
|
||||
* `package Mojo::Base;` (the scope blob's P records). When several files
|
||||
* declare it, the one the library layout names (…/Mojo/Base.pm) is the
|
||||
* module; more than one such file, or none among several, is ambiguous. A page
|
||||
* the repository does not declare (CPAN, the Perl core, a .pod page:
|
||||
* discovery keeps no .pod file, so nothing can say it is in the repository)
|
||||
* is external, and so is a man page (L<crontab(5)>).
|
||||
*
|
||||
* The target of a module is its File node: a POD page is the file it is
|
||||
* written in (the field test's audited rule). Its Module node would do for
|
||||
* most, but not where a same-named Folder holds the module's qualified name
|
||||
* (lib/Mojo.pm next to lib/Mojo/).
|
||||
*
|
||||
* A section, L<Module/section>: when a heading of the module's file reads
|
||||
* `section` (POD text as it renders, cbm_doclink_perl_plain_text) and names a
|
||||
* sub that has a definition in that file, the target is the sub; headings
|
||||
* that name two subs are ambiguous. A section that names no sub is a place in
|
||||
* the module's documentation: the target is the module. L</section> is the
|
||||
* same page: a sub its heading names binds; a heading that names no sub is a
|
||||
* place in the source's own page, no reference worth an edge or a row
|
||||
* (local); a section the page has no heading for is missing (doc rot).
|
||||
*
|
||||
* Test code (t/, xt/, *.t, and cbm_is_test_file) is bound only from test code.
|
||||
* Every edge is exact: a module name and a heading are spelled out in full.
|
||||
*
|
||||
* The scope blob carries no lines (doclink_perl.c), and without a scope_delta
|
||||
* hook every change of a blob is a full re-resolution: a change that adds or
|
||||
* drops a package, a sub or a heading; an edit inside a sub changes no blob.
|
||||
*/
|
||||
#include "pipeline/doc_links.h"
|
||||
#include "pipeline/pipeline_internal.h" /* cbm_pipeline_file_node */
|
||||
#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 <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── The index ───────────────────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
const char *sub; /* the sub the heading names; NULL: none */
|
||||
const char *key; /* the heading as it renders */
|
||||
} pr_head_t;
|
||||
|
||||
typedef struct {
|
||||
const char *path; /* rel_path */
|
||||
const char *module_qn; /* the file's module qualified name */
|
||||
bool test;
|
||||
int nsubs;
|
||||
const char **subs; /* subs with a definition in the file */
|
||||
int nheads;
|
||||
pr_head_t *heads;
|
||||
} pr_file_t;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
int file;
|
||||
} pr_decl_t;
|
||||
|
||||
typedef struct {
|
||||
int first; /* into decls */
|
||||
int count;
|
||||
} pr_range_t;
|
||||
|
||||
typedef struct {
|
||||
CBMArena arena;
|
||||
const char *project;
|
||||
pr_file_t *files;
|
||||
int nfiles;
|
||||
int *run_to_file; /* run_file -> files index, -1: none */
|
||||
int run_count;
|
||||
pr_decl_t *decls; /* every package declaration, by name, then path */
|
||||
int ndecls;
|
||||
CBMHashTable *pkg; /* package name -> pr_range_t */
|
||||
bool oom;
|
||||
} pr_index_t;
|
||||
|
||||
static bool pr_test_path(const char *path) {
|
||||
if (cbm_is_test_file(path, CBM_LANG_PERL)) {
|
||||
return true;
|
||||
}
|
||||
size_t n = strlen(path);
|
||||
if (n >= PAIR_LEN && strcmp(path + n - PAIR_LEN, ".t") == 0) {
|
||||
return true;
|
||||
}
|
||||
return strncmp(path, "t/", PAIR_LEN) == 0 || strncmp(path, "xt/", CBM_SZ_3) == 0 ||
|
||||
strstr(path, "/t/") != NULL || strstr(path, "/xt/") != NULL;
|
||||
}
|
||||
|
||||
/* One record line of a blob: its fields, split at tabs (at most `cap`). */
|
||||
static int pr_fields(const char *b, const char *e, const char **f, size_t *fl, int cap) {
|
||||
int n = 0;
|
||||
const char *s = b;
|
||||
for (const char *p = b; p <= e && n < cap; p++) {
|
||||
if (p == e || *p == '\t') {
|
||||
f[n] = s;
|
||||
fl[n] = (size_t)(p - s);
|
||||
n++;
|
||||
s = p + SKIP_ONE;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
enum { PR_MAX_FIELDS = 4, PR_F_TAG = 0, PR_F_A = 1, PR_F_B = 2 };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* Count, then fill: the records of one file's blob. */
|
||||
static void pr_read_blob(pr_index_t *ix, int fi, const char *blob, bool fill) {
|
||||
pr_file_t *f = &ix->files[fi];
|
||||
int np = 0;
|
||||
int ns = 0;
|
||||
int nh = 0;
|
||||
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 *fld[PR_MAX_FIELDS];
|
||||
size_t fln[PR_MAX_FIELDS];
|
||||
int n = pr_fields(b, e, fld, fln, PR_MAX_FIELDS);
|
||||
if (n >= PAIR_LEN && fln[PR_F_TAG] == SKIP_ONE) {
|
||||
char tag = fld[PR_F_TAG][0];
|
||||
if (tag == 'P') {
|
||||
if (fill) {
|
||||
ix->decls[ix->ndecls++] =
|
||||
(pr_decl_t){pr_copy(ix, fld[PR_F_A], fln[PR_F_A]), fi};
|
||||
}
|
||||
np++;
|
||||
} else if (tag == 'S' && n >= CBM_SZ_3) {
|
||||
if (fill) {
|
||||
f->subs[f->nsubs++] = pr_copy(ix, fld[PR_F_B], fln[PR_F_B]);
|
||||
}
|
||||
ns++;
|
||||
} else if (tag == 'H' && n >= CBM_SZ_3) {
|
||||
if (fill) {
|
||||
bool none = fln[PR_F_A] == SKIP_ONE && fld[PR_F_A][0] == '-';
|
||||
f->heads[f->nheads++] = (pr_head_t){
|
||||
none ? NULL : pr_copy(ix, fld[PR_F_A], fln[PR_F_A]),
|
||||
pr_copy(ix, fld[PR_F_B], fln[PR_F_B]),
|
||||
};
|
||||
}
|
||||
nh++;
|
||||
}
|
||||
}
|
||||
line = *e ? e : NULL;
|
||||
}
|
||||
if (!fill) {
|
||||
ix->ndecls += np;
|
||||
f->subs =
|
||||
ns ? (const char **)cbm_arena_alloc(&ix->arena, (size_t)ns * sizeof(char *)) : NULL;
|
||||
f->heads =
|
||||
nh ? (pr_head_t *)cbm_arena_alloc(&ix->arena, (size_t)nh * sizeof(pr_head_t)) : NULL;
|
||||
ix->oom = ix->oom || (ns && !f->subs) || (nh && !f->heads);
|
||||
}
|
||||
}
|
||||
|
||||
static int pr_decl_cmp(const void *a, const void *b) {
|
||||
const pr_decl_t *x = (const pr_decl_t *)a;
|
||||
const pr_decl_t *y = (const pr_decl_t *)b;
|
||||
int c = strcmp(x->name, y->name);
|
||||
return c != 0 ? c : (x->file > y->file) - (x->file < y->file);
|
||||
}
|
||||
|
||||
static void pr_destroy(void *index) {
|
||||
pr_index_t *ix = (pr_index_t *)index;
|
||||
if (!ix) {
|
||||
return;
|
||||
}
|
||||
cbm_ht_free(ix->pkg);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, ix->run_to_file);
|
||||
cbm_arena_destroy(&ix->arena);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, ix);
|
||||
}
|
||||
|
||||
static bool pr_index_files(pr_index_t *ix, const cbm_doclink_build_in_t *in) {
|
||||
ix->nfiles = in->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_count = in->run_file_count;
|
||||
ix->run_to_file = (int *)cbm_calloc(CBM_MEM_CLASS_OTHER,
|
||||
(size_t)(in->run_file_count + SKIP_ONE) * sizeof(int));
|
||||
if ((in->file_count && !ix->files) || !ix->run_to_file) {
|
||||
return false;
|
||||
}
|
||||
for (int r = 0; r < in->run_file_count; r++) {
|
||||
ix->run_to_file[r] = CBM_NOT_FOUND;
|
||||
}
|
||||
for (int i = 0; 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; /* valid until destroy */
|
||||
f->module_qn =
|
||||
cbm_fqn_module_source_lang(&ix->arena, ix->project, src->rel_path, CBM_LANG_PERL);
|
||||
f->test = pr_test_path(src->rel_path);
|
||||
ix->oom = ix->oom || !f->module_qn;
|
||||
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, i, src->scope, false);
|
||||
}
|
||||
}
|
||||
return !ix->oom;
|
||||
}
|
||||
|
||||
static bool pr_index_packages(pr_index_t *ix, const cbm_doclink_build_in_t *in) {
|
||||
int total = ix->ndecls;
|
||||
ix->ndecls = 0;
|
||||
ix->decls =
|
||||
total ? (pr_decl_t *)cbm_arena_alloc(&ix->arena, (size_t)total * sizeof(pr_decl_t)) : NULL;
|
||||
if (total && !ix->decls) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < in->file_count && !ix->oom; i++) {
|
||||
if (in->files[i].scope) {
|
||||
pr_read_blob(ix, i, in->files[i].scope, true);
|
||||
}
|
||||
}
|
||||
if (ix->oom) {
|
||||
return false;
|
||||
}
|
||||
qsort(ix->decls, (size_t)ix->ndecls, sizeof(pr_decl_t), pr_decl_cmp);
|
||||
ix->pkg = cbm_ht_create_in(CBM_MEM_CLASS_OTHER, (uint32_t)(ix->ndecls + SKIP_ONE));
|
||||
if (!ix->pkg) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < ix->ndecls;) {
|
||||
int j = i;
|
||||
while (j < ix->ndecls && strcmp(ix->decls[j].name, ix->decls[i].name) == 0) {
|
||||
j++;
|
||||
}
|
||||
pr_range_t *r = (pr_range_t *)cbm_arena_alloc(&ix->arena, sizeof(pr_range_t));
|
||||
if (!r) {
|
||||
return false;
|
||||
}
|
||||
*r = (pr_range_t){i, j - i};
|
||||
cbm_ht_set(ix->pkg, ix->decls[i].name, r);
|
||||
i = j;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
if (!pr_index_files(ix, in) || !pr_index_packages(ix, in)) {
|
||||
cbm_log_error("doc_links.perl.error", "step", "index", "reason", "alloc");
|
||||
pr_destroy(ix);
|
||||
return NULL;
|
||||
}
|
||||
char b[PAIR_LEN][CBM_SZ_32];
|
||||
snprintf(b[0], sizeof(b[0]), "%d", ix->nfiles);
|
||||
snprintf(b[SKIP_ONE], sizeof(b[SKIP_ONE]), "%u", cbm_ht_count(ix->pkg));
|
||||
cbm_log_info("doc_links.perl.index", "files", b[0], "packages", b[SKIP_ONE]);
|
||||
return ix;
|
||||
}
|
||||
|
||||
/* ── Resolution ──────────────────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
const pr_index_t *ix;
|
||||
const cbm_gbuf_t *g;
|
||||
const pr_file_t *src;
|
||||
cbm_doclink_outcome_t *out;
|
||||
} pr_ctx_t;
|
||||
|
||||
static void pr_row(const pr_ctx_t *c, int reason) {
|
||||
c->out->kind = CBM_DOCLINK_UNRESOLVED;
|
||||
c->out->reason = reason;
|
||||
}
|
||||
|
||||
static void pr_edge(const pr_ctx_t *c, const pr_file_t *f, const cbm_gbuf_node_t *node) {
|
||||
if (!node) {
|
||||
pr_row(c, CBM_DOCLINK_REASON_GRAPH_GAP);
|
||||
return;
|
||||
}
|
||||
if (f->test && !c->src->test) {
|
||||
pr_row(c, CBM_DOCLINK_REASON_TEST_ONLY);
|
||||
return;
|
||||
}
|
||||
c->out->kind = CBM_DOCLINK_EDGE;
|
||||
c->out->target = node;
|
||||
c->out->exact = true;
|
||||
}
|
||||
|
||||
static bool pr_node_is(const cbm_gbuf_node_t *n, const pr_file_t *f, const char *label) {
|
||||
return n && n->label && n->file_path && strcmp(n->label, label) == 0 &&
|
||||
strcmp(n->file_path, f->path) == 0;
|
||||
}
|
||||
|
||||
/* The module's node: its File node. */
|
||||
static const cbm_gbuf_node_t *pr_module_node(const pr_ctx_t *c, const pr_file_t *f) {
|
||||
return cbm_pipeline_file_node(c->g, c->ix->project, f->path);
|
||||
}
|
||||
|
||||
/* The node of sub `name` of file f; NULL when it has none of its own. */
|
||||
static const cbm_gbuf_node_t *pr_sub_node(const pr_ctx_t *c, const pr_file_t *f, const char *name) {
|
||||
size_t ql = strlen(f->module_qn);
|
||||
size_t nl = strlen(name);
|
||||
char stack[CBM_SZ_512];
|
||||
char *qn = ql + nl + PAIR_LEN <= sizeof(stack)
|
||||
? stack
|
||||
: (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, ql + nl + PAIR_LEN);
|
||||
if (!qn) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(qn, f->module_qn, ql);
|
||||
qn[ql] = '.';
|
||||
memcpy(qn + ql + SKIP_ONE, name, nl + SKIP_ONE);
|
||||
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(c->g, qn);
|
||||
if (qn != stack) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, qn);
|
||||
}
|
||||
return (pr_node_is(n, f, "Function") || pr_node_is(n, f, "Method")) ? n : NULL;
|
||||
}
|
||||
|
||||
static bool pr_has_sub(const pr_file_t *f, const char *name) {
|
||||
for (int i = 0; i < f->nsubs; i++) {
|
||||
if (strcmp(f->subs[i], name) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The one sub a heading `key` of file f names (it has a definition there):
|
||||
* 1 found, 0 none, 2 several. */
|
||||
static int pr_heading_sub(const pr_file_t *f, const char *key, const char **sub) {
|
||||
*sub = NULL;
|
||||
for (int i = 0; i < f->nheads; i++) {
|
||||
const pr_head_t *h = &f->heads[i];
|
||||
if (!h->sub || strcmp(h->key, key) != 0 || !pr_has_sub(f, h->sub)) {
|
||||
continue;
|
||||
}
|
||||
if (*sub && strcmp(*sub, h->sub) != 0) {
|
||||
return PAIR_LEN;
|
||||
}
|
||||
*sub = h->sub;
|
||||
}
|
||||
return *sub ? SKIP_ONE : 0;
|
||||
}
|
||||
|
||||
static bool pr_has_heading(const pr_file_t *f, const char *key) {
|
||||
for (int i = 0; i < f->nheads; i++) {
|
||||
if (strcmp(f->heads[i].key, key) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void pr_section(const pr_ctx_t *c, const pr_file_t *f, const char *section, bool same_page) {
|
||||
char key[CBM_DOCLINK_PERL_KEY_MAX];
|
||||
cbm_doclink_perl_plain_text(section, strlen(section), key, sizeof(key));
|
||||
const char *sub = NULL;
|
||||
int found = pr_heading_sub(f, key, &sub);
|
||||
if (found > SKIP_ONE) {
|
||||
pr_row(c, CBM_DOCLINK_REASON_AMBIGUOUS);
|
||||
} else if (found == SKIP_ONE) {
|
||||
pr_edge(c, f, pr_sub_node(c, f, sub));
|
||||
} else if (same_page && pr_has_heading(f, key)) {
|
||||
c->out->kind = CBM_DOCLINK_LOCAL; /* a place in the source's own page */
|
||||
} else if (same_page) {
|
||||
pr_row(c, CBM_DOCLINK_REASON_MISSING); /* a section the page does not have */
|
||||
} else {
|
||||
pr_edge(c, f, pr_module_node(c, f));
|
||||
}
|
||||
}
|
||||
|
||||
/* True when `path` is the file the library layout gives module `page`. */
|
||||
static bool pr_layout_names(const char *path, const char *page) {
|
||||
char want[CBM_DOCLINK_PERL_KEY_MAX + CBM_SZ_8];
|
||||
size_t w = 0;
|
||||
for (const char *p = page; *p && w + CBM_SZ_4 < sizeof(want); p++) {
|
||||
if (p[0] == ':' && p[SKIP_ONE] == ':') {
|
||||
want[w++] = '/';
|
||||
p++;
|
||||
} else {
|
||||
want[w++] = *p;
|
||||
}
|
||||
}
|
||||
memcpy(want + w, ".pm", CBM_SZ_4);
|
||||
w += CBM_SZ_3;
|
||||
size_t n = strlen(path);
|
||||
return n >= w && strcmp(path + n - w, want) == 0 && (n == w || path[n - w - SKIP_ONE] == '/');
|
||||
}
|
||||
|
||||
/* The file of module `page`: NULL with *ambiguous false when the repository
|
||||
* declares no such package. */
|
||||
static const pr_file_t *pr_page_file(const pr_index_t *ix, const char *page, bool *ambiguous) {
|
||||
*ambiguous = false;
|
||||
const pr_range_t *r = (const pr_range_t *)cbm_ht_get(ix->pkg, page);
|
||||
if (!r || r->count == 0) {
|
||||
return NULL;
|
||||
}
|
||||
if (r->count == SKIP_ONE) {
|
||||
return &ix->files[ix->decls[r->first].file];
|
||||
}
|
||||
const pr_file_t *hit = NULL;
|
||||
for (int i = r->first; i < r->first + r->count; i++) {
|
||||
const pr_file_t *f = &ix->files[ix->decls[i].file];
|
||||
if (f == hit || !pr_layout_names(f->path, page)) {
|
||||
continue;
|
||||
}
|
||||
if (hit) {
|
||||
*ambiguous = true;
|
||||
return NULL;
|
||||
}
|
||||
hit = f;
|
||||
}
|
||||
*ambiguous = hit == NULL;
|
||||
return hit;
|
||||
}
|
||||
|
||||
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; /* Perl 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->files[ix->run_to_file[run_file]], .out = out};
|
||||
CBMPerlLink pl;
|
||||
if (!cbm_doclink_perl_parse_link(link->raw, &pl)) {
|
||||
pr_row(&c, CBM_DOCLINK_REASON_UNPARSEABLE);
|
||||
return;
|
||||
}
|
||||
if (pl.kind == CBM_PERL_LINK_MAN) {
|
||||
pr_row(&c, CBM_DOCLINK_REASON_EXTERNAL);
|
||||
return;
|
||||
}
|
||||
if (pl.kind == CBM_PERL_LINK_SECTION) {
|
||||
pr_section(&c, c.src, pl.section, true);
|
||||
return;
|
||||
}
|
||||
bool ambiguous = false;
|
||||
const pr_file_t *f = pr_page_file(ix, pl.page, &ambiguous);
|
||||
if (!f) {
|
||||
pr_row(&c, ambiguous ? CBM_DOCLINK_REASON_AMBIGUOUS : CBM_DOCLINK_REASON_EXTERNAL);
|
||||
return;
|
||||
}
|
||||
if (pl.section[0]) {
|
||||
pr_section(&c, f, pl.section, false);
|
||||
return;
|
||||
}
|
||||
pr_edge(&c, f, pr_module_node(&c, f));
|
||||
}
|
||||
|
||||
const cbm_doclink_resolver_t cbm_doclink_perl_resolver = {
|
||||
.langs = {CBM_LANG_PERL},
|
||||
.lang_count = 1,
|
||||
.scope_tag = CBM_DOCLINK_PERL_SCOPE_TAG,
|
||||
.build = pr_build,
|
||||
.destroy = pr_destroy,
|
||||
.resolve = pr_resolve,
|
||||
};
|
||||
@@ -439,7 +439,7 @@ static const int DM_HELD_FAMILIES[] = {
|
||||
CBM_DOCLINK_JAVA_VALUE, CBM_DOCLINK_KT_KDOC_LINK,
|
||||
CBM_DOCLINK_KT_SEE, CBM_DOCLINK_KT_THROWS,
|
||||
CBM_DOCLINK_KT_SAMPLE, CBM_DOCLINK_RS_INTRA_DOC,
|
||||
CBM_DOCLINK_RS_LINK_DEF,
|
||||
CBM_DOCLINK_RS_LINK_DEF, CBM_DOCLINK_PERL_POD_L,
|
||||
};
|
||||
|
||||
static inline void dm_ship_held_families(void) {
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* test_doc_mentions_perl.c -- Perl POD L<> codes as doc-comment references.
|
||||
*
|
||||
* A sub's doc is the POD section that names it; the file's module doc (its
|
||||
* File node's docstring) is the POD no sub holds. The L<> codes of both are
|
||||
* the references: module-level ones come from the File node.
|
||||
*/
|
||||
#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 <string.h>
|
||||
|
||||
/* ── extraction ──────────────────────────────────────────────────── */
|
||||
|
||||
static const char *const PL_KNOB =
|
||||
"package Knob;\n" /* 1 */
|
||||
"\n" /* 2 */
|
||||
"=head1 NAME\n" /* 3 */
|
||||
"\n" /* 4 */
|
||||
"Knob - see L<Dial> and L<the knob|Knob::Turn/turning>.\n" /* 5 */
|
||||
"\n" /* 6 */
|
||||
"=cut\n" /* 7 */
|
||||
"\n" /* 8 */
|
||||
"sub turn { return 1; }\n" /* 9 */
|
||||
"1;\n" /* 10 */
|
||||
"__END__\n" /* 11 */
|
||||
"\n" /* 12 */
|
||||
"=head1 METHODS\n" /* 13 */
|
||||
"\n" /* 14 */
|
||||
"Methods, see L</turn> and L<https://example.org/k>.\n" /* 15 */
|
||||
"\n" /* 16 */
|
||||
"=head2 turn\n" /* 17 */
|
||||
"\n" /* 18 */
|
||||
"Turns, like L<Knob::Spin>.\n" /* 19 */
|
||||
"\n" /* 20 */
|
||||
"=head2 spin\n" /* 21 */
|
||||
"\n" /* 22 */
|
||||
"About L<perlfunc/open> and L<crontab(5)>.\n" /* 23 */
|
||||
"\n" /* 24 */
|
||||
"=cut\n"; /* 25 */
|
||||
|
||||
/* The one token `raw`: its line, whether it is the file's, its family. */
|
||||
static bool pl_token_is(const CBMFileResult *r, const char *raw, uint32_t line, bool file,
|
||||
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);
|
||||
bool is_file = (t->flags & CBM_DOCLINK_FLAG_FILE) != 0;
|
||||
if (t->line != line || is_file != file || t->syntax != syntax) {
|
||||
printf(" token %s: line %u file %d syntax %d\n", raw, t->line, is_file, t->syntax);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(doc_mentions_perl_tokens) {
|
||||
CBMFileResult *r = dm_extract(PL_KNOB, CBM_LANG_PERL, "lib/Knob.pm");
|
||||
ASSERT_NOT_NULL(r);
|
||||
/* the module doc's references come from the file, each on its own line */
|
||||
ASSERT_TRUE(pl_token_is(r, "Dial", 5, true, CBM_DOCLINK_PERL_POD_L));
|
||||
ASSERT_TRUE(pl_token_is(r, "the knob|Knob::Turn/turning", 5, true, CBM_DOCLINK_PERL_POD_L));
|
||||
ASSERT_TRUE(pl_token_is(r, "/turn", 15, true, CBM_DOCLINK_PERL_POD_L));
|
||||
ASSERT_TRUE(pl_token_is(r, "https://example.org/k", 15, true, CBM_DOCLINK_HREF));
|
||||
ASSERT_TRUE(pl_token_is(r, "perlfunc/open", 23, true, CBM_DOCLINK_PERL_POD_L));
|
||||
ASSERT_TRUE(pl_token_is(r, "crontab(5)", 23, true, CBM_DOCLINK_PERL_POD_L));
|
||||
/* a sub's section is the sub's, and only the sub's */
|
||||
ASSERT_TRUE(pl_token_is(r, "Knob::Spin", 19, false, CBM_DOCLINK_PERL_POD_L));
|
||||
const CBMDocLink *t = dm_find_token(r, "Knob::Spin");
|
||||
ASSERT_NOT_NULL(strstr(t->source_qn, "turn"));
|
||||
ASSERT_EQ(r->doc_links.count, 7);
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── resolution ──────────────────────────────────────────────────── */
|
||||
|
||||
static void pl_write_repo(const char *repo) {
|
||||
th_write_file(TH_PATH(repo, "lib/Knob.pm"),
|
||||
"package Knob;\n"
|
||||
"\n"
|
||||
"=head1 NAME\n"
|
||||
"\n"
|
||||
"Knob - L<Knob::Dial>, L<Knob::Dial/spin>, L<Knob::Dial/OVERVIEW>.\n"
|
||||
"\n"
|
||||
"=head1 SEE ALSO\n"
|
||||
"\n"
|
||||
"L</turn>, L</NOTES>, L</GONE>, L<Mojo::Base>, L<crontab(5)>, L<TestHelper>,\n"
|
||||
"L<Dup>.\n"
|
||||
"\n"
|
||||
"=head1 NOTES\n"
|
||||
"\n"
|
||||
"Nothing.\n"
|
||||
"\n"
|
||||
"=cut\n"
|
||||
"\n"
|
||||
"sub turn { return 1; }\n"
|
||||
"\n"
|
||||
"=head2 turn\n"
|
||||
"\n"
|
||||
"Turns, see L<Knob::Dial/spin>.\n"
|
||||
"\n"
|
||||
"=cut\n"
|
||||
"\n"
|
||||
"1;\n");
|
||||
th_write_file(TH_PATH(repo, "lib/Knob/Dial.pm"), "package Knob::Dial;\n"
|
||||
"\n"
|
||||
"=head1 OVERVIEW\n"
|
||||
"\n"
|
||||
"The dial of L<Knob>.\n"
|
||||
"\n"
|
||||
"=cut\n"
|
||||
"\n"
|
||||
"sub spin { return 2; }\n"
|
||||
"\n"
|
||||
"=head2 spin\n"
|
||||
"\n"
|
||||
"Spins.\n"
|
||||
"\n"
|
||||
"=cut\n"
|
||||
"\n"
|
||||
"1;\n");
|
||||
/* a second declaration of Knob::Dial, not where the library layout puts it */
|
||||
th_write_file(TH_PATH(repo, "lib/Extra.pm"), "package Knob::Dial;\nsub extra { 3 }\n1;\n");
|
||||
/* Dup twice, neither at its layout path */
|
||||
th_write_file(TH_PATH(repo, "lib/A.pm"), "package Dup;\nsub a { 1 }\n1;\n");
|
||||
th_write_file(TH_PATH(repo, "lib/B.pm"), "package Dup;\nsub b { 1 }\n1;\n");
|
||||
th_write_file(TH_PATH(repo, "t/lib/TestHelper.pm"),
|
||||
"package TestHelper;\nsub help { 1 }\n1;\n");
|
||||
}
|
||||
|
||||
static bool pl_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 || !strstr(props, "\"tier\":\"exact\"")) {
|
||||
printf(" edge %s -> %s: %d found %s\n", src, tgt, n, props);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool pl_row(const char *db, const char *rel, const char *raw, const char *reason) {
|
||||
char got[64];
|
||||
dm_row(db, rel, raw, got, sizeof(got), NULL, 0);
|
||||
if (strcmp(got, reason) != 0) {
|
||||
printf(" row %s [%s]: \"%s\", expected \"%s\"\n", rel, raw, got, reason);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TEST(doc_mentions_perl_resolution) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlperl_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);
|
||||
pl_write_repo(repo);
|
||||
ASSERT_EQ(dm_index(repo, db, NULL), 0);
|
||||
const char *knob = "lib/Knob.pm";
|
||||
/* a module the library layout names, though another file declares it too */
|
||||
ASSERT_TRUE(pl_edge(db, "lib.Knob.pm.__file__", "lib.Knob.Dial.pm.__file__"));
|
||||
/* a section whose heading names a sub of the module: the sub */
|
||||
ASSERT_TRUE(pl_edge(db, "lib.Knob.pm.__file__", "lib.Knob.Dial.spin"));
|
||||
ASSERT_TRUE(pl_edge(db, "lib.Knob.turn", "lib.Knob.Dial.spin"));
|
||||
/* the same page: a sub binds, a section that names none is local */
|
||||
ASSERT_TRUE(pl_edge(db, "lib.Knob.pm.__file__", "lib.Knob.turn"));
|
||||
ASSERT_TRUE(pl_row(db, knob, "/NOTES", ""));
|
||||
/* ... and one the page has no heading for is doc rot */
|
||||
ASSERT_TRUE(pl_row(db, knob, "/GONE", "missing"));
|
||||
/* outside the repository, a man page, test code, two declarations */
|
||||
ASSERT_TRUE(pl_row(db, knob, "Mojo::Base", "external"));
|
||||
ASSERT_TRUE(pl_row(db, knob, "crontab(5)", "external"));
|
||||
ASSERT_TRUE(pl_row(db, knob, "TestHelper", "test_only_target"));
|
||||
ASSERT_TRUE(pl_row(db, knob, "Dup", "ambiguous"));
|
||||
/* the page is the file, also where a Folder holds lib/Knob.pm's module name */
|
||||
ASSERT_TRUE(pl_edge(db, "lib.Knob.Dial.pm.__file__", "lib.Knob.pm.__file__"));
|
||||
th_cleanup(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── incremental == full; one worker == several ──────────────────── */
|
||||
|
||||
#define PL_INC_DIAL \
|
||||
"package Knob::Dial;\n\n=head1 OVERVIEW\n\nThe dial.\n\n=cut\n\nsub spin { 2 }\n\n" \
|
||||
"=head2 spin\n\nSpins.\n\n=cut\n\n1;\n"
|
||||
|
||||
TEST(doc_mentions_perl_incremental_equals_full) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlperl_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, "lib/Knob.pm"),
|
||||
"package Knob;\n\n=head1 NAME\n\nKnob - see L<Knob::Dial/spin>.\n\n=cut\n\n"
|
||||
"sub turn { 1 }\n1;\n");
|
||||
th_write_file(TH_PATH(repo, "lib/Knob/Dial.pm"), PL_INC_DIAL);
|
||||
ASSERT_EQ(dm_index(repo, inc_db, NULL), 0);
|
||||
ASSERT_TRUE(pl_edge(inc_db, "lib.Knob.pm.__file__", "lib.Knob.Dial.spin"));
|
||||
|
||||
/* the module doc's text changes: the file is repaired alone */
|
||||
th_write_file(
|
||||
TH_PATH(repo, "lib/Knob.pm"),
|
||||
"package Knob;\n\n=head1 NAME\n\nKnob, a knob - see L<Knob::Dial/spin>.\n\n=cut\n\n"
|
||||
"sub turn { 1 }\n1;\n");
|
||||
ASSERT_EQ(
|
||||
dm_step(repo, inc_db, full_db, "module doc edit", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
|
||||
ASSERT_TRUE(pl_edge(inc_db, "lib.Knob.pm.__file__", "lib.Knob.Dial.spin"));
|
||||
|
||||
/* a reference added to the module doc: an edge from the File node */
|
||||
th_write_file(TH_PATH(repo, "lib/Knob.pm"),
|
||||
"package Knob;\n\n=head1 NAME\n\nKnob, a knob - see L<Knob::Dial/spin> and\n"
|
||||
"L<Knob::Dial>.\n\n=cut\n\nsub turn { 1 }\n1;\n");
|
||||
ASSERT_EQ(
|
||||
dm_step(repo, inc_db, full_db, "module doc link", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
|
||||
ASSERT_TRUE(pl_edge(inc_db, "lib.Knob.pm.__file__", "lib.Knob.Dial.pm.__file__"));
|
||||
|
||||
/* a new heading changes the file's scope blob: without a scope_delta hook
|
||||
* that is a full run */
|
||||
th_write_file(TH_PATH(repo, "lib/Knob.pm"),
|
||||
"package Knob;\n\n=head1 NAME\n\nKnob, a knob - see L<Knob::Dial/spin> and\n"
|
||||
"L<Knob::Dial>.\n\n=head1 SEE ALSO\n\nL<Knob::Dial/OVERVIEW>\n\n=cut\n\n"
|
||||
"sub turn { 1 }\n1;\n");
|
||||
ASSERT_EQ(dm_step(repo, inc_db, full_db, "heading added", CBM_INCREMENTAL_ROUTE_FORCED_FULL),
|
||||
0);
|
||||
|
||||
/* the heading that named the sub goes: another file's section reference
|
||||
* now names a place in the module's documentation, the module */
|
||||
th_write_file(
|
||||
TH_PATH(repo, "lib/Knob/Dial.pm"),
|
||||
"package Knob::Dial;\n\n=head1 OVERVIEW\n\nThe dial.\n\n=cut\n\nsub spin { 2 }\n1;\n");
|
||||
ASSERT_EQ(dm_step(repo, inc_db, full_db, "heading removed", CBM_INCREMENTAL_ROUTE_FORCED_FULL),
|
||||
0);
|
||||
char props[512];
|
||||
int n = 0;
|
||||
dm_edge(inc_db, "lib.Knob.pm.__file__", "lib.Knob.Dial.spin", props, sizeof(props), &n);
|
||||
ASSERT_EQ(n, 0);
|
||||
th_cleanup(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(doc_mentions_perl_workers_agree) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dlperl_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), "lib/Par/M%02d.pm", i);
|
||||
snprintf(src, sizeof(src),
|
||||
"package Par::M%02d;\n\n=head1 NAME\n\nPar::M%02d - see L<Par::M%02d>,\n"
|
||||
"L<Par::M%02d/run%02d>, L</run%02d>, L<Gone::Away> and L</GONE>.\n\n=cut\n\n"
|
||||
"sub run%02d { %d }\n\n=head2 run%02d\n\nRuns; also L<Par::M%02d/OVERVIEW>.\n\n"
|
||||
"=cut\n\n1;\n",
|
||||
i, i, next, next, next, i, i, i, i, next);
|
||||
th_write_file(TH_PATH(repo, rel), src);
|
||||
}
|
||||
ASSERT_EQ(dm_workers_agree(repo, par_db, seq_db), 0);
|
||||
ASSERT_TRUE(pl_edge(par_db, "lib.Par.M00.pm.__file__", "lib.Par.M01.run01"));
|
||||
th_cleanup(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(doc_mentions_perl) {
|
||||
dm_ship_held_families();
|
||||
RUN_TEST(doc_mentions_perl_tokens);
|
||||
RUN_TEST(doc_mentions_perl_resolution);
|
||||
RUN_TEST(doc_mentions_perl_incremental_equals_full);
|
||||
RUN_TEST(doc_mentions_perl_workers_agree);
|
||||
}
|
||||
@@ -1828,6 +1828,7 @@ extern void suite_doc_links_adoc(void);
|
||||
extern void suite_doc_mentions_go(void);
|
||||
extern void suite_doc_mentions_jvm(void);
|
||||
extern void suite_doc_mentions_rs(void);
|
||||
extern void suite_doc_mentions_perl(void);
|
||||
extern void suite_infrascan(void);
|
||||
extern void suite_cli(void);
|
||||
extern void suite_agent_clients(void);
|
||||
@@ -2302,6 +2303,7 @@ int main(int argc, char **argv) {
|
||||
RUN_SELECTED_SUITE(doc_mentions_go);
|
||||
RUN_SELECTED_SUITE(doc_mentions_jvm);
|
||||
RUN_SELECTED_SUITE(doc_mentions_rs);
|
||||
RUN_SELECTED_SUITE(doc_mentions_perl);
|
||||
|
||||
/* Infrastructure scanning */
|
||||
RUN_SELECTED_SUITE(infrascan);
|
||||
|
||||
Reference in new issue
Block a user