test(doc_mentions_go): receiver-qualified Go method nodes (#1913)

main's Go doc-link tests were written against flat Go method QNs, where
`(A) Close` and `(B) Close` shared one node `alpha.Close`. That is the
fused-node defect #1913 removes. The expectations move to the new scheme,
and each changed assertion pins an edge onto, or a count of, the
receiver-qualified node (production: the previous commit and #1913):

- sources: RefM's source is p.pkg.S.M, not p.pkg.M.
- forms: [Widget.Run] lands on alpha.Widget.Run, [beta.Thing.Do] on
  beta.Thing.Do.
- plain_text, struct_fields: Run's doc is the source alpha.Widget.Run.
- lookup_order: two alpha.Run sources and beta.Promoted named no node
  any more, so they held vacuously. They are now alpha.Widget.Run and
  beta.Embedded.Promoted. The alias links land on beta.Thing.Do and
  alpha.Deep10.End.
- node_owner, the positive contract that replaces the fused case:
  - two Method nodes, alpha.A.Close and alpha.B.Close, and none named
    alpha.Close;
  - [A.Close] and [B.Close] are edges, not graph_gap rows;
  - each method's own [Target] is an edge from its own doc line (9 and
    12), with no unresolved row;
  - the type Open keeps alpha.Open: [Open] is the type (no graph_gap),
    [B.Open] is the method, and UsesOpen has 2 targets.
- scope_blob:
  - method records carry w `-`;
  - swapping two methods of one name no longer changes the blob;
  - a func/type pair swap still does (new case);
  - the k1-k3 records follow (a method neither keeps nor contests a
    name);
  - a new k4 keeps the "two on one line contest it" rule covered with a
    var and a const.
- incremental_equals_full:
  - [b.T.M], [b.W.Name] and [b.V.Name] all resolve, and U.M's [Target]
    is an edge;
  - removing T.M breaks only [b.T.M];
  - swapping V.Name and W.Name moves nothing and is now an ordinary
    closure repair instead of a forced full rebuild.
- scope_delta_rules:
  - a method/function swap is LOCAL;
  - removing the method of such a pair is LOCAL with the name "Open" (a
    member left);
  - new func/type cases keep the GLOBAL keeper-change rules covered for
    swap and removal.
- workers_agree: [p01.T3.Close] now resolves (exact edge, no row). The
  graph_gap reason stays covered by the named Shared assertion: one
  function declared by every file of a package, which is still a gap.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-18 21:21:37 +02:00
1 parent 033be76c70
commit 4c62b61845
1 file changed
+122 -68
+122 -68
View File
@@ -228,10 +228,10 @@ TEST(doc_mentions_go_sources) {
const char *raw;
const char *source;
} want[] = {
{"RefF", "p.pkg.F"}, {"RefS", "p.pkg.S"}, {"RefField", "p.pkg.S.Field"},
{"RefM", "p.pkg.M"}, /* Go method nodes have a flat name */
{"RefI", "p.pkg.I"}, {"RefDo", "p.pkg.I.Do"}, {"RefC", "p.pkg.C"},
{"RefK", "p.pkg.K"}, {"RefV", "p.pkg.V"}, {"RefA", "p.pkg.A"},
{"RefF", "p.pkg.F"}, {"RefS", "p.pkg.S"}, {"RefField", "p.pkg.S.Field"},
{"RefM", "p.pkg.S.M"}, /* #1913: Go method nodes are receiver-qualified */
{"RefI", "p.pkg.I"}, {"RefDo", "p.pkg.I.Do"}, {"RefC", "p.pkg.C"},
{"RefK", "p.pkg.K"}, {"RefV", "p.pkg.V"}, {"RefA", "p.pkg.A"},
};
for (size_t i = 0; i < GO_COUNT(want); i++) {
const CBMDocLink *t = dm_find_token(r, want[i].raw);
@@ -772,14 +772,15 @@ TEST(doc_mentions_go_scope_blob) {
"T\tAlias\ta\tal.Widget\t1",
"T\tSame\ta\tKind\t1",
"T\tList\ts\t-\t1",
/* functions and methods; two methods named Run compete for one node
* and the later one keeps it */
/* functions and methods; a method node is receiver-qualified
* (#1913), so a method claims no flat name and its w is `-`: the two
* methods named Run no longer compete */
"F\tNew\t1",
"F\tMap\t1",
"M\tWidget\tRun\t0",
"M\tKind\tRun\t1",
"M\tList\tPush\t1",
"M\tlower\tExported\t1",
"M\tWidget\tRun\t-",
"M\tKind\tRun\t-",
"M\tList\tPush\t-",
"M\tlower\tExported\t-",
/* own type parameters, by the declaration's first and last line */
"g\t36\t36\tT,U",
"g\t42\t42\tA,B",
@@ -814,15 +815,20 @@ TEST(doc_mentions_go_scope_blob) {
char *portable = cbm_doclink_portable_scope(s);
ASSERT_NOT_NULL(portable);
ASSERT_NULL(strstr(portable, "\ng\t"));
ASSERT_TRUE(go_has_rec(portable, "M\tKind\tRun\t1"));
ASSERT_TRUE(go_has_rec(portable, "M\tKind\tRun\t-"));
ASSERT(strncmp(portable, "go1\n", 4) == 0);
cbm_free(CBM_MEM_CLASS_OTHER, portable);
cbm_free_result(r);
/* moving declarations changes nothing unless a node changes its owner */
/* moving declarations changes nothing unless a node changes its owner.
* Methods own no flat name (#1913), so swapping two methods of one name
* changes nothing either; two package-level declarations of one name
* still hand <dir>.<name> over when swapped */
const char *a = "package p\n\nfunc (a A) Run() {}\nfunc (b B) Run() {}\nfunc F() {}\n";
const char *moved = "package p\n\nfunc F() {}\n\n\nfunc (a A) Run() {}\nfunc (b B) Run() {}\n";
const char *swapped = "package p\n\nfunc (b B) Run() {}\nfunc (a A) Run() {}\nfunc F() {}\n";
const char *pair = "package p\n\nfunc Open() {}\ntype Open struct{}\n";
const char *pair_swapped = "package p\n\ntype Open struct{}\nfunc Open() {}\n";
CBMFileResult *ra = go_extract(a, "p/x.go");
CBMFileResult *rm = go_extract(moved, "p/x.go");
CBMFileResult *rs = go_extract(swapped, "p/x.go");
@@ -830,16 +836,25 @@ TEST(doc_mentions_go_scope_blob) {
ASSERT_NOT_NULL(rm);
ASSERT_NOT_NULL(rs);
ASSERT_STR_EQ(ra->doc_scope, rm->doc_scope);
ASSERT(strcmp(ra->doc_scope, rs->doc_scope) != 0);
ASSERT_TRUE(go_has_rec(rs->doc_scope, "M\tA\tRun\t1"));
ASSERT_TRUE(go_has_rec(rs->doc_scope, "M\tB\tRun\t0"));
ASSERT_STR_EQ(ra->doc_scope, rs->doc_scope);
ASSERT_TRUE(go_has_rec(rs->doc_scope, "M\tA\tRun\t-"));
ASSERT_TRUE(go_has_rec(rs->doc_scope, "M\tB\tRun\t-"));
cbm_free_result(ra);
cbm_free_result(rm);
cbm_free_result(rs);
CBMFileResult *rp = go_extract(pair, "p/x.go");
CBMFileResult *rq = go_extract(pair_swapped, "p/x.go");
ASSERT_NOT_NULL(rp);
ASSERT_NOT_NULL(rq);
ASSERT(strcmp(rp->doc_scope, rq->doc_scope) != 0);
ASSERT_TRUE(go_has_rec(rp->doc_scope, "T\tOpen\ts\t-\t1"));
ASSERT_TRUE(go_has_rec(rq->doc_scope, "F\tOpen\t1"));
cbm_free_result(rp);
cbm_free_result(rq);
/* the node <dir>.<name> is one for a function, a type, a value and the
* methods of that name: the last declaration keeps it, whatever its kind,
* and two on one line contest it */
/* the node <dir>.<name> is one for a function, a type and a value of
* that name: the last declaration keeps it, whatever its kind, and two on
* one line contest it. A method of that name claims nothing (#1913) */
CBMFileResult *k1 = go_extract("package p\n\nfunc Open() {}\nfunc (t T) Open() {}\n", "p/k.go");
CBMFileResult *k2 = go_extract("package p\n\nfunc (t T) Open() {}\nfunc Open() {}\n", "p/k.go");
CBMFileResult *k3 = go_extract("package p\n\ntype Open int\nvar Open = 1\n"
@@ -848,14 +863,21 @@ TEST(doc_mentions_go_scope_blob) {
ASSERT_NOT_NULL(k1);
ASSERT_NOT_NULL(k2);
ASSERT_NOT_NULL(k3);
ASSERT_TRUE(go_has_rec(k1->doc_scope, "F\tOpen\t0"));
ASSERT_TRUE(go_has_rec(k1->doc_scope, "M\tT\tOpen\t1"));
ASSERT_TRUE(go_has_rec(k1->doc_scope, "F\tOpen\t1"));
ASSERT_TRUE(go_has_rec(k1->doc_scope, "M\tT\tOpen\t-"));
ASSERT_TRUE(go_has_rec(k2->doc_scope, "F\tOpen\t1"));
ASSERT_TRUE(go_has_rec(k2->doc_scope, "M\tT\tOpen\t0"));
ASSERT_TRUE(go_has_rec(k2->doc_scope, "M\tT\tOpen\t-"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "T\tOpen\to\t-\t0"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "V\tOpen\t0"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "V\tOpen\tx"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "M\tT\tOpen\tx"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "V\tOpen\t1"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "M\tT\tOpen\t-"));
/* two package-level declarations on one line contest the name */
CBMFileResult *k4 =
go_extract("package p\n\ntype Open int\nvar Open = 1; const Open = 2\n", "p/k.go");
ASSERT_NOT_NULL(k4);
ASSERT_TRUE(go_has_rec(k4->doc_scope, "T\tOpen\to\t-\t0"));
ASSERT_TRUE(go_has_rec(k4->doc_scope, "V\tOpen\tx"));
cbm_free_result(k4);
cbm_free_result(k1);
cbm_free_result(k2);
cbm_free_result(k3);
@@ -1030,7 +1052,7 @@ static const char GO_ALPHA_C[] = "package alpha\n"
"// Target is a target; see [A.Close] and [B.Close].\n"
"func Target() {}\n"
"\n"
"// Open is a type that loses its node name to the method below.\n"
"// Open is a type; a method of B below has the same name.\n"
"type Open struct{}\n"
"\n"
"// Open opens: a method named like the type.\n"
@@ -1295,7 +1317,7 @@ TEST(doc_mentions_go_forms) {
static const go_want_edge_t edges[] = {
/* [Name], [Recv.Method], an interface's method, a constant, a variable */
{"alpha.Widget", "alpha.New", "unique"},
{"alpha.Widget", "alpha.Run", "unique"},
{"alpha.Widget", "alpha.Widget.Run", "unique"},
{"alpha.Widget", "alpha.Runner.Run", "unique"},
{"alpha.Widget", "alpha.Max", "unique"},
{"alpha.Widget", "alpha.Default", "unique"},
@@ -1311,7 +1333,7 @@ TEST(doc_mentions_go_forms) {
{"alpha.New", "sub.deep.Deep", "exact"},
{"alpha.New", "nested.pkg.Far", "exact"},
/* [pkg.Recv.Name]: a method, a field, an interface method */
{"alpha.New", "beta.Do", "exact"},
{"alpha.New", "beta.Thing.Do", "exact"},
{"alpha.New", "beta.Thing.Field", "exact"},
{"alpha.New", "beta.Doer.Act", "exact"},
/* an identifier with letters beyond ASCII */
@@ -1395,7 +1417,7 @@ TEST(doc_mentions_go_plain_text) {
ASSERT_EQ(go_bad_rows(db, rows, GO_COUNT(rows)), 0);
/* and none of them is an edge: Run's doc holds rows, plain text and one
* link, to the field Widget.Size */
ASSERT_EQ(dm_mentions_from(db, "alpha.Run"), 1);
ASSERT_EQ(dm_mentions_from(db, "alpha.Widget.Run"), 1);
ASSERT_EQ(dm_mentions_from(db, "alpha.Bare"), 0);
static const go_want_edge_t edges[] = {
{"alpha.Sibling", "alpha.Widget", "unique"},
@@ -1424,14 +1446,14 @@ TEST(doc_mentions_go_struct_fields) {
static const go_want_edge_t edges[] = {
/* the same package: a field named from a method's doc and from a
* sibling field's doc */
{"alpha.Run", "alpha.Widget.Size", "exact"},
{"alpha.Widget.Run", "alpha.Widget.Size", "exact"},
{"alpha.Widget.Name", "alpha.Widget.Size", "exact"},
/* another package, by its import */
{"alpha.New", "beta.Thing.Field", "exact"},
{"gamma.ViaAlias", "alpha.Widget.Name", "exact"},
/* Depth is a field of beta.Embedded, which Widget embeds: it is no
* field of Widget */
{"alpha.Run", "beta.Embedded.Depth", ""},
{"alpha.Widget.Run", "beta.Embedded.Depth", ""},
};
ASSERT_EQ(go_bad_edges(db, edges, GO_COUNT(edges)), 0);
/* the promoted field exists as a node: the test above is not vacuous */
@@ -1545,7 +1567,7 @@ TEST(doc_mentions_go_lookup_order) {
* file that imports "fmt", the repository's own in a file that imports
* example.com/demo/fmt */
ASSERT_STR_EQ(go_reason(db, "alpha/a.go", "fmt.Println"), "external");
ASSERT_STR_EQ(go_tier(db, "alpha.Run", "fmt.Println"), "");
ASSERT_STR_EQ(go_tier(db, "alpha.Widget.Run", "fmt.Println"), "");
ASSERT_STR_EQ(go_tier(db, "gamma.Local", "fmt.Println"), "exact");
/* 5. the longest module path decides: a directory inside the root module
* that belongs to a nested module of another path is not reachable through
@@ -1555,17 +1577,17 @@ TEST(doc_mentions_go_lookup_order) {
ASSERT_STR_EQ(go_tier(db, "alpha.New", "sub.deep.Deep"), "exact");
/* 6. the bare name stops at the package: beta.Make is imported and still
* no [Make] */
ASSERT_STR_EQ(go_tier(db, "alpha.Run", "beta.Make"), "");
ASSERT_STR_EQ(go_tier(db, "alpha.Widget.Run", "beta.Make"), "");
/* 7. members stop at the type: nothing promoted through embedding */
ASSERT_STR_EQ(go_reason(db, "gamma/k.go", "alpha.Widget.Promoted"), "missing");
ASSERT_STR_EQ(go_tier(db, "gamma.ViaAlias", "beta.Promoted"), "");
ASSERT_STR_EQ(go_tier(db, "gamma.ViaAlias", "beta.Embedded.Promoted"), "");
/* 8. an alias is followed for a member written with its package, to the
* type that declares the member */
static const go_want_edge_t alias[] = {
{"gamma.ViaAlias", "beta.Do", "exact"},
{"gamma.ViaAlias", "beta.Thing.Do", "exact"},
{"gamma.ViaAlias", "beta.Thing.Field", "exact"},
{"gamma.ViaAlias", "alpha.Alias", "exact"},
{"gamma.ViaAlias", "alpha.End", "exact"}, /* seven aliases away */
{"gamma.ViaAlias", "alpha.Deep10.End", "exact"}, /* seven aliases away */
{"gamma.ViaAlias", "alpha.Widget.Name", "exact"},
};
ASSERT_EQ(go_bad_edges(db, alias, GO_COUNT(alias)), 0);
@@ -1638,28 +1660,42 @@ TEST(doc_mentions_go_node_owner) {
char tmp[256];
char db[512];
ASSERT_EQ(go_index_lookup(tmp, sizeof(tmp), db, sizeof(db)), 0);
/* Go method nodes have a flat name: A.Close and B.Close share
* alpha.Close, and the later declaration keeps it */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE qualified_name LIKE '%.alpha.Close'"),
/* #1913: Go method nodes are receiver-qualified. A.Close and B.Close are
* two nodes (no shared alpha.Close), each a target of its own */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Method' AND "
"qualified_name LIKE '%.alpha.A.Close'"),
1);
ASSERT_STR_EQ(go_tier(db, "alpha.Target", "alpha.Close"), "unique");
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "A.Close"), "graph_gap");
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Method' AND "
"qualified_name LIKE '%.alpha.B.Close'"),
1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE qualified_name LIKE '%.alpha.Close'"),
0);
ASSERT_STR_EQ(go_tier(db, "alpha.Target", "alpha.A.Close"), "unique");
ASSERT_STR_EQ(go_tier(db, "alpha.Target", "alpha.B.Close"), "unique");
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "A.Close"), "");
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "B.Close"), "");
/* the doc of the method that lost the name is not attributed to the one
* that kept it: B.Close's [Target] is the edge, A.Close's a row */
/* each method's doc is attributed to its own node: both [Target]s are
* edges, from the line of that method's doc, and neither is a row */
char props[512];
int n = 0;
dm_edge(db, "alpha.Close", "alpha.Target", props, sizeof(props), &n);
dm_edge(db, "alpha.A.Close", "alpha.Target", props, sizeof(props), &n);
ASSERT_EQ(n, 1);
ASSERT_NOT_NULL(strstr(props, "\"line\":9,\"count\":1"));
dm_edge(db, "alpha.B.Close", "alpha.Target", props, sizeof(props), &n);
ASSERT_EQ(n, 1);
ASSERT_NOT_NULL(strstr(props, "\"line\":12,\"count\":1"));
ASSERT_EQ(dm_count(db, "SELECT line FROM doc_link_unresolved WHERE rel_path = 'alpha/c.go' "
"AND raw = 'Target'"),
9);
/* a type that lost its name to a method of that name is no target, the
* method is */
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "Open"), "graph_gap");
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = "
"'alpha/c.go' AND raw = 'Target'"),
0);
/* a type and a method of the same name no longer share a node: [Open]
* is the type, [B.Open] the method */
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "Open"), "");
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label <> 'Method' AND "
"qualified_name LIKE '%.alpha.Open'"),
1);
ASSERT_STR_EQ(go_tier(db, "alpha.UsesOpen", "alpha.Open"), "unique");
ASSERT_EQ(dm_mentions_from(db, "alpha.UsesOpen"), 1);
ASSERT_STR_EQ(go_tier(db, "alpha.UsesOpen", "alpha.B.Open"), "unique");
ASSERT_EQ(dm_mentions_from(db, "alpha.UsesOpen"), 2);
/* build variants are one entity: whichever file holds the node */
static const go_want_edge_t variants[] = {
@@ -1836,15 +1872,16 @@ TEST(doc_mentions_go_incremental_equals_full) {
ASSERT_EQ(dm_index(repo, inc_db, NULL), 0);
/* the preconditions the steps below move away from */
static const go_want_edge_t before[] = {
{"a.Use", "b.Target", "exact"}, {"a.Use", "b.Gone", "exact"},
{"a.Use", "b.M", "exact"}, /* T.M, in the first file of the directory, keeps b.M */
{"a.Use", "b.Name", "exact"}, /* W.Name, the later declaration, keeps b.Name */
{"a.Use", "a.Local", "unique"}, {"a.doc.go.__file__", "a.Local", "unique"},
{"a.Use", "b.Target", "exact"}, {"a.Use", "b.Gone", "exact"},
{"a.Use", "b.T.M", "exact"}, /* #1913: every method has its own node */
{"a.Use", "b.W.Name", "exact"}, {"a.Use", "b.V.Name", "exact"},
{"b.U.M", "b.Target", "unique"}, /* U.M is a source of its own */
{"a.Use", "a.Local", "unique"}, {"a.doc.go.__file__", "a.Local", "unique"},
{"a.Other", "b.Target", ""}, /* other.go does not import b */
};
ASSERT_EQ(go_bad_edges(inc_db, before, GO_COUNT(before)), 0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.V.Name"), "graph_gap");
ASSERT_STR_EQ(go_reason(inc_db, "b/c.go", "Target"), "graph_gap"); /* U.M has no node */
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.V.Name"), "");
ASSERT_STR_EQ(go_reason(inc_db, "b/c.go", "Target"), "");
/* a doc-comment edit re-extracts the file alone */
go_w(repo, "a/a.go", GO_INC_A_DOC_EDIT);
@@ -1880,23 +1917,27 @@ TEST(doc_mentions_go_incremental_equals_full) {
0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.Gone"), "missing");
/* the method that kept a shared node name goes: the other file's method of
* that name takes the node over, and its doc becomes a source */
/* a method goes: links to it break, and another receiver's method of
* the same name, in another file, is untouched */
go_w(repo, "b/b.go", GO_INC_B_NO_M);
ASSERT_EQ(
dm_step(repo, inc_db, full_db, "name keeper removed", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR),
0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.T.M"), "missing");
ASSERT_STR_EQ(go_tier(inc_db, "b.M", "b.Target"), "unique");
ASSERT_STR_EQ(go_tier(inc_db, "b.U.M", "b.Target"), "unique");
ASSERT_STR_EQ(go_reason(inc_db, "b/c.go", "Target"), "");
/* declaration order decides which declaration keeps a shared name:
* swapping two methods moves the node, for files with no edge into it too */
/* declaration order no longer decides anything: swapping two methods of
* the same name keeps both nodes and both links */
go_w(repo, "b/d.go", GO_INC_D_SWAPPED);
ASSERT_EQ(dm_step(repo, inc_db, full_db, "methods swapped", CBM_INCREMENTAL_ROUTE_FORCED_FULL),
0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.W.Name"), "graph_gap");
/* and the scope blob does not change (no method w moves), so the step
* is an ordinary closure repair, no longer a forced full rebuild */
ASSERT_EQ(
dm_step(repo, inc_db, full_db, "methods swapped", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.W.Name"), "");
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.V.Name"), "");
ASSERT_STR_EQ(go_tier(inc_db, "a.Use", "b.W.Name"), "exact");
ASSERT_STR_EQ(go_tier(inc_db, "a.Use", "b.V.Name"), "exact");
/* renaming a target */
go_w(repo, "b/b.go", GO_INC_B_RENAME);
@@ -2006,10 +2047,20 @@ TEST(doc_mentions_go_scope_delta_rules) {
GO_DELTA_HEAD GO_DELTA_F GO_DELTA_G GO_DELTA_T GO_DELTA_M GO_DELTA_I GO_DELTA_C
"var (\n\tV = 2\n)\n",
CBM_DOCLINK_DELTA_GLOBAL, NULL},
/* the declaration that keeps a shared node name changes */
{"same-named declarations swapped", twins,
"package p\n\nfunc (T) Open() {}\nfunc Open() {}\n", CBM_DOCLINK_DELTA_GLOBAL, NULL},
{"one of two same-named declarations removed", twins, "package p\n\nfunc Open() {}\n",
/* a method and a function of one name no longer share a node
* (#1913): swapping them changes no keeper */
{"method and function swapped", twins, "package p\n\nfunc (T) Open() {}\nfunc Open() {}\n",
CBM_DOCLINK_DELTA_LOCAL, ""},
/* the declaration that keeps a shared node name changes: two
* package-level declarations of one name still share <dir>.<name> */
{"same-named declarations swapped", "package p\n\nfunc Open() {}\ntype Open struct{}\n",
"package p\n\ntype Open struct{}\nfunc Open() {}\n", CBM_DOCLINK_DELTA_GLOBAL, NULL},
/* the method of a method/function pair removed: a member left, the
* function keeps its node either way */
{"method of a same-named pair removed", twins, "package p\n\nfunc Open() {}\n",
CBM_DOCLINK_DELTA_LOCAL, "Open"},
{"one of two same-named declarations removed",
"package p\n\nfunc Open() {}\ntype Open struct{}\n", "package p\n\nfunc Open() {}\n",
CBM_DOCLINK_DELTA_GLOBAL, NULL},
/* other files follow `type X = q.Y` through this file's imports */
{"import changed under a qualified alias", alias,
@@ -2091,7 +2142,10 @@ TEST(doc_mentions_go_workers_agree) {
ASSERT_STR_EQ(go_tier(par_db, "p00.T0", "p01.T0"), "exact");
ASSERT_STR_EQ(go_reason(par_db, "p00/f0.go", "p01.Gone"), "missing");
ASSERT_STR_EQ(go_reason(par_db, "p00/f0.go", "fmt.Println"), "external");
ASSERT_STR_EQ(go_reason(par_db, "p00/f0.go", "p01.T3.Close"), "graph_gap");
/* #1913: [p01.T3.Close] lands on its own method node now; the graph_gap
* reason stays covered by Shared, one name declared by every file */
ASSERT_STR_EQ(go_reason(par_db, "p00/f0.go", "p01.T3.Close"), "");
ASSERT_STR_EQ(go_tier(par_db, "p00.T0", "p01.T3.Close"), "exact");
ASSERT_STR_EQ(go_reason(par_db, "p00/f3.go", "Shared"), "graph_gap");
ASSERT_TRUE(dm_count(par_db, "SELECT COUNT(*) FROM edges WHERE type = 'MENTIONS'") > 100);
dm_unlink_db(par_db);