fix(doc-links): the C# scope reads what the parse view hid

The parse view keeps one branch of each #if, which is enough for the graph's nodes.
The scope read that same tree, so a namespace or a type another branch names
differently looked placed and complete. A reference valid in the other build was
then reported as doc rot, where it used to be a graph gap.

When the view hid a branch, keep the file's own parse for the scope. A view that
only blanked `ref` hides nothing and stays in use. RED:
doc_mentions_cs_scope_parse_errors, doc_mentions_resolver_parse_errors.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-01 20:36:07 +01:00
1 parent 0774ecde2a
commit 0fc6bdcb84
3 files changed
+40 -11

No files matched your search

+23 -7
View File
@@ -1098,7 +1098,7 @@ static uint64_t parse_error_bytes(TSTree *tree) {
* taking every type it declares with it. This view has the same length and the same
* lines: conditional directives and the branches after a file's first one are blank,
* and so is that `ref`. Only a file the grammar could not otherwise read uses it. */
static char *csharp_parse_view(const char *source, uint32_t length) {
static char *csharp_parse_view(const char *source, uint32_t length, bool *hid_branch) {
enum { MAX_DEPTH = 64 };
char *view = cbm_alloc(CBM_MEM_CLASS_EXTRACT, (size_t)length + 1);
if (!view)
@@ -1122,7 +1122,7 @@ static char *csharp_parse_view(const char *source, uint32_t length) {
if (opens && depth < MAX_DEPTH)
inactive[depth++] = false;
else if (switches && depth > 0)
inactive[depth - 1] = true;
*hid_branch = inactive[depth - 1] = true;
else if (closes && depth > 0)
depth--;
bool hidden = false;
@@ -1153,12 +1153,14 @@ static char *csharp_parse_view(const char *source, uint32_t length) {
return view;
}
TSTree *cbm_parse_source_repairing(TSParser *parser, CBMLanguage language, const char *source,
uint32_t source_len, TSParseOptions opts) {
TSTree *cbm_parse_source_repairing_ex(TSParser *parser, CBMLanguage language, const char *source,
uint32_t source_len, TSParseOptions opts,
TSTree **unrepaired) {
TSTree *tree = cbm_parse_source(parser, source, source_len, opts);
if (!tree || language != CBM_LANG_CSHARP || !ts_node_has_error(ts_tree_root_node(tree)))
return tree;
char *view = csharp_parse_view(source, source_len);
bool hid_branch = false;
char *view = csharp_parse_view(source, source_len, &hid_branch);
if (!view)
return tree;
ts_parser_reset(parser);
@@ -1167,13 +1169,21 @@ TSTree *cbm_parse_source_repairing(TSParser *parser, CBMLanguage language, const
if (!repaired)
return tree;
if (parse_error_bytes(repaired) < parse_error_bytes(tree)) {
ts_tree_delete(tree);
if (unrepaired && hid_branch)
*unrepaired = tree;
else
ts_tree_delete(tree);
return repaired;
}
ts_tree_delete(repaired);
return tree;
}
TSTree *cbm_parse_source_repairing(TSParser *parser, CBMLanguage language, const char *source,
uint32_t source_len, TSParseOptions opts) {
return cbm_parse_source_repairing_ex(parser, language, source, source_len, opts, NULL);
}
static CBM_TLS TSParser *tl_parser = NULL;
static CBM_TLS CBMLanguage tl_parser_lang = CBM_LANG_COUNT; // invalid sentinel
@@ -4367,10 +4377,12 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
cbm_sql_values_kept_ranges(source, (uint32_t)source_len, &sql_kept) &&
ts_parser_set_included_ranges(parser, sql_kept.items, sql_kept.count);
bool c_family = language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA;
TSTree *unrepaired = NULL;
TSTree *tree =
c_family
? cbm_parse_c_source_defined(parser, source, (uint32_t)source_len, opts, extra_defines)
: cbm_parse_source_repairing(parser, language, source, (uint32_t)source_len, opts);
: cbm_parse_source_repairing_ex(parser, language, source, (uint32_t)source_len, opts,
&unrepaired);
if (sql_ranged) {
(void)ts_parser_set_included_ranges(parser, NULL, 0);
}
@@ -4444,6 +4456,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
.rel_path = rel_path,
.module_qn = result->module_qn,
.root = root,
.unrepaired_root = unrepaired ? ts_tree_root_node(unrepaired) : (TSNode){0},
.macro_table = macro_table,
.return_type_table = return_type_table,
.walk_budget_nodes = cbm_walk_max_nodes(),
@@ -5170,6 +5183,9 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
/* Doc-comment references of the documented definitions, and the file's
* doc-link scope: both read the complete docstrings and the tree. */
cbm_doclinks_extract(&ctx);
if (unrepaired) {
ts_tree_delete(unrepaired);
}
// Accumulate profiling counters
atomic_fetch_add(&total_parse_ns, t1 - t0);
+9
View File
@@ -1165,6 +1165,9 @@ typedef struct {
* The core never reads, interprets or frees it. It is the only place for
* such state: a static or thread-local cache is not. */
void *doclink_state;
/* C#: the parse of the file itself when `root` reads it through a view that hid a branch of
* a conditional (cbm_parse_source_repairing_ex); null otherwise. Alive for this call only. */
TSNode unrepaired_root;
} CBMExtractCtx;
/* Internal configured-definition seams. No declarations pointer enters a result. */
@@ -1280,6 +1283,12 @@ TSTree *cbm_parse_source(TSParser *parser, const char *source, uint32_t source_l
* same-length view of it. Offsets and lines are the source's own. */
TSTree *cbm_parse_source_repairing(TSParser *parser, CBMLanguage language, const char *source,
uint32_t source_len, TSParseOptions opts);
/* As above. When the view that was used hid a branch of a conditional, *unrepaired receives the
* parse of the file itself, for a reader that must see what the view left out; the caller deletes
* it. Otherwise *unrepaired is left alone. */
TSTree *cbm_parse_source_repairing_ex(TSParser *parser, CBMLanguage language, const char *source,
uint32_t source_len, TSParseOptions opts,
TSTree **unrepaired);
/* As above for C-family source. A word that can only be an attribute macro is left out
* of the parse, so `class LLVM_ABI StringRef {` reads as the class. Offsets and positions
* stay those of the real file. Every C-family whole-file parse goes through here, so the
+8 -4
View File
@@ -3036,7 +3036,11 @@ void cbm_doclink_cs_test_spoil_scope(const char *rel_path) {
#endif
const char *cbm_doclink_cs_scan_scope(CBMExtractCtx *ctx) {
if (ts_node_is_null(ctx->root)) {
/* The parse view reads one branch of each #if, which is enough for the graph's nodes. A scope
* read from it would call a namespace or a type complete when another branch of the file
* names a different one, and a reference valid there would be reported as doc rot. */
TSNode root = ts_node_is_null(ctx->unrepaired_root) ? ctx->root : ctx->unrepaired_root;
if (ts_node_is_null(root)) {
return NULL;
}
cs_scan_t s = {.ctx = ctx,
@@ -3046,13 +3050,13 @@ const char *cbm_doclink_cs_scan_scope(CBMExtractCtx *ctx) {
.untrusted = UINT32_MAX,
.next_region = SKIP_ONE};
s.root_end_byte = ctx->source_len > 0 ? (uint32_t)ctx->source_len : 0;
s.root_end_line = cs_end_line(ctx->root);
s.lexical = ts_node_has_error(ctx->root);
s.root_end_line = cs_end_line(root);
s.lexical = ts_node_has_error(root);
if (s.lexical) {
cs_scan_tokens(&s);
}
/* the root itself is an error node when nothing of the file parsed */
cs_collect(&s, ctx->root, cs_kind_is(ctx->root, "ERROR") ? CS_OWNER_LEXICAL : CS_OWNER_NONE);
cs_collect(&s, root, cs_kind_is(root, "ERROR") ? CS_OWNER_LEXICAL : CS_OWNER_NONE);
cs_add_text_items(&s);
sb_puts(&s.sb, CBM_DOCLINK_CS_SCOPE_TAG "\n");
cs_emit_items(&s);