mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
06e69d0c56
commit
f4a49feba1
30 files changed
+5621
-205
No files matched your search
+5
-1
@@ -393,6 +393,9 @@ EXTRACTION_SRCS = \
|
||||
$(CBM_DIR)/doclink_ts.c \
|
||||
$(CBM_DIR)/doclink_php.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)/helpers.c \
|
||||
$(CBM_DIR)/cpp_branch_views.c \
|
||||
@@ -526,6 +529,7 @@ PIPELINE_SRCS = \
|
||||
src/pipeline/doc_links_ts.c \
|
||||
src/pipeline/doc_links_php.c \
|
||||
src/pipeline/doc_links_c.c \
|
||||
src/pipeline/pass_diagrams.c \
|
||||
src/pipeline/doc_links_msbuild.c \
|
||||
src/pipeline/pass_route_nodes.c \
|
||||
src/pipeline/pass_enrichment.c \
|
||||
@@ -788,7 +792,7 @@ TEST_DISCOVER_SRCS = \
|
||||
|
||||
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
|
||||
|
||||
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_pipeline_conventions.c tests/test_pipeline_frozen.c tests/test_importance.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_doclinks.c tests/test_doc_mentions.c tests/test_doc_links_md.c tests/test_doc_links_pdf.c tests/test_doc_links_rst.c tests/test_doc_links_adoc.c tests/test_doc_mentions_go.c tests/test_doc_mentions_jvm.c tests/test_doc_mentions_rs.c tests/test_doc_mentions_perl.c tests/test_doc_mentions_ts.c tests/test_doc_mentions_php.c tests/test_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
|
||||
|
||||
|
||||
@@ -277,6 +277,11 @@ void cbm_channels_push(CBMChannelArray *arr, CBMArena *a, CBMChannel 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) ---
|
||||
|
||||
typedef struct {
|
||||
@@ -2837,6 +2842,26 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
|
||||
.module_qn = result->module_qn,
|
||||
};
|
||||
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);
|
||||
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.
|
||||
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).
|
||||
if (ctx.language == CBM_LANG_KUSTOMIZE || ctx.language == CBM_LANG_K8S) {
|
||||
cbm_extract_k8s(&ctx);
|
||||
|
||||
@@ -211,6 +211,10 @@ typedef enum {
|
||||
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_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
|
||||
} CBMLanguage;
|
||||
|
||||
@@ -612,6 +616,23 @@ typedef struct {
|
||||
int cap;
|
||||
} 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 {
|
||||
CBMFieldType *items;
|
||||
int count;
|
||||
@@ -661,6 +682,7 @@ typedef struct CBMFileResult {
|
||||
CBMStringRefArray string_refs; // URL/config string literals from AST
|
||||
CBMInfraBindingArray infra_bindings; // topic→URL pairs from IaC configs
|
||||
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)
|
||||
|
||||
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_resolvedcall_push(CBMResolvedCallArray *arr, CBMArena *a, CBMResolvedCall rc);
|
||||
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 ---
|
||||
|
||||
@@ -1140,6 +1163,26 @@ void cbm_extract_type_refs(CBMExtractCtx *ctx);
|
||||
void cbm_extract_env_accesses(CBMExtractCtx *ctx);
|
||||
void cbm_extract_type_assigns(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).
|
||||
void cbm_extract_unified(CBMExtractCtx *ctx);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+100
-66
@@ -83,72 +83,87 @@
|
||||
*
|
||||
* 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. */
|
||||
#define CBM_DOCLINK_FAMILY_LIST(X) \
|
||||
/* csharp */ \
|
||||
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_EXCEPTION, CBM_LANG_CSHARP, "exception", 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 */ \
|
||||
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 */ \
|
||||
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_SEE, CBM_LANG_JAVA, "see", 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_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_THROWS, CBM_LANG_KOTLIN, "throws", 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 */ \
|
||||
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") \
|
||||
/* 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_LINKCODE, CBM_LANG_TYPESCRIPT, "linkcode", false, false, "MENTIONS") \
|
||||
X(CBM_DOCLINK_TS_SEE, CBM_LANG_TYPESCRIPT, "see", false, false, "MENTIONS") \
|
||||
/* php (doclink_php.c): PHPDoc @see and {@see}; held until their audit passes */ \
|
||||
X(CBM_DOCLINK_PHP_SEE, CBM_LANG_PHP, "see", false, false, "MENTIONS") \
|
||||
/* c (doclink_c.c): Doxygen @ref and @see in .c and .h files; held until their audit */ \
|
||||
X(CBM_DOCLINK_C_REF, CBM_LANG_C, "doxygen_ref", false, false, "MENTIONS") \
|
||||
X(CBM_DOCLINK_C_SEE, CBM_LANG_C, "see", false, false, "MENTIONS") \
|
||||
/* markdown (doclink_md.c): what a document's text names explicitly */ \
|
||||
X(CBM_DOCLINK_MD_LINK, CBM_LANG_MARKDOWN, "link", false, true, "MENTIONS") \
|
||||
X(CBM_DOCLINK_MD_PATH, CBM_LANG_MARKDOWN, "path", false, true, "MENTIONS") \
|
||||
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/`) */ \
|
||||
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") \
|
||||
/* an ADR's own "supersedes" / "replaces" statement, opening its line or field */ \
|
||||
/* and naming a record directly (doc_adr.c): ADR -> ADR */ \
|
||||
X(CBM_DOCLINK_MD_SUPERSEDES, CBM_LANG_MARKDOWN, "supersedes", false, true, "SUPERSEDES") \
|
||||
/* the same words in running prose ("this record supersedes ADR-3") */ \
|
||||
X(CBM_DOCLINK_MD_SUPERSEDES_PROSE, CBM_LANG_MARKDOWN, "supersedes_prose", false, false, \
|
||||
"SUPERSEDES") \
|
||||
/* 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 */ \
|
||||
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_QN, CBM_LANG_PDF, "pdf_qn", false, true, "MENTIONS") \
|
||||
X(CBM_DOCLINK_PDF_NAME, CBM_LANG_PDF, "pdf_name", false, false, "MENTIONS") \
|
||||
/* 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_OBJECT, CBM_LANG_RST, "object", 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_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_CODE_PATH, CBM_LANG_RST, "code_path", 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 */ \
|
||||
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_CODE_PATH, CBM_LANG_ASCIIDOC, "code_path", 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 */ \
|
||||
#define CBM_DOCLINK_FAMILY_LIST(X) \
|
||||
/* csharp */ \
|
||||
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_EXCEPTION, CBM_LANG_CSHARP, "exception", 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 */ \
|
||||
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 */ \
|
||||
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_SEE, CBM_LANG_JAVA, "see", 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_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_THROWS, CBM_LANG_KOTLIN, "throws", 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 */ \
|
||||
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") \
|
||||
/* 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_LINKCODE, CBM_LANG_TYPESCRIPT, "linkcode", false, false, "MENTIONS") \
|
||||
X(CBM_DOCLINK_TS_SEE, CBM_LANG_TYPESCRIPT, "see", false, false, "MENTIONS") \
|
||||
/* php (doclink_php.c): PHPDoc @see and {@see}; held until their audit passes */ \
|
||||
X(CBM_DOCLINK_PHP_SEE, CBM_LANG_PHP, "see", false, false, "MENTIONS") \
|
||||
/* c (doclink_c.c): Doxygen @ref and @see in .c and .h files; held until their audit */ \
|
||||
X(CBM_DOCLINK_C_REF, CBM_LANG_C, "doxygen_ref", false, false, "MENTIONS") \
|
||||
X(CBM_DOCLINK_C_SEE, CBM_LANG_C, "see", false, false, "MENTIONS") \
|
||||
/* markdown (doclink_md.c): what a document's text names explicitly */ \
|
||||
X(CBM_DOCLINK_MD_LINK, CBM_LANG_MARKDOWN, "link", false, true, "MENTIONS") \
|
||||
X(CBM_DOCLINK_MD_PATH, CBM_LANG_MARKDOWN, "path", false, true, "MENTIONS") \
|
||||
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/`) */ \
|
||||
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") \
|
||||
/* an ADR's own "supersedes" / "replaces" statement, opening its line or field */ \
|
||||
/* and naming a record directly (doc_adr.c): ADR -> ADR */ \
|
||||
X(CBM_DOCLINK_MD_SUPERSEDES, CBM_LANG_MARKDOWN, "supersedes", false, true, "SUPERSEDES") \
|
||||
/* the same words in running prose ("this record supersedes ADR-3") */ \
|
||||
X(CBM_DOCLINK_MD_SUPERSEDES_PROSE, CBM_LANG_MARKDOWN, "supersedes_prose", false, false, \
|
||||
"SUPERSEDES") \
|
||||
/* 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 */ \
|
||||
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_QN, CBM_LANG_PDF, "pdf_qn", false, true, "MENTIONS") \
|
||||
X(CBM_DOCLINK_PDF_NAME, CBM_LANG_PDF, "pdf_name", false, false, "MENTIONS") \
|
||||
/* 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_OBJECT, CBM_LANG_RST, "object", 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_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_CODE_PATH, CBM_LANG_RST, "code_path", 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 */ \
|
||||
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_CODE_PATH, CBM_LANG_ASCIIDOC, "code_path", false, false, "MENTIONS") \
|
||||
X(CBM_DOCLINK_ADOC_CODE_NAME, CBM_LANG_ASCIIDOC, "code_name", false, false, "MENTIONS") \
|
||||
/* 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")
|
||||
|
||||
typedef enum {
|
||||
@@ -159,6 +174,25 @@ typedef enum {
|
||||
CBM_DOCLINK_SYNTAX_COUNT
|
||||
} 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. */
|
||||
enum {
|
||||
/* The reference is written in the FILE's own doc (CBMFileResult.module_doc:
|
||||
|
||||
@@ -2713,6 +2713,14 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
|
||||
empty_types, NULL, NULL, tree_sitter_objectscript_routine,
|
||||
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
|
||||
// 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().
|
||||
@@ -2721,6 +2729,9 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
|
||||
// CBM_LANG_ASCIIDOC — AsciiDoc documents. No grammar row, as PDF: extraction
|
||||
// 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
|
||||
// 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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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].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));
|
||||
for (int i = 0; i < r->channels.count && r->channels.items; i++) {
|
||||
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.infra_bindings.cap = tmp.infra_bindings.count;
|
||||
tmp.channels.cap = tmp.channels.count;
|
||||
tmp.diagram_arrows.cap = tmp.diagram_arrows.count;
|
||||
tmp.field_types.cap = tmp.field_types.count;
|
||||
tmp.doc_links.cap = tmp.doc_links.count;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -83,6 +83,9 @@ static const char *ALWAYS_IGNORED_SUFFIXES[] = {
|
||||
".webp", ".svg", ".wasm", ".node", ".exe", ".bin", ".dat", ".db",
|
||||
".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[] = {
|
||||
".zip", ".tar", ".gz", ".bz2", ".xz", ".rar", ".7z", ".jar",
|
||||
".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);
|
||||
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++) {
|
||||
if (ends_with(filename, filename_length, ALWAYS_IGNORED_SUFFIXES[i])) {
|
||||
return true;
|
||||
|
||||
+25
-10
@@ -621,6 +621,17 @@ static const ext_entry_t EXT_TABLE[] = {
|
||||
{".pdf", CBM_LANG_PDF},
|
||||
/* AsciiDoc; `.asc` is left out: it is as often an ASCII-armored signature */
|
||||
{".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},
|
||||
|
||||
/* Slang */
|
||||
@@ -712,6 +723,8 @@ typedef struct {
|
||||
static const filename_entry_t FILENAME_TABLE[] = {
|
||||
{"CMakeLists.txt", CBM_LANG_CMAKE},
|
||||
{"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},
|
||||
{"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_PDF] = "PDF",
|
||||
[CBM_LANG_ASCIIDOC] = "AsciiDoc",
|
||||
[CBM_LANG_DIAGRAM] = "Diagram",
|
||||
[CBM_LANG_OPENAPI] = "OpenAPI",
|
||||
[CBM_LANG_FENNEL] = "Fennel",
|
||||
[CBM_LANG_FISH] = "Fish",
|
||||
[CBM_LANG_AWK] = "AWK",
|
||||
@@ -993,6 +1008,9 @@ static CBMLanguage lang_for_filename(const cbm_userconfig_t *ucfg, const char *f
|
||||
CBMLanguage lang;
|
||||
} COMPOUND_EXT_TABLE[] = {
|
||||
{".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, '.');
|
||||
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. */
|
||||
static const char *const IGNORED_JSON_FILES[] = {
|
||||
"package.json", "package-lock.json", "tsconfig.json",
|
||||
"jsconfig.json", "composer.json", "composer.lock",
|
||||
"yarn.lock", "openapi.json", "swagger.json",
|
||||
"jest.config.json", ".eslintrc.json", ".prettierrc.json",
|
||||
".babelrc.json", "tslint.json", "angular.json",
|
||||
"firebase.json", "renovate.json", "lerna.json",
|
||||
"turbo.json", ".stylelintrc.json", "pnpm-lock.json",
|
||||
"deno.json", "biome.json", "devcontainer.json",
|
||||
".devcontainer.json", "launch.json", "settings.json",
|
||||
"extensions.json", "tasks.json", NULL};
|
||||
"package.json", "package-lock.json", "tsconfig.json", "jsconfig.json",
|
||||
"composer.json", "composer.lock", "yarn.lock", "jest.config.json",
|
||||
".eslintrc.json", ".prettierrc.json", ".babelrc.json", "tslint.json",
|
||||
"angular.json", "firebase.json", "renovate.json", "lerna.json",
|
||||
"turbo.json", ".stylelintrc.json", "pnpm-lock.json", "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) {
|
||||
for (int i = 0; list[i]; i++) {
|
||||
|
||||
@@ -162,6 +162,8 @@ static const lang_name_entry_t LANG_NAME_TABLE[] = {
|
||||
{"chialisp", CBM_LANG_CHIALISP},
|
||||
{"pdf", CBM_LANG_PDF},
|
||||
{"asciidoc", CBM_LANG_ASCIIDOC},
|
||||
{"diagram", CBM_LANG_DIAGRAM},
|
||||
{"openapi", CBM_LANG_OPENAPI},
|
||||
{"lean", CBM_LANG_LEAN},
|
||||
{"form", CBM_LANG_FORM},
|
||||
{"magma", CBM_LANG_MAGMA},
|
||||
|
||||
@@ -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)) {
|
||||
atomic_fetch_add_explicit(&dl->mentions, 1, memory_order_relaxed);
|
||||
mentions[nm++] = (doclink_mention_t){.src = src->id,
|
||||
.tgt = out.target->id,
|
||||
.line = link->line,
|
||||
.target_first = out.target_first,
|
||||
.target_last = out.target_last,
|
||||
.via = R->via ? R->via : "doc_comment",
|
||||
.syntax = link->syntax,
|
||||
.exact = out.exact};
|
||||
mentions[nm++] =
|
||||
(doclink_mention_t){.src = src->id,
|
||||
.tgt = out.target->id,
|
||||
.line = link->line,
|
||||
.target_first = out.target_first,
|
||||
.target_last = out.target_last,
|
||||
.via = cbm_doclink_syntax_via(link->syntax)
|
||||
? cbm_doclink_syntax_via(link->syntax)
|
||||
: R->via ? R->via
|
||||
: "doc_comment",
|
||||
.syntax = link->syntax,
|
||||
.exact = out.exact};
|
||||
continue;
|
||||
}
|
||||
/* the ship gate: resolved, but this link family is below the
|
||||
|
||||
@@ -178,7 +178,7 @@ typedef enum {
|
||||
* file by file */
|
||||
} 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
|
||||
* its pointer in doc_links.c's resolver table. */
|
||||
|
||||
@@ -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);
|
||||
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) {
|
||||
CBMDocLink md = *link;
|
||||
md.syntax = link->syntax == CBM_DOCLINK_ADOC_CODE_PATH ? CBM_DOCLINK_MD_CODE_PATH
|
||||
|
||||
@@ -110,8 +110,9 @@ static const char *mdr_base(const char *path) {
|
||||
static bool mdr_code_file(const char *path) {
|
||||
CBMLanguage lang = cbm_language_for_filename(mdr_base(path));
|
||||
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_CSS && lang != CBM_LANG_SCSS && !cbm_has_config_extension(path);
|
||||
lang != CBM_LANG_ASCIIDOC && lang != CBM_LANG_PDF && lang != CBM_LANG_DIAGRAM &&
|
||||
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. */
|
||||
@@ -660,6 +661,35 @@ static void mdr_edge(cbm_doclink_outcome_t *out, const cbm_gbuf_node_t *target,
|
||||
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 */
|
||||
|
||||
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];
|
||||
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_PROSE:
|
||||
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 = {
|
||||
.langs = {CBM_LANG_MARKDOWN},
|
||||
.lang_count = 1,
|
||||
/* + the links of diagram files and the operationIds of API documents */
|
||||
.langs = {CBM_LANG_MARKDOWN, CBM_LANG_MERMAID, CBM_LANG_DIAGRAM, CBM_LANG_OPENAPI,
|
||||
CBM_LANG_JSON},
|
||||
.lang_count = 5,
|
||||
.scope_tag = NULL,
|
||||
.build = mdr_build,
|
||||
.destroy = mdr_destroy,
|
||||
|
||||
@@ -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);
|
||||
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) {
|
||||
CBMDocLink md = *link;
|
||||
md.syntax = link->syntax == CBM_DOCLINK_RST_CODE_PATH ? CBM_DOCLINK_MD_CODE_PATH
|
||||
|
||||
@@ -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_pipeline_diagram_parent_edge(ctx, def, node_id);
|
||||
}
|
||||
|
||||
/* 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). */
|
||||
total_imports += create_import_edges_for_file(ctx, result, rel, NULL);
|
||||
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_free_result(result);
|
||||
}
|
||||
@@ -1037,6 +1039,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
total_imports +=
|
||||
create_import_edges_for_file(ctx, result, files[i].rel_path, namespace_map);
|
||||
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_namespace_map_free(namespace_map);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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", "{}");
|
||||
}
|
||||
}
|
||||
if (def_node) {
|
||||
cbm_pipeline_diagram_parent_edge(ctx, def, def_node->id);
|
||||
}
|
||||
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);
|
||||
create_channel_edges(ctx, result, rel);
|
||||
cbm_pipeline_diagram_arrows(ctx, result);
|
||||
cbm_pipeline_create_env_configures_for_file(ctx, result, rel);
|
||||
if (loaded) {
|
||||
cbm_free_result(result);
|
||||
|
||||
@@ -38,95 +38,6 @@ enum {
|
||||
#include <stdio.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.
|
||||
* 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) {
|
||||
|
||||
@@ -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->infra_bindings.count * sizeof(CBMInfraBinding) +
|
||||
(size_t)r->channels.count * sizeof(CBMChannel) +
|
||||
(size_t)r->diagram_arrows.count * sizeof(CBMDiagramArrow) +
|
||||
(size_t)r->field_types.count * sizeof(CBMFieldType);
|
||||
cap_other += (size_t)r->imports.cap * sizeof(CBMImport) +
|
||||
(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->infra_bindings.cap * sizeof(CBMInfraBinding) +
|
||||
(size_t)r->channels.cap * sizeof(CBMChannel) +
|
||||
(size_t)r->diagram_arrows.cap * sizeof(CBMDiagramArrow) +
|
||||
(size_t)r->field_types.cap * sizeof(CBMFieldType);
|
||||
n_defs += (size_t)r->defs.count;
|
||||
n_calls += (size_t)r->calls.count;
|
||||
|
||||
@@ -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. */
|
||||
#define CBM_PIPELINE_FORCE_FULL_REINDEX (-3)
|
||||
|
||||
/* Canonicalize route-path parameter placeholders (":id", "{id}", "<id>",
|
||||
* "${...}") 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);
|
||||
/* cbm_route_canon_path: cbm.h (route_path.c). */
|
||||
|
||||
/* 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
|
||||
@@ -564,6 +558,15 @@ static inline int cbm_pipeline_relpath_is_excluded(const char *rel_path, char *c
|
||||
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). */
|
||||
CBMHashTable *cbm_pipeline_get_pkgmap(void);
|
||||
void cbm_pipeline_set_pkgmap(CBMHashTable *map);
|
||||
|
||||
@@ -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
|
||||
* (doc_pdf.c, doc_adoc.c). */
|
||||
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;
|
||||
}
|
||||
if (!spec || spec->language != language) {
|
||||
|
||||
@@ -219,6 +219,8 @@ static const LanguageCapabilityEntry LANGUAGE_CAPABILITIES[CBM_LANG_COUNT] = {
|
||||
CALL_WITHOUT_REFERENCE_VOCAB(CHIALISP),
|
||||
DOCUMENT(PDF),
|
||||
DOCUMENT(ASCIIDOC),
|
||||
NO_CALL(OPENAPI), /* the JSON grammar; its operations are Routes, no calls */
|
||||
DOCUMENT(DIAGRAM),
|
||||
};
|
||||
|
||||
#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 ||
|
||||
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_DOCUMENT] != 2) {
|
||||
counts[CAP_DOCUMENT] != 3) {
|
||||
fprintf(stderr,
|
||||
" [language-registry] invariant=capability_partition call_ref_vocab=%d ref_gap=%d "
|
||||
"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_CALL_CAPABLE_LANGUAGES = 115,
|
||||
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,
|
||||
};
|
||||
CBMLanguage matrix_a_ids[CBM_LANG_COUNT];
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
* the production gate itself. */
|
||||
static const int DM_HELD_FAMILIES[] = {
|
||||
CBM_DOCLINK_MD_BARE_PATH, CBM_DOCLINK_MD_SUPERSEDES_PROSE,
|
||||
CBM_DOCLINK_RST_INCLUDE, CBM_DOCLINK_RST_KERNEL_DOC,
|
||||
CBM_DOCLINK_ADOC_ATTRIBUTE, CBM_DOCLINK_ADOC_CODE_PATH,
|
||||
CBM_DOCLINK_ADOC_CODE_NAME, CBM_DOCLINK_GO_DOC_LINK,
|
||||
CBM_DOCLINK_JAVA_LINK, CBM_DOCLINK_JAVA_LINKPLAIN,
|
||||
CBM_DOCLINK_JAVA_SEE, CBM_DOCLINK_JAVA_THROWS,
|
||||
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_MD_BARE_PATH,
|
||||
CBM_DOCLINK_MD_SUPERSEDES_PROSE,
|
||||
CBM_DOCLINK_RST_INCLUDE,
|
||||
CBM_DOCLINK_RST_KERNEL_DOC,
|
||||
CBM_DOCLINK_ADOC_ATTRIBUTE,
|
||||
CBM_DOCLINK_ADOC_CODE_PATH,
|
||||
CBM_DOCLINK_ADOC_CODE_NAME,
|
||||
CBM_DOCLINK_GO_DOC_LINK,
|
||||
CBM_DOCLINK_JAVA_LINK,
|
||||
CBM_DOCLINK_JAVA_LINKPLAIN,
|
||||
CBM_DOCLINK_JAVA_SEE,
|
||||
CBM_DOCLINK_JAVA_THROWS,
|
||||
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) {
|
||||
|
||||
@@ -242,7 +242,7 @@ static const LabelGolden LABEL_GOLDENS[] = {
|
||||
{"astro", "Module:1,Variable:1"},
|
||||
{"templ", "Class:1,Module:1"},
|
||||
{"typst", "Module:1"},
|
||||
{"mermaid", "Module:1"},
|
||||
{"mermaid", "Diagram:1,DiagramElement:2,Module:1"}, /* the fixture's flowchart */
|
||||
{NULL, NULL},
|
||||
};
|
||||
|
||||
|
||||
@@ -1832,6 +1832,8 @@ extern void suite_doc_mentions_perl(void);
|
||||
extern void suite_doc_mentions_ts(void);
|
||||
extern void suite_doc_mentions_php(void);
|
||||
extern void suite_doc_mentions_c(void);
|
||||
extern void suite_diagrams(void);
|
||||
extern void suite_openapi(void);
|
||||
extern void suite_infrascan(void);
|
||||
extern void suite_cli(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_php);
|
||||
RUN_SELECTED_SUITE(doc_mentions_c);
|
||||
RUN_SELECTED_SUITE(diagrams);
|
||||
RUN_SELECTED_SUITE(openapi);
|
||||
|
||||
/* Infrastructure scanning */
|
||||
RUN_SELECTED_SUITE(infrascan);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user