test(doc-links): Kotlin pins follow the emitted names; C# fixtures keep an unreadable type

Kotlin nested types are named under their outer class and an unnamed companion
has nodes, so the pins move to those names, each confirmed against a real index.
One of them, a zero count on gk.K4.Wrapped.m, passed vacuously under a stale
suffix.

A nested class and a member function of one name now share a qualified name, and
so do a member function and a property. The later declaration owns the node, so
[Holder.Part] and a doc on the earlier twin are graph gaps. No edge goes to the
wrong twin. Both are pinned as known gaps.

The C# parse view reads `ref partial struct`, so those fixtures write it with a
comment between the words, which the grammar still cannot read and the view does
not repair. The plain spelling gets its own positive.

RED until the two doc-link fixes that follow: the slot of an unnamed companion, and
the C# scope of a file whose parse view hid a branch of a conditional.

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 2616bd96e0
commit 679a03fbfe
2 files changed
+98 -23

No files matched your search

+40 -6
View File
@@ -1032,13 +1032,15 @@ TEST(doc_mentions_cs_scope_parse_errors) {
ASSERT_NULL(strstr(s, "\nX\t"));
cbm_free_result(r);
/* `ref partial struct` is not parsed as a declaration: its header is read
* from the text (kind, name; bases unknown), and the types after it stay
* in the namespace its closing brace seemed to end. */
/* The grammar does not read `ref partial struct`, and the parse view repairs
* only the plain spelling (csharp_parse_view), not one with a comment between
* the words. Not parsed as a declaration: its header is read from the text
* (kind, name; bases unknown), and the types after it stay in the namespace
* its closing brace seemed to end. */
const char *early = "namespace Acme\n" /* 1 */
"{\n"
" public class Before { }\n" /* 3 */
" public ref partial struct Iter<T>\n" /* 4 */
" public class Before { }\n" /* 3 */
" public ref /* stack only */ partial struct Iter<T>\n" /* 4 */
" {\n"
" public void End() { }\n"
" }\n" /* 7 */
@@ -1059,6 +1061,24 @@ TEST(doc_mentions_cs_scope_parse_errors) {
ASSERT_NULL(strstr(s, "T\t0\t")); /* nothing fell into the global namespace */
cbm_free_result(r);
/* The plain spelling is repaired before parsing: a complete declaration from
* the tree, with nothing hidden and its base list known to be empty. */
const char *repaired = "namespace Acme\n" /* 1 */
"{\n"
" public ref partial struct Iter<T>\n" /* 3 */
" {\n"
" public void End() { }\n"
" }\n" /* 6 */
" public class After { }\n" /* 7 */
"}\n"; /* 8 */
r = dm_scope(repaired);
ASSERT_NOT_NULL(r);
s = r->doc_scope;
ASSERT_NOT_NULL(s);
ASSERT_NOT_NULL(strstr(s, "T\t1\t3\t6\tsp\t-\tIter\tT\t\n"));
ASSERT_NOT_NULL(strstr(s, "T\t1\t7\t7\tc\t-\tAfter\t\t\n"));
cbm_free_result(r);
/* Both branches of a conditional open the same block: one closing brace
* serves both, and the members belong to the type either way. */
const char *branches = "namespace Net\n" /* 1 */
@@ -3891,7 +3911,7 @@ static void dm_write_resolver_fixture(const char *tmp) {
"namespace Acme.Rec\n"
"{\n"
" public class Before { }\n"
" public ref partial struct Iter\n"
" public ref /* stack only */ partial struct Iter\n"
" {\n"
" public void End() { }\n"
" }\n"
@@ -4188,6 +4208,17 @@ TEST(doc_mentions_resolver_parse_errors) {
snprintf(tmp, sizeof(tmp), "/tmp/cbm_dm_pe_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
dm_write_resolver_fixture(tmp);
/* the plain spelling of what Recovered.cs cannot parse: repaired before parsing */
th_write_file(TH_PATH(tmp, "src/Other/RefPartial.cs"),
"namespace Acme.RefPartial\n"
"{\n"
" public ref partial struct Reader\n"
" {\n"
" public void End() { }\n"
" }\n"
" /// <summary><see cref=\"Reader\"/></summary>\n"
" public class UsesReader { }\n"
"}\n");
char db[512];
snprintf(db, sizeof(db), "%s/res.db", tmp);
ASSERT_EQ(dm_index(tmp, db, NULL), 0);
@@ -4196,6 +4227,9 @@ TEST(doc_mentions_resolver_parse_errors) {
int n = 0;
const char *rec = "src/Other/Recovered.cs";
/* a `ref partial struct` the parse view repairs has a node and is linked */
dm_edge(db, "RefPartial.UsesReader", "RefPartial.Reader", props, sizeof(props), &n);
ASSERT_EQ(n, 1);
/* `Middle` follows a struct the grammar cannot parse; the tree puts it in
* the global namespace, the braces keep it in Acme.Rec, where `Before` is */
dm_edge(db, "Recovered.Middle", "Recovered.Before", props, sizeof(props), &n);
+58 -17
View File
@@ -430,11 +430,11 @@ TEST(doc_mentions_jvm_kotlin_scope_blob) {
ASSERT(jt_has_line(s, "M\t6\tv\t\t0\tbodyProp\t\t-\t\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t7\tc\t\t0\tmember\t\tInt\t\t=\t0\tparam"));
ASSERT(jt_has_line(s, "M\t10\tc\t\t0\tmember\t\tString\t\t=\t1\tparam"));
/* a nested type's node is flat: named below the file's module */
ASSERT(jt_has_line(s, "T\t13\t13\tc\tAlias.Nested\t\t\tNested\t1\t"));
/* an unnamed companion and its members: declared, no definitions */
ASSERT(jt_has_line(s, "T\t14\t16\tC\tAlias.Companion\t\t\t-\t0\t"));
ASSERT(jt_has_line(s, "M\t15\tv\t\t2\tLIMIT\t\t-\t\t-\t0\t"));
/* a nested type's node is named under its outer class: its logical path */
ASSERT(jt_has_line(s, "T\t13\t13\tc\tAlias.Nested\t\t\t=\t1\t"));
/* an unnamed companion has a node of its own, as its members do */
ASSERT(jt_has_line(s, "T\t14\t16\tC\tAlias.Companion\t\t\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t15\tv\t\t2\tLIMIT\t\t-\t\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t17\tc\tK\t0\tAlias\t\tInt\t\t=\t1\tx"));
/* an extension: its receiver as written; a type parameter stands for its
* bound, one without a bound for any type */
@@ -1151,7 +1151,7 @@ TEST(doc_mentions_jvm_kotlin_families_and_reasons) {
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"exact\""));
ASSERT_EQ(jt_edge(db, widget, "far.Helper.Helper.run"), 1);
/* a member of the companion object, named through its class */
ASSERT_EQ(jt_edge(db, widget, "k.Gadget.Factory.create"), 1);
ASSERT_EQ(jt_edge(db, widget, "k.Gadget.Gadget.Factory.create"), 1);
/* a type alias has a node of its own */
ASSERT_EQ(jt_edge(db, widget, "k.Gadget.Alias"), 1);
/* throws */
@@ -1336,12 +1336,12 @@ TEST(doc_mentions_jvm_kotlin_levels) {
0);
/* 3 */
ASSERT_EQ(jt_edge(db, "kl.K3.K3.m", "kl.K3.K3.own"), 1);
ASSERT_EQ(jt_edge(db, "kl.K3.K3.m", "kl.K3.Tools.make"), 1);
ASSERT_EQ(jt_edge(db, "kl.K3.K3.m", "kl.K3.K3.Tools.make"), 1);
ASSERT_EQ(jt_edge(db, "kl.K3.K3.m", "kl.K3.K3Base.inherited"), 1);
ASSERT_EQ(jt_edge(db, "kl.K3.K3.m", "kl.K3.K3.shared"), 1);
ASSERT_EQ(jt_edge(db, "kl.K3.K3.m", "kl.K3.K3Base.shared"), 0);
/* 4 */
ASSERT_EQ(jt_edge(db, "kl.K4.K4.m", "kl.K4.Thing"), 1);
ASSERT_EQ(jt_edge(db, "kl.K4.K4.m", "kl.K4.K4.Thing"), 1);
ASSERT_EQ(jt_edge(db, "kl.K4.K4.m", "far.Thing.Thing"), 0);
/* 5 */
ASSERT_EQ(jt_edge(db, "kl.K5.K5", "far.Thing.Thing"), 1);
@@ -1358,11 +1358,11 @@ TEST(doc_mentions_jvm_kotlin_levels) {
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/kl/K8.kt", "listOf"), "external");
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/kl/K8.kt", "Runnable"), "external");
/* 9 */
ASSERT_EQ(jt_edge(db, "kl.K9.K9", "kl.K4.Thing"), 1);
ASSERT_EQ(jt_edge(db, "kl.K9.K9", "kl.K4.K4.Thing"), 1);
ASSERT_EQ(jt_edge(db, "kl.K9.K9", "far.Thing.Thing"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"exact\""));
ASSERT_EQ(jt_edge(db, "kl.K9.K9", "far.Funs.topFun"), 1);
ASSERT_EQ(jt_edge(db, "kl.K9.K9", "kl.K3.Tools.make"), 1);
ASSERT_EQ(jt_edge(db, "kl.K9.K9", "kl.K3.K3.Tools.make"), 1);
ASSERT_EQ(jt_edge(db, "kl.K9.K9", "kl.K3.K3Base.inherited"), 1);
ASSERT_EQ(jt_edge(db, "kl.K9.K9", "kl.K3.K3.shared"), 1);
ASSERT_EQ(jt_edge(db, "kl.K9.K9", "kl.K3.K3Base.shared"), 0);
@@ -1785,12 +1785,12 @@ TEST(doc_mentions_jvm_kotlin_open_supertypes) {
0);
/* the class around instead of the package: the same rule */
ASSERT_EQ(jt_rows(db, k4, "aid", "external"), 1);
ASSERT_EQ(dm_mentions_from(db, "gk.K4.Wrapped.m"), 0);
ASSERT_EQ(jt_edge(db, "gk.K4.Simple.m", "gk.K4.Outer.aid"), 1);
ASSERT_EQ(dm_mentions_from(db, "gk.K4.Outer.Wrapped.m"), 0);
ASSERT_EQ(jt_edge(db, "gk.K4.Outer.Simple.m", "gk.K4.Outer.aid"), 1);
/* the companion object's supertypes count: its members are in the
* class's scope. What the companion declares itself binds */
ASSERT_EQ(jt_rows(db, k4, "helper", "external"), 1);
ASSERT_EQ(jt_edge(db, "gk.K4.Locked.m", "gk.K4.Lock.close"), 1);
ASSERT_EQ(jt_edge(db, "gk.K4.Locked.m", "gk.K4.Locked.Lock.close"), 1);
ASSERT_EQ(dm_mentions_from(db, "gk.K4.Locked.m"), 1);
jt_end(&r);
PASS();
@@ -1822,8 +1822,22 @@ TEST(doc_mentions_jvm_kotlin_companion_inherits) {
"\n"
"/** [Entity.create] */\n"
"class User\n");
/* an unnamed companion is named Companion, and has a node. A named one is
* not: `Named.Companion` declares nothing */
jt_write(&r, "src/main/kotlin/gk/K5b.kt",
"package gk\n"
"\n"
"class Named {\n"
" companion object Tools\n"
"}\n"
"\n"
"/** [Entity.Companion], [Named.Companion] */\n"
"class CompanionUser\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
ASSERT_EQ(jt_edge(db, "gk.K5b.CompanionUser", "gk.K5.Entity.Companion"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/gk/K5b.kt", "Entity.Companion"), "");
ASSERT_EQ(dm_mentions_from(db, "gk.K5b.CompanionUser"), 1);
ASSERT_EQ(jt_edge(db, "gk.K5.Entity.m", "gk.K5.Maker.create"), 1);
ASSERT_EQ(jt_edge(db, "gk.K5.Entity.m", "gk.Top.create"), 0);
/* decoy: a name the companion's hierarchy does not have is the package's */
@@ -2171,7 +2185,9 @@ TEST(doc_mentions_jvm_kotlin_type_and_function) {
"}\n"
"\n"
"fun Alias(text: String): Alias = Alias()\n");
/* the same idiom inside a type: a nested class and a member function */
/* the same idiom inside a type: a nested class is named under its outer
* class, so it shares `f.Holder.Holder.Part` with the member function, and
* the function, declared later, owns the node */
jt_write(&r, "src/main/kotlin/f/Holder.kt",
"package f\n"
"\n"
@@ -2179,6 +2195,16 @@ TEST(doc_mentions_jvm_kotlin_type_and_function) {
" class Part\n"
" fun Part(size: Int): Part = Part()\n"
"}\n");
/* a member function and a property of one name share one qualified name too,
* and the property, declared later, owns the node (R2dbcDatabase.kt in Exposed) */
jt_write(&r, "src/main/kotlin/f/Twin.kt",
"package f\n"
"\n"
"class Twin {\n"
" /** A [Json]. */\n"
" fun same() {}\n"
" private val same = 1\n"
"}\n");
/* not the idiom: a class and a PROPERTY of one name stay ambiguous */
jt_write(&r, "src/main/kotlin/f/Mode.kt", "package f\n\nclass Mode\n");
jt_write(&r, "src/main/kotlin/f/Modes.kt", "package f\n\nval Mode: Int = 1\n");
@@ -2229,13 +2255,28 @@ TEST(doc_mentions_jvm_kotlin_type_and_function) {
ASSERT_EQ(jt_edge(db, "f.User.User", "f.Builders.Json"), 0);
/* a qualifier is a type: the function is no candidate there */
ASSERT_EQ(jt_edge(db, "f.User.User", "f.Json.Json.encode"), 1);
/* inside a type: the nested class, not the member function */
ASSERT_EQ(jt_edge(db, "f.User.User", "f.Holder.Part"), 1);
/* inside a type the class is what the reference names and it has no node:
* a graph gap, and never an edge to the member function that owns the QN.
* KNOWN GAP since nested types are named under their outer class; the old
* flat `f.Holder.Part` gave the class a node. Kind-disambiguated QNs would
* give it one back (graph_buffer.c, the same-QN collision rule). */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE qualified_name LIKE "
"'%.f.Holder.Holder.Part' AND label = 'Method' AND start_line = 5"),
1);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/f/User.kt", "Holder.Part"), "graph_gap");
ASSERT_EQ(jt_edge(db, "f.User.User", "f.Holder.Holder.Part"), 0);
/* a class and a property: ambiguous, as before */
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/f/User.kt", "Mode"), "ambiguous");
ASSERT_EQ(jt_edge(db, "f.User.User", "f.Mode.Mode"), 0);
ASSERT_EQ(dm_mentions_from(db, "f.User.User"), 3);
ASSERT_EQ(dm_mentions_from(db, "f.User.User"), 2);
/* the reference resolves, but the documented function has no node to leave
* from: a graph gap, never an edge from the property that owns the QN.
* KNOWN GAP for the same reason as the nested class above. */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE qualified_name LIKE "
"'%.f.Twin.Twin.same' AND label = 'Variable' AND start_line = 6"),
1);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/f/Twin.kt", "Json"), "graph_gap");
ASSERT_EQ(dm_mentions_from(db, "f.Twin.Twin.same"), 0);
/* the class has NO node: a graph gap, and no edge at all -- the node at
* `f.Alias.Alias` is the function's */
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/f/GapUser.kt", "Alias"), "graph_gap");