feat(docs): diagrams and OpenAPI operations in the graph

Diagrams name the parts of a system and how they connect; an OpenAPI
document names its endpoints and their handlers. Both now enter the graph.

Diagrams:
- a Diagram node per diagram: a fenced block of a Markdown file (under its
  Section), a directive of a reST file (`.. mermaid::`, `.. uml::`,
  `.. graphviz::`, Sphinx `.. digraph::`), an Asciidoctor Diagram block of
  an AsciiDoc file (`[plantuml]`, `[source,mermaid]`, ...), or a diagram
  file of its own, named by its title, else by its file;
- a DiagramElement node per element, DEFINES from its Diagram, and a
  DIAGRAM_ARROW edge per arrow (label, style, line);
- formats: Mermaid (flowchart, sequence, class, ER, state, C4,
  architecture), PlantUML, Graphviz DOT, D2, draw.io (plain, compressed,
  .drawio.svg) and Excalidraw (.excalidraw, .excalidraw.json,
  .excalidraw.svg); .drawio.svg and .excalidraw.svg are no longer dropped
  as images;
- an element's explicit link (Mermaid click/link, PlantUML [[url]], url of
  and !include, draw.io and Excalidraw link, DOT URL/href, D2 link) is a
  doc link read as a Markdown link, MENTIONS when it resolves.

OpenAPI:
- each operation of an OpenAPI or Swagger document (openapi.json and
  swagger.json, now read with the JSON grammar but no node per key, and
  any YAML or JSON file whose root says openapi/swagger) is a Route, named
  by the same QN a route its handler registers in code gets
  (cbm_route_canon_path, moved into the extraction library);
- its operationId is a doc link to the handler: a dotted one (Connexion,
  with x-openapi-router-controller) as a qualified code name, a bare one
  when exactly one function or method of the repository has that name.

The eight new link families are held until their held-out audit passes.

Read on real repositories, which shaped the rules: Excalidraw drawings put
text over shapes and draw arrows up to them without binding, so loose text
names the smallest box it sits in and a loose arrow end meets the smallest
named box it touches (the drawing's frame only at its border); arrows bound
to free text meet the text; D2 block strings and nested blocks; DOT line
breaks and sentence-long ids; PlantUML kinds of classes inside packages
and nested classes (`..+`, nested -> container, so they do not collide with
the container's association). Connexion's 24 specs: 160 operationIds, all
linked to their handlers.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-11 02:08:58 +02:00
1 parent 06e69d0c56
commit f4a49feba1
30 files changed
+5621 -205

No files matched your search

+5 -1
View File
@@ -393,6 +393,9 @@ EXTRACTION_SRCS = \
$(CBM_DIR)/doclink_ts.c \ $(CBM_DIR)/doclink_ts.c \
$(CBM_DIR)/doclink_php.c \ $(CBM_DIR)/doclink_php.c \
$(CBM_DIR)/doclink_c.c \ $(CBM_DIR)/doclink_c.c \
$(CBM_DIR)/diagram.c \
$(CBM_DIR)/route_path.c \
$(CBM_DIR)/openapi.c \
$(CBM_DIR)/sql_values.c \ $(CBM_DIR)/sql_values.c \
$(CBM_DIR)/helpers.c \ $(CBM_DIR)/helpers.c \
$(CBM_DIR)/cpp_branch_views.c \ $(CBM_DIR)/cpp_branch_views.c \
@@ -526,6 +529,7 @@ PIPELINE_SRCS = \
src/pipeline/doc_links_ts.c \ src/pipeline/doc_links_ts.c \
src/pipeline/doc_links_php.c \ src/pipeline/doc_links_php.c \
src/pipeline/doc_links_c.c \ src/pipeline/doc_links_c.c \
src/pipeline/pass_diagrams.c \
src/pipeline/doc_links_msbuild.c \ src/pipeline/doc_links_msbuild.c \
src/pipeline/pass_route_nodes.c \ src/pipeline/pass_route_nodes.c \
src/pipeline/pass_enrichment.c \ src/pipeline/pass_enrichment.c \
@@ -788,7 +792,7 @@ TEST_DISCOVER_SRCS = \
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c 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_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_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_diagrams.c tests/test_openapi.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 TEST_WATCHER_SRCS = tests/test_watcher.c
+31
View File
@@ -277,6 +277,11 @@ void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch) {
arr->items[arr->count++] = ch; arr->items[arr->count++] = ch;
} }
void cbm_diagram_arrows_push(CBMDiagramArrowArray *arr, CBMArena *a, CBMDiagramArrow da) {
GROW_ARRAY(arr, a);
arr->items[arr->count++] = da;
}
// --- String input reader (for parse_with_options) --- // --- String input reader (for parse_with_options) ---
typedef struct { typedef struct {
@@ -2837,6 +2842,26 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
.module_qn = result->module_qn, .module_qn = result->module_qn,
}; };
cbm_adoc_extract_document(&adoc_ctx); cbm_adoc_extract_document(&adoc_ctx);
cbm_extract_diagrams(&adoc_ctx); /* after the Sections they hang under */
cbm_index_mark_done(rel_path);
return result;
}
/* Diagram files have no grammar either: each format is read from the text
* (diagram.c). */
if (language == CBM_LANG_DIAGRAM) {
result->module_qn = cbm_fqn_module_source_lang(a, project, rel_path, language);
CBMExtractCtx diagram_ctx = {
.arena = a,
.scratch = scratch,
.result = result,
.source = source,
.source_len = source_len,
.language = language,
.project = project,
.rel_path = rel_path,
.module_qn = result->module_qn,
};
cbm_extract_diagram_file(&diagram_ctx);
cbm_index_mark_done(rel_path); cbm_index_mark_done(rel_path);
return result; return result;
} }
@@ -3021,6 +3046,12 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
// Channel detection (Socket.IO / EventEmitter) — JS/TS only. // Channel detection (Socket.IO / EventEmitter) — JS/TS only.
cbm_extract_channels(&ctx); cbm_extract_channels(&ctx);
// Diagrams: blocks of documents and diagram files (self-gated by language).
cbm_extract_diagrams(&ctx);
// OpenAPI operations as Routes (self-gated: API description documents).
cbm_extract_openapi(&ctx);
// K8s / Kustomize semantic pass (additional structured extraction for YAML-based infra files). // K8s / Kustomize semantic pass (additional structured extraction for YAML-based infra files).
if (ctx.language == CBM_LANG_KUSTOMIZE || ctx.language == CBM_LANG_K8S) { if (ctx.language == CBM_LANG_KUSTOMIZE || ctx.language == CBM_LANG_K8S) {
cbm_extract_k8s(&ctx); cbm_extract_k8s(&ctx);
+43
View File
@@ -211,6 +211,10 @@ typedef enum {
CBM_LANG_CHIALISP, // Chialisp (.clsp/.clib/.clinc — Chia smart-coin s-expression language) CBM_LANG_CHIALISP, // Chialisp (.clsp/.clib/.clinc — Chia smart-coin s-expression language)
CBM_LANG_PDF, // PDF documents: the text layer, read without a grammar (doc_pdf.c) CBM_LANG_PDF, // PDF documents: the text layer, read without a grammar (doc_pdf.c)
CBM_LANG_ASCIIDOC, // AsciiDoc documents, read without a grammar (doc_adoc.c) CBM_LANG_ASCIIDOC, // AsciiDoc documents, read without a grammar (doc_adoc.c)
CBM_LANG_OPENAPI, // openapi.json / swagger.json: the JSON grammar, no node per key; their
// operations are Routes (openapi.c)
CBM_LANG_DIAGRAM, // diagram files (PlantUML, DOT, D2, draw.io, Excalidraw), read
// without a grammar (diagram.c)
CBM_LANG_COUNT CBM_LANG_COUNT
} CBMLanguage; } CBMLanguage;
@@ -612,6 +616,23 @@ typedef struct {
int cap; int cap;
} CBMChannelArray; } CBMChannelArray;
/* One arrow of a diagram (diagram.c): from one DiagramElement definition of
* the file to another, written as a DIAGRAM_ARROW edge. */
typedef struct {
const char *from_qn;
const char *to_qn;
const char *label; /* the arrow's text, NULL for none */
const char *style; /* what the arrow means as its format writes it: "flow",
* "message", "inherits", "composition", ... */
uint32_t line;
} CBMDiagramArrow;
typedef struct {
CBMDiagramArrow *items;
int count;
int cap;
} CBMDiagramArrowArray;
typedef struct { typedef struct {
CBMFieldType *items; CBMFieldType *items;
int count; int count;
@@ -661,6 +682,7 @@ typedef struct CBMFileResult {
CBMStringRefArray string_refs; // URL/config string literals from AST CBMStringRefArray string_refs; // URL/config string literals from AST
CBMInfraBindingArray infra_bindings; // topic→URL pairs from IaC configs CBMInfraBindingArray infra_bindings; // topic→URL pairs from IaC configs
CBMChannelArray channels; // Socket.IO / EventEmitter pub/sub participation CBMChannelArray channels; // Socket.IO / EventEmitter pub/sub participation
CBMDiagramArrowArray diagram_arrows; // arrows between the file's DiagramElement defs
CBMFieldTypeArray field_types; // Python: annotated instance fields (#1277) CBMFieldTypeArray field_types; // Python: annotated instance fields (#1277)
const char *module_qn; // module qualified name const char *module_qn; // module qualified name
@@ -1122,6 +1144,7 @@ void cbm_infrabinding_push(CBMInfraBindingArray *arr, CBMArena *a, CBMInfraBindi
void cbm_impltrait_push(CBMImplTraitArray *arr, CBMArena *a, CBMImplTrait it); void cbm_impltrait_push(CBMImplTraitArray *arr, CBMArena *a, CBMImplTrait it);
void cbm_resolvedcall_push(CBMResolvedCallArray *arr, CBMArena *a, CBMResolvedCall rc); void cbm_resolvedcall_push(CBMResolvedCallArray *arr, CBMArena *a, CBMResolvedCall rc);
void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch); void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel ch);
void cbm_diagram_arrows_push(CBMDiagramArrowArray *arr, CBMArena *a, CBMDiagramArrow da);
// --- Sub-extractor entry points --- // --- Sub-extractor entry points ---
@@ -1140,6 +1163,26 @@ void cbm_extract_type_refs(CBMExtractCtx *ctx);
void cbm_extract_env_accesses(CBMExtractCtx *ctx); void cbm_extract_env_accesses(CBMExtractCtx *ctx);
void cbm_extract_type_assigns(CBMExtractCtx *ctx); void cbm_extract_type_assigns(CBMExtractCtx *ctx);
void cbm_extract_channels(CBMExtractCtx *ctx); void cbm_extract_channels(CBMExtractCtx *ctx);
/* Diagrams (diagram.c): a Diagram definition per diagram -- a Markdown, reST
* or AsciiDoc block, or a diagram file -- with a DiagramElement definition per
* element and the arrows between them. */
void cbm_extract_diagrams(CBMExtractCtx *ctx);
/* A diagram file (CBM_LANG_DIAGRAM): its format comes from its extension. */
void cbm_extract_diagram_file(CBMExtractCtx *ctx);
/* OpenAPI / Swagger documents (openapi.c): a Route definition per operation and
* a doc link per operationId. Self-gated: openapi.json / swagger.json, and YAML
* or JSON files whose root says `openapi` or `swagger`. */
void cbm_extract_openapi(CBMExtractCtx *ctx);
/* Canonicalize route-path parameter placeholders (":id", "{id}", "<id>",
* "${...}") to a single "{}" token so that client call sites, server handlers
* and OpenAPI operations rendezvous on the same Route QN regardless of
* framework syntax. Parameter names are intentionally discarded ("/u/{id}" and
* "/u/{slug}" both canonicalize to "/u/{}"). The result never exceeds the
* input length, so out_sz >= strlen(in) + 1 always suffices. Returns out.
* (route_path.c) */
const char *cbm_route_canon_path(const char *in, char *out, size_t out_sz);
// Single-pass unified extraction (replaces the 7 calls above except defs+imports). // Single-pass unified extraction (replaces the 7 calls above except defs+imports).
void cbm_extract_unified(CBMExtractCtx *ctx); void cbm_extract_unified(CBMExtractCtx *ctx);
File diff suppressed because it is too large. Load diff
+100 -66
View File
@@ -83,72 +83,87 @@
* *
* The enum below and the family table (doclink.c) are both generated from this * The enum below and the family table (doclink.c) are both generated from this
* list, so a value cannot exist without its name and its gate. */ * list, so a value cannot exist without its name and its gate. */
#define CBM_DOCLINK_FAMILY_LIST(X) \ #define CBM_DOCLINK_FAMILY_LIST(X) \
/* csharp */ \ /* csharp */ \
X(CBM_DOCLINK_CS_SEE, CBM_LANG_CSHARP, "see", false, true, "MENTIONS") \ X(CBM_DOCLINK_CS_SEE, CBM_LANG_CSHARP, "see", false, true, "MENTIONS") \
X(CBM_DOCLINK_CS_SEEALSO, CBM_LANG_CSHARP, "seealso", false, true, "MENTIONS") \ X(CBM_DOCLINK_CS_SEEALSO, CBM_LANG_CSHARP, "seealso", false, true, "MENTIONS") \
X(CBM_DOCLINK_CS_EXCEPTION, CBM_LANG_CSHARP, "exception", false, true, "MENTIONS") \ X(CBM_DOCLINK_CS_EXCEPTION, CBM_LANG_CSHARP, "exception", false, true, "MENTIONS") \
X(CBM_DOCLINK_CS_INHERITDOC, CBM_LANG_CSHARP, "inheritdoc", false, true, "MENTIONS") \ X(CBM_DOCLINK_CS_INHERITDOC, CBM_LANG_CSHARP, "inheritdoc", false, true, "MENTIONS") \
/* go (doclink_go.c): [Name] doc links; held until its held-out audit passes */ \ /* go (doclink_go.c): [Name] doc links; held until its held-out audit passes */ \
X(CBM_DOCLINK_GO_DOC_LINK, CBM_LANG_GO, "doc_link", false, false, "MENTIONS") \ X(CBM_DOCLINK_GO_DOC_LINK, CBM_LANG_GO, "doc_link", false, false, "MENTIONS") \
/* java and kotlin (doclink_jvm.c): Javadoc and KDoc; held until their audit passes */ \ /* java and kotlin (doclink_jvm.c): Javadoc and KDoc; held until their audit passes */ \
X(CBM_DOCLINK_JAVA_LINK, CBM_LANG_JAVA, "link", false, false, "MENTIONS") \ X(CBM_DOCLINK_JAVA_LINK, CBM_LANG_JAVA, "link", false, false, "MENTIONS") \
X(CBM_DOCLINK_JAVA_LINKPLAIN, CBM_LANG_JAVA, "linkplain", false, false, "MENTIONS") \ X(CBM_DOCLINK_JAVA_LINKPLAIN, CBM_LANG_JAVA, "linkplain", false, false, "MENTIONS") \
X(CBM_DOCLINK_JAVA_SEE, CBM_LANG_JAVA, "see", false, false, "MENTIONS") \ X(CBM_DOCLINK_JAVA_SEE, CBM_LANG_JAVA, "see", false, false, "MENTIONS") \
X(CBM_DOCLINK_JAVA_THROWS, CBM_LANG_JAVA, "throws", false, false, "MENTIONS") \ X(CBM_DOCLINK_JAVA_THROWS, CBM_LANG_JAVA, "throws", false, false, "MENTIONS") \
X(CBM_DOCLINK_JAVA_VALUE, CBM_LANG_JAVA, "value", false, false, "MENTIONS") \ X(CBM_DOCLINK_JAVA_VALUE, CBM_LANG_JAVA, "value", false, false, "MENTIONS") \
X(CBM_DOCLINK_KT_KDOC_LINK, CBM_LANG_KOTLIN, "kdoc_link", false, false, "MENTIONS") \ X(CBM_DOCLINK_KT_KDOC_LINK, CBM_LANG_KOTLIN, "kdoc_link", false, false, "MENTIONS") \
X(CBM_DOCLINK_KT_SEE, CBM_LANG_KOTLIN, "see", false, false, "MENTIONS") \ X(CBM_DOCLINK_KT_SEE, CBM_LANG_KOTLIN, "see", false, false, "MENTIONS") \
X(CBM_DOCLINK_KT_THROWS, CBM_LANG_KOTLIN, "throws", false, false, "MENTIONS") \ X(CBM_DOCLINK_KT_THROWS, CBM_LANG_KOTLIN, "throws", false, false, "MENTIONS") \
X(CBM_DOCLINK_KT_SAMPLE, CBM_LANG_KOTLIN, "sample", false, false, "MENTIONS") \ X(CBM_DOCLINK_KT_SAMPLE, CBM_LANG_KOTLIN, "sample", false, false, "MENTIONS") \
/* rust (doclink_rs.c): intra-doc links; held until their audit passes */ \ /* 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_INTRA_DOC, CBM_LANG_RUST, "intra_doc", false, false, "MENTIONS") \
X(CBM_DOCLINK_RS_LINK_DEF, CBM_LANG_RUST, "link_def", 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 */ \ /* 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") \ X(CBM_DOCLINK_PERL_POD_L, CBM_LANG_PERL, "pod_L", false, false, "MENTIONS") \
/* typescript and javascript (doclink_ts.c): JSDoc/TSDoc; held until their audit */ \ /* typescript and javascript (doclink_ts.c): JSDoc/TSDoc; held until their audit */ \
X(CBM_DOCLINK_TS_LINK, CBM_LANG_TYPESCRIPT, "link", false, false, "MENTIONS") \ X(CBM_DOCLINK_TS_LINK, CBM_LANG_TYPESCRIPT, "link", false, false, "MENTIONS") \
X(CBM_DOCLINK_TS_LINKCODE, CBM_LANG_TYPESCRIPT, "linkcode", false, false, "MENTIONS") \ X(CBM_DOCLINK_TS_LINKCODE, CBM_LANG_TYPESCRIPT, "linkcode", false, false, "MENTIONS") \
X(CBM_DOCLINK_TS_SEE, CBM_LANG_TYPESCRIPT, "see", false, false, "MENTIONS") \ X(CBM_DOCLINK_TS_SEE, CBM_LANG_TYPESCRIPT, "see", false, false, "MENTIONS") \
/* php (doclink_php.c): PHPDoc @see and {@see}; held until their audit passes */ \ /* 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") \ 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 */ \ /* 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_REF, CBM_LANG_C, "doxygen_ref", false, false, "MENTIONS") \
X(CBM_DOCLINK_C_SEE, CBM_LANG_C, "see", 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 */ \ /* 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_LINK, CBM_LANG_MARKDOWN, "link", false, true, "MENTIONS") \
X(CBM_DOCLINK_MD_PATH, CBM_LANG_MARKDOWN, "path", false, true, "MENTIONS") \ X(CBM_DOCLINK_MD_PATH, CBM_LANG_MARKDOWN, "path", false, true, "MENTIONS") \
X(CBM_DOCLINK_MD_CODE_PATH, CBM_LANG_MARKDOWN, "code_path", false, true, "MENTIONS") \ X(CBM_DOCLINK_MD_CODE_PATH, CBM_LANG_MARKDOWN, "code_path", false, true, "MENTIONS") \
/* a code span naming a file or directory alone (`config.yaml`, `doc/`) */ \ /* a code span naming a file or directory alone (`config.yaml`, `doc/`) */ \
X(CBM_DOCLINK_MD_BARE_PATH, CBM_LANG_MARKDOWN, "bare_path", false, false, "MENTIONS") \ X(CBM_DOCLINK_MD_BARE_PATH, CBM_LANG_MARKDOWN, "bare_path", false, false, "MENTIONS") \
X(CBM_DOCLINK_MD_CODE_NAME, CBM_LANG_MARKDOWN, "code_name", false, true, "MENTIONS") \ X(CBM_DOCLINK_MD_CODE_NAME, CBM_LANG_MARKDOWN, "code_name", false, true, "MENTIONS") \
/* an ADR's own "supersedes" / "replaces" statement, opening its line or field */ \ /* an ADR's own "supersedes" / "replaces" statement, opening its line or field */ \
/* and naming a record directly (doc_adr.c): ADR -> ADR */ \ /* and naming a record directly (doc_adr.c): ADR -> ADR */ \
X(CBM_DOCLINK_MD_SUPERSEDES, CBM_LANG_MARKDOWN, "supersedes", false, true, "SUPERSEDES") \ X(CBM_DOCLINK_MD_SUPERSEDES, CBM_LANG_MARKDOWN, "supersedes", false, true, "SUPERSEDES") \
/* the same words in running prose ("this record supersedes ADR-3") */ \ /* the same words in running prose ("this record supersedes ADR-3") */ \
X(CBM_DOCLINK_MD_SUPERSEDES_PROSE, CBM_LANG_MARKDOWN, "supersedes_prose", false, false, \ X(CBM_DOCLINK_MD_SUPERSEDES_PROSE, CBM_LANG_MARKDOWN, "supersedes_prose", false, false, \
"SUPERSEDES") \ "SUPERSEDES") \
/* pdf (doc_pdf.c): structural mentions in a page's text; pdf_name (bare */ \ /* pdf (doc_pdf.c): structural mentions in a page's text; pdf_name (bare */ \
/* names) only decides which line fragments a cross-line join replaces */ \ /* names) only decides which line fragments a cross-line join replaces */ \
X(CBM_DOCLINK_PDF_PATH, CBM_LANG_PDF, "pdf_path", false, true, "MENTIONS") \ X(CBM_DOCLINK_PDF_PATH, CBM_LANG_PDF, "pdf_path", false, true, "MENTIONS") \
X(CBM_DOCLINK_PDF_FILE, CBM_LANG_PDF, "pdf_file", false, true, "MENTIONS") \ X(CBM_DOCLINK_PDF_FILE, CBM_LANG_PDF, "pdf_file", false, true, "MENTIONS") \
X(CBM_DOCLINK_PDF_QN, CBM_LANG_PDF, "pdf_qn", false, true, "MENTIONS") \ X(CBM_DOCLINK_PDF_QN, CBM_LANG_PDF, "pdf_qn", false, true, "MENTIONS") \
X(CBM_DOCLINK_PDF_NAME, CBM_LANG_PDF, "pdf_name", false, false, "MENTIONS") \ X(CBM_DOCLINK_PDF_NAME, CBM_LANG_PDF, "pdf_name", false, false, "MENTIONS") \
/* restructuredtext (doclink_rst.c): Sphinx domain markup, directives, inline literals */ \ /* restructuredtext (doclink_rst.c): Sphinx domain markup, directives, inline literals */ \
X(CBM_DOCLINK_RST_ROLE, CBM_LANG_RST, "role", false, true, "MENTIONS") \ X(CBM_DOCLINK_RST_ROLE, CBM_LANG_RST, "role", false, true, "MENTIONS") \
X(CBM_DOCLINK_RST_OBJECT, CBM_LANG_RST, "object", false, true, "MENTIONS") \ X(CBM_DOCLINK_RST_OBJECT, CBM_LANG_RST, "object", false, true, "MENTIONS") \
X(CBM_DOCLINK_RST_AUTODOC, CBM_LANG_RST, "autodoc", false, true, "MENTIONS") \ X(CBM_DOCLINK_RST_AUTODOC, CBM_LANG_RST, "autodoc", false, true, "MENTIONS") \
X(CBM_DOCLINK_RST_INCLUDE, CBM_LANG_RST, "include", false, false, "MENTIONS") \ X(CBM_DOCLINK_RST_INCLUDE, CBM_LANG_RST, "include", false, false, "MENTIONS") \
X(CBM_DOCLINK_RST_LITERALINCLUDE, CBM_LANG_RST, "literalinclude", false, true, "MENTIONS") \ X(CBM_DOCLINK_RST_LITERALINCLUDE, CBM_LANG_RST, "literalinclude", false, true, "MENTIONS") \
X(CBM_DOCLINK_RST_KERNEL_DOC, CBM_LANG_RST, "kernel_doc", false, false, "MENTIONS") \ X(CBM_DOCLINK_RST_KERNEL_DOC, CBM_LANG_RST, "kernel_doc", false, false, "MENTIONS") \
X(CBM_DOCLINK_RST_CODE_PATH, CBM_LANG_RST, "code_path", false, true, "MENTIONS") \ X(CBM_DOCLINK_RST_CODE_PATH, CBM_LANG_RST, "code_path", false, true, "MENTIONS") \
X(CBM_DOCLINK_RST_CODE_NAME, CBM_LANG_RST, "code_name", false, true, "MENTIONS") \ X(CBM_DOCLINK_RST_CODE_NAME, CBM_LANG_RST, "code_name", false, true, "MENTIONS") \
/* asciidoc (doc_adoc.c): includes, Antora API attributes, monospace spans */ \ /* asciidoc (doc_adoc.c): includes, Antora API attributes, monospace spans */ \
X(CBM_DOCLINK_ADOC_INCLUDE, CBM_LANG_ASCIIDOC, "include", false, true, "MENTIONS") \ X(CBM_DOCLINK_ADOC_INCLUDE, CBM_LANG_ASCIIDOC, "include", false, true, "MENTIONS") \
X(CBM_DOCLINK_ADOC_ATTRIBUTE, CBM_LANG_ASCIIDOC, "attribute", false, false, "MENTIONS") \ X(CBM_DOCLINK_ADOC_ATTRIBUTE, CBM_LANG_ASCIIDOC, "attribute", false, false, "MENTIONS") \
X(CBM_DOCLINK_ADOC_CODE_PATH, CBM_LANG_ASCIIDOC, "code_path", false, false, "MENTIONS") \ X(CBM_DOCLINK_ADOC_CODE_PATH, CBM_LANG_ASCIIDOC, "code_path", false, false, "MENTIONS") \
X(CBM_DOCLINK_ADOC_CODE_NAME, CBM_LANG_ASCIIDOC, "code_name", false, false, "MENTIONS") \ X(CBM_DOCLINK_ADOC_CODE_NAME, CBM_LANG_ASCIIDOC, "code_name", false, false, "MENTIONS") \
/* any language (CBM_LANG_COUNT): a URL is never an edge */ \ /* diagrams (diagram.c): an element's explicit link (or a PlantUML !include), */ \
/* read as a Markdown link; held until their held-out audit passes */ \
X(CBM_DOCLINK_DG_MERMAID_CLICK, CBM_LANG_MERMAID, "mermaid_click", false, false, "MENTIONS") \
X(CBM_DOCLINK_DG_PLANTUML_URL, CBM_LANG_DIAGRAM, "plantuml_url", false, false, "MENTIONS") \
X(CBM_DOCLINK_DG_PLANTUML_INCLUDE, CBM_LANG_DIAGRAM, "plantuml_include", false, false, \
"MENTIONS") \
X(CBM_DOCLINK_DG_DRAWIO_LINK, CBM_LANG_DIAGRAM, "drawio_link", false, false, "MENTIONS") \
X(CBM_DOCLINK_DG_EXCALIDRAW_LINK, CBM_LANG_DIAGRAM, "excalidraw_link", false, false, \
"MENTIONS") \
X(CBM_DOCLINK_DG_DOT_URL, CBM_LANG_DIAGRAM, "dot_url", false, false, "MENTIONS") \
X(CBM_DOCLINK_DG_D2_LINK, CBM_LANG_DIAGRAM, "d2_link", false, false, "MENTIONS") \
/* openapi (openapi.c): an operation's operationId, read as a qualified code */ \
/* name; held until its held-out audit passes */ \
X(CBM_DOCLINK_OPENAPI_OPERATION_ID, CBM_LANG_OPENAPI, "operation_id", false, false, \
"MENTIONS") \
/* any language (CBM_LANG_COUNT): a URL is never an edge */ \
X(CBM_DOCLINK_HREF, CBM_LANG_COUNT, "href", true, false, "MENTIONS") X(CBM_DOCLINK_HREF, CBM_LANG_COUNT, "href", true, false, "MENTIONS")
typedef enum { typedef enum {
@@ -159,6 +174,25 @@ typedef enum {
CBM_DOCLINK_SYNTAX_COUNT CBM_DOCLINK_SYNTAX_COUNT
} CBMDocLinkSyntax; } CBMDocLinkSyntax;
/* The Markdown family a reference of another document reads as, wherever it
* is written (CBM_DOCLINK_NONE for none): a diagram's explicit link
* (diagram.c) is a link, an OpenAPI operationId (openapi.c) a code name. */
static inline int cbm_doclink_md_twin(int syntax) {
if (syntax >= CBM_DOCLINK_DG_MERMAID_CLICK && syntax <= CBM_DOCLINK_DG_D2_LINK) {
return CBM_DOCLINK_MD_LINK;
}
return syntax == CBM_DOCLINK_OPENAPI_OPERATION_ID ? CBM_DOCLINK_MD_CODE_NAME : CBM_DOCLINK_NONE;
}
/* What an edge of these families says it came through: the kind of source,
* whichever document holds it (NULL: the resolver's own name). */
static inline const char *cbm_doclink_syntax_via(int syntax) {
if (syntax >= CBM_DOCLINK_DG_MERMAID_CLICK && syntax <= CBM_DOCLINK_DG_D2_LINK) {
return "diagram";
}
return syntax == CBM_DOCLINK_OPENAPI_OPERATION_ID ? "openapi" : NULL;
}
/* CBMDocLink.flags. */ /* CBMDocLink.flags. */
enum { enum {
/* The reference is written in the FILE's own doc (CBMFileResult.module_doc: /* The reference is written in the FILE's own doc (CBMFileResult.module_doc:
+11
View File
@@ -2713,6 +2713,14 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
empty_types, NULL, NULL, tree_sitter_objectscript_routine, empty_types, NULL, NULL, tree_sitter_objectscript_routine,
NULL}, NULL},
// CBM_LANG_OPENAPI — openapi.json / swagger.json: the JSON grammar without
// JSON's node per key (thousands in a generated spec); cbm_extract_openapi
// (openapi.c) reads the operations from the tree.
[CBM_LANG_OPENAPI] = {CBM_LANG_OPENAPI, empty_types, empty_types, empty_types,
json_module_types, empty_types, empty_types, empty_types, empty_types,
empty_types, empty_types, empty_types, NULL, empty_types, NULL, NULL,
tree_sitter_json, NULL},
// CBM_LANG_PDF — PDF documents. No grammar row: extraction reads the text // CBM_LANG_PDF — PDF documents. No grammar row: extraction reads the text
// layer itself (cbm_pdf_extract_document, doc_pdf.c) before any grammar is // layer itself (cbm_pdf_extract_document, doc_pdf.c) before any grammar is
// looked up, so this language never reaches cbm_lang_spec()/cbm_ts_language(). // looked up, so this language never reaches cbm_lang_spec()/cbm_ts_language().
@@ -2721,6 +2729,9 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
// CBM_LANG_ASCIIDOC — AsciiDoc documents. No grammar row, as PDF: extraction // CBM_LANG_ASCIIDOC — AsciiDoc documents. No grammar row, as PDF: extraction
// reads the file itself (cbm_adoc_extract_document, doc_adoc.c). Zero spec. // reads the file itself (cbm_adoc_extract_document, doc_adoc.c). Zero spec.
// CBM_LANG_DIAGRAM — diagram files. No grammar row, as PDF: extraction reads
// the file itself (cbm_extract_diagram_file, diagram.c). Zero spec.
// CBM_LANG_OBJECTSCRIPT_EXPORT — Studio Export XML. No grammar row: the // CBM_LANG_OBJECTSCRIPT_EXPORT — Studio Export XML. No grammar row: the
// pipeline transcodes Export XML to UDL (iris_export_xml.c) and re-extracts // pipeline transcodes Export XML to UDL (iris_export_xml.c) and re-extracts
// each class as CBM_LANG_OBJECTSCRIPT_UDL, so this language never reaches // each class as CBM_LANG_OBJECTSCRIPT_UDL, so this language never reaches
+239
View File
@@ -0,0 +1,239 @@
/*
* openapi.c -- OpenAPI and Swagger documents: each operation a Route, named by
* the same QN as the route its handler registers in code (route_path.c), and
* its `operationId` a reference to that handler.
*
* A Route is a definition of the document (its file DEFINES it). The
* operationId is a doc-link record (CBM_DOCLINK_OPENAPI_OPERATION_ID) the
* pipeline resolves as a qualified code name: Connexion's dotted paths
* (`app.api.users.get_user`, or `x-openapi-router-controller` plus the id)
* name one function; a bare id (`getUser`) names none for sure and stays
* unresolved.
*
* The documents: `openapi.json` / `swagger.json` (CBM_LANG_OPENAPI, read with
* the JSON grammar but no node per key), and any YAML or JSON file whose root
* says `openapi:` or `swagger:` and has `paths`.
*/
#include "cbm.h"
#include "doclink.h"
#include "arena.h"
#include "foundation/constants.h"
#include "tree_sitter/api.h"
#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
enum {
OA_SCALAR_MAX = 1024, /* a key, a path, an operationId */
OA_UNWRAP_MAX = 8, /* stream > document > block_node > mapping */
OA_METHOD_MAX = 8,
};
static bool oa_is(TSNode n, const char *type) {
return !ts_node_is_null(n) && strcmp(ts_node_type(n), type) == 0;
}
/* The mapping a node holds (a YAML block or flow mapping, a JSON object),
* through the wrappers around it; a null node for none. */
static TSNode oa_mapping(TSNode n) {
for (int k = 0; k < OA_UNWRAP_MAX && !ts_node_is_null(n); k++) {
if (oa_is(n, "block_mapping") || oa_is(n, "flow_mapping") || oa_is(n, "object")) {
return n;
}
if (!oa_is(n, "stream") && !oa_is(n, "document") && !oa_is(n, "block_node") &&
!oa_is(n, "flow_node")) {
break;
}
TSNode next = {0};
uint32_t nc = ts_node_named_child_count(n);
for (uint32_t i = 0; i < nc && ts_node_is_null(next); i++) {
TSNode c = ts_node_named_child(n, i);
if (!oa_is(c, "comment") && !oa_is(c, "anchor") && !oa_is(c, "tag")) {
next = c;
}
}
n = next;
}
return (TSNode){0};
}
static bool oa_pair(TSNode n) {
return oa_is(n, "block_mapping_pair") || oa_is(n, "flow_pair") || oa_is(n, "pair");
}
/* A scalar's text (quotes dropped) into out: its length, 0 for none. */
static size_t oa_scalar(const CBMExtractCtx *ctx, TSNode n, char *out, size_t cap) {
for (int k = 0; k < OA_UNWRAP_MAX && (oa_is(n, "flow_node") || oa_is(n, "block_node")); k++) {
n = ts_node_named_child(n, 0);
}
if (ts_node_is_null(n) || !ts_node_is_null(oa_mapping(n)) || oa_is(n, "block_sequence") ||
oa_is(n, "flow_sequence") || oa_is(n, "array")) {
out[0] = '\0';
return 0;
}
uint32_t b = ts_node_start_byte(n);
uint32_t e = ts_node_end_byte(n);
const char *s = ctx->source + b;
size_t len = e - b;
while (len && isspace((unsigned char)s[len - SKIP_ONE])) {
len--;
}
if (len >= PAIR_LEN && (s[0] == '"' || s[0] == '\'') && s[len - SKIP_ONE] == s[0]) {
s++;
len -= PAIR_LEN;
}
if (len >= cap) {
out[0] = '\0';
return 0; /* longer than any key or reference read here */
}
memcpy(out, s, len);
out[len] = '\0';
return len;
}
/* The value of `key` in a mapping; a null node for none. */
static TSNode oa_get(const CBMExtractCtx *ctx, TSNode map, const char *key) {
char k[OA_SCALAR_MAX];
uint32_t nc = ts_node_is_null(map) ? 0 : ts_node_named_child_count(map);
for (uint32_t i = 0; i < nc; i++) {
TSNode p = ts_node_named_child(map, i);
if (oa_pair(p) &&
oa_scalar(ctx, ts_node_child_by_field_name(p, "key", CBM_SZ_3), k, sizeof(k)) &&
strcmp(k, key) == 0) {
return ts_node_child_by_field_name(p, "value", CBM_SZ_5);
}
}
return (TSNode){0};
}
static const char *oa_dup_scalar(const CBMExtractCtx *ctx, TSNode n) {
char v[OA_SCALAR_MAX];
size_t len = oa_scalar(ctx, n, v, sizeof(v));
return len ? cbm_arena_strndup(ctx->arena, v, len) : NULL;
}
/* Connexion's controller: on the operation, else its path. */
static const char *oa_controller(const CBMExtractCtx *ctx, TSNode op, TSNode path_item) {
static const char *const keys[] = {"x-openapi-router-controller", "x-swagger-router-controller",
NULL};
for (int k = 0; keys[k]; k++) {
const char *c = oa_dup_scalar(ctx, oa_get(ctx, op, keys[k]));
c = c ? c : oa_dup_scalar(ctx, oa_get(ctx, path_item, keys[k]));
if (c) {
return c;
}
}
return NULL;
}
/* One operation: its Route, and its operationId's reference. */
static void oa_operation(CBMExtractCtx *ctx, const char *path, const char *method, TSNode op,
TSNode path_item, TSNode pair) {
CBMArena *a = ctx->arena;
char upper[OA_METHOD_MAX];
size_t ml = strlen(method);
for (size_t i = 0; i < ml && i + SKIP_ONE < sizeof(upper); i++) {
upper[i] = (char)toupper((unsigned char)method[i]);
upper[i + SKIP_ONE] = '\0';
}
size_t pl = strlen(path);
char *canon = (char *)cbm_arena_alloc(a, pl + SKIP_ONE);
const char *qn = canon ? cbm_arena_sprintf(a, "__route__%s__%s", upper,
cbm_route_canon_path(path, canon, pl + SKIP_ONE))
: NULL;
const char *op_id = oa_dup_scalar(ctx, oa_get(ctx, op, "operationId"));
const char **kv = (const char **)cbm_arena_alloc(a, CBM_SZ_7 * sizeof(char *));
const char *m = cbm_arena_strdup(a, upper);
if (!qn || !kv || !m) {
return;
}
kv[0] = "method";
kv[SKIP_ONE] = m;
kv[PAIR_LEN] = "source";
kv[CBM_SZ_3] = "openapi";
kv[CBM_SZ_4] = op_id ? "operation_id" : NULL;
kv[CBM_SZ_5] = op_id;
kv[CBM_SZ_6] = NULL;
CBMDefinition def;
memset(&def, 0, sizeof(def));
def.name = cbm_arena_strdup(a, path);
def.qualified_name = qn;
def.label = "Route";
def.file_path = ctx->rel_path;
def.start_line = ts_node_start_point(pair).row + SKIP_ONE;
def.end_line = ts_node_end_point(pair).row + SKIP_ONE;
def.extra_props = kv;
if (!def.name) {
return;
}
cbm_defs_push(&ctx->result->defs, a, def);
if (!op_id) {
return;
}
const char *controller = oa_controller(ctx, op, path_item);
CBMDocLink link = {
.source_qn = qn,
.raw = controller ? cbm_arena_sprintf(a, "%s.%s", controller, op_id) : op_id,
.line = def.start_line,
.def_line = def.start_line,
.syntax = CBM_DOCLINK_OPENAPI_OPERATION_ID,
};
if (link.raw) {
cbm_doclinks_push(&ctx->result->doc_links, a, link);
}
}
static bool oa_method(const char *k) {
static const char *const methods[] = {"get", "put", "post", "delete", "options",
"head", "patch", "trace", NULL};
for (int i = 0; methods[i]; i++) {
if (strcmp(k, methods[i]) == 0) {
return true;
}
}
return false;
}
void cbm_extract_openapi(CBMExtractCtx *ctx) {
if (!ctx || !ctx->result || !ctx->source || ts_node_is_null(ctx->root) ||
(ctx->language != CBM_LANG_OPENAPI && ctx->language != CBM_LANG_YAML &&
ctx->language != CBM_LANG_JSON)) {
return;
}
TSNode root = oa_mapping(ctx->root);
if (ts_node_is_null(oa_get(ctx, root, "openapi")) &&
ts_node_is_null(oa_get(ctx, root, "swagger"))) {
return; /* no API description */
}
TSNode paths = oa_mapping(oa_get(ctx, root, "paths"));
char path[OA_SCALAR_MAX];
char key[OA_SCALAR_MAX];
uint32_t np = ts_node_is_null(paths) ? 0 : ts_node_named_child_count(paths);
for (uint32_t i = 0; i < np; i++) {
TSNode pp = ts_node_named_child(paths, i);
if (!oa_pair(pp) ||
!oa_scalar(ctx, ts_node_child_by_field_name(pp, "key", CBM_SZ_3), path, sizeof(path)) ||
path[0] != '/') {
continue;
}
TSNode item = oa_mapping(ts_node_child_by_field_name(pp, "value", CBM_SZ_5));
uint32_t nm = ts_node_is_null(item) ? 0 : ts_node_named_child_count(item);
for (uint32_t j = 0; j < nm; j++) {
TSNode mp = ts_node_named_child(item, j);
if (!oa_pair(mp) ||
!oa_scalar(ctx, ts_node_child_by_field_name(mp, "key", CBM_SZ_3), key,
sizeof(key)) ||
!oa_method(key)) {
continue;
}
TSNode op = oa_mapping(ts_node_child_by_field_name(mp, "value", CBM_SZ_5));
if (!ts_node_is_null(op)) {
oa_operation(ctx, path, key, op, item, mp);
}
}
}
}
+9
View File
@@ -375,6 +375,14 @@ static void cr_walk(cr_ctx_t *c, CBMFileResult *r) {
cr_str(c, &r->infra_bindings.items[i].target_url); cr_str(c, &r->infra_bindings.items[i].target_url);
cr_str(c, &r->infra_bindings.items[i].broker); cr_str(c, &r->infra_bindings.items[i].broker);
} }
cr_array(c, (void **)&r->diagram_arrows.items, r->diagram_arrows.count,
sizeof(CBMDiagramArrow));
for (int i = 0; i < r->diagram_arrows.count && r->diagram_arrows.items; i++) {
cr_str(c, &r->diagram_arrows.items[i].from_qn);
cr_str(c, &r->diagram_arrows.items[i].to_qn);
cr_str(c, &r->diagram_arrows.items[i].label);
cr_str(c, &r->diagram_arrows.items[i].style);
}
cr_array(c, (void **)&r->channels.items, r->channels.count, sizeof(CBMChannel)); cr_array(c, (void **)&r->channels.items, r->channels.count, sizeof(CBMChannel));
for (int i = 0; i < r->channels.count && r->channels.items; i++) { for (int i = 0; i < r->channels.count && r->channels.items; i++) {
cr_str(c, &r->channels.items[i].channel_name); cr_str(c, &r->channels.items[i].channel_name);
@@ -540,6 +548,7 @@ void cbm_result_compact(CBMFileResult *result) {
tmp.string_refs.cap = tmp.string_refs.count; tmp.string_refs.cap = tmp.string_refs.count;
tmp.infra_bindings.cap = tmp.infra_bindings.count; tmp.infra_bindings.cap = tmp.infra_bindings.count;
tmp.channels.cap = tmp.channels.count; tmp.channels.cap = tmp.channels.count;
tmp.diagram_arrows.cap = tmp.diagram_arrows.count;
tmp.field_types.cap = tmp.field_types.count; tmp.field_types.cap = tmp.field_types.count;
tmp.doc_links.cap = tmp.doc_links.count; tmp.doc_links.cap = tmp.doc_links.count;
+98
View File
@@ -0,0 +1,98 @@
/*
* route_path.c -- one Route QN per endpoint, whoever names it: a client call
* site, a handler's decorator or an OpenAPI operation (moved here from the
* route pass so extraction can name a Route as the pipeline does).
*/
#include "cbm.h"
#include <stdbool.h>
#include <stddef.h>
/* True for characters that may appear in a ":name" route parameter. */
static inline bool is_route_ident_char(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}
/* Canonicalize route-path parameter placeholders to a single "{}" token so that
* client call sites and server handlers rendezvous on the same Route QN
* regardless of framework syntax. Each parameter token collapses to "{}":
*
* :name Express / React-Router / Rails / typical JS API clients
* {name} Axum / Spring / OpenAPI / ASP.NET
* <name> Flask / Rocket (incl. typed "<int:id>")
* ${...} JS template interpolation captured into the path
*
* Parameter names are intentionally discarded so the same logical endpoint
* matches across services that name the path variable differently. Static path
* text is copied verbatim; the result never exceeds the input length. */
const char *cbm_route_canon_path(const char *in, char *out, size_t out_sz) {
if (out == NULL || out_sz == 0) {
/* WHY: cppcheck value-flow follows a caller that passes an as-yet
* uninitialized output buffer (e.g. `char cpath[256];`) and flags this
* `return out` as uninitvar. False positive: we only return the pointer
* here and never read the buffer; returning it (NULL, or a buffer too
* small to write into) is the intended early-out contract. */
// cppcheck-suppress uninitvar
return out;
}
if (in == NULL) {
out[0] = '\0';
return out;
}
const size_t last = out_sz - 1;
size_t oi = 0;
size_t i = 0;
while (in[i] != '\0' && oi < last) {
char c = in[i];
bool at_seg_start = (oi == 0) || (out[oi - 1] == '/');
bool is_param = false;
if (c == ':' && at_seg_start && is_route_ident_char(in[i + 1])) {
i++;
while (in[i] != '\0' && is_route_ident_char(in[i])) {
i++;
}
is_param = true;
} else if (c == '{') {
i++;
while (in[i] != '\0' && in[i] != '}' && in[i] != '/') {
i++;
}
if (in[i] == '}') {
i++;
}
is_param = true;
} else if (c == '<') {
i++;
while (in[i] != '\0' && in[i] != '>' && in[i] != '/') {
i++;
}
if (in[i] == '>') {
i++;
}
is_param = true;
} else if (c == '$' && in[i + 1] == '{') {
i += 2;
while (in[i] != '\0' && in[i] != '}' && in[i] != '/') {
i++;
}
if (in[i] == '}') {
i++;
}
is_param = true;
}
if (is_param) {
if (oi + 2 > last) {
break;
}
out[oi++] = '{';
out[oi++] = '}';
continue;
}
out[oi++] = c;
i++;
}
out[oi] = '\0';
return out;
}
+8
View File
@@ -83,6 +83,9 @@ static const char *ALWAYS_IGNORED_SUFFIXES[] = {
".webp", ".svg", ".wasm", ".node", ".exe", ".bin", ".dat", ".db", ".webp", ".svg", ".wasm", ".node", ".exe", ".bin", ".dat", ".db",
".sqlite", ".sqlite3", ".woff", ".woff2", ".ttf", ".eot", ".otf", NULL}; ".sqlite", ".sqlite3", ".woff", ".woff2", ".ttf", ".eot", ".otf", NULL};
/* SVG files that carry a diagram (diagram.c): read, not ignored as images. */
static const char *DIAGRAM_SVG_SUFFIXES[] = {".drawio.svg", ".excalidraw.svg", NULL};
static const char *FAST_IGNORED_SUFFIXES[] = { static const char *FAST_IGNORED_SUFFIXES[] = {
".zip", ".tar", ".gz", ".bz2", ".xz", ".rar", ".7z", ".jar", ".zip", ".tar", ".gz", ".bz2", ".xz", ".rar", ".7z", ".jar",
".war", ".ear", ".mp3", ".mp4", ".avi", ".mov", ".wav", ".flac", ".war", ".ear", ".mp3", ".mp4", ".avi", ".mov", ".wav", ".flac",
@@ -381,6 +384,11 @@ bool cbm_has_ignored_suffix(const char *filename, cbm_index_mode_t mode) {
} }
size_t filename_length = strlen(filename); size_t filename_length = strlen(filename);
for (int i = 0; DIAGRAM_SVG_SUFFIXES[i]; i++) {
if (ends_with(filename, filename_length, DIAGRAM_SVG_SUFFIXES[i])) {
return false;
}
}
for (int i = 0; ALWAYS_IGNORED_SUFFIXES[i]; i++) { for (int i = 0; ALWAYS_IGNORED_SUFFIXES[i]; i++) {
if (ends_with(filename, filename_length, ALWAYS_IGNORED_SUFFIXES[i])) { if (ends_with(filename, filename_length, ALWAYS_IGNORED_SUFFIXES[i])) {
return true; return true;
+25 -10
View File
@@ -621,6 +621,17 @@ static const ext_entry_t EXT_TABLE[] = {
{".pdf", CBM_LANG_PDF}, {".pdf", CBM_LANG_PDF},
/* AsciiDoc; `.asc` is left out: it is as often an ASCII-armored signature */ /* AsciiDoc; `.asc` is left out: it is as often an ASCII-armored signature */
{".adoc", CBM_LANG_ASCIIDOC}, {".adoc", CBM_LANG_ASCIIDOC},
/* Diagram files, read without a grammar (diagram.c) */
{".puml", CBM_LANG_DIAGRAM},
{".plantuml", CBM_LANG_DIAGRAM},
{".iuml", CBM_LANG_DIAGRAM},
{".wsd", CBM_LANG_DIAGRAM},
{".dot", CBM_LANG_DIAGRAM},
{".gv", CBM_LANG_DIAGRAM},
{".d2", CBM_LANG_DIAGRAM},
{".drawio", CBM_LANG_DIAGRAM},
{".dio", CBM_LANG_DIAGRAM},
{".excalidraw", CBM_LANG_DIAGRAM},
{".asciidoc", CBM_LANG_ASCIIDOC}, {".asciidoc", CBM_LANG_ASCIIDOC},
/* Slang */ /* Slang */
@@ -712,6 +723,8 @@ typedef struct {
static const filename_entry_t FILENAME_TABLE[] = { static const filename_entry_t FILENAME_TABLE[] = {
{"CMakeLists.txt", CBM_LANG_CMAKE}, {"CMakeLists.txt", CBM_LANG_CMAKE},
{"Dockerfile", CBM_LANG_DOCKERFILE}, {"Dockerfile", CBM_LANG_DOCKERFILE},
{"openapi.json", CBM_LANG_OPENAPI}, /* API descriptions: their operations, not each key */
{"swagger.json", CBM_LANG_OPENAPI},
{"GNUmakefile", CBM_LANG_MAKEFILE}, {"GNUmakefile", CBM_LANG_MAKEFILE},
{"Makefile", CBM_LANG_MAKEFILE}, {"Makefile", CBM_LANG_MAKEFILE},
{"makefile", CBM_LANG_MAKEFILE}, {"makefile", CBM_LANG_MAKEFILE},
@@ -835,6 +848,8 @@ static const char *LANG_NAMES[CBM_LANG_COUNT] = {
[CBM_LANG_CHIALISP] = "Chialisp", [CBM_LANG_CHIALISP] = "Chialisp",
[CBM_LANG_PDF] = "PDF", [CBM_LANG_PDF] = "PDF",
[CBM_LANG_ASCIIDOC] = "AsciiDoc", [CBM_LANG_ASCIIDOC] = "AsciiDoc",
[CBM_LANG_DIAGRAM] = "Diagram",
[CBM_LANG_OPENAPI] = "OpenAPI",
[CBM_LANG_FENNEL] = "Fennel", [CBM_LANG_FENNEL] = "Fennel",
[CBM_LANG_FISH] = "Fish", [CBM_LANG_FISH] = "Fish",
[CBM_LANG_AWK] = "AWK", [CBM_LANG_AWK] = "AWK",
@@ -993,6 +1008,9 @@ static CBMLanguage lang_for_filename(const cbm_userconfig_t *ucfg, const char *f
CBMLanguage lang; CBMLanguage lang;
} COMPOUND_EXT_TABLE[] = { } COMPOUND_EXT_TABLE[] = {
{".blade.php", CBM_LANG_BLADE}, {".blade.php", CBM_LANG_BLADE},
{".drawio.svg", CBM_LANG_DIAGRAM}, /* draw.io's editable SVG: its diagram, not XML */
{".excalidraw.svg", CBM_LANG_DIAGRAM}, /* an Excalidraw export with its scene */
{".excalidraw.json", CBM_LANG_DIAGRAM},
}; };
const char *p = strchr(filename, '.'); const char *p = strchr(filename, '.');
while (p && p < last_dot) { while (p && p < last_dot) {
@@ -1528,16 +1546,13 @@ CBMLanguage cbm_disambiguate_cfc(const char *path) {
/* JSON files that are configuration, not source worth indexing. */ /* JSON files that are configuration, not source worth indexing. */
static const char *const IGNORED_JSON_FILES[] = { static const char *const IGNORED_JSON_FILES[] = {
"package.json", "package-lock.json", "tsconfig.json", "package.json", "package-lock.json", "tsconfig.json", "jsconfig.json",
"jsconfig.json", "composer.json", "composer.lock", "composer.json", "composer.lock", "yarn.lock", "jest.config.json",
"yarn.lock", "openapi.json", "swagger.json", ".eslintrc.json", ".prettierrc.json", ".babelrc.json", "tslint.json",
"jest.config.json", ".eslintrc.json", ".prettierrc.json", "angular.json", "firebase.json", "renovate.json", "lerna.json",
".babelrc.json", "tslint.json", "angular.json", "turbo.json", ".stylelintrc.json", "pnpm-lock.json", "deno.json",
"firebase.json", "renovate.json", "lerna.json", "biome.json", "devcontainer.json", ".devcontainer.json", "launch.json",
"turbo.json", ".stylelintrc.json", "pnpm-lock.json", "settings.json", "extensions.json", "tasks.json", NULL};
"deno.json", "biome.json", "devcontainer.json",
".devcontainer.json", "launch.json", "settings.json",
"extensions.json", "tasks.json", NULL};
static bool lang_name_in(const char *name, const char *const *list) { static bool lang_name_in(const char *name, const char *const *list) {
for (int i = 0; list[i]; i++) { for (int i = 0; list[i]; i++) {
+2
View File
@@ -162,6 +162,8 @@ static const lang_name_entry_t LANG_NAME_TABLE[] = {
{"chialisp", CBM_LANG_CHIALISP}, {"chialisp", CBM_LANG_CHIALISP},
{"pdf", CBM_LANG_PDF}, {"pdf", CBM_LANG_PDF},
{"asciidoc", CBM_LANG_ASCIIDOC}, {"asciidoc", CBM_LANG_ASCIIDOC},
{"diagram", CBM_LANG_DIAGRAM},
{"openapi", CBM_LANG_OPENAPI},
{"lean", CBM_LANG_LEAN}, {"lean", CBM_LANG_LEAN},
{"form", CBM_LANG_FORM}, {"form", CBM_LANG_FORM},
{"magma", CBM_LANG_MAGMA}, {"magma", CBM_LANG_MAGMA},
+12 -8
View File
@@ -512,14 +512,18 @@ void cbm_doclinks_resolve_file(cbm_doclinks_t *dl, int file_idx, const CBMFileRe
} }
if (cbm_doclink_syntax_ships(link->syntax)) { if (cbm_doclink_syntax_ships(link->syntax)) {
atomic_fetch_add_explicit(&dl->mentions, 1, memory_order_relaxed); atomic_fetch_add_explicit(&dl->mentions, 1, memory_order_relaxed);
mentions[nm++] = (doclink_mention_t){.src = src->id, mentions[nm++] =
.tgt = out.target->id, (doclink_mention_t){.src = src->id,
.line = link->line, .tgt = out.target->id,
.target_first = out.target_first, .line = link->line,
.target_last = out.target_last, .target_first = out.target_first,
.via = R->via ? R->via : "doc_comment", .target_last = out.target_last,
.syntax = link->syntax, .via = cbm_doclink_syntax_via(link->syntax)
.exact = out.exact}; ? cbm_doclink_syntax_via(link->syntax)
: R->via ? R->via
: "doc_comment",
.syntax = link->syntax,
.exact = out.exact};
continue; continue;
} }
/* the ship gate: resolved, but this link family is below the /* the ship gate: resolved, but this link family is below the
+1 -1
View File
@@ -178,7 +178,7 @@ typedef enum {
* file by file */ * file by file */
} cbm_doclink_delta_t; } cbm_doclink_delta_t;
enum { CBM_DOCLINK_RESOLVER_LANGS = 4 }; enum { CBM_DOCLINK_RESOLVER_LANGS = 6 };
/* Everything the resolving half needs from a language leg: one of these, and /* Everything the resolving half needs from a language leg: one of these, and
* its pointer in doc_links.c's resolver table. */ * its pointer in doc_links.c's resolver table. */
+5
View File
@@ -887,6 +887,11 @@ static void ra_resolve(const void *index, void *state, int run_file, const CBMDo
ra_unresolved(out, CBM_DOCLINK_REASON_UNPARSEABLE); ra_unresolved(out, CBM_DOCLINK_REASON_UNPARSEABLE);
return; return;
} }
if (cbm_doclink_md_twin(link->syntax) != CBM_DOCLINK_NONE) { /* a diagram's link, an
* API's operationId */
cbm_doclink_md_resolver.resolve(x->md, NULL, run_file, link, graph, out);
return;
}
if (link->syntax == CBM_DOCLINK_ADOC_CODE_PATH || link->syntax == CBM_DOCLINK_ADOC_CODE_NAME) { if (link->syntax == CBM_DOCLINK_ADOC_CODE_PATH || link->syntax == CBM_DOCLINK_ADOC_CODE_NAME) {
CBMDocLink md = *link; CBMDocLink md = *link;
md.syntax = link->syntax == CBM_DOCLINK_ADOC_CODE_PATH ? CBM_DOCLINK_MD_CODE_PATH md.syntax = link->syntax == CBM_DOCLINK_ADOC_CODE_PATH ? CBM_DOCLINK_MD_CODE_PATH
+43 -5
View File
@@ -110,8 +110,9 @@ static const char *mdr_base(const char *path) {
static bool mdr_code_file(const char *path) { static bool mdr_code_file(const char *path) {
CBMLanguage lang = cbm_language_for_filename(mdr_base(path)); CBMLanguage lang = cbm_language_for_filename(mdr_base(path));
return lang != CBM_LANG_COUNT && lang != CBM_LANG_MARKDOWN && lang != CBM_LANG_RST && return lang != CBM_LANG_COUNT && lang != CBM_LANG_MARKDOWN && lang != CBM_LANG_RST &&
lang != CBM_LANG_ASCIIDOC && lang != CBM_LANG_PDF && lang != CBM_LANG_HTML && lang != CBM_LANG_ASCIIDOC && lang != CBM_LANG_PDF && lang != CBM_LANG_DIAGRAM &&
lang != CBM_LANG_CSS && lang != CBM_LANG_SCSS && !cbm_has_config_extension(path); lang != CBM_LANG_OPENAPI && lang != CBM_LANG_HTML && lang != CBM_LANG_CSS &&
lang != CBM_LANG_SCSS && !cbm_has_config_extension(path);
} }
/* The stems of a package directory's index file. */ /* The stems of a package directory's index file. */
@@ -660,6 +661,35 @@ static void mdr_edge(cbm_doclink_outcome_t *out, const cbm_gbuf_node_t *target,
out->target_last = last; out->target_last = last;
} }
/* A bare OpenAPI operationId (`getUser`): the one function or method of the
* repository with exactly that name, outside the tests; several are
* ambiguous, none is missing. (A dotted one, Connexion's, reads as a
* qualified code name.) */
static void mdr_resolve_unique_callable(const cbm_gbuf_t *graph, const char *name,
cbm_doclink_outcome_t *out) {
const cbm_gbuf_node_t **nodes = NULL;
int n = 0;
const cbm_gbuf_node_t *hit = NULL;
int hits = 0;
if (cbm_gbuf_find_by_name(graph, name, &nodes, &n) == 0) {
for (int i = 0; i < n; i++) {
const char *l = nodes[i]->label;
if (l && (strcmp(l, "Function") == 0 || strcmp(l, "Method") == 0) &&
!(nodes[i]->file_path && mdr_test_path(nodes[i]->file_path))) {
hit = nodes[i];
hits++;
}
}
}
if (hits == 1) {
mdr_edge(out, hit, 0, 0);
out->exact = false; /* unique by name, not named in full */
return;
}
out->kind = CBM_DOCLINK_UNRESOLVED;
out->reason = hits > 1 ? CBM_DOCLINK_REASON_AMBIGUOUS : CBM_DOCLINK_REASON_MISSING;
}
enum { MDR_MAX_PIECES = 80 }; /* a code span is at most 150 bytes */ enum { MDR_MAX_PIECES = 80 }; /* a code span is at most 150 bytes */
typedef struct { typedef struct {
@@ -1286,7 +1316,13 @@ static void mdr_resolve(const void *index, void *state, int run_file, const CBMD
} }
char buf[MDR_PATH_CAP]; char buf[MDR_PATH_CAP];
mdr_ref_t r = {0}; mdr_ref_t r = {0};
switch (link->syntax) { if (link->syntax == CBM_DOCLINK_OPENAPI_OPERATION_ID && !strchr(link->raw, '.')) {
mdr_resolve_unique_callable(graph, link->raw, out);
return;
}
/* a diagram's link reads as a link, an operationId as a code name */
int twin = cbm_doclink_md_twin(link->syntax);
switch (twin != CBM_DOCLINK_NONE ? twin : link->syntax) {
case CBM_DOCLINK_MD_SUPERSEDES: case CBM_DOCLINK_MD_SUPERSEDES:
case CBM_DOCLINK_MD_SUPERSEDES_PROSE: case CBM_DOCLINK_MD_SUPERSEDES_PROSE:
mdr_resolve_adr(x, graph, doc, link->raw, out); mdr_resolve_adr(x, graph, doc, link->raw, out);
@@ -1586,8 +1622,10 @@ bool cbm_doclink_md_test_path(const char *path) {
} }
const cbm_doclink_resolver_t cbm_doclink_md_resolver = { const cbm_doclink_resolver_t cbm_doclink_md_resolver = {
.langs = {CBM_LANG_MARKDOWN}, /* + the links of diagram files and the operationIds of API documents */
.lang_count = 1, .langs = {CBM_LANG_MARKDOWN, CBM_LANG_MERMAID, CBM_LANG_DIAGRAM, CBM_LANG_OPENAPI,
CBM_LANG_JSON},
.lang_count = 5,
.scope_tag = NULL, .scope_tag = NULL,
.build = mdr_build, .build = mdr_build,
.destroy = mdr_destroy, .destroy = mdr_destroy,
+5
View File
@@ -1685,6 +1685,11 @@ static void rr_resolve(const void *index, void *state, int run_file, const CBMDo
cbm_doclink_md_resolver.resolve(x->md, NULL, run_file, link, graph, out); cbm_doclink_md_resolver.resolve(x->md, NULL, run_file, link, graph, out);
return; return;
} }
if (cbm_doclink_md_twin(link->syntax) != CBM_DOCLINK_NONE) { /* a diagram's link, an
* API's operationId */
cbm_doclink_md_resolver.resolve(x->md, NULL, run_file, link, graph, out);
return;
}
if (link->syntax == CBM_DOCLINK_RST_CODE_PATH || link->syntax == CBM_DOCLINK_RST_CODE_NAME) { if (link->syntax == CBM_DOCLINK_RST_CODE_PATH || link->syntax == CBM_DOCLINK_RST_CODE_NAME) {
CBMDocLink md = *link; CBMDocLink md = *link;
md.syntax = link->syntax == CBM_DOCLINK_RST_CODE_PATH ? CBM_DOCLINK_MD_CODE_PATH md.syntax = link->syntax == CBM_DOCLINK_RST_CODE_PATH ? CBM_DOCLINK_MD_CODE_PATH
+3
View File
@@ -446,6 +446,7 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
cbm_gbuf_insert_edge(ctx->gbuf, parent->id, node_id, "DEFINES_METHOD", "{}"); cbm_gbuf_insert_edge(ctx->gbuf, parent->id, node_id, "DEFINES_METHOD", "{}");
} }
} }
cbm_pipeline_diagram_parent_edge(ctx, def, node_id);
} }
/* Create Channel nodes + EMITS / LISTENS_ON edges for one file's channels. /* Create Channel nodes + EMITS / LISTENS_ON edges for one file's channels.
@@ -1004,6 +1005,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
* map is available without the cache (single-file scope). */ * map is available without the cache (single-file scope). */
total_imports += create_import_edges_for_file(ctx, result, rel, NULL); total_imports += create_import_edges_for_file(ctx, result, rel, NULL);
create_channel_edges_for_file(ctx, result, rel); create_channel_edges_for_file(ctx, result, rel);
cbm_pipeline_diagram_arrows(ctx, result);
cbm_pipeline_create_env_configures_for_file(ctx, result, rel); cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
cbm_free_result(result); cbm_free_result(result);
} }
@@ -1037,6 +1039,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
total_imports += total_imports +=
create_import_edges_for_file(ctx, result, files[i].rel_path, namespace_map); create_import_edges_for_file(ctx, result, files[i].rel_path, namespace_map);
create_channel_edges_for_file(ctx, result, files[i].rel_path); create_channel_edges_for_file(ctx, result, files[i].rel_path);
cbm_pipeline_diagram_arrows(ctx, result);
cbm_pipeline_create_env_configures_for_file(ctx, result, files[i].rel_path); cbm_pipeline_create_env_configures_for_file(ctx, result, files[i].rel_path);
} }
cbm_pipeline_namespace_map_free(namespace_map); cbm_pipeline_namespace_map_free(namespace_map);
+68
View File
@@ -0,0 +1,68 @@
/*
* pass_diagrams.c — the graph side of diagrams (internal/cbm/diagram.c).
*
* A Diagram node is a definition of the file that holds it; a diagram block of
* a document also hangs under the Section it is written in, and each
* DiagramElement under its Diagram (DEFINES, from the definition's
* parent_class). The arrows of a diagram are DIAGRAM_ARROW edges between the
* file's DiagramElement nodes, with the arrow's text, its style and its line.
* Both paths of the definitions pass (pass_definitions.c, pass_parallel.c)
* call in here, so they cannot drift apart.
*/
#include "pipeline/pipeline_internal.h"
#include "cbm.h"
#include "foundation/constants.h"
#include "foundation/str_util.h"
#include "graph_buffer/graph_buffer.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
enum { PD_TEXT_MAX = 512, PD_PROPS_MAX = 1536 };
static bool pd_is_diagram_label(const char *label) {
return label && (strcmp(label, "Diagram") == 0 || strcmp(label, "DiagramElement") == 0);
}
void cbm_pipeline_diagram_parent_edge(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def,
int64_t node_id) {
if (!ctx || !def || !def->parent_class || node_id <= 0 || !pd_is_diagram_label(def->label)) {
return;
}
const cbm_gbuf_node_t *parent = cbm_gbuf_find_by_qn(ctx->gbuf, def->parent_class);
if (parent && parent->id != node_id) {
cbm_gbuf_insert_edge(ctx->gbuf, parent->id, node_id, "DEFINES", "{}");
}
}
void cbm_pipeline_diagram_arrows(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result) {
if (!ctx || !result) {
return;
}
for (int i = 0; i < result->diagram_arrows.count; i++) {
const CBMDiagramArrow *a = &result->diagram_arrows.items[i];
if (!a->from_qn || !a->to_qn) {
continue;
}
const cbm_gbuf_node_t *from = cbm_gbuf_find_by_qn(ctx->gbuf, a->from_qn);
const cbm_gbuf_node_t *to = cbm_gbuf_find_by_qn(ctx->gbuf, a->to_qn);
if (!from || !to) {
continue;
}
char label[PD_TEXT_MAX];
char style[PD_TEXT_MAX];
cbm_json_escape(label, sizeof(label), a->label ? a->label : "");
cbm_json_escape(style, sizeof(style), a->style ? a->style : "");
char props[PD_PROPS_MAX];
int n =
a->label && a->label[0]
? snprintf(props, sizeof(props), "{\"label\":\"%s\",\"style\":\"%s\",\"line\":%u}",
label, style, a->line)
: snprintf(props, sizeof(props), "{\"style\":\"%s\",\"line\":%u}", style, a->line);
if (n <= 0 || (size_t)n >= sizeof(props)) {
continue;
}
cbm_gbuf_insert_edge(ctx->gbuf, from->id, to->id, "DIAGRAM_ARROW", props);
}
}
+4
View File
@@ -1783,6 +1783,9 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
cbm_gbuf_insert_edge(ctx->gbuf, parent->id, def_node->id, "DEFINES_METHOD", "{}"); cbm_gbuf_insert_edge(ctx->gbuf, parent->id, def_node->id, "DEFINES_METHOD", "{}");
} }
} }
if (def_node) {
cbm_pipeline_diagram_parent_edge(ctx, def, def_node->id);
}
return edges; return edges;
} }
@@ -1971,6 +1974,7 @@ int cbm_build_registry_from_cache(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
imports_edges += create_imports_edges(ctx, result, rel, namespace_map); imports_edges += create_imports_edges(ctx, result, rel, namespace_map);
create_channel_edges(ctx, result, rel); create_channel_edges(ctx, result, rel);
cbm_pipeline_diagram_arrows(ctx, result);
cbm_pipeline_create_env_configures_for_file(ctx, result, rel); cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
if (loaded) { if (loaded) {
cbm_free_result(result); cbm_free_result(result);
-89
View File
@@ -38,95 +38,6 @@ enum {
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
/* True for characters that may appear in a ":name" route parameter. */
static inline bool is_route_ident_char(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}
/* Canonicalize route-path parameter placeholders to a single "{}" token so that
* client call sites and server handlers rendezvous on the same Route QN
* regardless of framework syntax. Each parameter token collapses to "{}":
*
* :name Express / React-Router / Rails / typical JS API clients
* {name} Axum / Spring / OpenAPI / ASP.NET
* <name> Flask / Rocket (incl. typed "<int:id>")
* ${...} JS template interpolation captured into the path
*
* Parameter names are intentionally discarded so the same logical endpoint
* matches across services that name the path variable differently. Static path
* text is copied verbatim; the result never exceeds the input length. */
const char *cbm_route_canon_path(const char *in, char *out, size_t out_sz) {
if (out == NULL || out_sz == 0) {
/* WHY: cppcheck value-flow follows a caller that passes an as-yet
* uninitialized output buffer (e.g. `char cpath[256];`) and flags this
* `return out` as uninitvar. False positive: we only return the pointer
* here and never read the buffer; returning it (NULL, or a buffer too
* small to write into) is the intended early-out contract. */
// cppcheck-suppress uninitvar
return out;
}
if (in == NULL) {
out[0] = '\0';
return out;
}
const size_t last = out_sz - 1;
size_t oi = 0;
size_t i = 0;
while (in[i] != '\0' && oi < last) {
char c = in[i];
bool at_seg_start = (oi == 0) || (out[oi - 1] == '/');
bool is_param = false;
if (c == ':' && at_seg_start && is_route_ident_char(in[i + 1])) {
i++;
while (in[i] != '\0' && is_route_ident_char(in[i])) {
i++;
}
is_param = true;
} else if (c == '{') {
i++;
while (in[i] != '\0' && in[i] != '}' && in[i] != '/') {
i++;
}
if (in[i] == '}') {
i++;
}
is_param = true;
} else if (c == '<') {
i++;
while (in[i] != '\0' && in[i] != '>' && in[i] != '/') {
i++;
}
if (in[i] == '>') {
i++;
}
is_param = true;
} else if (c == '$' && in[i + 1] == '{') {
i += 2;
while (in[i] != '\0' && in[i] != '}' && in[i] != '/') {
i++;
}
if (in[i] == '}') {
i++;
}
is_param = true;
}
if (is_param) {
if (oi + 2 > last) {
break;
}
out[oi++] = '{';
out[oi++] = '}';
continue;
}
out[oi++] = c;
i++;
}
out[oi] = '\0';
return out;
}
/* Extract a JSON string value by key from properties. /* Extract a JSON string value by key from properties.
* Returns pointer into buf (caller provides buffer). NULL if not found. */ * Returns pointer into buf (caller provides buffer). NULL if not found. */
static const char *json_extract(const char *json, const char *key, char *buf, int bufsz) { static const char *json_extract(const char *json, const char *key, char *buf, int bufsz) {
+2
View File
@@ -1856,6 +1856,7 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
(size_t)r->impl_traits.count * sizeof(CBMImplTrait) + (size_t)r->impl_traits.count * sizeof(CBMImplTrait) +
(size_t)r->infra_bindings.count * sizeof(CBMInfraBinding) + (size_t)r->infra_bindings.count * sizeof(CBMInfraBinding) +
(size_t)r->channels.count * sizeof(CBMChannel) + (size_t)r->channels.count * sizeof(CBMChannel) +
(size_t)r->diagram_arrows.count * sizeof(CBMDiagramArrow) +
(size_t)r->field_types.count * sizeof(CBMFieldType); (size_t)r->field_types.count * sizeof(CBMFieldType);
cap_other += (size_t)r->imports.cap * sizeof(CBMImport) + cap_other += (size_t)r->imports.cap * sizeof(CBMImport) +
(size_t)r->resolved_calls.cap * sizeof(CBMResolvedCall) + (size_t)r->resolved_calls.cap * sizeof(CBMResolvedCall) +
@@ -1866,6 +1867,7 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
(size_t)r->impl_traits.cap * sizeof(CBMImplTrait) + (size_t)r->impl_traits.cap * sizeof(CBMImplTrait) +
(size_t)r->infra_bindings.cap * sizeof(CBMInfraBinding) + (size_t)r->infra_bindings.cap * sizeof(CBMInfraBinding) +
(size_t)r->channels.cap * sizeof(CBMChannel) + (size_t)r->channels.cap * sizeof(CBMChannel) +
(size_t)r->diagram_arrows.cap * sizeof(CBMDiagramArrow) +
(size_t)r->field_types.cap * sizeof(CBMFieldType); (size_t)r->field_types.cap * sizeof(CBMFieldType);
n_defs += (size_t)r->defs.count; n_defs += (size_t)r->defs.count;
n_calls += (size_t)r->calls.count; n_calls += (size_t)r->calls.count;
+10 -7
View File
@@ -264,13 +264,7 @@ bool cbm_pipeline_frozen_manifests_equal(cbm_pipeline_t *p, const cbm_file_hash_
* "no incremental route; continue with a full index" sentinel. */ * "no incremental route; continue with a full index" sentinel. */
#define CBM_PIPELINE_FORCE_FULL_REINDEX (-3) #define CBM_PIPELINE_FORCE_FULL_REINDEX (-3)
/* Canonicalize route-path parameter placeholders (":id", "{id}", "<id>", /* cbm_route_canon_path: cbm.h (route_path.c). */
* "${...}") to a single "{}" token so that client call sites and server
* handlers rendezvous on the same Route QN regardless of framework syntax.
* Parameter names are intentionally discarded ("/u/{id}" and "/u/{slug}" both
* canonicalize to "/u/{}"). The result never exceeds the input length, so
* out_sz >= strlen(in) + 1 always suffices. Returns out. */
const char *cbm_route_canon_path(const char *in, char *out, size_t out_sz);
/* True when a graph node is a structural directory container (Folder/Project) /* True when a graph node is a structural directory container (Folder/Project)
* rather than a code node. In a directory-based-module language (Java/Go, see * rather than a code node. In a directory-based-module language (Java/Go, see
@@ -564,6 +558,15 @@ static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *c
return 0; return 0;
} }
/* Diagrams (pass_diagrams.c). The DEFINES edge from a Diagram's or a
* DiagramElement's container (the Section a diagram block sits in, the
* Diagram of an element) to the definition's node; nothing for other
* definitions. */
void cbm_pipeline_diagram_parent_edge(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def,
int64_t node_id);
/* The DIAGRAM_ARROW edges of one file's diagrams. */
void cbm_pipeline_diagram_arrows(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result);
/* Get the current pipeline's package map (NULL if none). */ /* Get the current pipeline's package map (NULL if none). */
CBMHashTable *cbm_pipeline_get_pkgmap(void); CBMHashTable *cbm_pipeline_get_pkgmap(void);
void cbm_pipeline_set_pkgmap(CBMHashTable *map); void cbm_pipeline_set_pkgmap(CBMHashTable *map);
+2 -1
View File
@@ -552,7 +552,8 @@ TEST(repro_call_node_manifest_live_specs_contain_only_expected_primary_entries)
* transform-only one and the DOCUMENT ones of PDF and AsciiDoc * transform-only one and the DOCUMENT ones of PDF and AsciiDoc
* (doc_pdf.c, doc_adoc.c). */ * (doc_pdf.c, doc_adoc.c). */
if (language == CBM_LANG_NIM || language == CBM_LANG_OBJECTSCRIPT_EXPORT || if (language == CBM_LANG_NIM || language == CBM_LANG_OBJECTSCRIPT_EXPORT ||
language == CBM_LANG_PDF || language == CBM_LANG_ASCIIDOC) { language == CBM_LANG_PDF || language == CBM_LANG_ASCIIDOC ||
language == CBM_LANG_DIAGRAM || language == CBM_LANG_OPENAPI) {
continue; continue;
} }
if (!spec || spec->language != language) { if (!spec || spec->language != language) {
+5 -3
View File
@@ -219,6 +219,8 @@ static const LanguageCapabilityEntry LANGUAGE_CAPABILITIES[CBM_LANG_COUNT] = {
CALL_WITHOUT_REFERENCE_VOCAB(CHIALISP), CALL_WITHOUT_REFERENCE_VOCAB(CHIALISP),
DOCUMENT(PDF), DOCUMENT(PDF),
DOCUMENT(ASCIIDOC), DOCUMENT(ASCIIDOC),
NO_CALL(OPENAPI), /* the JSON grammar; its operations are Routes, no calls */
DOCUMENT(DIAGRAM),
}; };
#undef CALL_WITH_REFERENCE_VOCAB #undef CALL_WITH_REFERENCE_VOCAB
@@ -310,9 +312,9 @@ TEST(repro_language_capability_ledger_covers_every_enum) {
} }
if (counts[CAP_CALL_WITH_REFERENCE_VOCAB] != 88 || if (counts[CAP_CALL_WITH_REFERENCE_VOCAB] != 88 ||
counts[CAP_CALL_WITHOUT_REFERENCE_VOCAB] != 27 || counts[CAP_NO_CALL] != 49 || counts[CAP_CALL_WITHOUT_REFERENCE_VOCAB] != 27 || counts[CAP_NO_CALL] != 50 ||
counts[CAP_TRANSFORM_ONLY] != 1 || counts[CAP_UNSUPPORTED] != 1 || counts[CAP_TRANSFORM_ONLY] != 1 || counts[CAP_UNSUPPORTED] != 1 ||
counts[CAP_DOCUMENT] != 2) { counts[CAP_DOCUMENT] != 3) {
fprintf(stderr, fprintf(stderr,
" [language-registry] invariant=capability_partition call_ref_vocab=%d ref_gap=%d " " [language-registry] invariant=capability_partition call_ref_vocab=%d ref_gap=%d "
"no_call=%d transform_only=%d unsupported=%d document=%d\n", "no_call=%d transform_only=%d unsupported=%d document=%d\n",
@@ -332,7 +334,7 @@ TEST(repro_call_argument_matrices_equal_call_capability_ledger) {
EXPECTED_MATRIX_B_ROWS = 49, EXPECTED_MATRIX_B_ROWS = 49,
EXPECTED_CALL_CAPABLE_LANGUAGES = 115, EXPECTED_CALL_CAPABLE_LANGUAGES = 115,
EXPECTED_NON_CALL_LANGUAGES = EXPECTED_NON_CALL_LANGUAGES =
53, /* 49 no-call + transform-only + unsupported + PDF, AsciiDoc */ 55, /* 50 no-call + transform-only + unsupported + PDF, AsciiDoc, Diagram */
EXPECTED_NON_CALL_DOMAIN_CONTROLS = 2, EXPECTED_NON_CALL_DOMAIN_CONTROLS = 2,
}; };
CBMLanguage matrix_a_ids[CBM_LANG_COUNT]; CBMLanguage matrix_a_ids[CBM_LANG_COUNT];
File diff suppressed because it is too large. Load diff
+34 -13
View File
@@ -430,19 +430,40 @@ static inline int dm_reason_lines(const char *db, const char *block) {
* so it is the resolution an audit would ship; doc_links_md_ship_gate tests * so it is the resolution an audit would ship; doc_links_md_ship_gate tests
* the production gate itself. */ * the production gate itself. */
static const int DM_HELD_FAMILIES[] = { static const int DM_HELD_FAMILIES[] = {
CBM_DOCLINK_MD_BARE_PATH, CBM_DOCLINK_MD_SUPERSEDES_PROSE, CBM_DOCLINK_MD_BARE_PATH,
CBM_DOCLINK_RST_INCLUDE, CBM_DOCLINK_RST_KERNEL_DOC, CBM_DOCLINK_MD_SUPERSEDES_PROSE,
CBM_DOCLINK_ADOC_ATTRIBUTE, CBM_DOCLINK_ADOC_CODE_PATH, CBM_DOCLINK_RST_INCLUDE,
CBM_DOCLINK_ADOC_CODE_NAME, CBM_DOCLINK_GO_DOC_LINK, CBM_DOCLINK_RST_KERNEL_DOC,
CBM_DOCLINK_JAVA_LINK, CBM_DOCLINK_JAVA_LINKPLAIN, CBM_DOCLINK_ADOC_ATTRIBUTE,
CBM_DOCLINK_JAVA_SEE, CBM_DOCLINK_JAVA_THROWS, CBM_DOCLINK_ADOC_CODE_PATH,
CBM_DOCLINK_JAVA_VALUE, CBM_DOCLINK_KT_KDOC_LINK, CBM_DOCLINK_ADOC_CODE_NAME,
CBM_DOCLINK_KT_SEE, CBM_DOCLINK_KT_THROWS, CBM_DOCLINK_GO_DOC_LINK,
CBM_DOCLINK_KT_SAMPLE, CBM_DOCLINK_RS_INTRA_DOC, CBM_DOCLINK_JAVA_LINK,
CBM_DOCLINK_RS_LINK_DEF, CBM_DOCLINK_PERL_POD_L, CBM_DOCLINK_JAVA_LINKPLAIN,
CBM_DOCLINK_TS_LINK, CBM_DOCLINK_TS_LINKCODE, CBM_DOCLINK_JAVA_SEE,
CBM_DOCLINK_TS_SEE, CBM_DOCLINK_PHP_SEE, CBM_DOCLINK_JAVA_THROWS,
CBM_DOCLINK_C_REF, CBM_DOCLINK_C_SEE, 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_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,
CBM_DOCLINK_DG_MERMAID_CLICK,
CBM_DOCLINK_DG_PLANTUML_URL,
CBM_DOCLINK_DG_PLANTUML_INCLUDE,
CBM_DOCLINK_DG_DRAWIO_LINK,
CBM_DOCLINK_DG_EXCALIDRAW_LINK,
CBM_DOCLINK_DG_DOT_URL,
CBM_DOCLINK_DG_D2_LINK,
CBM_DOCLINK_OPENAPI_OPERATION_ID,
}; };
static inline void dm_ship_held_families(void) { static inline void dm_ship_held_families(void) {
+1 -1
View File
@@ -242,7 +242,7 @@ static const LabelGolden LABEL_GOLDENS[] = {
{"astro", "Module:1,Variable:1"}, {"astro", "Module:1,Variable:1"},
{"templ", "Class:1,Module:1"}, {"templ", "Class:1,Module:1"},
{"typst", "Module:1"}, {"typst", "Module:1"},
{"mermaid", "Module:1"}, {"mermaid", "Diagram:1,DiagramElement:2,Module:1"}, /* the fixture's flowchart */
{NULL, NULL}, {NULL, NULL},
}; };
+4
View File
@@ -1832,6 +1832,8 @@ extern void suite_doc_mentions_perl(void);
extern void suite_doc_mentions_ts(void); extern void suite_doc_mentions_ts(void);
extern void suite_doc_mentions_php(void); extern void suite_doc_mentions_php(void);
extern void suite_doc_mentions_c(void); extern void suite_doc_mentions_c(void);
extern void suite_diagrams(void);
extern void suite_openapi(void);
extern void suite_infrascan(void); extern void suite_infrascan(void);
extern void suite_cli(void); extern void suite_cli(void);
extern void suite_agent_clients(void); extern void suite_agent_clients(void);
@@ -2310,6 +2312,8 @@ int main(int argc, char **argv) {
RUN_SELECTED_SUITE(doc_mentions_ts); RUN_SELECTED_SUITE(doc_mentions_ts);
RUN_SELECTED_SUITE(doc_mentions_php); RUN_SELECTED_SUITE(doc_mentions_php);
RUN_SELECTED_SUITE(doc_mentions_c); RUN_SELECTED_SUITE(doc_mentions_c);
RUN_SELECTED_SUITE(diagrams);
RUN_SELECTED_SUITE(openapi);
/* Infrastructure scanning */ /* Infrastructure scanning */
RUN_SELECTED_SUITE(infrascan); RUN_SELECTED_SUITE(infrascan);
+242
View File
@@ -0,0 +1,242 @@
/*
* test_openapi.c -- OpenAPI / Swagger documents: each operation a Route named
* as code names it, each operationId a reference to its handler (openapi.c).
*/
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include "test_doc_mentions_helpers.h"
#include "cbm.h"
#include "discover/discover.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const CBMDefinition *oa_route(const CBMFileResult *r, const char *qn) {
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (d->qualified_name && strcmp(d->qualified_name, qn) == 0) {
return d;
}
}
printf(" no definition %s\n", qn);
return NULL;
}
static const char *oa_prop(const CBMDefinition *d, const char *key) {
for (int i = 0; d && d->extra_props && d->extra_props[i]; i += 2) {
if (strcmp(d->extra_props[i], key) == 0) {
return d->extra_props[i + 1];
}
}
return NULL;
}
static int oa_count(const CBMFileResult *r, const char *label) {
int n = 0;
for (int i = 0; i < r->defs.count; i++) {
n += r->defs.items[i].label && strcmp(r->defs.items[i].label, label) == 0;
}
return n;
}
static int oa_link(const CBMFileResult *r, const char *source_qn, const char *raw) {
int n = 0;
for (int i = 0; i < r->doc_links.count; i++) {
const CBMDocLink *l = &r->doc_links.items[i];
n += l->syntax == CBM_DOCLINK_OPENAPI_OPERATION_ID &&
strcmp(l->source_qn, source_qn) == 0 && strcmp(l->raw, raw) == 0;
}
if (n != 1) {
printf(" link %s -> %s: %d found\n", source_qn, raw, n);
}
return n;
}
static const char OA_YAML[] = "openapi: 3.0.0\n"
"info:\n"
" title: Users\n"
"paths:\n"
" /users/{id}:\n"
" x-openapi-router-controller: app.api.users\n"
" get:\n"
" operationId: get_user\n"
" delete:\n"
" operationId: deleteUser\n"
" x-openapi-router-controller: app.admin\n"
" /users:\n"
" post:\n"
" operationId: \"app.api.users.create_user\"\n"
" parameters: []\n"
" /health:\n"
" get:\n"
" summary: no id\n";
TEST(openapi_yaml) {
CBMFileResult *r = dm_extract(OA_YAML, CBM_LANG_YAML, "api/openapi.yaml");
ASSERT_NOT_NULL(r);
const CBMDefinition *g = oa_route(r, "__route__GET__/users/{}");
ASSERT_NOT_NULL(g);
ASSERT_STR_EQ(g->label, "Route");
ASSERT_STR_EQ(g->name, "/users/{id}");
ASSERT_STR_EQ(oa_prop(g, "method"), "GET");
ASSERT_STR_EQ(oa_prop(g, "source"), "openapi");
ASSERT_STR_EQ(oa_prop(g, "operation_id"), "get_user");
ASSERT_NOT_NULL(oa_route(r, "__route__DELETE__/users/{}"));
ASSERT_NOT_NULL(oa_route(r, "__route__POST__/users"));
ASSERT_NULL(oa_prop(oa_route(r, "__route__GET__/health"), "operation_id"));
ASSERT_EQ(oa_count(r, "Route"), 4); /* `parameters` is no operation */
/* the controller names the module of a bare id; a dotted id is whole */
ASSERT_EQ(oa_link(r, "__route__GET__/users/{}", "app.api.users.get_user"), 1);
ASSERT_EQ(oa_link(r, "__route__DELETE__/users/{}", "app.admin.deleteUser"), 1);
ASSERT_EQ(oa_link(r, "__route__POST__/users", "app.api.users.create_user"), 1);
ASSERT_EQ(r->doc_links.count, 3);
cbm_free_result(r);
/* YAML that describes no API is none */
r = dm_extract("paths:\n /x:\n get:\n operationId: x\n", CBM_LANG_YAML, "conf.yaml");
ASSERT_NOT_NULL(r);
ASSERT_EQ(oa_count(r, "Route"), 0);
cbm_free_result(r);
PASS();
}
TEST(openapi_json) {
static const char spec[] =
"{\"swagger\": \"2.0\", \"info\": {\"title\": \"Pets\"},\n"
" \"paths\": {\"/pets\": {\"get\": {\"operationId\": \"listPets\"},\n"
" \"post\": {\"operationId\": \"pets.create\"}}}}\n";
/* openapi.json: its operations, not a node per key */
CBMFileResult *r = dm_extract(spec, CBM_LANG_OPENAPI, "openapi.json");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(oa_route(r, "__route__GET__/pets"));
ASSERT_EQ(oa_link(r, "__route__GET__/pets", "listPets"), 1);
ASSERT_EQ(oa_link(r, "__route__POST__/pets", "pets.create"), 1);
ASSERT_EQ(oa_count(r, "Variable"), 0);
cbm_free_result(r);
/* a JSON file of another name that describes an API */
r = dm_extract(spec, CBM_LANG_JSON, "spec/pets-api.json");
ASSERT_NOT_NULL(r);
ASSERT_EQ(oa_count(r, "Route"), 2);
cbm_free_result(r);
/* the document names it */
ASSERT_EQ(cbm_language_for_filename("openapi.json"), CBM_LANG_OPENAPI);
ASSERT_EQ(cbm_language_for_filename("swagger.json"), CBM_LANG_OPENAPI);
PASS();
}
/* MENTIONS edges from the Route `route_qn` to a node whose QN ends with `tgt`. */
static int oa_db_mentions(const char *db, const char *route_qn, const char *tgt) {
sqlite3 *h = NULL;
int n = 0;
if (sqlite3_open_v2(db, &h, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
sqlite3_close(h);
return -1;
}
sqlite3_stmt *st = NULL;
const char *sql = "SELECT count(*) FROM edges e JOIN nodes s ON s.id = e.source_id "
"JOIN nodes t ON t.id = e.target_id WHERE e.type = 'MENTIONS' "
"AND s.qualified_name = ?1 AND t.qualified_name LIKE '%' || ?2";
if (sqlite3_prepare_v2(h, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, route_qn, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 2, tgt, -1, SQLITE_TRANSIENT);
if (sqlite3_step(st) == SQLITE_ROW) {
n = sqlite3_column_int(st, 0);
}
}
sqlite3_finalize(st);
sqlite3_close(h);
return n;
}
/* MENTIONS edges from the Route `route_qn` whose properties hold `part`. */
static int oa_db_mentions_props(const char *db, const char *route_qn, const char *part) {
sqlite3 *h = NULL;
int n = 0;
if (sqlite3_open_v2(db, &h, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
sqlite3_close(h);
return -1;
}
sqlite3_stmt *st = NULL;
const char *sql = "SELECT count(*) FROM edges e JOIN nodes s ON s.id = e.source_id "
"WHERE e.type = 'MENTIONS' AND s.qualified_name = ?1 "
"AND e.properties LIKE '%' || ?2 || '%'";
if (sqlite3_prepare_v2(h, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, route_qn, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 2, part, -1, SQLITE_TRANSIENT);
if (sqlite3_step(st) == SQLITE_ROW) {
n = sqlite3_column_int(st, 0);
}
}
sqlite3_finalize(st);
sqlite3_close(h);
return n;
}
static int oa_db_nodes(const char *db, const char *label, const char *qn) {
sqlite3 *h = NULL;
int n = 0;
if (sqlite3_open_v2(db, &h, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
sqlite3_close(h);
return -1;
}
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(h, "SELECT count(*) FROM nodes WHERE label = ?1 AND qualified_name = ?2",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, label, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 2, qn, -1, SQLITE_TRANSIENT);
if (sqlite3_step(st) == SQLITE_ROW) {
n = sqlite3_column_int(st, 0);
}
}
sqlite3_finalize(st);
sqlite3_close(h);
return n;
}
TEST(openapi_graph) {
dm_ship_held_families(); /* the operationId family is held until its audit */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_oa_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);
th_write_file(TH_PATH(repo, "app/api/users.py"),
"def get_user(id):\n return id\n\n\ndef create_user():\n return 1\n");
th_write_file(TH_PATH(repo, "api/openapi.yaml"), OA_YAML);
th_write_file(TH_PATH(repo, "openapi.json"),
"{\"openapi\": \"3.1.0\", \"paths\": {\"/pets\": {\"get\": "
"{\"operationId\": \"listPets\"}}, \"/dup\": {\"get\": "
"{\"operationId\": \"dup\"}}, \"/none\": {\"get\": "
"{\"operationId\": \"noSuchHandler\"}}}}\n");
th_write_file(TH_PATH(repo, "app/pets.py"), "def listPets():\n return []\n\n\ndef dup():\n"
" return 1\n");
th_write_file(TH_PATH(repo, "app/other.py"), "def dup():\n return 2\n");
ASSERT_EQ(dm_index(repo, db, NULL), 0);
ASSERT_EQ(oa_db_nodes(db, "Route", "__route__GET__/users/{}"), 1);
ASSERT_EQ(oa_db_nodes(db, "Route", "__route__GET__/pets"), 1);
/* a dotted operationId names its handler */
ASSERT_EQ(oa_db_mentions(db, "__route__GET__/users/{}", "users.get_user"), 1);
ASSERT_EQ(oa_db_mentions(db, "__route__POST__/users", "users.create_user"), 1);
/* through the API document, whichever resolver read its file (a YAML one) */
ASSERT_EQ(oa_db_mentions_props(db, "__route__POST__/users", "\"via\":\"openapi\""), 1);
ASSERT_EQ(oa_db_mentions_props(db, "__route__POST__/users", "\"tier\":\"exact\""), 1);
ASSERT_EQ(oa_db_mentions_props(db, "__route__GET__/pets", "\"tier\":\"unique\""), 1);
/* a bare id names the one function of that name */
ASSERT_EQ(oa_db_mentions(db, "__route__GET__/pets", "pets.listPets"), 1);
/* a module with no such function; a bare id two functions carry; one none does */
ASSERT_EQ(oa_db_mentions(db, "__route__DELETE__/users/{}", ""), 0);
ASSERT_EQ(oa_db_mentions(db, "__route__GET__/dup", ""), 0);
ASSERT_EQ(oa_db_mentions(db, "__route__GET__/none", ""), 0);
th_cleanup(tmp);
cbm_doclink_test_reset_ships();
PASS();
}
SUITE(openapi) {
RUN_TEST(openapi_yaml);
RUN_TEST(openapi_json);
RUN_TEST(openapi_graph);
}