Files
codebase-memory-mcp/tests/test_doc_mentions_jvm.c
Martin Vogel f05eccefa2 test(doc-links): move JVM not_indexed fixtures to a skipped layout (#2661)
The Java and Kotlin doc-link reason tests modelled "a package in a
directory discovery skips" with src/main/java/a/env/ and
src/main/kotlin/k/env/. Discovery now keeps packages below
src/<set>/<lang>/, so those files are indexed and the two not_indexed
assertions failed. Move both fixtures to a plain src/<pkg>/ layout,
which discovery still skips by name (the open part of #2661), and
reword the comments. Assertions unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-26 00:54:53 +01:00

3674 lines
180 KiB
C

/*
* test_doc_mentions_jvm.c — doc-comment references of Java and Kotlin
* (internal/cbm/doclink_jvm.c, src/pipeline/doc_links_jvm.c).
*
* Extraction is tested on source text (tokens, scope blobs, hostile input);
* resolution through the pipeline on fixture repositories, read back from
* the published database: every family and reason of both languages, the
* lookup order of each level by level with a decoy one level further out,
* references across the two languages, test code, parse errors, the scope
* changes an incremental run repairs file by file, incremental == full and
* one worker == several.
*/
#include "test_framework.h"
#include "test_doc_mentions_helpers.h"
/* ── extraction: tokens ──────────────────────────────────────────── */
/* The token written as `raw`, or a stand-in that fails every check. */
static const CBMDocLink *jt_token(const CBMFileResult *r, const char *raw) {
static const CBMDocLink none = {.raw = "", .syntax = CBM_DOCLINK_NONE};
const CBMDocLink *l = dm_find_token(r, raw);
return l ? l : &none;
}
TEST(doc_mentions_jvm_java_tokens) {
const char *src =
"package a;\n" /* 1 */
"\n" /* 2 */
"/**\n" /* 3 */
" * See {@link Gadget} and {@linkplain Gadget#spin() spin it},\n" /* 4 */
" * {@link Helper#run(int, String) label text}, {@value #MAX}.\n" /* 5 */
" * A tag over two lines {@link\n" /* 6 */
" * Wrapped#name}. {@code {@link InCode}} {@literal {@link InLiteral}}\n" /* 7 */
" * <!-- {@link InComment} --> <![CDATA[ {@link InCdata} ]]>\n" /* 8 */
" * {@linkage NotATag} {@link Open\n" /* 9 */
" * @see Gadget#spin()\n" /* 10 */
" * @see <a href=\"https://example.org/x\">site</a>\n" /* 11 */
" * @see \"The Book\"\n" /* 12 */
" * @throws IllegalStateException never\n" /* 13 */
" * @exception a.CustomError worse\n" /* 14 */
" * @param <T> the type {@link T}\n" /* 15 */
" * @return {@value} nothing, {@value \"%x\" Helper#MASK}\n" /* 16 */
" */\n" /* 17 */
"public class Widget<T> {\n" /* 18 */
" /** Max {@value}. {@link Widget#MAX} */\n" /* 19 */
" public static final int MAX = 10;\n" /* 20 */
" /**\n" /* 21 */
" * <pre>{@code\n" /* 22 */
" * @Override {@link InSample}\n" /* 23 */
" * }</pre> after {@link AfterSample}\n" /* 24 */
" */\n" /* 25 */
" void sample() {}\n" /* 26 */
"}\n";
CBMFileResult *r = dm_extract(src, CBM_LANG_JAVA, "a/Widget.java");
ASSERT_NOT_NULL(r);
/* every family, with the line the reference is written on */
const CBMDocLink *t = jt_token(r, "Gadget");
ASSERT_EQ(t->syntax, CBM_DOCLINK_JAVA_LINK);
ASSERT_EQ(t->line, 4);
ASSERT_EQ(t->def_line, 18);
ASSERT_STR_EQ(t->source_qn, "p.a.Widget");
ASSERT_EQ(dm_count_tokens(r, "Gadget#spin()"), 2);
ASSERT_EQ(jt_token(r, "Gadget#spin()")->syntax, CBM_DOCLINK_JAVA_LINKPLAIN);
/* the label is not part of the reference; blanks inside it are one */
ASSERT_EQ(jt_token(r, "Helper#run(int, String)")->line, 5);
ASSERT_EQ(jt_token(r, "#MAX")->syntax, CBM_DOCLINK_JAVA_VALUE);
ASSERT_EQ(jt_token(r, "Wrapped#name")->line, 7);
ASSERT_EQ(jt_token(r, "IllegalStateException")->syntax, CBM_DOCLINK_JAVA_THROWS);
ASSERT_EQ(jt_token(r, "a.CustomError")->syntax, CBM_DOCLINK_JAVA_THROWS);
ASSERT_EQ(jt_token(r, "a.CustomError")->line, 14);
ASSERT_EQ(jt_token(r, "https://example.org/x")->syntax, CBM_DOCLINK_HREF);
ASSERT_EQ(jt_token(r, "https://example.org/x")->line, 11);
/* a format string is not the reference of {@value} */
ASSERT_EQ(jt_token(r, "Helper#MASK")->syntax, CBM_DOCLINK_JAVA_VALUE);
/* a reference to the definition's own type parameter is still a token:
* the resolver, which knows the scope, calls it local */
ASSERT_EQ(jt_token(r, "T")->line, 15);
/* not references: code, literal, HTML comment, CDATA, another tag whose
* name only starts like one, a quoted title, a bare {@value} */
ASSERT_NULL(dm_find_token(r, "InCode"));
ASSERT_NULL(dm_find_token(r, "InLiteral"));
ASSERT_NULL(dm_find_token(r, "InComment"));
ASSERT_NULL(dm_find_token(r, "InCdata"));
ASSERT_NULL(dm_find_token(r, "NotATag"));
ASSERT_NULL(dm_find_token(r, "\"The"));
/* an inline tag that a block tag cuts off never closed: javadoc reports
* it, and it links nothing */
ASSERT_NULL(dm_find_token(r, "Open"));
/* the field's doc is harvested once, on the Field (its package-level
* Variable twin carries the same doc) */
ASSERT_EQ(dm_count_tokens(r, "Widget#MAX"), 1);
ASSERT_STR_EQ(jt_token(r, "Widget#MAX")->source_qn, "p.a.Widget.MAX");
/* `@` at the start of a line inside {@code} is sample code, not a block
* tag, and what follows inside the block is still code */
ASSERT_NULL(dm_find_token(r, "InSample"));
ASSERT_EQ(jt_token(r, "AfterSample")->line, 24);
cbm_free_result(r);
PASS();
}
TEST(doc_mentions_jvm_kotlin_tokens) {
const char *src =
"package k\n" /* 1 */
"\n" /* 2 */
"/**\n" /* 3 */
" * A [Gadget], [the helper][Helper.run], [text](https://x.org), `[InCode]`.\n" /* 4 */
" * ``double `tick` [InDouble]`` then [Outer.Inner] and [`quoted name`].\n" /* 5 */
" * [label]: https://example.org\n" /* 6 */
" * <!-- [InComment] --> ![image][ImgLabel] \\[Escaped]\n" /* 7 */
" * Nested /* comment [StillDoc] */ goes on to [AfterNested].\n" /* 8 */
" *\n" /* 9 */
" * [IndentedCode]\n" /* 10 */
" *\n" /* 11 */
" * ```\n" /* 12 */
" * [Fenced]\n" /* 13 */
" * ```\n" /* 14 */
" * @param name the [name] of [T]\n" /* 15 */
" * @property size the size\n" /* 16 */
" * @see Gadget.spin\n" /* 17 */
" * @see https://example.org\n" /* 18 */
" * @throws IllegalStateException never\n" /* 19 */
" * @exception CustomError worse\n" /* 20 */
" * @sample k.samples.widgetSample\n" /* 21 */
" * @return [Result] of [0] and [two words]\n" /* 22 */
" */\n" /* 23 */
"class Widget<T>(val name: String, size: Int)\n"; /* 24 */
CBMFileResult *r = dm_extract(src, CBM_LANG_KOTLIN, "k/Widget.kt");
ASSERT_NOT_NULL(r);
const CBMDocLink *t = jt_token(r, "Gadget");
ASSERT_EQ(t->syntax, CBM_DOCLINK_KT_KDOC_LINK);
ASSERT_EQ(t->line, 4);
ASSERT_EQ(t->def_line, 24);
ASSERT_STR_EQ(t->source_qn, "p.k.Widget.Widget");
/* [text][Name]: the second bracket names the code */
ASSERT_EQ(jt_token(r, "Helper.run")->syntax, CBM_DOCLINK_KT_KDOC_LINK);
ASSERT_NULL(dm_find_token(r, "the helper"));
ASSERT_EQ(jt_token(r, "Outer.Inner")->line, 5);
ASSERT_EQ(jt_token(r, "`quoted name`")->line, 5);
/* a block comment inside the doc is part of its text: Kotlin comments
* nest, and the doc goes on after it */
ASSERT_EQ(jt_token(r, "StillDoc")->line, 8);
ASSERT_EQ(jt_token(r, "AfterNested")->line, 8);
ASSERT_EQ(jt_token(r, "Gadget.spin")->syntax, CBM_DOCLINK_KT_SEE);
ASSERT_EQ(jt_token(r, "IllegalStateException")->syntax, CBM_DOCLINK_KT_THROWS);
ASSERT_EQ(jt_token(r, "CustomError")->syntax, CBM_DOCLINK_KT_THROWS);
ASSERT_EQ(jt_token(r, "CustomError")->line, 20);
ASSERT_EQ(jt_token(r, "k.samples.widgetSample")->syntax, CBM_DOCLINK_KT_SAMPLE);
ASSERT_EQ(jt_token(r, "Result")->line, 22);
/* own parameter and type parameter: tokens, local to the resolver */
ASSERT_EQ(jt_token(r, "name")->line, 15);
ASSERT_EQ(jt_token(r, "T")->line, 15);
/* not references: a Markdown link, code spans, a link definition, an
* HTML comment, an image's label, an escaped bracket, an indented and a
* fenced code block, a URL, and brackets that hold no name */
ASSERT_NULL(dm_find_token(r, "text"));
ASSERT_NULL(dm_find_token(r, "InCode"));
ASSERT_NULL(dm_find_token(r, "InDouble"));
ASSERT_NULL(dm_find_token(r, "label"));
ASSERT_NULL(dm_find_token(r, "InComment"));
ASSERT_NULL(dm_find_token(r, "image"));
ASSERT_NULL(dm_find_token(r, "ImgLabel"));
ASSERT_NULL(dm_find_token(r, "Escaped"));
ASSERT_NULL(dm_find_token(r, "IndentedCode"));
ASSERT_NULL(dm_find_token(r, "Fenced"));
ASSERT_NULL(dm_find_token(r, "https://example.org"));
ASSERT_NULL(dm_find_token(r, "0"));
ASSERT_NULL(dm_find_token(r, "two words"));
ASSERT_NULL(dm_find_token(r, "size"));
cbm_free_result(r);
PASS();
}
/* ── extraction: hostile input ───────────────────────────────────── */
enum { JT_BIG = 200000, JT_MEGA = 1100000 };
/* Extract `head` + `count` copies of `unit` + `tail` as one doc comment on a
* class; the result, or NULL when memory ran out. */
static CBMFileResult *jt_extract_repeated(CBMLanguage lang, const char *head, const char *unit,
int count, const char *tail) {
size_t ul = strlen(unit);
size_t cap = strlen(head) + (ul * (size_t)count) + strlen(tail) + 64;
char *src = malloc(cap);
if (!src) {
return NULL;
}
size_t w = (size_t)snprintf(src, cap, "/**\n * %s", head);
for (int i = 0; i < count; i++) {
memcpy(src + w, unit, ul);
w += ul;
}
snprintf(src + w, cap - w, "%s\n */\n%s", tail,
lang == CBM_LANG_JAVA ? "public class C {}\n" : "class C\n");
CBMFileResult *r = dm_extract(src, lang, lang == CBM_LANG_JAVA ? "C.java" : "C.kt");
free(src);
return r;
}
TEST(doc_mentions_jvm_malformed_and_oversized) {
/* a reference longer than any name (here: over a megabyte) is kept just
* past the limit, which is how the resolver knows it was cut: it never
* resolves a prefix */
CBMFileResult *r = jt_extract_repeated(CBM_LANG_JAVA, "{@link ", "A", JT_MEGA, "}");
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->doc_links.count, 1);
ASSERT_GT(strlen(r->doc_links.items[0].raw), (size_t)CBM_DOCLINK_JVM_REF_MAX);
ASSERT(strlen(r->doc_links.items[0].raw) < (size_t)CBM_DOCLINK_JVM_REF_MAX + 16);
cbm_free_result(r);
r = jt_extract_repeated(CBM_LANG_KOTLIN, "[", "A", JT_MEGA, "]");
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->doc_links.count, 1);
ASSERT_GT(strlen(r->doc_links.items[0].raw), (size_t)CBM_DOCLINK_JVM_REF_MAX);
ASSERT(strlen(r->doc_links.items[0].raw) < (size_t)CBM_DOCLINK_JVM_REF_MAX + 16);
cbm_free_result(r);
/* tags that never close, nested deeper than any text nests */
r = jt_extract_repeated(CBM_LANG_JAVA, "", "{@link A ", JT_BIG, "");
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->doc_links.count, 0);
cbm_free_result(r);
/* ... and closed again: the innermost ones are references, no more than
* the parser keeps open at once, and nothing overflows */
r = jt_extract_repeated(CBM_LANG_JAVA, "", "{@link A ", 100, "}}}}}}}}}}");
ASSERT_NOT_NULL(r);
ASSERT(r->doc_links.count <= 10);
cbm_free_result(r);
/* brackets, backticks and comment openers by the hundred thousand on one
* line: one pass, no token */
r = jt_extract_repeated(CBM_LANG_KOTLIN, "", "[", JT_BIG, "");
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->doc_links.count, 0);
cbm_free_result(r);
r = jt_extract_repeated(CBM_LANG_KOTLIN, "", "` `` ", JT_BIG, " [After]");
ASSERT_NOT_NULL(r);
cbm_free_result(r);
r = jt_extract_repeated(CBM_LANG_KOTLIN, "[Before] ", "<!-- ", JT_BIG, " [Hidden]");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(dm_find_token(r, "Before"));
ASSERT_NULL(dm_find_token(r, "Hidden")); /* the comment never ends */
cbm_free_result(r);
/* bytes that are no UTF-8, a NUL in the middle of the comment */
const char bad[] = "/**\n * {@link Caf\xC3} {@link \xFF\xFE#x} [x\x80y]\n */\nclass C {}\n";
r = dm_extract(bad, CBM_LANG_JAVA, "C.java");
ASSERT_NOT_NULL(r);
cbm_free_result(r);
r = dm_extract(bad, CBM_LANG_KOTLIN, "C.kt");
ASSERT_NOT_NULL(r);
cbm_free_result(r);
const char nul[] = "/**\n * {@link Before} \0 {@link After}\n */\nclass C {}\n";
r = cbm_extract_file(nul, (int)sizeof(nul) - 1, CBM_LANG_JAVA, "p", "C.java", 0, NULL, NULL);
ASSERT_NOT_NULL(r);
cbm_free_result(r);
r = cbm_extract_file(nul, (int)sizeof(nul) - 1, CBM_LANG_KOTLIN, "p", "C.kt", 0, NULL, NULL);
ASSERT_NOT_NULL(r);
cbm_free_result(r);
PASS();
}
TEST(doc_mentions_jvm_java_norm_type) {
static const struct {
const char *in;
const char *want;
} cases[] = {
{"int", "int"},
{"final String s", "String"},
{"java.util.List<String>", "List"},
{"Map.Entry<K, V>", "Entry"},
{"Map<String, List<int[]>>[]", "Map[]"},
{"String...", "String[]"},
{"int[] xs", "int[]"},
{"int xs[]", "int[]"},
{"int [] xs", "int[]"},
{"@NonNull String", "String"},
{"@Size(max = 3) java.lang.String name", "String"},
{" T ", "T"},
{"", "?"},
{"<>", "?"},
};
char out[64];
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
cbm_doclink_java_norm_type(cases[i].in, strlen(cases[i].in), out, sizeof(out));
if (strcmp(out, cases[i].want) != 0) {
printf(" norm_type(\"%s\") = \"%s\", expected \"%s\"\n", cases[i].in, out,
cases[i].want);
}
ASSERT_STR_EQ(out, cases[i].want);
}
/* a name that does not fit is no type: never a truncated one, which
* could equal another type's name */
char small[8];
cbm_doclink_java_norm_type("LongTypeName", strlen("LongTypeName"), small, sizeof(small));
ASSERT_STR_EQ(small, "?");
PASS();
}
/* ── extraction: scope blobs ─────────────────────────────────────── */
/* True when the scope blob has the record `line` (tab-separated, a whole
* line). */
static bool jt_has_line(const char *scope, const char *line) {
size_t n = strlen(line);
for (const char *p = scope; p && *p;) {
const char *nl = strchr(p, '\n');
size_t len = nl ? (size_t)(nl - p) : strlen(p);
if (len == n && memcmp(p, line, n) == 0) {
return true;
}
p = nl ? nl + 1 : NULL;
}
return false;
}
TEST(doc_mentions_jvm_java_scope_blob) {
const char *src =
"package com.acme.core;\n" /* 1 */
"import com.acme.util.Helper;\n" /* 2 */
"import com.acme.util.*;\n" /* 3 */
"import static com.acme.util.Helper.once;\n" /* 4 */
"import static com.acme.util.Helper.*;\n" /* 5 */
"public class Widget<T, U extends Comparable<U>> extends Base implements Runnable {\n" /* 6
*/
" public static final int MAX = 10;\n" /* 7 */
" private int size, other;\n" /* 8 */
" public Widget() {}\n" /* 9 */
" public Widget(int size) {}\n" /* 10 */
" public <V extends Number> int size(V scale, java.util.List<String> xs, int... rest) "
"{ return 0; }\n" /* 11 */
" public void run() {}\n" /* 12 */
" public static class Inner {\n" /* 13 */
" public int deep;\n" /* 14 */
" }\n" /* 15 */
" public enum Mode { FAST, SLOW; public Mode flip() { return this; } }\n" /* 16 */
" public record Point(int x, String y) { public Point { } }\n" /* 17 */
" public interface Shape { int SIDES = 4; double area(); }\n" /* 18 */
" public @interface Marker { String value() default \"\"; }\n" /* 19 */
"}\n";
CBMFileResult *r = dm_extract(src, CBM_LANG_JAVA, "com/acme/core/Widget.java");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(r->doc_scope);
const char *s = r->doc_scope;
ASSERT(strncmp(s, CBM_DOCLINK_JVM_SCOPE_TAG "\n", strlen(CBM_DOCLINK_JVM_SCOPE_TAG) + 1) == 0);
ASSERT(jt_has_line(s, "L\tj"));
ASSERT(jt_has_line(s, "P\tcom.acme.core"));
ASSERT(jt_has_line(s, "I\ts\tHelper\tcom.acme.util.Helper"));
ASSERT(jt_has_line(s, "I\tw\t-\tcom.acme.util"));
ASSERT(jt_has_line(s, "I\tm\tonce\tcom.acme.util.Helper"));
ASSERT(jt_has_line(s, "I\tn\t-\tcom.acme.util.Helper"));
/* a type: lines, kind, path, type parameters (a Java one with its first
* bound, its erasure), supertypes, where its node is, whether it owns it */
ASSERT(jt_has_line(s, "T\t6\t20\tc\tWidget\tT,U:Comparable\tBase|Runnable\t=\t1\t"));
/* a field's node is named by the field; one named like a method of its
* class has <Owner>.<name>#field; a second declarator has no definition */
ASSERT(jt_has_line(s, "M\t7\tv\t\t0\tMAX\t\t-\t\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t8\tv\t\t0\tsize\t\t-\t\tWidget.size#field\t1\t"));
ASSERT(jt_has_line(s, "M\t8\tv\t\t0\tother\t\t-\t\t-\t0\t"));
/* constructors, overloads of one name: one node, owned by the last */
ASSERT(jt_has_line(s, "M\t9\tc\tK\t0\tWidget\t\t\t\t=\t0\t"));
ASSERT(jt_has_line(s, "M\t10\tc\tK\t0\tWidget\t\tint\t\t=\t1\t"));
/* a signature: simple names, no type arguments, an ellipsis is an array */
ASSERT(jt_has_line(s, "M\t11\tc\t\t0\tsize\tV:Number\tV|List|int[]\t\t=\t1\t"));
ASSERT(jt_has_line(s, "T\t13\t15\tc\tWidget.Inner\t\t\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t14\tv\t\t1\tdeep\t\t-\t\t=\t1\t"));
ASSERT(jt_has_line(s, "T\t16\t16\te\tWidget.Mode\t\t\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t16\tv\tn\t2\tFAST\t\t-\t\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t16\tc\t\t2\tflip\t\t\t\t=\t1\t"));
/* a record: its components and its compact constructor (whose signature
* is the components' types) are declared, and have no definition */
ASSERT(jt_has_line(s, "T\t17\t17\tr\tWidget.Point\t\t\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t17\tv\tp\t3\tx\t\tint\t\t-\t0\t"));
ASSERT(jt_has_line(s, "M\t17\tv\tp\t3\ty\t\tString\t\t-\t0\t"));
ASSERT(jt_has_line(s, "M\t17\tc\tK\t3\tPoint\t\t*\t\t-\t0\t"));
/* an interface constant and an annotation element: declared, no node */
ASSERT(jt_has_line(s, "M\t18\tv\t\t4\tSIDES\t\t-\t\t-\t0\t"));
ASSERT(jt_has_line(s, "M\t19\tc\t\t5\tvalue\t\t\t\t-\t0\t"));
/* the persisted form: no line numbers, nothing else changed */
char *portable = cbm_doclink_portable_scope(s);
ASSERT_NOT_NULL(portable);
ASSERT(jt_has_line(portable, "T\t0\t0\tc\tWidget\tT,U:Comparable\tBase|Runnable\t=\t1\t"));
ASSERT(jt_has_line(portable, "M\t0\tc\tK\t0\tWidget\t\tint\t\t=\t1\t"));
ASSERT(jt_has_line(portable, "I\ts\tHelper\tcom.acme.util.Helper"));
cbm_free(CBM_MEM_CLASS_OTHER, portable);
cbm_free_result(r);
PASS();
}
TEST(doc_mentions_jvm_kotlin_scope_blob) {
const char *src =
"package com.acme.kt\n" /* 1 */
"import com.acme.core.Gadget\n" /* 2 */
"import com.acme.far.Far as Farther\n" /* 3 */
"import com.acme.far.*\n" /* 4 */
"class Alias<T>(val delegate: T, name: String) : Base<T>(), Marker {\n" /* 5 */
" val bodyProp: Int = 1\n" /* 6 */
" fun member(param: Int): Int {\n" /* 7 */
" return param\n" /* 8 */
" }\n" /* 9 */
" fun member(param: String): Int {\n" /* 10 */
" return 0\n" /* 11 */
" }\n" /* 12 */
" class Nested\n" /* 13 */
" companion object {\n" /* 14 */
" val LIMIT = 3\n" /* 15 */
" }\n" /* 16 */
" constructor(x: Int) : this(x as T, \"n\")\n" /* 17 */
"}\n" /* 18 */
"fun <T : Base<*>> T.alias(name: String): Int {\n" /* 19 */
" return 1\n" /* 20 */
"}\n" /* 21 */
"fun <T> T.anything(): Int {\n" /* 22 */
" return 1\n" /* 23 */
"}\n" /* 24 */
"val com.acme.far.Far.ext: Int get() = 1\n" /* 25 */
"enum class Color { RED, GREEN }\n" /* 26 */
"typealias Name = com.acme.far.Far\n" /* 27 */
"typealias Handler = (Int) -> Unit\n"; /* 28 */
CBMFileResult *r = dm_extract(src, CBM_LANG_KOTLIN, "com/acme/kt/Alias.kt");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(r->doc_scope);
const char *s = r->doc_scope;
ASSERT(jt_has_line(s, "L\tk"));
ASSERT(jt_has_line(s, "P\tcom.acme.kt"));
ASSERT(jt_has_line(s, "I\ts\tGadget\tcom.acme.core.Gadget"));
ASSERT(jt_has_line(s, "I\ts\tFarther\tcom.acme.far.Far")); /* an import alias */
ASSERT(jt_has_line(s, "I\tw\t-\tcom.acme.far"));
/* a class: its constructor's parameter names are its own (the last
* field; the persisted form drops them) */
ASSERT(jt_has_line(s, "T\t5\t18\tc\tAlias\tT\tBase|Marker\t=\t1\tdelegate,name"));
/* a `val` constructor parameter is a property without a definition */
ASSERT(jt_has_line(s, "M\t5\tv\tp\t0\tdelegate\t\t-\t\t-\t0\t"));
/* a class-body property: its node is named below the FILE's module */
ASSERT(jt_has_line(s, "M\t6\tv\t\t0\tbodyProp\t\t-\t\tbodyProp\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"));
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 */
ASSERT(jt_has_line(s, "M\t19\tc\t\t-\talias\tT\tR=T|String\tBase\t=\t1\tname"));
ASSERT(jt_has_line(s, "M\t22\tc\t\t-\tanything\tT\tR=T\t?\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t25\tv\t\t-\text\t\tR=com.acme.far.Far\tcom.acme.far.Far\t=\t1\t"));
ASSERT(jt_has_line(s, "T\t26\t26\te\tColor\t\t\t=\t1\t"));
ASSERT(jt_has_line(s, "M\t26\tv\tn\t3\tRED\t\t-\t\t-\t0\t"));
/* a type alias: the type it names stands where a supertype does */
ASSERT(jt_has_line(s, "T\t27\t27\tt\tName\t\tcom.acme.far.Far\t=\t1\t"));
ASSERT(jt_has_line(s, "T\t28\t28\tt\tHandler\t\t?\t=\t1\t"));
char *portable = cbm_doclink_portable_scope(s);
ASSERT_NOT_NULL(portable);
ASSERT(jt_has_line(portable, "T\t0\t0\tc\tAlias\tT\tBase|Marker\t=\t1\t"));
ASSERT(jt_has_line(portable, "M\t0\tc\t\t0\tmember\t\tString\t\t=\t1\t"));
cbm_free(CBM_MEM_CLASS_OTHER, portable);
cbm_free_result(r);
PASS();
}
/* ── the published database ──────────────────────────────────────── */
/* A fixture repository in a temporary directory, and its index. */
typedef struct {
char tmp[256];
char repo[400];
char db[512];
} jt_repo_t;
static bool jt_begin(jt_repo_t *r, const char *tag) {
snprintf(r->tmp, sizeof(r->tmp), "/tmp/cbm_dm_jvm_%s_XXXXXX", tag);
if (!cbm_mkdtemp(r->tmp)) {
return false;
}
snprintf(r->repo, sizeof(r->repo), "%s/repo", r->tmp);
snprintf(r->db, sizeof(r->db), "%s/index.db", r->tmp);
return true;
}
static void jt_write(const jt_repo_t *r, const char *rel, const char *content) {
th_write_file(TH_PATH(r->repo, rel), content);
}
static void jt_end(jt_repo_t *r) {
dm_unlink_db(r->db);
th_rmtree(r->tmp);
}
/* Number of MENTIONS edges between the definitions whose qualified names end
* with these paths; their properties are left in jt_props. */
static char jt_props[512];
static int jt_edge(const char *db, const char *src, const char *tgt) {
int n = 0;
dm_edge(db, src, tgt, jt_props, sizeof(jt_props), &n);
return n;
}
/* Reason of the unresolved row with this raw text in this file; "" when the
* reference has no row. */
static const char *jt_reason(const char *db, const char *rel, const char *raw) {
static char reason[64];
dm_row(db, rel, raw, reason, sizeof(reason), NULL, 0);
return reason;
}
/* ── Java: every family, every reason ────────────────────────────── */
#define JT_GADGET \
"package a;\n" \
"public class Gadget {\n" \
" public int count;\n" \
" public static final String NAME = \"g\";\n" \
" public void spin() {}\n" \
" public void spin(int times) {}\n" \
" public void stop() {}\n" \
"}\n" \
"class Gr\xC3\xB6\xC3\x9F" \
"e {\n" \
" public int l\xC3\xA4nge = 1;\n" \
"}\n"
#define JT_HELPER \
"package b;\n" \
"public class Helper {\n" \
" public static final int MASK = 255;\n" \
" public void run(int n, String s) {}\n" \
"}\n"
#define JT_SHAPES \
"package a;\n" \
"public class Shapes {\n" \
" public record Point(int x, int y) {}\n" \
" public interface Shape { int SIDES = 4; double area(); }\n" \
" public @interface Marker { String value() default \"\"; }\n" \
" public static class Sized {\n" \
" private int size;\n" \
" public int size() { return size; }\n" \
" }\n" \
" public static class Made {\n" \
" public Made(int n) {}\n" \
" }\n" \
"}\n"
#define JT_WIDGET \
"package a;\n" /* 1 */ \
"\n" /* 2 */ \
"import b.Helper;\n" /* 3 */ \
"import a.env.Config;\n" /* 4 */ \
"import java.util.List;\n" /* 5 */ \
"import b.Generated;\n" /* 6 */ \
"/**\n" /* 7 */ \
" * {@link Gadget} {@linkplain Gadget#spin(int) spin} {@link Helper#run(int, String)}\n" /* 8 \
*/ \
" * {@value Helper#MASK} {@link List} {@link String} {@link T} {@link Gadget#spin}\n" /* 9 */ \
" * {@link NoSuchType} {@link Gadget#nope} {@link GadgetTests} {@link Config}\n" /* 10 */ \
" * {@link a.env.Other} {@link b} {@link Shapes.Point#x} {@link Shapes.Shape#SIDES}\n" /* 11 \
*/ \
" * {@link Shapes.Marker#value()} {@link Shapes.Sized#size} {@link Shapes.Sized#size()}\n" /* 12 */ \
" * {@link Gadget#Gadget()} {@link Shapes.Made#Made(int)} {@link Shapes.Made#Made(String)}\n" /* 13 */ \
" * {@link Gadget#a#b} {@link Gadget#spin(int} {@link Gr\xC3\xB6\xC3\x9F" \
"e#l\xC3\xA4nge}\n" /* 14 */ \
" * {@link org.outside.Thing} {@link b.Missing} {@link Gadget.stop} {@link Generated}\n" /* 15 \
*/ \
" * {@link Gadget#spin(int, int)} {@link Gadget#spin(long)} {@link Shapes#Sized}\n" /* 16 */ \
" * {@value Gadget#stop}\n" /* 17 */ \
" * @see Gadget#count\n" /* 18 */ \
" * @see <a href=\"https://example.org\">site</a>\n" /* 19 */ \
" * @throws CustomError never\n" /* 20 */ \
" * @throws java.io.IOException bad\n" /* 21 */ \
" */\n" /* 22 */ \
"public class Widget<T> {\n" /* 23 */ \
"}\n"
TEST(doc_mentions_jvm_java_families_and_reasons) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jfam"));
jt_write(&r, "src/main/java/a/Gadget.java", JT_GADGET);
jt_write(&r, "src/main/java/a/CustomError.java",
"package a;\npublic class CustomError extends RuntimeException {}\n");
jt_write(&r, "src/main/java/b/Helper.java", JT_HELPER);
jt_write(&r, "src/main/java/a/Shapes.java", JT_SHAPES);
jt_write(&r, "src/main/java/a/Widget.java", JT_WIDGET);
/* a real package in a layout discovery still skips by name: a plain
* src/<pkg>/ tree (the open part of #2661); below src/<set>/<lang>/ the
* same package is kept since #2724 */
jt_write(&r, "src/a/env/Config.java", "package a.env;\npublic class Config {}\n");
jt_write(&r, "src/test/java/a/GadgetTests.java",
"package a;\n/** Tests {@link Gadget}. */\npublic class GadgetTests {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
const char *w = "src/main/java/a/Widget.java";
/* link: a type of the file's package, unique at its first level */
ASSERT_EQ(jt_edge(db, "a.Widget", "a.Gadget"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"via\":\"doc_comment\""));
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"link\""));
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"unique\""));
ASSERT_NOT_NULL(strstr(jt_props, "\"line\":8"));
/* linkplain, with a signature that selects one overload (overloads of a
* type share a node) */
ASSERT_EQ(jt_edge(db, "a.Widget", "a.Gadget.spin"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"linkplain\""));
/* ... and nothing is relaxed: another parameter count, another parameter
* type, a nested type named as a member and a method named as a value
* bind nothing (that edge counts the one reference) */
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":1"));
ASSERT_STR_EQ(jt_reason(db, w, "Gadget#spin(int, int)"), "missing");
ASSERT_STR_EQ(jt_reason(db, w, "Gadget#spin(long)"), "missing");
ASSERT_STR_EQ(jt_reason(db, w, "Shapes#Sized"), "missing");
ASSERT_EQ(jt_edge(db, "a.Widget", "a.Shapes.Sized"), 0);
ASSERT_STR_EQ(jt_reason(db, w, "Gadget#stop"), "missing");
/* a type named by a single import: exact */
ASSERT_EQ(jt_edge(db, "a.Widget", "b.Helper.run"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"exact\""));
/* value: a field, whose node is named by its declarator */
ASSERT_EQ(jt_edge(db, "a.Widget", "b.Helper.MASK"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"value\""));
/* see */
ASSERT_EQ(jt_edge(db, "a.Widget", "a.Gadget.count"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"see\""));
/* throws */
ASSERT_EQ(jt_edge(db, "a.Widget", "a.CustomError"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"throws\""));
/* external: a single import of a package the repository does not declare,
* java.lang, a qualified name of such a package, a URL */
ASSERT_STR_EQ(jt_reason(db, w, "List"), "external");
ASSERT_STR_EQ(jt_reason(db, w, "String"), "external");
ASSERT_STR_EQ(jt_reason(db, w, "java.io.IOException"), "external");
ASSERT_STR_EQ(jt_reason(db, w, "org.outside.Thing"), "external");
ASSERT_STR_EQ(jt_reason(db, w, "https://example.org"), "external");
/* local: the class's own type parameter is neither an edge nor a row */
ASSERT_STR_EQ(jt_reason(db, w, "T"), "");
/* ambiguous: an overload group named without a parameter list */
ASSERT_STR_EQ(jt_reason(db, w, "Gadget#spin"), "ambiguous");
/* missing: nothing of that name anywhere; a member the type does not
* have; a type the repository's own package does not declare */
ASSERT_STR_EQ(jt_reason(db, w, "NoSuchType"), "missing");
ASSERT_STR_EQ(jt_reason(db, w, "Gadget#nope"), "missing");
ASSERT_STR_EQ(jt_reason(db, w, "b.Missing"), "missing");
/* `Type.member` is no Javadoc reference (it wants a '#'): no binding of
* the method that happens to have that name */
ASSERT_STR_EQ(jt_reason(db, w, "Gadget.stop"), "missing");
ASSERT_EQ(jt_edge(db, "a.Widget", "a.Gadget.stop"), 0);
/* test_only_target: product code names a declaration only tests hold */
ASSERT_STR_EQ(jt_reason(db, w, "GadgetTests"), "test_only_target");
ASSERT_EQ(jt_edge(db, "a.Widget", "a.GadgetTests"), 0);
/* ... while test code binds product code */
ASSERT_EQ(jt_edge(db, "a.GadgetTests", "a.Gadget"), 1);
/* not_indexed: a package of the repository in a layout discovery still
* skips by name, named by an import and by its qualified name */
ASSERT_STR_EQ(jt_reason(db, w, "Config"), "not_indexed");
ASSERT_STR_EQ(jt_reason(db, w, "a.env.Other"), "not_indexed");
/* graph_gap: a package (no node), a record component, an interface
* constant and an annotation element (declared, no definition), a
* constructor the class has without declaring it */
ASSERT_STR_EQ(jt_reason(db, w, "b"), "graph_gap");
/* ... and a type the file imports from a package of the repository that
* no indexed file declares (generated code): the import vouches for it,
* where the same name written out without one is missing (b.Missing) */
ASSERT_STR_EQ(jt_reason(db, w, "Generated"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, w, "Shapes.Point#x"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, w, "Shapes.Shape#SIDES"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, w, "Shapes.Marker#value()"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, w, "Gadget#Gadget()"), "graph_gap");
/* a field named like its accessor has a node of its own
* (<Owner>.<name>#field): the field reference binds it */
ASSERT_STR_EQ(jt_reason(db, w, "Shapes.Sized#size"), "");
ASSERT_EQ(jt_edge(db, "a.Widget", "a.Shapes.Sized.size#field"), 1);
/* ... while the accessor itself, named as a method, binds */
ASSERT_EQ(jt_edge(db, "a.Widget", "a.Shapes.Sized.size"), 1);
/* a constructor reference: the declared one binds, another signature is
* missing (never the constructor that is there) */
ASSERT_EQ(jt_edge(db, "a.Widget", "a.Shapes.Made.Made"), 1);
ASSERT_STR_EQ(jt_reason(db, w, "Shapes.Made#Made(String)"), "missing");
/* unparseable: two '#', a parameter list that does not close */
ASSERT_STR_EQ(jt_reason(db, w, "Gadget#a#b"), "unparseable");
ASSERT_STR_EQ(jt_reason(db, w, "Gadget#spin(int"), "unparseable");
/* identifiers are Unicode */
ASSERT_EQ(jt_edge(db, "a.Widget", "l\xC3\xA4nge"), 1);
jt_end(&r);
PASS();
}
/* An oversized reference is unparseable in both languages: the resolver
* never resolves the prefix that was kept. */
TEST(doc_mentions_jvm_oversized_reference) {
jt_repo_t r;
ASSERT(jt_begin(&r, "big"));
enum { JT_LONG = 3000 };
char *java = malloc(JT_LONG + 256);
char *kotlin = malloc(JT_LONG + 256);
ASSERT_NOT_NULL(java);
ASSERT_NOT_NULL(kotlin);
size_t w = (size_t)snprintf(java, 128, "package a;\n/** {@link Target");
memset(java + w, 'x', JT_LONG);
snprintf(java + w + JT_LONG, 100, "} */\npublic class Big {}\n");
w = (size_t)snprintf(kotlin, 128, "package a\n/** [Target");
memset(kotlin + w, 'x', JT_LONG);
snprintf(kotlin + w + JT_LONG, 100, "] */\nclass BigK\n");
jt_write(&r, "src/main/java/a/Big.java", java);
jt_write(&r, "src/main/kotlin/a/BigK.kt", kotlin);
/* the prefix that survives the cut names a real class */
jt_write(&r, "src/main/java/a/Target.java", "package a;\npublic class Target {}\n");
free(java);
free(kotlin);
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
ASSERT_EQ(dm_count(r.db, "SELECT COUNT(*) FROM edges WHERE type = 'MENTIONS'"), 0);
ASSERT_EQ(dm_count(r.db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE reason = "
"'unparseable'"),
2);
ASSERT_EQ(dm_count(r.db, "SELECT COUNT(*) FROM doc_link_unresolved"), 2);
jt_end(&r);
PASS();
}
/* ── Java: the lookup order, level by level ──────────────────────── */
/* One class per boundary of javac's order for a simple type name. Each binds
* at the nearer level although the same name exists one level further out. */
TEST(doc_mentions_jvm_java_type_levels) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jlvl"));
jt_write(&r, "src/main/java/far/Thing.java",
"package far;\npublic class Thing { public void go() {} }\n");
jt_write(&r, "src/main/java/far/Tool.java", "package far;\npublic class Tool {}\n");
jt_write(&r, "src/main/java/far/Dup.java", "package far;\npublic class Dup {}\n");
jt_write(&r, "src/main/java/far/Iterable.java", "package far;\npublic class Iterable {}\n");
jt_write(&r, "src/main/java/near/Dup.java", "package near;\npublic class Dup {}\n");
jt_write(&r, "src/main/java/lv/Thing.java", "package lv;\npublic class Thing {}\n");
jt_write(&r, "src/main/java/lv/Tool.java", "package lv;\npublic class Tool {}\n");
/* 1. a method's own type variable, before a member type of its class */
jt_write(&r, "src/main/java/lv/B1.java",
"package lv;\n"
"public class B1 {\n"
" public static class T {}\n"
" /** {@link T} */\n"
" public <T> void m(T x) {}\n"
"}\n");
/* 2. a class's own member type, before its type variable of that name */
jt_write(&r, "src/main/java/lv/B2.java",
"package lv;\n"
"public class B2<Part> {\n"
" public static class Part {}\n"
" /** {@link Part} */\n"
" public void m() {}\n"
"}\n");
/* 3. a class's type variable, before a member type it inherits */
jt_write(&r, "src/main/java/lv/B3.java",
"package lv;\n"
"public class B3<Item> extends B3Base {\n"
" /** {@link Item} */\n"
" public void m() {}\n"
"}\n"
"class B3Base {\n"
" public static class Item {}\n"
"}\n");
/* 4. what the inner type inherits, before a member type of the type
* around it */
jt_write(&r, "src/main/java/lv/B4.java",
"package lv;\n"
"public class B4 {\n"
" public static class Piece {}\n"
" public static class Inner extends B4Base {\n"
" /** {@link Piece} */\n"
" public void m() {}\n"
" }\n"
"}\n"
"class B4Base {\n"
" public static class Piece {}\n"
"}\n");
/* 5. a member type of an enclosing type, before a single import */
jt_write(&r, "src/main/java/lv/B5.java",
"package lv;\n"
"import far.Thing;\n"
"public class B5 {\n"
" public static class Thing {}\n"
" /** {@link Thing} */\n"
" public void m() {}\n"
"}\n");
/* 6. a single import, before the file's package */
jt_write(&r, "src/main/java/lv/B6.java",
"package lv;\n"
"import far.Thing;\n"
"/** {@link Thing} */\n"
"public class B6 {}\n");
/* 7. the file's package, before an on-demand import */
jt_write(&r, "src/main/java/lv/B7.java",
"package lv;\n"
"import far.*;\n"
"/** {@link Tool} */\n"
"public class B7 {}\n");
/* 8. on-demand imports are one level, java.lang among them: two
* suppliers of a name are ambiguous, never a pick */
jt_write(&r, "src/main/java/lv/B8.java",
"package lv;\n"
"import far.*;\n"
"import near.*;\n"
"/** {@link Dup} {@link Iterable} {@link Runnable} */\n"
"public class B8 {}\n");
/* 9. a qualified name: the head through the scope, else from the root */
jt_write(&r, "src/main/java/lv/B9.java",
"package lv;\n"
"/**\n"
" * {@link B5.Thing} {@link far.Thing} {@link B5.Nope} {@link far.Thing.go}\n"
" * {@link lv.B4.Inner}\n"
" */\n"
"public class B9 {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* 1: local, neither an edge nor a row */
ASSERT_EQ(dm_mentions_from(db, "lv.B1.m"), 0);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/lv/B1.java", "T"), "");
/* 2: the member type */
ASSERT_EQ(jt_edge(db, "lv.B2.m", "lv.B2.Part"), 1);
/* 3: local, not the inherited member type */
ASSERT_EQ(dm_mentions_from(db, "lv.B3.m"), 0);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/lv/B3.java", "Item"), "");
/* 4: the inherited one, not the outer type's */
ASSERT_EQ(jt_edge(db, "lv.B4.Inner.m", "lv.B4Base.Piece"), 1);
ASSERT_EQ(jt_edge(db, "lv.B4.Inner.m", "lv.B4.Piece"), 0);
/* 5: the member type, not the import */
ASSERT_EQ(jt_edge(db, "lv.B5.m", "lv.B5.Thing"), 1);
ASSERT_EQ(jt_edge(db, "lv.B5.m", "far.Thing"), 0);
/* 6: the import (exact), not the package's type */
ASSERT_EQ(jt_edge(db, "lv.B6", "far.Thing"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"exact\""));
ASSERT_EQ(jt_edge(db, "lv.B6", "lv.Thing"), 0);
/* 7: the package's type (unique), not the on-demand import's */
ASSERT_EQ(jt_edge(db, "lv.B7", "lv.Tool"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"unique\""));
ASSERT_EQ(jt_edge(db, "lv.B7", "far.Tool"), 0);
/* 8 */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/lv/B8.java", "Dup"), "ambiguous");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/lv/B8.java", "Iterable"), "ambiguous");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/lv/B8.java", "Runnable"), "external");
ASSERT_EQ(dm_mentions_from(db, "lv.B8"), 0);
/* 9 */
ASSERT_EQ(jt_edge(db, "lv.B9", "lv.B5.Thing"), 1);
ASSERT_EQ(jt_edge(db, "lv.B9", "far.Thing"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"exact\""));
ASSERT_EQ(jt_edge(db, "lv.B9", "lv.B4.Inner"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/lv/B9.java", "B5.Nope"), "missing");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/lv/B9.java", "far.Thing.go"), "missing");
ASSERT_EQ(jt_edge(db, "lv.B9", "far.Thing.go"), 0);
jt_end(&r);
PASS();
}
/* Member references: the order of the levels, the kind rule, and no
* relaxation across overloads, arities, kinds or constructors. */
TEST(doc_mentions_jvm_java_member_levels) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jmem"));
jt_write(&r, "src/main/java/far/Thing.java",
"package far;\npublic class Thing { public static void go() {} }\n");
/* 1. the type's own method, before the one it inherits */
jt_write(&r, "src/main/java/mv/M1.java",
"package mv;\n"
"public class M1 extends M1Base {\n"
" public void go() {}\n"
" /** {@link #go()} */\n"
" public void m() {}\n"
"}\n"
"class M1Base {\n"
" public void go() {}\n"
"}\n");
/* 2. what the inner type inherits, before the type around it */
jt_write(&r, "src/main/java/mv/M2.java",
"package mv;\n"
"public class M2 {\n"
" public void go() {}\n"
" public class Inner extends M2Base {\n"
" /** {@link #go()} */\n"
" public void m() {}\n"
" }\n"
"}\n"
"class M2Base {\n"
" public void go() {}\n"
"}\n");
/* 3. a FIELD: javadoc looks in the enclosing types before the
* supertypes, javac scoping the other way round. Where both have one
* the reference binds neither; with only the inherited one it binds */
jt_write(&r, "src/main/java/mv/M3.java",
"package mv;\n"
"public class M3 {\n"
" public int level = 1;\n"
" public class Inner extends M3Base {\n"
" /** {@link #level} {@link #depth} */\n"
" public void m() {}\n"
" }\n"
"}\n"
"class M3Base {\n"
" public int level = 2;\n"
" public int depth = 3;\n"
"}\n");
/* 4. `Type#m` is javadoc's search from the type named: its supertypes,
* then the types around it (JDK source, JavacTrees.searchMethod) */
jt_write(&r, "src/main/java/mv/M4.java",
"package mv;\n"
"public class M4 {\n"
" public void outer() {}\n"
" public static class Inner {}\n"
"}\n"
"/** {@link M4.Inner#outer()} */\n"
"class M4User {}\n");
/* 5. kinds: a field comes first when its class is the method's or below
* it; for a field ABOVE the method javadoc links the method */
jt_write(&r, "src/main/java/mv/M5.java",
"package mv;\n"
"public class M5 extends M5Base {\n"
" public int size = 0;\n"
" public int size() { return size; }\n"
" public int total() { return 0; }\n"
" /** {@link #size} {@link #size()} {@link #total} {@link #total()} */\n"
" public void m() {}\n"
"}\n"
"class M5Base {\n"
" public int total = 1;\n"
"}\n");
/* 6. signatures */
jt_write(&r, "src/main/java/mv/M6.java",
"package mv;\n"
"public class M6 extends M6Base {\n"
" public void put(int a) {}\n"
" public <T> void take(T item) {}\n"
" public <N extends Number> void add(N n) {}\n"
" public static M6 of(int... xs) { return null; }\n"
" /**\n"
" * {@link #put(int)} {@link #put(long)} {@link #put(String)} {@link #put()}\n"
" * {@link #take(Object)} {@link #take(String)} {@link #add(Number)}\n"
" * {@link #add(Object)} {@link #of(int...)} {@link #of(int[])} {@link #of(int)}\n"
" */\n"
" public void m() {}\n"
"}\n"
"class M6Base {\n"
" public void put(long a) {}\n"
"}\n");
/* 7. constructors: of the named type only, none inherited */
jt_write(&r, "src/main/java/mv/M7.java",
"package mv;\n"
"public class M7 {\n"
" public M7(int n) {}\n"
" /** {@link #M7(int)} {@link #M7(String)} {@link M7Sub#M7Sub(int)} */\n"
" public void m() {}\n"
"}\n"
"class M7Sub extends M7 {\n"
" M7Sub() { super(1); }\n"
"}\n");
/* 8. a record: its component's accessor is its own method (no node);
* the interface method of that name does not stand in */
jt_write(&r, "src/main/java/mv/M8.java",
"package mv;\n"
"/** {@link M8#x()} {@link M8#x} {@link M8#M8(int, String)} {@link M8#M8(int)} */\n"
"public record M8(int x, String y) implements M8Shape {}\n"
"interface M8Shape {\n"
" int x();\n"
"}\n");
/* 9. among supertypes a class's method comes before an interface's,
* wherever the interface sits */
jt_write(&r, "src/main/java/mv/M9.java",
"package mv;\n"
"public class M9 extends M9Mid implements M9Api {\n"
" /** {@link #name()} */\n"
" public void m() {}\n"
"}\n"
"interface M9Api {\n"
" String name();\n"
"}\n"
"class M9Mid extends M9Base {}\n"
"class M9Base {\n"
" public String name() { return null; }\n"
"}\n");
/* 10. javadoc reads a bare identifier that is no type as `#identifier`;
* not where an on-demand import of an unknown package may hold a type
* of that name, which would come first */
jt_write(&r, "src/main/java/mv/M10.java",
"package mv;\n"
"public class M10 {\n"
" public void helper() {}\n"
" /** {@link helper} */\n"
" public void m() {}\n"
"}\n");
jt_write(&r, "src/main/java/mv/M10b.java",
"package mv;\n"
"import org.outside.*;\n"
"public class M10b {\n"
" public void helper() {}\n"
" /** {@link helper} */\n"
" public void m() {}\n"
"}\n");
/* 11. a static import is no level for a member reference */
jt_write(&r, "src/main/java/mv/M11.java",
"package mv;\n"
"import static far.Thing.go;\n"
"public class M11 {\n"
" /** {@link #go()} */\n"
" public void m() {}\n"
"}\n");
/* 12. {@value} names a field: a method of that name is no target */
jt_write(&r, "src/main/java/mv/M12.java",
"package mv;\n"
"public class M12 {\n"
" public static final int LIMIT = 3;\n"
" public int count() { return 0; }\n"
" /** {@value #LIMIT} {@value #count} */\n"
" public void m() {}\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* 1 */
ASSERT_EQ(jt_edge(db, "mv.M1.m", "mv.M1.go"), 1);
ASSERT_EQ(jt_edge(db, "mv.M1.m", "mv.M1Base.go"), 0);
/* 2 */
ASSERT_EQ(jt_edge(db, "mv.M2.Inner.m", "mv.M2Base.go"), 1);
ASSERT_EQ(jt_edge(db, "mv.M2.Inner.m", "mv.M2.go"), 0);
/* 3 */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M3.java", "#level"), "ambiguous");
ASSERT_EQ(jt_edge(db, "mv.M3.Inner.m", "mv.M3Base.depth"), 1);
ASSERT_EQ(dm_mentions_from(db, "mv.M3.Inner.m"), 1);
/* 4 */
ASSERT_EQ(jt_edge(db, "mv.M4User", "mv.M4.outer"), 1);
ASSERT_EQ(dm_mentions_from(db, "mv.M4User"), 1);
/* 5 */
ASSERT_EQ(jt_edge(db, "mv.M5.m", "mv.M5.size"), 1); /* the field */
ASSERT_EQ(jt_edge(db, "mv.M5.m", "mv.M5.size"), 1); /* the method, by its list */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M5.java", "#total"), ""); /* no row */
ASSERT_EQ(jt_edge(db, "mv.M5.m", "mv.M5.total"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":2")); /* `#total` and `#total()` */
ASSERT_EQ(jt_edge(db, "mv.M5.m", "mv.M5Base.total"), 0);
/* 6 */
ASSERT_EQ(jt_edge(db, "mv.M6.m", "mv.M6.put"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":1")); /* put(int) only */
ASSERT_EQ(jt_edge(db, "mv.M6.m", "mv.M6Base.put"), 1); /* put(long): the supertype's */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M6.java", "#put(String)"), "missing");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M6.java", "#put()"), "missing");
ASSERT_EQ(jt_edge(db, "mv.M6.m", "mv.M6.take"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":1")); /* take(Object), the erasure */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M6.java", "#take(String)"), "missing");
ASSERT_EQ(jt_edge(db, "mv.M6.m", "mv.M6.add"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M6.java", "#add(Object)"), "missing");
ASSERT_EQ(jt_edge(db, "mv.M6.m", "mv.M6.of"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":2")); /* of(int...) and of(int[]) */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M6.java", "#of(int)"), "missing");
/* 7 */
ASSERT_EQ(jt_edge(db, "mv.M7.m", "mv.M7.M7"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M7.java", "#M7(String)"), "missing");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M7.java", "M7Sub#M7Sub(int)"), "missing");
/* 8 */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M8.java", "M8#x()"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M8.java", "M8#x"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M8.java", "M8#M8(int, String)"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M8.java", "M8#M8(int)"), "missing");
ASSERT_EQ(jt_edge(db, "mv.M8", "mv.M8Shape.x"), 0);
/* 9 */
ASSERT_EQ(jt_edge(db, "mv.M9.m", "mv.M9Base.name"), 1);
ASSERT_EQ(jt_edge(db, "mv.M9.m", "mv.M9Api.name"), 0);
/* 10 */
ASSERT_EQ(jt_edge(db, "mv.M10.m", "mv.M10.helper"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M10b.java", "helper"), "external");
ASSERT_EQ(dm_mentions_from(db, "mv.M10b.m"), 0);
/* 11 */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M11.java", "#go()"), "missing");
ASSERT_EQ(dm_mentions_from(db, "mv.M11.m"), 0);
/* 12 */
ASSERT_EQ(jt_edge(db, "mv.M12.m", "mv.M12.LIMIT"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/mv/M12.java", "#count"), "missing");
ASSERT_EQ(jt_edge(db, "mv.M12.m", "mv.M12.count"), 0);
jt_end(&r);
PASS();
}
/* ── Kotlin: every family, every reason ──────────────────────────── */
#define JT_K_GADGET \
"package k\n" \
"\n" \
"class Gadget {\n" \
" val count: Int = 0\n" \
" fun spin() {}\n" \
" fun turn(times: Int) {}\n" \
" fun turn(name: String) {}\n" \
" val both: Int = 1\n" \
" fun both() {}\n" \
" companion object Factory {\n" \
" fun create(): Gadget = Gadget()\n" \
" }\n" \
"}\n" \
"enum class Color { RED, GREEN }\n" \
"class CustomError : RuntimeException()\n" \
"typealias Alias = Gadget\n"
#define JT_K_WIDGET \
"package k\n" /* 1 */ \
"\n" /* 2 */ \
"import far.Helper\n" /* 3 */ \
"import java.sql.SQLException\n" /* 4 */ \
"\n" /* 5 */ \
"/**\n" /* 6 */ \
" * A [Gadget], [the count][Gadget.count], [Gadget.spin], [Helper], [String], [T],\n" /* 7 */ \
" * [name], [Gadget.turn], [Gadget.both], [Color.RED], [NoSuchThing], [Gadget.nope],\n" /* 8 \
*/ \
" * [GadgetTests], [Gadget.create], [Alias], [Alias.spin], [far.Helper.run], [far.sub],\n" /* 9 */ \
" * [org.outside.Thing], [far.Missing], [k.env.Config]\n" /* 10 */ \
" * @see Gadget.spin\n" /* 11 */ \
" * @see NoSuchThing\n" /* 12 */ \
" * @throws CustomError never\n" /* 13 */ \
" * @throws SQLException bad\n" /* 14 */ \
" * @throws IllegalStateException worse\n" /* 15 */ \
" * @sample k.samples.widgetSample\n" /* 16 */ \
" * @sample k.samples.noSuchSample\n" /* 17 */ \
" * @see Gadget..spin\n" /* 18 */ \
" */\n" /* 19 */ \
"class Widget<T>(val name: String)\n"
TEST(doc_mentions_jvm_kotlin_families_and_reasons) {
jt_repo_t r;
ASSERT(jt_begin(&r, "kfam"));
jt_write(&r, "src/main/kotlin/k/Gadget.kt", JT_K_GADGET);
jt_write(&r, "src/main/kotlin/k/Widget.kt", JT_K_WIDGET);
jt_write(&r, "src/main/kotlin/far/Helper.kt",
"package far\n\nclass Helper {\n fun run() {}\n}\n");
jt_write(&r, "src/main/kotlin/far/sub/Deep.kt", "package far.sub\n\nclass Deep\n");
/* a real package in a layout discovery still skips by name: a plain
* src/<pkg>/ tree (the open part of #2661); below src/<set>/<lang>/ the
* same package is kept since #2724 */
jt_write(&r, "src/k/env/Config.kt", "package k.env\n\nclass Config\n");
jt_write(&r, "src/test/kotlin/k/GadgetTests.kt", "package k\n\nclass GadgetTests\n");
jt_write(&r, "src/test/kotlin/k/samples/Samples.kt",
"package k.samples\n\nfun widgetSample() {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
const char *w = "src/main/kotlin/k/Widget.kt";
const char *widget = "k.Widget.Widget";
/* kdoc_link: a type of the file's package */
ASSERT_EQ(jt_edge(db, widget, "k.Gadget.Gadget"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"kdoc_link\""));
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"unique\""));
ASSERT_NOT_NULL(strstr(jt_props, "\"line\":7"));
/* [text][Type.member]: a property (its node is named below the file) */
ASSERT_EQ(jt_edge(db, widget, "k.Gadget.count"), 1);
/* a member function, named twice: once in the text, once by @see */
ASSERT_EQ(jt_edge(db, widget, "k.Gadget.Gadget.spin"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":3")); /* ... and once through the alias */
/* an explicit import: exact */
ASSERT_EQ(jt_edge(db, widget, "far.Helper.Helper"), 1);
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);
/* a type alias has a node of its own */
ASSERT_EQ(jt_edge(db, widget, "k.Gadget.Alias"), 1);
/* throws */
ASSERT_EQ(jt_edge(db, widget, "k.Gadget.CustomError"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"throws\""));
/* sample: a function in test sources binds although Widget is product
* code (samples live in tests) */
ASSERT_EQ(jt_edge(db, widget, "k.samples.Samples.widgetSample"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"sample\""));
/* external: a default import, an import of a package the repository
* does not declare, a qualified name of such a package */
ASSERT_STR_EQ(jt_reason(db, w, "String"), "external");
ASSERT_STR_EQ(jt_reason(db, w, "SQLException"), "external");
ASSERT_STR_EQ(jt_reason(db, w, "IllegalStateException"), "external");
ASSERT_STR_EQ(jt_reason(db, w, "org.outside.Thing"), "external");
/* local: the class's own type parameter and constructor parameter */
ASSERT_STR_EQ(jt_reason(db, w, "T"), "");
ASSERT_STR_EQ(jt_reason(db, w, "name"), "");
/* ambiguous: an overload group; a property and a function of one name */
ASSERT_STR_EQ(jt_reason(db, w, "Gadget.turn"), "ambiguous");
ASSERT_STR_EQ(jt_reason(db, w, "Gadget.both"), "ambiguous");
/* graph_gap: an enum entry (declared, no node), a package (no node) */
ASSERT_STR_EQ(jt_reason(db, w, "Color.RED"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, w, "far.sub"), "graph_gap");
/* test_only_target */
ASSERT_STR_EQ(jt_reason(db, w, "GadgetTests"), "test_only_target");
/* not_indexed: a package of the repository in a layout discovery still skips by name */
ASSERT_STR_EQ(jt_reason(db, w, "k.env.Config"), "not_indexed");
/* unparseable: a path with an empty name in it */
ASSERT_STR_EQ(jt_reason(db, w, "Gadget..spin"), "unparseable");
/* a `[name]` that names nothing is Markdown text to KDoc: no row. The
* block tags name their subject: missing */
ASSERT_STR_EQ(jt_reason(db, w, "Gadget.nope"), "");
ASSERT_STR_EQ(jt_reason(db, w, "far.Missing"), "");
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE raw = 'NoSuchThing'"),
1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE raw = 'NoSuchThing' "
"AND syntax = 'see' AND reason = 'missing' AND line = 12"),
1);
ASSERT_STR_EQ(jt_reason(db, w, "k.samples.noSuchSample"), "missing");
jt_end(&r);
PASS();
}
/* ── Kotlin: the lookup order, level by level ────────────────────── */
TEST(doc_mentions_jvm_kotlin_levels) {
jt_repo_t r;
ASSERT(jt_begin(&r, "klvl"));
jt_write(&r, "src/main/kotlin/far/Thing.kt", "package far\n\nclass Thing\n");
jt_write(&r, "src/main/kotlin/far/Tool.kt", "package far\n\nclass Tool\n");
jt_write(&r, "src/main/kotlin/far/List.kt", "package far\n\nclass List\n");
jt_write(&r, "src/main/kotlin/far/Funs.kt", "package far\n\nfun topFun() {}\n");
jt_write(&r, "src/main/kotlin/far/Ext.kt", "package far\n\nfun Thing.shine() {}\n");
jt_write(&r, "src/main/kotlin/kl/Thing.kt", "package kl\n\nclass Thing\n");
jt_write(&r, "src/main/kotlin/kl/Tool.kt", "package kl\n\nclass Tool\n");
/* 1. the definition's own parameter, before a member of its class */
jt_write(&r, "src/main/kotlin/kl/K1.kt",
"package kl\n"
"\n"
"class K1 {\n"
" val label: Int = 0\n"
" /** The [label]. */\n"
" fun m(label: String) {}\n"
"}\n");
/* 2. a constructor parameter is the class's own in the class's doc. In
* a member's doc a `val` one is a property of the class (declared, no
* node): it shadows the package's property of that name */
jt_write(&r, "src/main/kotlin/kl/K2.kt",
"package kl\n"
"\n"
"val shade: Int = 1\n"
"\n"
"/** Uses [shade] and [plain]. */\n"
"class K2(val shade: Int, plain: Int) {\n"
" /** Reads [shade], also [plain]. */\n"
" fun m() {}\n"
"}\n");
/* 3. what the type holds itself and in its companion (one scope), then
* what it inherits: its own override, not the supertype's */
jt_write(&r, "src/main/kotlin/kl/K3.kt",
"package kl\n"
"\n"
"open class K3Base {\n"
" fun inherited() {}\n"
" open fun shared() {}\n"
"}\n"
"\n"
"class K3 : K3Base() {\n"
" fun own() {}\n"
" override fun shared() {}\n"
" companion object Tools {\n"
" fun make() {}\n"
" }\n"
" /** [own], [make], [inherited], [shared] */\n"
" fun m() {}\n"
"}\n"
"\n"
"fun K3Base.glow() {}\n"
"\n"
"fun <T> T.anyExt() {}\n");
/* 4. a type nested in an enclosing type, before an explicit import */
jt_write(&r, "src/main/kotlin/kl/K4.kt",
"package kl\n"
"\n"
"import far.Thing\n"
"\n"
"class K4 {\n"
" class Thing\n"
" /** [Thing] */\n"
" fun m() {}\n"
"}\n");
/* 5. an explicit import, before the file's package */
jt_write(&r, "src/main/kotlin/kl/K5.kt",
"package kl\n"
"\n"
"import far.Thing\n"
"\n"
"/** [Thing] */\n"
"class K5\n");
/* 6. the file's package, before a star import */
jt_write(&r, "src/main/kotlin/kl/K6.kt",
"package kl\n"
"\n"
"import far.*\n"
"\n"
"/** [Tool] */\n"
"class K6\n");
/* 7. a star import, before the default imports; 8. the default imports */
jt_write(&r, "src/main/kotlin/kl/K7.kt",
"package kl\n"
"\n"
"import far.*\n"
"\n"
"/** [List] */\n"
"class K7\n");
jt_write(&r, "src/main/kotlin/kl/K8.kt",
"package kl\n"
"\n"
"/** [List], [listOf], [Runnable] */\n"
"class K8\n");
/* 9. dotted names */
jt_write(
&r, "src/main/kotlin/kl/K9.kt",
"package kl\n"
"\n"
"/** [K4.Thing], [far.Thing], [far.topFun], [K3.make], [K3.inherited], [K3.shared] */\n"
"class K9\n");
/* 10. an extension is found for `Type.name` among the callables in
* scope, by its receiver: imported, declared in the file's package (on a
* supertype, on any type), and not when nothing brings it into scope */
jt_write(&r, "src/main/kotlin/kl/K10.kt",
"package kl\n"
"\n"
"import far.Thing\n"
"import far.shine\n"
"\n"
"/** [Thing.shine], [K3.glow], [K3.anyExt] */\n"
"class K10\n");
jt_write(&r, "src/main/kotlin/kl/K10b.kt",
"package kl\n"
"\n"
"/** @see far.Thing.shine */\n"
"class K10b\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* 1 */
ASSERT_EQ(dm_mentions_from(db, "kl.K1.K1.m"), 0);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/kl/K1.kt", "label"), "");
/* 2: the class's doc leaves no row; the member's doc one gap for the
* property and nothing for the plain parameter */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = "
"'src/main/kotlin/kl/K2.kt'"),
1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = "
"'src/main/kotlin/kl/K2.kt' AND raw = 'shade' AND line = 7 AND "
"reason = 'graph_gap'"),
1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM edges e JOIN nodes t ON t.id = e.target_id "
"WHERE e.type = 'MENTIONS' AND t.qualified_name LIKE '%.kl.K2.shade'"),
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.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", "far.Thing.Thing"), 0);
/* 5 */
ASSERT_EQ(jt_edge(db, "kl.K5.K5", "far.Thing.Thing"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"exact\""));
ASSERT_EQ(jt_edge(db, "kl.K5.K5", "kl.Thing.Thing"), 0);
/* 6 */
ASSERT_EQ(jt_edge(db, "kl.K6.K6", "kl.Tool.Tool"), 1);
ASSERT_EQ(jt_edge(db, "kl.K6.K6", "far.Tool.Tool"), 0);
/* 7 */
ASSERT_EQ(jt_edge(db, "kl.K7.K7", "far.List.List"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/kl/K7.kt", "List"), "");
/* 8 */
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/kl/K8.kt", "List"), "external");
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", "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.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);
/* 10 */
ASSERT_EQ(jt_edge(db, "kl.K10.K10", "far.Ext.shine"), 1);
ASSERT_EQ(jt_edge(db, "kl.K10.K10", "kl.K3.glow"), 1);
ASSERT_EQ(jt_edge(db, "kl.K10.K10", "kl.K3.anyExt"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/kl/K10b.kt", "far.Thing.shine"), "missing");
ASSERT_EQ(dm_mentions_from(db, "kl.K10b.K10b"), 0);
jt_end(&r);
PASS();
}
/* ── a member a nearer type may have unseen ──────────────────────── */
/* Both languages resolve a member name in a type, with what the type
* inherits, before they look at the type around it (or, in Kotlin, at the
* file's imports and package). A member found further out binds only when the
* nearer type is KNOWN not to have one: not where the language gives every
* type a member of that name, and not where a supertype outside the
* repository may declare it. Those references are external rows. */
/* Number of unresolved rows of file `rel` with this raw text and reason. */
static int jt_rows(const char *db, const char *rel, const char *raw, const char *reason) {
char sql[700];
snprintf(sql, sizeof(sql),
"SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = '%s' AND raw = '%s' "
"AND reason = '%s'",
rel, raw, reason);
return dm_count(db, sql);
}
/* Java: a method every class has (java.lang.Object's), or every enum, is the
* documented class's own. `#toString()` in a nested class never names the
* method of the class around it. */
TEST(doc_mentions_jvm_java_given_members) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jgiv"));
const char *g1 = "src/main/java/gj/G1.java";
jt_write(&r, g1,
"package gj;\n"
"public class G1 {\n"
" public int hashCode = 2;\n"
" public String toString() { return \"\"; }\n"
" public boolean equals(String key, int n) { return false; }\n"
" public void helper() {}\n"
" public static class Bare {\n"
" /**\n"
" * {@link #toString()} {@link #toString} {@link #equals(Object)}\n"
" * {@link #wait(long)} {@link #equals(String, int)} {@link #helper()}\n"
" * {@link #hashCode}\n"
" */\n"
" public void m() {}\n"
" }\n"
" public static class Own {\n"
" public String toString() { return \"\"; }\n"
" /** {@link #toString()} */\n"
" public void m() {}\n"
" }\n"
"}\n");
const char *g2 = "src/main/java/gj/G2.java";
jt_write(&r, g2,
"package gj;\n"
"public class G2 {\n"
" public static void values() {}\n"
" public void helper() {}\n"
" public enum Mode {\n"
" ON;\n"
" /** {@link #values()} {@link #helper()} */\n"
" public void m() {}\n"
" }\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* Object's methods, with one of their own parameter lists or with none
* written: external, and no edge to the method of the class around */
ASSERT_EQ(jt_rows(db, g1, "#toString()", "external"), 1);
ASSERT_EQ(jt_rows(db, g1, "#toString", "external"), 1);
ASSERT_EQ(jt_rows(db, g1, "#equals(Object)", "external"), 1);
ASSERT_EQ(jt_rows(db, g1, "#wait(long)", "external"), 1);
ASSERT_EQ(jt_edge(db, "gj.G1.Bare.m", "gj.G1.toString"), 0);
/* decoys: another parameter list is no method of Object; another name is
* none at all; a FIELD of such a name keeps javadoc's order for fields */
ASSERT_EQ(jt_edge(db, "gj.G1.Bare.m", "gj.G1.equals"), 1);
ASSERT_EQ(jt_edge(db, "gj.G1.Bare.m", "gj.G1.helper"), 1);
ASSERT_EQ(jt_edge(db, "gj.G1.Bare.m", "gj.G1.hashCode"), 1);
ASSERT_EQ(dm_mentions_from(db, "gj.G1.Bare.m"), 3);
/* ... and a class that declares the method binds its own */
ASSERT_EQ(jt_edge(db, "gj.G1.Own.m", "gj.G1.Own.toString"), 1);
/* what an enum has, and beside it a name it does not have */
ASSERT_EQ(jt_rows(db, g2, "#values()", "external"), 1);
ASSERT_EQ(jt_edge(db, "gj.G2.Mode.m", "gj.G2.values"), 0);
ASSERT_EQ(jt_edge(db, "gj.G2.Mode.m", "gj.G2.helper"), 1);
jt_end(&r);
PASS();
}
/* The supertypes the Java cases share: one whose own hierarchy leaves the
* repository, one without members, one with them. */
static void jt_guard_java_bases(const jt_repo_t *r) {
jt_write(r, "src/main/java/gj/Mid.java",
"package gj;\n"
"public class Mid extends java.io.InputStream {\n"
" public int read() { return 0; }\n"
"}\n");
jt_write(r, "src/main/java/gj/Plain.java", "package gj;\npublic class Plain {}\n");
jt_write(r, "src/main/java/gj/Has.java",
"package gj;\n"
"public class Has {\n"
" public void close() {}\n"
" public void helper() {}\n"
"}\n");
}
/* Java: a method of the class around a nested class binds only when every
* supertype of the nested class is in the repository and none has it. A
* supertype outside the repository, directly or further up, may declare it,
* and then javadoc links that one. */
TEST(doc_mentions_jvm_java_open_supertypes) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jopen"));
jt_guard_java_bases(&r);
const char *g3 = "src/main/java/gj/G3.java";
jt_write(&r, g3,
"package gj;\n"
"public class G3 {\n"
" public int count = 1;\n"
" public void close() {}\n"
" public void helper() {}\n"
" public static class Stream extends java.io.InputStream {\n"
" public int read() { return 0; }\n"
" /** {@link #close()} {@link #helper} {@link #count} {@link #read()} */\n"
" public void m() {}\n"
" }\n"
" public static class Deep extends Mid {\n"
" /** {@link #close()} {@link #count} */\n"
" public void m() {}\n"
" }\n"
" public static class Wrap extends java.io.InputStream {\n"
" public int read() { return 0; }\n"
" public class In {\n"
" /** {@link #helper()} {@link #read()} */\n"
" public void m() {}\n"
" }\n"
" }\n"
"}\n");
const char *g4 = "src/main/java/gj/G4.java";
jt_write(&r, g4,
"package gj;\n"
"public class G4 {\n"
" public int count = 1;\n"
" public void close() {}\n"
" public void helper() {}\n"
" public void extra() {}\n"
" public static class Lacks extends Plain {\n"
" /** {@link #close()} {@link #helper} {@link #count} */\n"
" public void m() {}\n"
" }\n"
" public static class Gets extends Has {\n"
" /** {@link #close()} {@link #helper} {@link #extra()} */\n"
" public void m() {}\n"
" }\n"
"}\n");
const char *g5 = "src/main/java/gj/G5.java";
jt_write(&r, g5,
"package gj;\n"
"public class G5 {\n"
" public int total() { return 0; }\n"
" public class Mid5 extends java.io.InputStream {\n"
" public int read() { return 0; }\n"
" public class In {\n"
" public int total = 1;\n"
" /** {@link #total} */\n"
" public void m() {}\n"
" }\n"
" }\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* a supertype outside the repository, written on the class (Stream) and
* one level up (Deep, through Mid): java.io.InputStream may declare
* close() and helper() */
ASSERT_EQ(jt_rows(db, g3, "#close()", "external"), 2);
ASSERT_EQ(jt_rows(db, g3, "#helper", "external"), 1);
ASSERT_EQ(jt_edge(db, "gj.G3.Stream.m", "gj.G3.close"), 0);
ASSERT_EQ(jt_edge(db, "gj.G3.Stream.m", "gj.G3.helper"), 0);
ASSERT_EQ(jt_edge(db, "gj.G3.Deep.m", "gj.G3.close"), 0);
/* the class's own method is found before the lookup would leave it */
ASSERT_EQ(jt_edge(db, "gj.G3.Stream.m", "gj.G3.Stream.read"), 1);
/* a FIELD of the class around, through the same shapes: the supertype
* outside may have a METHOD count(), which javadoc would link instead */
ASSERT_EQ(jt_rows(db, g3, "#count", "external"), 2);
ASSERT_EQ(jt_edge(db, "gj.G3.Stream.m", "gj.G3.count"), 0);
ASSERT_EQ(dm_mentions_from(db, "gj.G3.Stream.m"), 1);
ASSERT_EQ(dm_mentions_from(db, "gj.G3.Deep.m"), 0);
/* the type that leaves the repository need not be the innermost one */
ASSERT_EQ(jt_rows(db, g3, "#helper()", "external"), 1);
ASSERT_EQ(jt_edge(db, "gj.G3.Wrap.In.m", "gj.G3.Wrap.read"), 1);
ASSERT_EQ(dm_mentions_from(db, "gj.G3.Wrap.In.m"), 1);
/* every supertype in the repository and none has the method: the class
* around it, as before */
ASSERT_EQ(jt_edge(db, "gj.G4.Lacks.m", "gj.G4.close"), 1);
ASSERT_EQ(jt_edge(db, "gj.G4.Lacks.m", "gj.G4.helper"), 1);
ASSERT_EQ(jt_edge(db, "gj.G4.Lacks.m", "gj.G4.count"), 1);
/* a supertype in the repository has it: that one */
ASSERT_EQ(jt_edge(db, "gj.G4.Gets.m", "gj.Has.close"), 1);
ASSERT_EQ(jt_edge(db, "gj.G4.Gets.m", "gj.Has.helper"), 1);
ASSERT_EQ(jt_edge(db, "gj.G4.Gets.m", "gj.G4.close"), 0);
ASSERT_EQ(jt_edge(db, "gj.G4.Gets.m", "gj.G4.extra"), 1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = "
"'src/main/java/gj/G4.java'"),
0);
/* the field of In against javadoc's search for a method: it passes Mid5,
* whose supertype lies outside, before it reaches total() of G5. In is
* not below Mid5: a method total() there would win, and so would G5's
* (In is not below G5 either): external */
ASSERT_EQ(jt_rows(db, g5, "#total", "external"), 1);
ASSERT_EQ(dm_mentions_from(db, "gj.G5.Mid5.In.m"), 0);
jt_end(&r);
PASS();
}
/* Java: a Kotlin declaration of the name cannot be compared with a written
* Javadoc parameter list. It may be the method meant, so the lookup does not
* go on to the class around it. */
TEST(doc_mentions_jvm_java_uncompared_level) {
jt_repo_t r;
ASSERT(jt_begin(&r, "juncmp"));
jt_write(&r, "src/main/kotlin/gj/KBase.kt",
"package gj\n"
"\n"
"open class KBase {\n"
" fun send(x: Int) {}\n"
"}\n"
"\n"
"open class KPlain\n");
const char *g6 = "src/main/java/gj/G6.java";
jt_write(&r, g6,
"package gj;\n"
"public class G6 {\n"
" public void send(int x) {}\n"
" public static class Sub extends KBase {\n"
" /** {@link #send(int)} */\n"
" public void m() {}\n"
" }\n"
" public static class Sub2 extends KPlain {\n"
" /** {@link #send(int)} */\n"
" public void m() {}\n"
" }\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
ASSERT_EQ(jt_rows(db, g6, "#send(int)", "ambiguous"), 1);
ASSERT_EQ(dm_mentions_from(db, "gj.G6.Sub.m"), 0);
/* decoy: a Kotlin supertype without the name is known not to have it */
ASSERT_EQ(jt_edge(db, "gj.G6.Sub2.m", "gj.G6.send"), 1);
jt_end(&r);
PASS();
}
/* Kotlin: a member of kotlin.Any, or of an enum class, is the documented
* class's own. `[toString]` in a class is neither the package's function of
* that name nor plain text. */
TEST(doc_mentions_jvm_kotlin_given_members) {
jt_repo_t r;
ASSERT(jt_begin(&r, "kgiv"));
jt_write(&r, "src/main/kotlin/gk/Top.kt",
"package gk\n"
"\n"
"fun toString(x: Int): String = \"\"\n"
"\n"
"fun values() {}\n"
"\n"
"fun helper() {}\n");
const char *k1 = "src/main/kotlin/gk/K1.kt";
jt_write(&r, k1,
"package gk\n"
"\n"
"/** [toString], [hashCode], [helper] */\n"
"class Bare\n"
"\n"
"class Own {\n"
" override fun toString(): String = \"\"\n"
" /** [toString] */\n"
" fun m() {}\n"
"}\n"
"\n"
"enum class Color {\n"
" RED;\n"
" /** [values], [helper] */\n"
" fun m() {}\n"
"}\n"
"\n"
"/** [toString] */\n"
"fun free() {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* external: with a function of that name in the package, and with
* nothing of that name anywhere */
ASSERT_EQ(jt_rows(db, k1, "toString", "external"), 1);
ASSERT_EQ(jt_rows(db, k1, "hashCode", "external"), 1);
ASSERT_EQ(jt_edge(db, "gk.K1.Bare", "gk.Top.toString"), 0);
/* decoys: another name; a class that declares the member; a definition
* with no class around it */
ASSERT_EQ(jt_edge(db, "gk.K1.Bare", "gk.Top.helper"), 1);
ASSERT_EQ(dm_mentions_from(db, "gk.K1.Bare"), 1);
ASSERT_EQ(jt_edge(db, "gk.K1.Own.m", "gk.K1.Own.toString"), 1);
ASSERT_EQ(jt_edge(db, "gk.K1.free", "gk.Top.toString"), 1);
/* what an enum class has, and beside it a name it does not have (the
* graph names a method of an enum class below the file) */
ASSERT_EQ(jt_rows(db, k1, "values", "external"), 1);
ASSERT_EQ(jt_edge(db, "gk.K1.m", "gk.Top.helper"), 1);
ASSERT_EQ(dm_mentions_from(db, "gk.K1.m"), 1);
jt_end(&r);
PASS();
}
/* Kotlin: a function or property of the package, or of the class around,
* binds only when every supertype of the class (and of its companion) is in
* the repository and none has the name. A type found further out binds as
* before. */
TEST(doc_mentions_jvm_kotlin_open_supertypes) {
jt_repo_t r;
ASSERT(jt_begin(&r, "kopen"));
jt_write(&r, "src/main/kotlin/gk/Top.kt",
"package gk\n"
"\n"
"fun close() {}\n"
"\n"
"fun helper() {}\n"
"\n"
"val depth: Int = 0\n"
"\n"
"class Thing\n");
jt_write(&r, "src/main/kotlin/gk/Bases.kt",
"package gk\n"
"\n"
"open class Mid : java.io.InputStream() {\n"
" override fun read(): Int = 0\n"
"}\n"
"\n"
"open class Plain\n"
"\n"
"open class Has {\n"
" fun helper() {}\n"
"}\n");
const char *k2 = "src/main/kotlin/gk/K2.kt";
jt_write(&r, k2,
"package gk\n"
"\n"
"/** [close], [helper], [depth], [read], [Thing] */\n"
"class Stream : java.io.InputStream() {\n"
" override fun read(): Int = 0\n"
"}\n"
"\n"
"/** [close], [Thing] */\n"
"class Deep : Mid()\n");
const char *k3 = "src/main/kotlin/gk/K3.kt";
jt_write(&r, k3,
"package gk\n"
"\n"
"/** [close], [helper], [depth] */\n"
"class Lacks : Plain()\n"
"\n"
"/** [close], [helper] */\n"
"class Gets : Has()\n");
const char *k4 = "src/main/kotlin/gk/K4.kt";
jt_write(&r, k4,
"package gk\n"
"\n"
"class Outer {\n"
" fun aid() {}\n"
"\n"
" inner class Wrapped : java.io.InputStream() {\n"
" override fun read(): Int = 0\n"
" /** [aid] */\n"
" fun m() {}\n"
" }\n"
"\n"
" inner class Simple {\n"
" /** [aid] */\n"
" fun m() {}\n"
" }\n"
"}\n"
"\n"
"class Locked {\n"
" companion object Lock : java.io.Closeable {\n"
" override fun close() {}\n"
" }\n"
" /** [helper], [close] */\n"
" fun m() {}\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* a supertype outside the repository, written on the class (Stream) and
* one level up (Deep, through Mid): no function or property of the
* package */
ASSERT_EQ(jt_rows(db, k2, "close", "external"), 2);
ASSERT_EQ(jt_rows(db, k2, "helper", "external"), 1);
ASSERT_EQ(jt_rows(db, k2, "depth", "external"), 1);
ASSERT_EQ(jt_edge(db, "gk.K2.Stream", "gk.Top.close"), 0);
ASSERT_EQ(jt_edge(db, "gk.K2.Deep", "gk.Top.close"), 0);
/* the class's own member, and a TYPE of the package, bind */
ASSERT_EQ(jt_edge(db, "gk.K2.Stream", "gk.K2.Stream.read"), 1);
ASSERT_EQ(jt_edge(db, "gk.K2.Stream", "gk.Top.Thing"), 1);
ASSERT_EQ(dm_mentions_from(db, "gk.K2.Stream"), 2);
ASSERT_EQ(jt_edge(db, "gk.K2.Deep", "gk.Top.Thing"), 1);
ASSERT_EQ(dm_mentions_from(db, "gk.K2.Deep"), 1);
/* every supertype in the repository and none has the name: the package,
* as before */
ASSERT_EQ(jt_edge(db, "gk.K3.Lacks", "gk.Top.close"), 1);
ASSERT_EQ(jt_edge(db, "gk.K3.Lacks", "gk.Top.helper"), 1);
ASSERT_EQ(jt_edge(db, "gk.K3.Lacks", "gk.Top.depth"), 1);
/* a supertype in the repository has it: that one */
ASSERT_EQ(jt_edge(db, "gk.K3.Gets", "gk.Bases.Has.helper"), 1);
ASSERT_EQ(jt_edge(db, "gk.K3.Gets", "gk.Top.helper"), 0);
ASSERT_EQ(jt_edge(db, "gk.K3.Gets", "gk.Top.close"), 1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = "
"'src/main/kotlin/gk/K3.kt'"),
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);
/* 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(dm_mentions_from(db, "gk.K4.Locked.m"), 1);
jt_end(&r);
PASS();
}
/* Kotlin: what the companion object inherits is in the class's scope, before
* the package. */
TEST(doc_mentions_jvm_kotlin_companion_inherits) {
jt_repo_t r;
ASSERT(jt_begin(&r, "kcomp"));
jt_write(&r, "src/main/kotlin/gk/Top.kt",
"package gk\n"
"\n"
"fun create() {}\n"
"\n"
"fun helper() {}\n");
jt_write(&r, "src/main/kotlin/gk/K5.kt",
"package gk\n"
"\n"
"open class Maker {\n"
" fun create() {}\n"
"}\n"
"\n"
"class Entity {\n"
" companion object : Maker()\n"
" /** [create], [helper] */\n"
" fun m() {}\n"
"}\n"
"\n"
"/** [Entity.create] */\n"
"class User\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
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 */
ASSERT_EQ(jt_edge(db, "gk.K5.Entity.m", "gk.Top.helper"), 1);
/* the same member, named through the class */
ASSERT_EQ(jt_edge(db, "gk.K5.User", "gk.K5.Maker.create"), 1);
jt_end(&r);
PASS();
}
/* Kotlin `Type.name`: a member comes before an extension. Where a supertype
* of the type lies outside the repository it may declare the name, so the
* extension in scope is not taken. */
TEST(doc_mentions_jvm_kotlin_extension_guard) {
jt_repo_t r;
ASSERT(jt_begin(&r, "kext"));
jt_write(&r, "src/main/kotlin/gk/Types.kt",
"package gk\n"
"\n"
"class Shut : java.io.Closeable {\n"
" override fun close() {}\n"
"}\n"
"\n"
"open class Plain\n"
"\n"
"class Calm : Plain()\n");
jt_write(&r, "src/main/kotlin/gk/ExtA.kt", "package gk\n\nfun Shut.shine() {}\n");
jt_write(&r, "src/main/kotlin/gk/ExtB.kt", "package gk\n\nfun Calm.glow() {}\n");
const char *k6 = "src/main/kotlin/gk/K6.kt";
jt_write(&r, k6,
"package gk\n"
"\n"
"/** [Shut.shine], [Calm.glow], [Shut.close] */\n"
"class ExtUser\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
ASSERT_EQ(jt_rows(db, k6, "Shut.shine", "external"), 1);
ASSERT_EQ(jt_edge(db, "gk.K6.ExtUser", "gk.ExtA.shine"), 0);
/* decoys: a receiver whose supertypes are all in the repository; a
* member the open type declares itself */
ASSERT_EQ(jt_edge(db, "gk.K6.ExtUser", "gk.ExtB.glow"), 1);
ASSERT_EQ(jt_edge(db, "gk.K6.ExtUser", "gk.Types.Shut.close"), 1);
ASSERT_EQ(dm_mentions_from(db, "gk.K6.ExtUser"), 2);
jt_end(&r);
PASS();
}
/* java.lang.Object and kotlin.Any written out as a supertype are no supertype
* outside the repository: every type has them, and their members are known. */
TEST(doc_mentions_jvm_root_supertype_written) {
jt_repo_t r;
ASSERT(jt_begin(&r, "groot"));
jt_write(&r, "src/main/java/gj/G7.java",
"package gj;\n"
"public class G7 {\n"
" public void helper() {}\n"
" public static class Rooted extends Object {\n"
" /** {@link #helper()} */\n"
" public void m() {}\n"
" }\n"
" public static class Full extends java.lang.Object {\n"
" /** {@link #helper()} */\n"
" public void m() {}\n"
" }\n"
"}\n");
jt_write(&r, "src/main/kotlin/gk/Top.kt", "package gk\n\nfun helper() {}\n");
jt_write(&r, "src/main/kotlin/gk/K7.kt",
"package gk\n"
"\n"
"/** [helper] */\n"
"class Rooted : Any()\n"
"\n"
"/** [helper] */\n"
"class Full : kotlin.Any()\n");
/* decoy: where an import supplies the simple name, the supertype is
* whatever that import names */
const char *k7b = "src/main/kotlin/gk/K7b.kt";
jt_write(&r, k7b,
"package gk\n"
"\n"
"import org.outside.Any\n"
"\n"
"/** [helper] */\n"
"class Other : Any()\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
ASSERT_EQ(jt_edge(db, "gj.G7.Rooted.m", "gj.G7.helper"), 1);
ASSERT_EQ(jt_edge(db, "gj.G7.Full.m", "gj.G7.helper"), 1);
ASSERT_EQ(jt_edge(db, "gk.K7.Rooted", "gk.Top.helper"), 1);
ASSERT_EQ(jt_edge(db, "gk.K7.Full", "gk.Top.helper"), 1);
ASSERT_EQ(jt_rows(db, k7b, "helper", "external"), 1);
ASSERT_EQ(dm_mentions_from(db, "gk.K7b.Other"), 0);
jt_end(&r);
PASS();
}
/* The guard reads supertypes that OTHER files declare: what a file's own
* references bind changes when a supertype elsewhere loses a member, or gains
* or loses a supertype outside the repository. Incremental equals full. */
TEST(doc_mentions_jvm_guard_incremental_equals_full) {
jt_repo_t r;
ASSERT(jt_begin(&r, "ginc"));
const char *base = "src/main/java/gi/Base.java";
const char *outer = "src/main/java/gi/Outer.java";
jt_write(&r, base, "package gi;\npublic class Base {\n public void near() {}\n}\n");
jt_write(&r, outer,
"package gi;\n"
"public class Outer {\n"
" public void near() {}\n"
" public void far() {}\n"
" public class In extends Base {\n"
" /** {@link #near()} {@link #far()} */\n"
" public void m() {}\n"
" }\n"
"}\n");
char full_db[600];
snprintf(full_db, sizeof(full_db), "%s/full.db", r.tmp);
const char *db = r.db;
ASSERT_EQ(dm_index(r.repo, db, NULL), 0);
ASSERT_EQ(jt_edge(db, "gi.Outer.In.m", "gi.Base.near"), 1);
ASSERT_EQ(jt_edge(db, "gi.Outer.In.m", "gi.Outer.far"), 1);
/* the supertype loses the member: the class around has one */
jt_write(&r, base, "package gi;\npublic class Base {\n}\n");
ASSERT_EQ(dm_step(r.repo, db, full_db, "supertype member removed",
CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR),
0);
ASSERT_EQ(jt_edge(db, "gi.Outer.In.m", "gi.Outer.near"), 1);
/* the supertype leaves the repository: no method of the class around */
jt_write(&r, base,
"package gi;\n"
"public class Base extends java.io.InputStream {\n"
" public int read() { return 0; }\n"
"}\n");
ASSERT_EQ(dm_step(r.repo, db, full_db, "supertype opened", CBM_INCREMENTAL_ROUTE_FORCED_FULL),
0);
ASSERT_EQ(dm_mentions_from(db, "gi.Outer.In.m"), 0);
ASSERT_EQ(jt_rows(db, outer, "#far()", "external"), 1);
/* ... and comes back into it */
jt_write(&r, base, "package gi;\npublic class Base {\n}\n");
ASSERT_EQ(dm_step(r.repo, db, full_db, "supertype closed", CBM_INCREMENTAL_ROUTE_FORCED_FULL),
0);
ASSERT_EQ(jt_edge(db, "gi.Outer.In.m", "gi.Outer.near"), 1);
ASSERT_EQ(jt_edge(db, "gi.Outer.In.m", "gi.Outer.far"), 1);
dm_unlink_db(full_db);
jt_end(&r);
PASS();
}
/* ── both languages in one index ─────────────────────────────────── */
/* A package is one namespace whatever language declares into it: a KDoc name
* finds a Java class, a Javadoc link a Kotlin class. What cannot be compared
* across the two (a Javadoc signature against a Kotlin declaration, a type
* alias seen from Java) is a row, never a guess. */
TEST(doc_mentions_jvm_cross_language) {
jt_repo_t r;
ASSERT(jt_begin(&r, "cross"));
jt_write(&r, "src/main/java/x/JBox.java",
"package x;\n"
"public class JBox {\n"
" public int size = 1;\n"
" public void open() {}\n"
" public void open(int n) {}\n"
" public void shut() {}\n"
"}\n");
jt_write(&r, "src/main/kotlin/x/KBox.kt",
"package x\n"
"\n"
"class KBox {\n"
" fun close() {}\n"
" val lid: Int = 0\n"
"}\n"
"\n"
"typealias KAlias = KBox\n");
jt_write(&r, "src/main/kotlin/x/KUser.kt",
"package x\n"
"\n"
"/** [JBox], [JBox.size], [JBox.shut], [JBox.open] */\n"
"class KUser\n");
jt_write(&r, "src/main/java/x/JUser.java",
"package x;\n"
"/**\n"
" * {@link KBox} {@link KBox#close} {@link KBox#lid} {@link KBox#close()}\n"
" * {@link KAlias}\n"
" */\n"
"public class JUser {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* Kotlin -> Java */
ASSERT_EQ(jt_edge(db, "x.KUser.KUser", "x.JBox"), 1);
ASSERT_EQ(jt_edge(db, "x.KUser.KUser", "x.JBox.size"), 1);
ASSERT_EQ(jt_edge(db, "x.KUser.KUser", "x.JBox.shut"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/x/KUser.kt", "JBox.open"), "ambiguous");
/* Java -> Kotlin */
ASSERT_EQ(jt_edge(db, "x.JUser", "x.KBox.KBox"), 1);
ASSERT_EQ(jt_edge(db, "x.JUser", "x.KBox.KBox.close"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":1")); /* `#close`, not `#close()` */
ASSERT_EQ(jt_edge(db, "x.JUser", "x.KBox.lid"), 1);
/* a Javadoc parameter list cannot be compared with a Kotlin signature */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/x/JUser.java", "KBox#close()"), "ambiguous");
/* Java does not see a Kotlin type alias */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/x/JUser.java", "KAlias"), "missing");
ASSERT_EQ(jt_edge(db, "x.JUser", "x.KBox.KAlias"), 0);
jt_end(&r);
PASS();
}
/* ── one node per qualified name ─────────────────────────────────── */
/* Several declarations can have one qualified name, and the graph keeps one
* node. A reference binds only the declaration that owns the node, and an
* edge leaves only from a definition that owns its own: a doc written on a
* definition whose name another declaration took has nowhere to leave from. */
TEST(doc_mentions_jvm_node_ownership) {
jt_repo_t r;
ASSERT(jt_begin(&r, "own"));
/* Kotlin: a class and the factory function named like it share the
* file-based name; the later declaration owns the node */
jt_write(&r, "src/main/kotlin/c/Maker.kt",
"package c\n"
"\n"
"/** Makes a [Part], takes [T] and [String]. */\n"
"class Maker<T>\n"
"\n"
"class Part\n"
"\n"
"/** Factory of a [Part]. */\n"
"fun Maker(): Maker<Int> = Maker()\n");
/* Kotlin: two classes' properties of one name in one file */
jt_write(&r, "src/main/kotlin/c/Twins.kt",
"package c\n"
"\n"
"class First {\n"
" /** In [First]. */\n"
" val id: Int = 1\n"
"}\n"
"\n"
"class Second {\n"
" /** In [Second]. */\n"
" val id: Int = 2\n"
"}\n"
"\n"
"/** [First.id] and [Second.id] */\n"
"class TwinUser\n");
/* Java: a field and its accessor share `Holder.value`; the method,
* declared later, owns the node */
jt_write(&r, "src/main/java/c/Holder.java",
"package c;\n"
"public class Holder {\n"
" /** The {@link Other}. */\n"
" private int value;\n"
" /** Reads {@link Other}, see {@link #value} and {@link #value()}. */\n"
" public int value() { return value; }\n"
"}\n"
"class Other {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
const char *maker = "src/main/kotlin/c/Maker.kt";
/* the function's doc: an edge from the node the function owns */
ASSERT_EQ(jt_edge(db, "c.Maker.Maker", "c.Maker.Part"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":1"));
ASSERT_NOT_NULL(strstr(jt_props, "\"line\":8"));
/* the class's doc: what would be an edge is a gap; what is local or
* external anyway keeps its own outcome */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = "
"'src/main/kotlin/c/Maker.kt' AND raw = 'Part' AND line = 3 AND "
"reason = 'graph_gap'"),
1);
ASSERT_STR_EQ(jt_reason(db, maker, "T"), "");
ASSERT_STR_EQ(jt_reason(db, maker, "String"), "external");
/* the property that owns `id` is the later one: Second's */
ASSERT_EQ(jt_edge(db, "c.Twins.id", "c.Twins.Second"), 1);
ASSERT_EQ(jt_edge(db, "c.Twins.id", "c.Twins.First"), 0);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = "
"'src/main/kotlin/c/Twins.kt' AND raw = 'First' AND reason = "
"'graph_gap'"),
1);
ASSERT_EQ(jt_edge(db, "c.Twins.TwinUser", "c.Twins.id"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":1")); /* Second.id only */
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/c/Twins.kt", "First.id"), "graph_gap");
/* Java: a field named like its accessor has a node of its own
* (<Owner>.<name>#field), so its doc is a source too; `#value` is the
* field, `#value()` the method itself */
ASSERT_EQ(jt_edge(db, "c.Holder.value", "c.Other"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":1"));
ASSERT_NOT_NULL(strstr(jt_props, "\"line\":5"));
ASSERT_EQ(jt_edge(db, "c.Holder.value#field", "c.Other"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"line\":3"));
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = "
"'src/main/java/c/Holder.java' AND raw = 'Other' AND line = 3"),
0);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/c/Holder.java", "#value"), "");
ASSERT_EQ(jt_edge(db, "c.Holder.value", "c.Holder.value#field"), 1);
jt_end(&r);
PASS();
}
/* A Kotlin class and a function of its name at one scope level (the factory
* idiom, `class Json` beside `fun Json(..)`): KDoc resolves the classifier
* first, so the name is the CLASS, with the tier the lookup gives. A class
* without a node of its own is a graph gap. The function never stands in for
* the class, with a node or without. */
TEST(doc_mentions_jvm_kotlin_type_and_function) {
jt_repo_t r;
ASSERT(jt_begin(&r, "tfun"));
/* class and factory in different files: each has its node. Two factory
* overloads: the function side is a group, which changes nothing */
jt_write(&r, "src/main/kotlin/f/Json.kt",
"package f\n\nclass Json {\n fun encode() {}\n}\n");
jt_write(&r, "src/main/kotlin/f/Builders.kt",
"package f\n"
"\n"
"fun Json(strict: Boolean): Json = Json()\n"
"\n"
"fun Json(name: String): Json = Json()\n");
/* class and factory in ONE file share the file-based qualified name
* `f.Alias.Alias`; the function, declared later, owns the node, and the
* class has none */
jt_write(&r, "src/main/kotlin/f/Alias.kt",
"package f\n"
"\n"
"class Alias {\n"
" fun name() {}\n"
"}\n"
"\n"
"fun Alias(text: String): Alias = Alias()\n");
/* the same idiom inside a type: a nested class and a member function */
jt_write(&r, "src/main/kotlin/f/Holder.kt",
"package f\n"
"\n"
"class Holder {\n"
" class Part\n"
" fun Part(size: Int): Part = Part()\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");
jt_write(&r, "src/main/kotlin/f/User.kt",
"package f\n"
"\n"
"/** A [Json] with [Json.encode], a [Holder.Part], a [Mode]. */\n"
"class User\n");
jt_write(&r, "src/main/kotlin/f/GapUser.kt",
"package f\n"
"\n"
"/** An [Alias]. */\n"
"class GapUser\n");
/* the block tags that name code resolve like a link ... */
jt_write(&r, "src/main/kotlin/f/TagUser.kt",
"package f\n"
"\n"
"/**\n"
" * @see Json\n"
" * @throws Json never\n"
" * @see Alias\n"
" */\n"
"class TagUser\n");
/* ... but a sample is a function: there the two stay ambiguous */
jt_write(&r, "src/main/kotlin/f/SampleUser.kt",
"package f\n"
"\n"
"/** @sample f.Json */\n"
"class SampleUser\n");
/* an explicit import brings the class and the function: the class, exact */
jt_write(&r, "src/main/kotlin/g/Importer.kt",
"package g\n"
"\n"
"import f.Json\n"
"\n"
"/** A [Json]. */\n"
"class Importer\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* the class has a node: an edge to the class, with the tier of the level
* that found it (the file's package: unique), and no row */
ASSERT_EQ(jt_edge(db, "f.User.User", "f.Json.Json"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"kdoc_link\""));
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"unique\""));
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":1"));
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/f/User.kt", "Json"), "");
/* ... never to the 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);
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);
/* 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");
ASSERT_EQ(jt_edge(db, "f.GapUser.GapUser", "f.Alias.Alias"), 0);
ASSERT_EQ(dm_mentions_from(db, "f.GapUser.GapUser"), 0);
/* @see and @throws: the class (one edge for both references), and the gap */
ASSERT_EQ(jt_edge(db, "f.TagUser.TagUser", "f.Json.Json"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"see\""));
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":2"));
ASSERT_EQ(jt_edge(db, "f.TagUser.TagUser", "f.Builders.Json"), 0);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/f/TagUser.kt", "Alias"), "graph_gap");
ASSERT_EQ(dm_mentions_from(db, "f.TagUser.TagUser"), 1);
/* @sample: neither */
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/f/SampleUser.kt", "f.Json"), "ambiguous");
ASSERT_EQ(dm_mentions_from(db, "f.SampleUser.SampleUser"), 0);
/* through an explicit import: the class, exact */
ASSERT_EQ(jt_edge(db, "g.Importer.Importer", "f.Json.Json"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"tier\":\"exact\""));
ASSERT_EQ(jt_edge(db, "g.Importer.Importer", "f.Builders.Json"), 0);
ASSERT_EQ(dm_mentions_from(db, "g.Importer.Importer"), 1);
jt_end(&r);
PASS();
}
/* ── parse errors ────────────────────────────────────────────────── */
TEST(doc_mentions_jvm_scope_parse_errors) {
/* an error inside a method's own block: the type is complete */
CBMFileResult *r = dm_extract("package pe;\n"
"public class InBody {\n"
" public void before() {}\n"
" public void bad() {\n"
" int x = ;\n"
" foo(;\n"
" }\n"
" public void after() {}\n"
"}\n",
CBM_LANG_JAVA, "pe/InBody.java");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(r->doc_scope);
ASSERT(jt_has_line(r->doc_scope, "T\t2\t9\tc\tInBody\t\t\t=\t1\t"));
ASSERT(jt_has_line(r->doc_scope, "M\t4\tc\t\t0\tbad\t\t\t\t=\t1\t"));
ASSERT(jt_has_line(r->doc_scope, "M\t8\tc\t\t0\tafter\t\t\t\t=\t1\t"));
cbm_free_result(r);
/* an error among the members: the type does not show all of them ('!'),
* and the member the error hit is not listed */
r = dm_extract("package pe;\n"
"public class Among {\n"
" public void before() {}\n"
" public void bad( { }\n"
" public void after() {}\n"
"}\n",
CBM_LANG_JAVA, "pe/Among.java");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(r->doc_scope);
ASSERT(jt_has_line(r->doc_scope, "T\t2\t6\tc!\tAmong\t\t\t=\t1\t"));
ASSERT(jt_has_line(r->doc_scope, "M\t3\tc\t\t0\tbefore\t\t\t\t=\t1\t"));
ASSERT_NULL(strstr(r->doc_scope, "\tbad\t"));
cbm_free_result(r);
/* braces that do not pair: nothing behind the first one is placed. The
* names declared there are kept apart (Q), the lines marked (X) */
r = dm_extract("package pe;\n"
"public class Open {\n"
" public void before() {}\n"
" public static class Lost {\n"
" public void inLost() {\n"
" }\n"
" public void after() {}\n"
"}\n",
CBM_LANG_JAVA, "pe/Open.java");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(r->doc_scope);
ASSERT_NULL(strstr(r->doc_scope, "\nT\t"));
ASSERT(jt_has_line(r->doc_scope, "Q\tOpen"));
ASSERT(jt_has_line(r->doc_scope, "Q\tLost"));
ASSERT_NOT_NULL(strstr(r->doc_scope, "\nX\t2\t"));
cbm_free_result(r);
/* Kotlin: an error held by braces inside a member (an accessor's body, a
* `when`) does not reach past the member: the type is complete */
r = dm_extract("package pe\n"
"\n"
"class Getter {\n"
" val ok: Int = 1\n"
" val broken: Int\n"
" get() {\n"
" val x = when (this) {\n"
" is Getter if true -> 1\n"
" else -> 2\n"
" }\n"
" return x\n"
" }\n"
" fun bad(e: Any): Int = when (e) {\n"
" is String if e.isEmpty() -> 1\n"
" else -> 2\n"
" }\n"
" fun after() {}\n"
"}\n",
CBM_LANG_KOTLIN, "pe/Getter.kt");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(r->doc_scope);
ASSERT(jt_has_line(r->doc_scope, "T\t3\t18\tc\tGetter\t\t\t=\t1\t"));
ASSERT(jt_has_line(r->doc_scope, "M\t5\tv\t\t0\tbroken\t\t-\t\tbroken\t1\t"));
ASSERT(jt_has_line(r->doc_scope, "M\t13\tc\t\t0\tbad\t\tAny\t\t=\t1\te"));
ASSERT(jt_has_line(r->doc_scope, "M\t17\tc\t\t0\tafter\t\t\t\t=\t1\t"));
cbm_free_result(r);
/* ... while one among the members leaves it incomplete */
r = dm_extract("package pe\n"
"\n"
"class KStray {\n"
" fun before() {}\n"
" ) ) )\n"
" fun after() {}\n"
"}\n",
CBM_LANG_KOTLIN, "pe/KStray.kt");
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(r->doc_scope);
ASSERT(jt_has_line(r->doc_scope, "T\t3\t7\tc!\tKStray\t\t\t=\t1\t"));
ASSERT(jt_has_line(r->doc_scope, "X\t5\t5"));
cbm_free_result(r);
/* types nested deeper than any program nests them: the scan stops
* descending, marks the rest unplaced, and does not overflow */
enum { JT_DEEP = 400 };
size_t deep_cap = (size_t)JT_DEEP * 40 + 64;
char *deep = malloc(deep_cap);
ASSERT_NOT_NULL(deep);
size_t w = (size_t)snprintf(deep, deep_cap, "package pe;\n");
for (int i = 0; i < JT_DEEP; i++) {
w += (size_t)snprintf(deep + w, deep_cap - w, "class D%d {\n", i);
}
for (int i = 0; i < JT_DEEP; i++) {
w += (size_t)snprintf(deep + w, deep_cap - w, "}\n");
}
r = dm_extract(deep, CBM_LANG_JAVA, "pe/Deep.java");
free(deep);
ASSERT_NOT_NULL(r);
ASSERT_NOT_NULL(r->doc_scope);
ASSERT_NOT_NULL(strstr(r->doc_scope, "\tD0.D1.D2\t"));
ASSERT_NULL(strstr(r->doc_scope,
"\tD0.D1.D2.D3.D4.D5.D6.D7.D8.D9.D10.D11.D12.D13.D14.D15.D16."
"D17.D18.D19.D20.D21.D22.D23.D24.D25.D26.D27.D28.D29.D30.D31."
"D32.D33.D34.D35.D36.D37.D38.D39.D40.D41.D42.D43.D44.D45.D46."
"D47.D48.D49.D50.D51.D52.D53.D54.D55.D56.D57.D58.D59.D60.D61."
"D62.D63.D64.D65.D66.D67.D68.D69.D70.D71.D72.D73.D74.D75.D76."
"D77.D78.D79.D80.D81.D82.D83.D84.D85.D86.D87.D88.D89.D90.D91."
"D92.D93.D94.D95.D96.D97.D98.D99.D100.D101.D102.D103.D104.D105."
"D106.D107.D108.D109.D110.D111.D112.D113.D114.D115.D116.D117."
"D118.D119.D120.D121.D122.D123.D124.D125.D126.D127.D128\t"));
ASSERT_NOT_NULL(strstr(r->doc_scope, "\nQ\tD128\n"));
cbm_free_result(r);
PASS();
}
/* What the resolver does with those scopes: a member a parse error may hide
* is a graph gap (never "missing", never an outer declaration of that name);
* a name declared where the scan lost its place is not resolved around. */
TEST(doc_mentions_jvm_resolver_parse_errors) {
jt_repo_t r;
ASSERT(jt_begin(&r, "perr"));
jt_write(&r, "src/main/java/pe/Among.java",
"package pe;\n"
"public class Among {\n"
" /** {@link #bad()} {@link #after()} {@link Whole#gone()} */\n"
" public void before() {}\n"
" public void bad( { }\n"
" public void after() {}\n"
"}\n");
jt_write(&r, "src/main/java/pe/Whole.java",
"package pe;\n"
"/** {@link Among#bad()} {@link Among#before()} {@link Open} {@link Lost} */\n"
"public class Whole {}\n");
jt_write(&r, "src/main/java/pe/Open.java",
"package pe;\n"
"public class Open {\n"
" public void before() {}\n"
" public static class Lost {\n"
" public void inLost() {\n"
" }\n"
" public void after() {}\n"
"}\n");
/* the same simple name, placed, in a package imported on demand: the
* unplaced declaration could be the one meant, so it does not bind */
jt_write(&r, "src/main/java/other/Lost.java", "package other;\npublic class Lost {}\n");
jt_write(&r, "src/main/java/pe/Importer.java",
"package pe;\n"
"import other.*;\n"
"/** {@link Lost} */\n"
"public class Importer {}\n");
jt_write(&r, "src/main/kotlin/pk/Getter.kt",
"package pk\n"
"\n"
"class Getter {\n"
" val ok: Int = 1\n"
" val broken: Int\n"
" get() {\n"
" val x = when (this) {\n"
" is Getter if true -> 1\n"
" else -> 2\n"
" }\n"
" return x\n"
" }\n"
" /**\n"
" * [ok], [broken]\n"
" * @see Getter.nope\n"
" */\n"
" fun after() {}\n"
"}\n");
jt_write(&r, "src/main/kotlin/pk/KStray.kt",
"package pk\n"
"\n"
"fun topLevel() {}\n"
"\n"
"class KStray {\n"
" fun own() {}\n"
" /**\n"
" * [own], [topLevel], [Getter]\n"
" * @see KStray.hidden\n"
" */\n"
" fun documented() {}\n"
" ) ) )\n"
" fun after() {}\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* Java, inside the incomplete type: its parsed members bind; the one the
* error hid is a gap; a complete type's absent member is missing */
ASSERT_EQ(jt_edge(db, "pe.Among.before", "pe.Among.after"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/pe/Among.java", "#bad()"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/pe/Among.java", "Whole#gone()"), "missing");
/* ... and named from outside */
ASSERT_EQ(jt_edge(db, "pe.Whole", "pe.Among.before"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/java/pe/Whole.java", "Among#bad()"), "graph_gap");
/* names declared where the braces do not pair */
ASSERT_STR_EQ(jt_reason(db, "src/main/java/pe/Whole.java", "Open"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/pe/Whole.java", "Lost"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, "src/main/java/pe/Importer.java", "Lost"), "graph_gap");
ASSERT_EQ(jt_edge(db, "pe.Importer", "other.Lost"), 0);
/* Kotlin: errors held inside a member's braces leave the type whole */
ASSERT_EQ(jt_edge(db, "pk.Getter.Getter.after", "pk.Getter.ok"), 1);
ASSERT_EQ(jt_edge(db, "pk.Getter.Getter.after", "pk.Getter.broken"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/pk/Getter.kt", "Getter.nope"), "missing");
/* Kotlin, inside an incomplete type: its own parsed member binds, a TYPE
* further out binds (a type the scan lost is kept apart by name), a
* MEMBER further out does not (a hidden member could shadow it) */
ASSERT_EQ(jt_edge(db, "pk.KStray.KStray.documented", "pk.KStray.KStray.own"), 1);
ASSERT_EQ(jt_edge(db, "pk.KStray.KStray.documented", "pk.Getter.Getter"), 1);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/pk/KStray.kt", "topLevel"), "graph_gap");
ASSERT_EQ(jt_edge(db, "pk.KStray.KStray.documented", "pk.KStray.topLevel"), 0);
ASSERT_STR_EQ(jt_reason(db, "src/main/kotlin/pk/KStray.kt", "KStray.hidden"), "graph_gap");
jt_end(&r);
PASS();
}
/* ── test code ───────────────────────────────────────────────────── */
/* Where a build puts test code, in the layouts the two languages use. In
* each a product class names a test-only class (a row) and the test class
* names the product class (an edge). */
TEST(doc_mentions_jvm_test_only_layouts) {
static const struct {
const char *dir; /* the directory of the test class */
const char *name;
bool test;
} cases[] = {
{"src/test/kotlin/t", "InSrcTest", true},
{"src/jvmTest/kotlin/t", "InSourceSet", true},
{"src/testFixtures/kotlin/t", "InFixtures", true},
{"core/commonTest/src/t", "SetBeforeSrc", true},
{"core/common/test/t", "PlainTestDir", true},
{"json-tests/src/main/kotlin/t", "TestProject", true},
{"samples/app/src/main/kotlin/t", "InSamples", true},
/* a directory BELOW the source set is a package, and a name that
* merely contains the letters is no test directory */
{"src/main/kotlin/t/test", "PackageNamedTest", false},
{"latest/src/main/kotlin/t", "InLatest", false},
};
jt_repo_t r;
ASSERT(jt_begin(&r, "tonly"));
char doc[2048] = "package t\n\n/**";
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
char path[512];
char body[512];
snprintf(path, sizeof(path), "%s/%s.kt", cases[i].dir, cases[i].name);
snprintf(body, sizeof(body), "package t\n\n/** Tests [Product]. */\nclass %s\n",
cases[i].name);
jt_write(&r, path, body);
size_t used = strlen(doc);
snprintf(doc + used, sizeof(doc) - used, " [%s]", cases[i].name);
}
size_t used = strlen(doc);
snprintf(doc + used, sizeof(doc) - used, " */\nclass Product\n");
jt_write(&r, "src/main/kotlin/t/Product.kt", doc);
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
char sql[512];
snprintf(sql, sizeof(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.name = 'Product' AND t.name = '%s'",
cases[i].name);
const char *reason = jt_reason(r.db, "src/main/kotlin/t/Product.kt", cases[i].name);
if (dm_count(r.db, sql) != (cases[i].test ? 0 : 1) ||
strcmp(reason, cases[i].test ? "test_only_target" : "") != 0) {
printf(" %s/%s: edges %d, reason '%s'\n", cases[i].dir, cases[i].name,
dm_count(r.db, sql), reason);
}
ASSERT_EQ(dm_count(r.db, sql), cases[i].test ? 0 : 1);
ASSERT_STR_EQ(reason, cases[i].test ? "test_only_target" : "");
/* every one of them, test or not, may name the product class */
snprintf(sql, sizeof(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.name = '%s' AND t.name = 'Product'",
cases[i].name);
ASSERT_EQ(dm_count(r.db, sql), 1);
}
jt_end(&r);
PASS();
}
/* ── the family table ────────────────────────────────────────────── */
/* The families' names are public surface (the "syntax" edge property and
* column); each belongs to its language and ships. */
TEST(doc_mentions_jvm_family_names) {
static const struct {
int syntax;
CBMLanguage lang;
const char *name;
} families[] = {
{CBM_DOCLINK_JAVA_LINK, CBM_LANG_JAVA, "link"},
{CBM_DOCLINK_JAVA_LINKPLAIN, CBM_LANG_JAVA, "linkplain"},
{CBM_DOCLINK_JAVA_SEE, CBM_LANG_JAVA, "see"},
{CBM_DOCLINK_JAVA_THROWS, CBM_LANG_JAVA, "throws"},
{CBM_DOCLINK_JAVA_VALUE, CBM_LANG_JAVA, "value"},
{CBM_DOCLINK_KT_KDOC_LINK, CBM_LANG_KOTLIN, "kdoc_link"},
{CBM_DOCLINK_KT_SEE, CBM_LANG_KOTLIN, "see"},
{CBM_DOCLINK_KT_THROWS, CBM_LANG_KOTLIN, "throws"},
{CBM_DOCLINK_KT_SAMPLE, CBM_LANG_KOTLIN, "sample"},
};
/* both languages have a doc-reference parser */
ASSERT_TRUE(cbm_doclink_lang_supported(CBM_LANG_JAVA));
ASSERT_TRUE(cbm_doclink_lang_supported(CBM_LANG_KOTLIN));
for (size_t i = 0; i < sizeof(families) / sizeof(families[0]); i++) {
const CBMDocLinkFamily *f = cbm_doclink_family(families[i].syntax);
ASSERT_NOT_NULL(f);
ASSERT_EQ(f->lang, families[i].lang);
ASSERT_STR_EQ(f->name, families[i].name);
ASSERT_STR_EQ(cbm_doclink_syntax_name(families[i].syntax), families[i].name);
ASSERT_FALSE(cbm_doclink_syntax_is_external(families[i].syntax));
}
/* all ship in production: their held-out audit passed */
cbm_doclink_test_reset_ships();
int shipping_in_production = 0;
for (size_t i = 0; i < sizeof(families) / sizeof(families[0]); i++) {
shipping_in_production += cbm_doclink_syntax_ships(families[i].syntax) ? 1 : 0;
}
dm_ship_held_families();
ASSERT_EQ(shipping_in_production, (int)(sizeof(families) / sizeof(families[0])));
for (size_t i = 0; i < sizeof(families) / sizeof(families[0]); i++) {
ASSERT_TRUE(cbm_doclink_syntax_ships(families[i].syntax));
}
PASS();
}
/* ── which scope changes are repairable file by file ─────────────── */
#define JT_DJ_HEAD "package n;\nimport acme.local.Base;\nimport acme.util.Tool;\n"
#define JT_DJ_OPEN "public class W extends Base {\n"
#define JT_DJ_RUN_INT " public void run(int n) {}\n"
#define JT_DJ_RUN_STR " public void run(String s) {}\n"
#define JT_DJ_SIZE " public int size;\n"
#define JT_DJ_CLOSE "}\nclass Other {}\n"
#define JT_DJ_BODY JT_DJ_RUN_INT JT_DJ_RUN_STR JT_DJ_SIZE
#define JT_DJ_TYPES JT_DJ_OPEN JT_DJ_BODY JT_DJ_CLOSE
#define JT_DJ_BASE JT_DJ_HEAD JT_DJ_TYPES
/* no declaration header of this file names a type */
#define JT_DJ_PLAIN "public class Plain {\n public void go() {}\n}\n"
#define JT_DK_HEAD "package n\n\nimport acme.local.Base\nimport acme.util.Tool\n\n"
#define JT_DK_OPEN "class W(val id: Int) : Base() {\n"
#define JT_DK_RUN_INT " fun run(n: Int) {}\n"
#define JT_DK_RUN_STR " fun run(s: String) {}\n"
#define JT_DK_SIZE " val size: Int = 0\n"
#define JT_DK_CLOSE "}\n\nfun Base.ext() {}\n\nclass Other\n"
#define JT_DK_BODY JT_DK_RUN_INT JT_DK_RUN_STR JT_DK_SIZE
#define JT_DK_TYPES JT_DK_OPEN JT_DK_BODY JT_DK_CLOSE
#define JT_DK_BASE JT_DK_HEAD JT_DK_TYPES
#define JT_DK_PLAIN "class Plain {\n fun go() {}\n}\n"
TEST(doc_mentions_jvm_scope_delta_rules) {
const int local = CBM_DOCLINK_DELTA_LOCAL;
const int global = CBM_DOCLINK_DELTA_GLOBAL;
const CBMLanguage java = CBM_LANG_JAVA;
const CBMLanguage kotlin = CBM_LANG_KOTLIN;
struct {
const char *what;
CBMLanguage lang;
const char *before;
const char *after;
int want;
const char *names; /* NULL: any */
} cases[] = {
/* ── Java ── */
{"java unchanged", java, JT_DJ_BASE, JT_DJ_BASE, local, ""},
/* nothing of the persisted scope carries a line number, a body or a
* doc comment */
{"java lines moved", java, JT_DJ_BASE, "// moved\n\n" JT_DJ_BASE, local, ""},
{"java body edit", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN
" public void run(int n) { n++; }\n" JT_DJ_RUN_STR JT_DJ_SIZE JT_DJ_CLOSE,
local, ""},
{"java doc edit", java, JT_DJ_BASE, JT_DJ_HEAD "/** {@link Other} */\n" JT_DJ_TYPES, local,
""},
/* imports scope the file itself ... */
{"java import added", java, JT_DJ_BASE, JT_DJ_HEAD "import acme.more.Thing;\n" JT_DJ_TYPES,
local, ""},
{"java import removed", java, JT_DJ_BASE,
"package n;\nimport acme.local.Base;\n" JT_DJ_TYPES, local, ""},
{"java static import added", java, JT_DJ_BASE,
JT_DJ_HEAD "import static acme.S.max;\n" JT_DJ_TYPES, local, ""},
{"java on-demand import, no header names a type", java, "package n;\n" JT_DJ_PLAIN,
"package n;\nimport acme.more.*;\n" JT_DJ_PLAIN, local, ""},
/* ... except for the names its declaration headers write: other files
* resolve inherited members through the supertype they bind */
{"java supertype's import changed", java, JT_DJ_BASE,
"package n;\nimport acme.other.Base;\nimport acme.util.Tool;\n" JT_DJ_TYPES, global, NULL},
{"java supertype's import removed", java, JT_DJ_BASE,
"package n;\nimport acme.util.Tool;\n" JT_DJ_TYPES, global, NULL},
{"java on-demand import added", java, JT_DJ_BASE,
JT_DJ_HEAD "import acme.more.*;\n" JT_DJ_TYPES, global, NULL},
{"java static on-demand import added", java, JT_DJ_BASE,
JT_DJ_HEAD "import static acme.S.*;\n" JT_DJ_TYPES, global, NULL},
{"java static import of the supertype's name", java, JT_DJ_BASE,
JT_DJ_HEAD "import static acme.S.Base;\n" JT_DJ_TYPES, global, NULL},
/* a removed member is reported by name */
{"java overload removed", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN JT_DJ_RUN_STR JT_DJ_SIZE JT_DJ_CLOSE, local, "run"},
{"java field removed", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN JT_DJ_RUN_INT JT_DJ_RUN_STR JT_DJ_CLOSE, local, "size"},
{"java two members removed", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN JT_DJ_RUN_STR JT_DJ_CLOSE, local, "run,size"},
{"java constructor removed", java,
"package n;\npublic class C {\n public C(int n) {}\n public void m() {}\n}\n",
"package n;\npublic class C {\n public void m() {}\n}\n", local, "C"},
{"java enum constant removed", java, "package n;\npublic enum Color { RED, GREEN }\n",
"package n;\npublic enum Color { RED }\n", local, "GREEN"},
/* ... unless the overloads' one node was its own: the node moves to
* the declaration that stays */
{"java node owner removed", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN JT_DJ_RUN_INT JT_DJ_SIZE JT_DJ_CLOSE, global, NULL},
/* everything else can re-route references of files with no edge here */
{"java member added", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN JT_DJ_BODY " public int more;\n" JT_DJ_CLOSE, global, NULL},
{"java signature changed", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN
" public void run(long n) {}\n" JT_DJ_RUN_STR JT_DJ_SIZE JT_DJ_CLOSE,
global, NULL},
{"java members reordered", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN JT_DJ_RUN_STR JT_DJ_RUN_INT JT_DJ_SIZE JT_DJ_CLOSE, global, NULL},
/* the first line of a field's declarator is part of its node's name */
/* a field's node is named by the field alone: a new initializer
* declares the same names */
{"java field initializer", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN JT_DJ_RUN_INT JT_DJ_RUN_STR " public int size = 1;\n" JT_DJ_CLOSE,
local, ""},
{"java type removed", java, JT_DJ_BASE, JT_DJ_HEAD JT_DJ_OPEN JT_DJ_BODY "}\n", global,
NULL},
{"java type added", java, JT_DJ_BASE, JT_DJ_BASE "class New {}\n", global, NULL},
{"java supertype changed", java, JT_DJ_BASE,
JT_DJ_HEAD "public class W extends Tool {\n" JT_DJ_BODY JT_DJ_CLOSE, global, NULL},
{"java type parameter bound", java, JT_DJ_BASE,
JT_DJ_HEAD "public class W<T extends Other> extends Base {\n" JT_DJ_BODY JT_DJ_CLOSE,
global, NULL},
{"java package renamed", java, JT_DJ_BASE,
"package m;\nimport acme.local.Base;\nimport acme.util.Tool;\n" JT_DJ_TYPES, global, NULL},
/* a record's canonical constructor is matched against its components */
{"java record component removed", java, "package n;\npublic record P(int x, int y) {}\n",
"package n;\npublic record P(int x) {}\n", global, NULL},
/* a member the parser can no longer show makes its type incomplete */
{"java member hidden", java, JT_DJ_BASE,
JT_DJ_HEAD JT_DJ_OPEN JT_DJ_BODY " public void bad( { }\n" JT_DJ_CLOSE, global, NULL},
{"java file deleted", java, JT_DJ_BASE, NULL, global, NULL},
{"java file added", java, NULL, JT_DJ_BASE, global, NULL},
/* ── Kotlin ── */
{"kotlin unchanged", kotlin, JT_DK_BASE, JT_DK_BASE, local, ""},
{"kotlin lines moved", kotlin, JT_DK_BASE, "// moved\n\n" JT_DK_BASE, local, ""},
{"kotlin body edit", kotlin, JT_DK_BASE,
JT_DK_HEAD JT_DK_OPEN
" fun run(n: Int) { println(n) }\n" JT_DK_RUN_STR JT_DK_SIZE JT_DK_CLOSE,
local, ""},
{"kotlin property initializer", kotlin, JT_DK_BASE,
JT_DK_HEAD JT_DK_OPEN JT_DK_RUN_INT JT_DK_RUN_STR " val size: Int = 1\n" JT_DK_CLOSE,
local, ""},
{"kotlin import added", kotlin, JT_DK_BASE,
"package n\n\nimport acme.local.Base\nimport acme.more.Thing\nimport "
"acme.util.Tool\n\n" JT_DK_TYPES,
local, ""},
{"kotlin import alias that no header writes", kotlin, JT_DK_BASE,
"package n\n\nimport acme.local.Base\nimport acme.more.Thing as Renamed\n"
"import acme.util.Tool\n\n" JT_DK_TYPES,
local, ""},
{"kotlin star import, no header names a type", kotlin, "package n\n\n" JT_DK_PLAIN,
"package n\n\nimport acme.more.*\n\n" JT_DK_PLAIN, local, ""},
/* a supertype and an extension's receiver are bound in the file's scope */
{"kotlin import alias equal to the supertype", kotlin, JT_DK_BASE,
"package n\n\nimport acme.other.Thing as Base\nimport acme.util.Tool\n\n" JT_DK_TYPES,
global, NULL},
{"kotlin star import added", kotlin, JT_DK_BASE,
"package n\n\nimport acme.local.Base\nimport acme.more.*\nimport "
"acme.util.Tool\n\n" JT_DK_TYPES,
global, NULL},
{"kotlin receiver's import changed", kotlin,
"package n\n\nimport acme.local.Recv\n\nfun Recv.ext() {}\n",
"package n\n\nimport acme.other.Recv\n\nfun Recv.ext() {}\n", global, NULL},
{"kotlin overload removed", kotlin, JT_DK_BASE,
JT_DK_HEAD JT_DK_OPEN JT_DK_RUN_STR JT_DK_SIZE JT_DK_CLOSE, local, "run"},
{"kotlin property removed", kotlin, JT_DK_BASE,
JT_DK_HEAD JT_DK_OPEN JT_DK_RUN_INT JT_DK_RUN_STR JT_DK_CLOSE, local, "size"},
{"kotlin extension removed", kotlin, JT_DK_BASE,
JT_DK_HEAD JT_DK_OPEN JT_DK_BODY "}\n\nclass Other\n", local, "ext"},
/* a constructor property goes with the constructor's signature */
{"kotlin constructor property removed", kotlin, JT_DK_BASE,
JT_DK_HEAD "class W() : Base() {\n" JT_DK_BODY JT_DK_CLOSE, global, NULL},
{"kotlin member added", kotlin, JT_DK_BASE,
JT_DK_HEAD JT_DK_OPEN JT_DK_BODY " val more: Int = 0\n" JT_DK_CLOSE, global, NULL},
{"kotlin signature changed", kotlin, JT_DK_BASE,
JT_DK_HEAD JT_DK_OPEN " fun run(n: Long) {}\n" JT_DK_RUN_STR JT_DK_SIZE JT_DK_CLOSE,
global, NULL},
{"kotlin receiver changed", kotlin, JT_DK_BASE,
JT_DK_HEAD JT_DK_OPEN JT_DK_BODY "}\n\nfun Other.ext() {}\n\nclass Other\n", global, NULL},
{"kotlin type removed", kotlin, JT_DK_BASE,
JT_DK_HEAD JT_DK_OPEN JT_DK_BODY "}\n\nfun Base.ext() {}\n", global, NULL},
{"kotlin type added", kotlin, JT_DK_BASE, JT_DK_BASE "\nclass New\n", global, NULL},
{"kotlin supertype changed", kotlin, JT_DK_BASE,
JT_DK_HEAD "class W(val id: Int) : Tool() {\n" JT_DK_BODY JT_DK_CLOSE, global, NULL},
{"kotlin package renamed", kotlin, JT_DK_BASE,
"package m\n\nimport acme.local.Base\nimport acme.util.Tool\n\n" JT_DK_TYPES, global,
NULL},
{"kotlin type alias target", kotlin, "package n\n\nclass A\nclass B\ntypealias N = A\n",
"package n\n\nclass A\nclass B\ntypealias N = B\n", global, NULL},
{"kotlin file deleted", kotlin, JT_DK_BASE, NULL, global, NULL},
{"kotlin file added", kotlin, NULL, JT_DK_BASE, global, NULL},
};
char names[256];
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
const char *rel = cases[i].lang == java ? "n/W.java" : "n/W.kt";
int got = dm_scope_delta(cases[i].lang, rel, cases[i].before, cases[i].after, names,
sizeof(names));
if (got != cases[i].want || (cases[i].names && strcmp(names, cases[i].names) != 0)) {
printf(" %s: delta %d [%s], want %d [%s]\n", cases[i].what, got, names, cases[i].want,
cases[i].names ? cases[i].names : "any");
FAIL("scope delta");
}
}
PASS();
}
/* ── incremental == full ─────────────────────────────────────────── */
#define JT_I_MAIN_IMPORTS \
"package app;\n" \
"import lib.Ov;\n" \
"import lib.Sig;\n" \
"import lib.Target;\n" \
"import p.*;\n" \
"import q.*;\n"
#define JT_I_MAIN_DOC \
"/**\n" \
" * {@link Target}, {@link Target#go(int)}, {@link Target#LIMIT}, {@link Ov#run},\n" \
" * {@link Dup}, {@link Extra}, {@link Sig#go(int)}, {@link d.Derived#inherited()},\n" \
" * {@link kt.KHelper}, {@link lib.Gone#m()}"
#define JT_I_MAIN_END "\n */\npublic class Main {}\n"
static const char JT_I_MAIN[] = JT_I_MAIN_IMPORTS JT_I_MAIN_DOC JT_I_MAIN_END;
static const char JT_I_MAIN_EDITED[] =
JT_I_MAIN_IMPORTS JT_I_MAIN_DOC ", {@link Ov#run(String)}" JT_I_MAIN_END;
static const char JT_I_MAIN_IMPORT[] =
JT_I_MAIN_IMPORTS "import s.Extra;\n" JT_I_MAIN_DOC ", {@link Ov#run(String)}" JT_I_MAIN_END;
static const char JT_I_TARGET[] = "package lib;\n"
"public class Target {\n"
" public static final int LIMIT = 10;\n"
" public void go(int n) {}\n"
"}\n";
static const char JT_I_TARGET_INIT[] = "package lib;\n"
"public class Target {\n"
" public static final int LIMIT = 20;\n"
" public void go(int n) {}\n"
"}\n";
static const char JT_I_TARGET_RENAMED[] = "package lib;\n"
"public class Renamed {\n"
" public static final int LIMIT = 20;\n"
" public void go(int n) {}\n"
"}\n";
/* one node for both overloads; it belongs to the later declaration */
static const char JT_I_OV[] = "package lib;\n"
"public class Ov {\n"
" public void run(int n) {}\n"
" public void run(String s) {}\n"
"}\n";
static const char JT_I_OV_ONE[] = "package lib;\n"
"public class Ov {\n"
" public void run(String s) {}\n"
"}\n";
static const char JT_I_SIG[] = "package lib;\n"
"public class Sig {\n"
" public void go(int n) {}\n"
"}\n";
static const char JT_I_SIG_BODY[] = "package lib;\n"
"public class Sig {\n"
" public void go(int n) { n++; }\n"
"}\n";
static const char JT_I_SIG_MOVED[] = "// moved\n"
"\n"
"package lib;\n"
"\n"
"public class Sig {\n"
"\n"
" public void go(int n) { n++; }\n"
"}\n";
static const char JT_I_P[] = "package p;\n"
"public class Dup {}\n"
"class Keep {}\n";
static const char JT_I_P_NO_DUP[] = "package p;\n"
"class Keep {}\n";
static const char JT_I_DERIVED[] = "package d;\n"
"import lib.Base;\n"
"public class Derived extends Base {}\n";
static const char JT_I_DERIVED_ALT[] = "package d;\n"
"import alt.Base;\n"
"public class Derived extends Base {}\n";
static const char JT_I_KHELPER[] = "package kt\n"
"\n"
"import lib.Target\n"
"\n"
"/**\n"
" * [Target], [Target.go], [help]\n"
" * @see lib.Ov.run\n"
" * @see KOther.gone\n"
" */\n"
"class KHelper {\n"
" fun help() {}\n"
"}\n";
static const char JT_I_KHELPER_RENAMED[] = "package kt\n"
"\n"
"import lib.Target\n"
"\n"
"/**\n"
" * [Target], [Target.go], [help]\n"
" * @see lib.Ov.run\n"
" * @see KOther.gone\n"
" */\n"
"class KHelp {\n"
" fun help() {}\n"
"}\n";
static const char JT_I_KOTHER[] = "package kt\n"
"\n"
"/** [KHelper.help] */\n"
"class KOther {\n"
" fun gone() {}\n"
" fun stays() {}\n"
"}\n";
static const char JT_I_KOTHER_EDITED[] = "package kt\n"
"\n"
"/** [KHelper.help], [KHelper] */\n"
"class KOther {\n"
" fun gone() {}\n"
" fun stays() {}\n"
"}\n";
static const char JT_I_KOTHER_NO_GONE[] = "package kt\n"
"\n"
"/** [KHelper.help], [KHelper] */\n"
"class KOther {\n"
" fun stays() {}\n"
"}\n";
/* an extension function: the import decides which `Recv` is its receiver */
static const char JT_I_EXT[] = "package kt\n"
"\n"
"import recv.one.Recv\n"
"\n"
"fun Recv.shine() {}\n";
static const char JT_I_EXT_OTHER[] = "package kt\n"
"\n"
"import recv.two.Recv\n"
"\n"
"fun Recv.shine() {}\n";
static const char JT_I_EXTUSER[] = "package kt\n"
"\n"
"import recv.one.Recv\n"
"\n"
"/** @see Recv.shine */\n"
"class ExtUser\n";
TEST(doc_mentions_jvm_incremental_equals_full) {
jt_repo_t r;
ASSERT(jt_begin(&r, "inc"));
const char *main_java = "src/main/java/app/Main.java";
const char *khelper_kt = "src/main/kotlin/kt/KHelper.kt";
jt_write(&r, main_java, JT_I_MAIN);
jt_write(&r, "src/main/java/lib/Target.java", JT_I_TARGET);
jt_write(&r, "src/main/java/lib/Ov.java", JT_I_OV);
jt_write(&r, "src/main/java/lib/Sig.java", JT_I_SIG);
jt_write(&r, "src/main/java/lib/Gone.java",
"package lib;\npublic class Gone {\n public void m() {}\n}\n");
jt_write(&r, "src/main/java/lib/Base.java",
"package lib;\npublic class Base {\n public void inherited() {}\n}\n");
jt_write(&r, "src/main/java/alt/Base.java",
"package alt;\npublic class Base {\n public void other() {}\n}\n");
jt_write(&r, "src/main/java/d/Derived.java", JT_I_DERIVED);
jt_write(&r, "src/main/java/p/Types.java", JT_I_P);
jt_write(&r, "src/main/java/q/Dup.java", "package q;\npublic class Dup {}\n");
jt_write(&r, "src/main/java/s/Extra.java", "package s;\npublic class Extra {}\n");
jt_write(&r, khelper_kt, JT_I_KHELPER);
jt_write(&r, "src/main/kotlin/kt/KOther.kt", JT_I_KOTHER);
const char *ext_kt = "src/main/kotlin/kt/Ext.kt";
const char *extuser_kt = "src/main/kotlin/kt/ExtUser.kt";
jt_write(&r, "src/main/kotlin/recv/one/Recv.kt", "package recv.one\n\nclass Recv\n");
jt_write(&r, "src/main/kotlin/recv/two/Recv.kt", "package recv.two\n\nclass Recv\n");
jt_write(&r, ext_kt, JT_I_EXT);
jt_write(&r, extuser_kt, JT_I_EXTUSER);
char full_db[600];
snprintf(full_db, sizeof(full_db), "%s/full.db", r.tmp);
const char *db = r.db;
const cbm_incremental_route_t repair = CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR;
const cbm_incremental_route_t forced = CBM_INCREMENTAL_ROUTE_FORCED_FULL;
ASSERT_EQ(dm_index(r.repo, db, NULL), 0);
/* the preconditions the steps below move away from */
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Target"), 1);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Target.go"), 1);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Target.LIMIT"), 1);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Sig.go"), 1);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Gone.m"), 1);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Base.inherited"), 1); /* through d.Derived */
ASSERT_EQ(jt_edge(db, "app.Main", "kt.KHelper.KHelper"), 1); /* a Kotlin class */
ASSERT_STR_EQ(jt_reason(db, main_java, "Ov#run"), "ambiguous"); /* an overload group */
ASSERT_STR_EQ(jt_reason(db, main_java, "Dup"), "ambiguous"); /* p.* and q.* */
ASSERT_STR_EQ(jt_reason(db, main_java, "Extra"), "missing"); /* s is not imported */
ASSERT_EQ(jt_edge(db, "kt.KHelper.KHelper", "lib.Target"), 1); /* a Java class */
ASSERT_EQ(jt_edge(db, "kt.KHelper.KHelper", "lib.Target.go"), 1);
ASSERT_EQ(jt_edge(db, "kt.KHelper.KHelper", "kt.KOther.KOther.gone"), 1);
ASSERT_STR_EQ(jt_reason(db, khelper_kt, "lib.Ov.run"), "ambiguous");
ASSERT_EQ(jt_edge(db, "kt.KOther.KOther", "kt.KHelper.KHelper.help"), 1);
ASSERT_EQ(jt_edge(db, "kt.ExtUser.ExtUser", "kt.Ext.shine"), 1); /* an extension of Recv */
/* a doc-comment edit re-extracts the file alone */
jt_write(&r, main_java, JT_I_MAIN_EDITED);
ASSERT_EQ(dm_step(r.repo, db, full_db, "doc edit", repair), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Ov.run"), 1);
/* a body edit in a TARGET file: its scope is unchanged, and the edges
* into it stand */
jt_write(&r, "src/main/java/lib/Sig.java", JT_I_SIG_BODY);
ASSERT_EQ(dm_step(r.repo, db, full_db, "target body edit", repair), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Sig.go"), 1);
/* moving every declaration of a target file to another line changes no
* scope: the stored scopes carry no line numbers */
jt_write(&r, "src/main/java/lib/Sig.java", JT_I_SIG_MOVED);
ASSERT_EQ(dm_step(r.repo, db, full_db, "lines moved", repair), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Sig.go"), 1);
/* an import no declaration header of the file writes scopes the file
* alone */
jt_write(&r, main_java, JT_I_MAIN_IMPORT);
ASSERT_EQ(dm_step(r.repo, db, full_db, "import added", repair), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "s.Extra"), 1);
/* ... but the import that binds a supertype decides what OTHER files
* inherit through the type: Main has no edge into Derived.java */
jt_write(&r, "src/main/java/d/Derived.java", JT_I_DERIVED_ALT);
ASSERT_EQ(dm_step(r.repo, db, full_db, "supertype re-bound by an import", forced), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Base.inherited"), 0);
ASSERT_STR_EQ(jt_reason(db, main_java, "d.Derived#inherited()"), "missing");
/* the same for the import that binds an extension's RECEIVER: of which
* type `shine` is an extension decides what other files find under
* `Recv.shine`. Away from the type ExtUser names ... */
jt_write(&r, ext_kt, JT_I_EXT_OTHER);
ASSERT_EQ(dm_step(r.repo, db, full_db, "receiver re-bound by an import", forced), 0);
ASSERT_EQ(jt_edge(db, "kt.ExtUser.ExtUser", "kt.Ext.shine"), 0);
ASSERT_STR_EQ(jt_reason(db, extuser_kt, "Recv.shine"), "missing");
/* ... and back to it. ExtUser has no edge into Ext.kt now and Ext.kt
* removed no name, so no file-by-file repair would look at ExtUser again */
jt_write(&r, ext_kt, JT_I_EXT);
ASSERT_EQ(dm_step(r.repo, db, full_db, "receiver bound back by an import", forced), 0);
ASSERT_EQ(jt_edge(db, "kt.ExtUser.ExtUser", "kt.Ext.shine"), 1);
ASSERT_STR_EQ(jt_reason(db, extuser_kt, "Recv.shine"), "");
/* a removed member (an overload group shrinks to one) re-resolves the
* files whose rows name it, of either language, although they have no
* edge into the changed file */
jt_write(&r, "src/main/java/lib/Ov.java", JT_I_OV_ONE);
ASSERT_EQ(dm_step(r.repo, db, full_db, "member removed", repair), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Ov.run"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":2"));
ASSERT_STR_EQ(jt_reason(db, main_java, "Ov#run"), "");
ASSERT_EQ(jt_edge(db, "kt.KHelper.KHelper", "lib.Ov.run"), 1);
/* a Java field's node is named by the field alone: a changed initializer
* keeps the node, and the edge stays on it */
jt_write(&r, "src/main/java/lib/Target.java", JT_I_TARGET_INIT);
ASSERT_EQ(dm_step(r.repo, db, full_db, "field initializer", repair), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Target.LIMIT"), 1);
/* a removed TYPE is not repaired file by file (it may be another type's
* supertype): one of two ambiguous candidates goes, the other binds */
jt_write(&r, "src/main/java/p/Types.java", JT_I_P_NO_DUP);
ASSERT_EQ(dm_step(r.repo, db, full_db, "type removed", forced), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "q.Dup"), 1);
/* renaming a target, named from both languages. Both files still import
* the old name: an import of a type that the repository's own package
* does not show vouches for a declaration the graph lacks */
jt_write(&r, "src/main/java/lib/Target.java", JT_I_TARGET_RENAMED);
ASSERT_EQ(dm_step(r.repo, db, full_db, "rename", forced), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Target"), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Target.go"), 0);
ASSERT_STR_EQ(jt_reason(db, main_java, "Target"), "graph_gap");
ASSERT_STR_EQ(jt_reason(db, main_java, "Target#go(int)"), "graph_gap");
ASSERT_EQ(jt_edge(db, "kt.KHelper.KHelper", "lib.Target"), 0);
ASSERT_STR_EQ(jt_reason(db, khelper_kt, "Target"), "graph_gap");
/* Kotlin: a doc-comment edit */
jt_write(&r, "src/main/kotlin/kt/KOther.kt", JT_I_KOTHER_EDITED);
ASSERT_EQ(dm_step(r.repo, db, full_db, "kotlin doc edit", repair), 0);
ASSERT_EQ(jt_edge(db, "kt.KOther.KOther", "kt.KHelper.KHelper"), 1);
/* Kotlin: a removed member takes the edge into it along */
jt_write(&r, "src/main/kotlin/kt/KOther.kt", JT_I_KOTHER_NO_GONE);
ASSERT_EQ(dm_step(r.repo, db, full_db, "kotlin member removed", repair), 0);
ASSERT_EQ(jt_edge(db, "kt.KHelper.KHelper", "kt.KOther.KOther.gone"), 0);
ASSERT_STR_EQ(jt_reason(db, khelper_kt, "KOther.gone"), "missing");
/* a deleted file takes its types along: the name Main writes out in full
* (no import vouches for it) is now missing in a package that stays */
ASSERT_EQ(unlink(TH_PATH(r.repo, "src/main/java/lib/Gone.java")), 0);
ASSERT_EQ(dm_step(r.repo, db, full_db, "delete file", forced), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "lib.Gone.m"), 0);
ASSERT_STR_EQ(jt_reason(db, main_java, "lib.Gone#m()"), "missing");
/* Kotlin: renaming a target a Java file names */
jt_write(&r, khelper_kt, JT_I_KHELPER_RENAMED);
ASSERT_EQ(dm_step(r.repo, db, full_db, "kotlin rename", forced), 0);
ASSERT_EQ(jt_edge(db, "app.Main", "kt.KHelper.KHelper"), 0);
ASSERT_STR_EQ(jt_reason(db, main_java, "kt.KHelper"), "missing");
dm_unlink_db(full_db);
jt_end(&r);
PASS();
}
/* ── the parallel path == the sequential path ────────────────────── */
enum { JT_RING = 32 }; /* per language: above MIN_FILES_FOR_PARALLEL together */
/* A ring of Java classes and a ring of Kotlin classes. Every class documents
* its successor, its counterpart in the other language, a shared target (by
* name, by overload group and by signature) and a name nothing declares. */
static void jt_write_ring_fixture(const jt_repo_t *r) {
for (int i = 0; i < JT_RING; i++) {
char path[128];
char body[1024];
int next = (i + 1) % JT_RING;
snprintf(path, sizeof(path), "src/main/java/ring/J%02d.java", i);
snprintf(body, sizeof(body),
"package ring;\n"
"import ring.shared.Hub;\n"
"/**\n"
" * Next {@link J%02d}, shared {@linkplain Hub the hub}, {@link Hub#run},\n"
" * {@link Nothing%02d}, in Kotlin {@link ringk.K%02d}.\n"
" * @see Hub#run(String)\n"
" */\n"
"public class J%02d {}\n",
next, i, i, i);
jt_write(r, path, body);
snprintf(path, sizeof(path), "src/main/kotlin/ringk/K%02d.kt", i);
snprintf(body, sizeof(body),
"package ringk\n"
"\n"
"import ring.shared.Hub\n"
"\n"
"/**\n"
" * Next [K%02d], shared [Hub], in Java [ring.J%02d].\n"
" * @see Hub.run\n"
" * @see Nothing%02d\n"
" */\n"
"class K%02d\n",
next, i, i, i);
jt_write(r, path, body);
}
jt_write(r, "src/main/java/ring/shared/Hub.java",
"package ring.shared;\n"
"public class Hub {\n"
" public void run(int n) {}\n"
" public void run(String s) {}\n"
"}\n");
}
/* The worker pipeline (extract workers, resolve workers, per-file rows merged
* at the end) and the sequential passes publish the same edges and rows. */
TEST(doc_mentions_jvm_parallel_equals_sequential) {
jt_repo_t r;
ASSERT(jt_begin(&r, "par"));
jt_write_ring_fixture(&r);
char seq_db[600];
snprintf(seq_db, sizeof(seq_db), "%s/seq.db", r.tmp);
ASSERT_EQ(dm_workers_agree(r.repo, r.db, seq_db), 0);
/* per Java class: successor, Hub, Hub.run(String), its Kotlin counterpart;
* per Kotlin class: successor, Hub, its Java counterpart */
ASSERT_EQ(dm_count(r.db, "SELECT COUNT(*) FROM edges WHERE type = 'MENTIONS'"),
JT_RING * 4 + JT_RING * 3);
/* per class of either language: the overload group and the undeclared
* name */
ASSERT_EQ(dm_count(r.db, "SELECT COUNT(*) FROM doc_link_unresolved"), JT_RING * 4);
ASSERT_EQ(dm_count(r.db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE reason = 'ambiguous'"),
JT_RING * 2);
ASSERT_EQ(dm_count(r.db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE reason = 'missing'"),
JT_RING * 2);
dm_unlink_db(seq_db);
jt_end(&r);
PASS();
}
/* ── the platforms' own namespaces ───────────────────────────────── */
/* An all-lower-case path that nothing declares is external under the
* namespaces the two platforms define themselves: `java`, `javax`, `kotlin`.
* No library family is such a root. A path under `kotlinx` is external only
* where the file imports from that package (which the repository does not
* declare); without an import it names nothing, like any other path. */
TEST(doc_mentions_jvm_platform_roots) {
jt_repo_t r;
ASSERT(jt_begin(&r, "roots"));
const char *with = "src/main/kotlin/rt/WithImport.kt";
jt_write(&r, with,
"package rt\n"
"\n"
"import kotlinx.coroutines.flow.map\n"
"\n"
"/**\n"
" * @see java.util.concurrent\n"
" * @see javax.annotation.processing\n"
" * @see kotlin.collections.map\n"
" * @see kotlinx.coroutines.flow.onEach\n"
" */\n"
"class WithImport\n");
const char *without = "src/main/kotlin/rt/NoImport.kt";
jt_write(&r, without,
"package rt\n"
"\n"
"/**\n"
" * [kotlinx.coroutines.flow.map]\n"
" * @see kotlinx.coroutines.flow.onEach\n"
" * @see org.outside.lib.helper\n"
" */\n"
"class NoImport\n");
const char *java = "src/main/java/rt/JRoots.java";
jt_write(&r, java,
"package rt;\n"
"/** {@link java.util.concurrent} {@link kotlinx.coroutines.flow} */\n"
"public class JRoots {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* the three roots */
ASSERT_EQ(jt_rows(db, with, "java.util.concurrent", "external"), 1);
ASSERT_EQ(jt_rows(db, with, "javax.annotation.processing", "external"), 1);
ASSERT_EQ(jt_rows(db, with, "kotlin.collections.map", "external"), 1);
ASSERT_EQ(jt_rows(db, java, "java.util.concurrent", "external"), 1);
/* `kotlinx` with an import of that package: external by the import */
ASSERT_EQ(jt_rows(db, with, "kotlinx.coroutines.flow.onEach", "external"), 1);
/* ... and without one: a block tag keeps its row as missing, a `[name]`
* is plain text, exactly as for any other library's path */
ASSERT_EQ(jt_rows(db, without, "kotlinx.coroutines.flow.onEach", "missing"), 1);
ASSERT_EQ(jt_rows(db, without, "org.outside.lib.helper", "missing"), 1);
ASSERT_STR_EQ(jt_reason(db, without, "kotlinx.coroutines.flow.map"), "");
ASSERT_EQ(jt_rows(db, java, "kotlinx.coroutines.flow", "missing"), 1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM edges WHERE type = 'MENTIONS'"), 0);
jt_end(&r);
PASS();
}
/* ── a field against a method from outside ───────────────────────── */
/* A bare Javadoc `#x` that finds a FIELD. javadoc looks for a method of the
* name as well and takes the field only when its class is the method's class
* or below it. The index cannot see the methods of a supertype outside the
* repository, or what an enum has from the language; where one of those may
* come first, the reference is external. */
/* A type consulted BEFORE the field's level that may have the method unseen:
* the field of the class around is not below that type's supertypes. */
TEST(doc_mentions_jvm_java_field_before_level) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jfbef"));
const char *f1 = "src/main/java/gf/F1.java";
jt_write(&r, f1,
"package gf;\n"
"public class F1 {\n"
" public int available = 1;\n"
" public int hashCode = 2;\n"
" public int name = 3;\n"
" public int other = 4;\n"
" public class In extends java.io.InputStream {\n"
" public int read() { return 0; }\n"
" /** {@link #available} {@link #hashCode} */\n"
" public void m() {}\n"
" }\n"
" public class Closed {\n"
" /** {@link #available} {@link #hashCode} */\n"
" public void m() {}\n"
" }\n"
" public enum Mode {\n"
" ON;\n"
" /** {@link #name} {@link #other} */\n"
" public void m() {}\n"
" }\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* java.io.InputStream may declare available(); it may also override
* hashCode(), and then the method is no longer Object's */
ASSERT_EQ(jt_rows(db, f1, "#available", "external"), 1);
ASSERT_EQ(jt_rows(db, f1, "#hashCode", "external"), 1);
ASSERT_EQ(dm_mentions_from(db, "gf.F1.In.m"), 0);
/* decoy: a nested class without a supertype outside binds both. Object's
* own hashCode() is no reason to yield: every class is below Object */
ASSERT_EQ(jt_edge(db, "gf.F1.Closed.m", "gf.F1.available"), 1);
ASSERT_EQ(jt_edge(db, "gf.F1.Closed.m", "gf.F1.hashCode"), 1);
/* an enum has name() from the language, and the class around is not
* below java.lang.Enum; a name the enum does not have binds */
ASSERT_EQ(jt_rows(db, f1, "#name", "external"), 1);
ASSERT_EQ(jt_edge(db, "gf.F1.Mode.m", "gf.F1.other"), 1);
ASSERT_EQ(dm_mentions_from(db, "gf.F1.Mode.m"), 1);
jt_end(&r);
PASS();
}
/* At the field's OWN level: the field binds when every supertype outside the
* repository of that level's type is a supertype of the field's class too. A
* field the type declares itself always is; an inherited one only when the
* type adds no supertype from outside of its own. The same for the type a
* reference names (`Type#x`). */
TEST(doc_mentions_jvm_java_field_at_open_level) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jflvl"));
jt_write(&r, "src/main/java/gf/F2.java",
"package gf;\n"
"public class F2 extends java.io.InputStream {\n"
" public int own = 1;\n"
" public int read() { return 0; }\n"
" /** {@link #own} */\n"
" public void m() {}\n"
"}\n");
const char *f3 = "src/main/java/gf/F3.java";
jt_write(&r, f3,
"package gf;\n"
"public class F3 extends F3Base implements java.io.Closeable {\n"
" public void close() {}\n"
" /** {@link #LIMIT} */\n"
" public void m() {}\n"
"}\n"
"class F3Base {\n"
" public static final int LIMIT = 1;\n"
"}\n");
jt_write(&r, "src/main/java/gf/F4.java",
"package gf;\n"
"public class F4 extends F4Base {\n"
" /** {@link #shared} */\n"
" public void m() {}\n"
"}\n"
"class F4Base extends java.io.InputStream {\n"
" public int shared = 1;\n"
" public int read() { return 0; }\n"
"}\n");
const char *f5 = "src/main/java/gf/F5.java";
jt_write(&r, f5,
"package gf;\n"
"/** {@link F2#own} {@link F3#LIMIT} {@link F4#shared} {@value F3#LIMIT} */\n"
"public class F5 {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* declared by the type itself: below everything the type is below */
ASSERT_EQ(jt_edge(db, "gf.F2.m", "gf.F2.own"), 1);
/* inherited, and F3 itself adds java.io.Closeable, which is no supertype
* of F3Base: a method LIMIT() there would be what javadoc links */
ASSERT_EQ(jt_rows(db, f3, "#LIMIT", "external"), 1);
ASSERT_EQ(dm_mentions_from(db, "gf.F3.m"), 0);
/* inherited from the one type of the hierarchy that leaves the
* repository: its supertypes are the field's class's own */
ASSERT_EQ(jt_edge(db, "gf.F4.m", "gf.F4Base.shared"), 1);
/* the same three at a type the reference names */
ASSERT_EQ(jt_edge(db, "gf.F5", "gf.F2.own"), 1);
ASSERT_EQ(jt_rows(db, f5, "F3#LIMIT", "external"), 1);
ASSERT_EQ(jt_edge(db, "gf.F5", "gf.F4Base.shared"), 1);
/* {@value} names a field and nothing else: as before */
ASSERT_EQ(jt_edge(db, "gf.F5", "gf.F3Base.LIMIT"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"syntax\":\"value\""));
ASSERT_EQ(dm_mentions_from(db, "gf.F5"), 3);
jt_end(&r);
PASS();
}
/* ── javadoc's search for a method (JavacTrees.searchMethod in the JDK) ──
*
* From a type: its own methods, its superclass by the whole search (with the
* superclass's own interfaces and the type around the superclass), its
* interfaces in the order written, then the type around it; each type once.
* A type outside the repository on the way makes the reference external. */
/* Step 2 takes in the type around the SUPERCLASS, before the type around the
* documented one. */
TEST(doc_mentions_jvm_java_search_superclass_around) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jsenc"));
jt_write(&r, "src/main/java/sa/Base2.java",
"package sa;\n"
"public class Base2 {\n"
" public void go() {}\n"
" public static class Inner {}\n"
"}\n");
jt_write(&r, "src/main/java/sa/Plain2.java", "package sa;\npublic class Plain2 {}\n");
jt_write(&r, "src/main/java/sa/Holder.java",
"package sa;\n"
"public class Holder {\n"
" public void go() {}\n"
" public static class Sub extends Base2.Inner {\n"
" /** {@link #go()} */\n"
" public void m() {}\n"
" }\n"
" public static class Sub2 extends Plain2 {\n"
" /** {@link #go()} */\n"
" public void m() {}\n"
" }\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* Base2.Inner is searched with Base2 around it, before Holder */
ASSERT_EQ(jt_edge(db, "sa.Holder.Sub.m", "sa.Base2.go"), 1);
ASSERT_EQ(jt_edge(db, "sa.Holder.Sub.m", "sa.Holder.go"), 0);
/* decoy: a superclass nested in nothing: the type around Sub2 */
ASSERT_EQ(jt_edge(db, "sa.Holder.Sub2.m", "sa.Holder.go"), 1);
jt_end(&r);
PASS();
}
/* Step 2 before step 3: a superclass outside the repository comes before an
* interface of the repository. */
TEST(doc_mentions_jvm_java_search_outside_superclass) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jsout"));
const char *s1 = "src/main/java/sb/S1.java";
jt_write(&r, s1,
"package sb;\n"
"public abstract class S1 extends java.io.InputStream implements Api {\n"
" /** {@link #close()} */\n"
" public void m() {}\n"
"}\n"
"interface Api {\n"
" void close();\n"
"}\n"
"class Plain {}\n"
"abstract class S2 extends Plain implements Api {\n"
" /** {@link #close()} */\n"
" public void m() {}\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* java.io.InputStream is searched first; its close() is the one */
ASSERT_EQ(jt_rows(db, s1, "#close()", "external"), 1);
ASSERT_EQ(jt_edge(db, "sb.S1.m", "sb.Api.close"), 0);
/* decoy: a superclass of the repository without the method */
ASSERT_EQ(jt_edge(db, "sb.S2.m", "sb.Api.close"), 1);
jt_end(&r);
PASS();
}
/* Step 3: the interfaces in the order written; the first with the method is
* the one. */
TEST(doc_mentions_jvm_java_search_interface_order) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jsord"));
jt_write(&r, "src/main/java/sc/Both.java",
"package sc;\n"
"public abstract class Both implements First, Second {\n"
" /** {@link #run()} */\n"
" public void m() {}\n"
"}\n"
"interface First {\n"
" void run();\n"
"}\n"
"interface Second {\n"
" void run();\n"
"}\n"
"abstract class Other implements Second, First {\n"
" /** {@link #run()} */\n"
" public void m() {}\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
ASSERT_EQ(jt_edge(db, "sc.Both.m", "sc.First.run"), 1);
ASSERT_EQ(jt_edge(db, "sc.Both.m", "sc.Second.run"), 0);
/* decoy: the other order */
ASSERT_EQ(jt_edge(db, "sc.Other.m", "sc.Second.run"), 1);
ASSERT_EQ(jt_edge(db, "sc.Other.m", "sc.First.run"), 0);
jt_end(&r);
PASS();
}
/* An interface has no superclass step: Object's methods are not searched at
* it, and the search goes on to the type around it. */
TEST(doc_mentions_jvm_java_search_interface_no_object) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jsifc"));
const char *h = "src/main/java/sd/Host.java";
jt_write(&r, h,
"package sd;\n"
"public class Host {\n"
" public String toString() { return \"\"; }\n"
" public interface Shape {\n"
" /** {@link #toString()} */\n"
" void m();\n"
" }\n"
" public static class Box {\n"
" /** {@link #toString()} */\n"
" public void m() {}\n"
" }\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
ASSERT_EQ(jt_edge(db, "sd.Host.Shape.m", "sd.Host.toString"), 1);
/* decoy: a class has the superclass Object, whose toString() comes first */
ASSERT_EQ(jt_rows(db, h, "#toString()", "external"), 1);
ASSERT_EQ(jt_edge(db, "sd.Host.Box.m", "sd.Host.toString"), 0);
jt_end(&r);
PASS();
}
/* A Kotlin class on the way writes its superclass anywhere in its list: the
* class among its supertypes is searched before its interfaces. */
TEST(doc_mentions_jvm_java_search_kotlin_superclass) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jskt"));
jt_write(&r, "src/main/kotlin/se/KApi.kt",
"package se\n\ninterface KApi {\n fun run()\n}\n");
jt_write(&r, "src/main/kotlin/se/KBase.kt",
"package se\n\nopen class KBase {\n fun run() {}\n}\n");
jt_write(&r, "src/main/kotlin/se/KMix.kt", "package se\n\nopen class KMix : KApi, KBase()\n");
jt_write(&r, "src/main/kotlin/se/KOnly.kt", "package se\n\nabstract class KOnly : KApi\n");
jt_write(&r, "src/main/java/se/J.java",
"package se;\n"
"public class J extends KMix {\n"
" /** {@link #run} */\n"
" public void m() {}\n"
"}\n"
"abstract class J2 extends KOnly {\n"
" /** {@link #run} */\n"
" public void m() {}\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
ASSERT_EQ(jt_edge(db, "se.J.m", "se.KBase.KBase.run"), 1);
ASSERT_EQ(jt_edge(db, "se.J.m", "se.KApi.KApi.run"), 0);
/* decoy: no class among the supertypes: the interface */
ASSERT_EQ(jt_edge(db, "se.J2.m", "se.KApi.KApi.run"), 1);
jt_end(&r);
PASS();
}
/* ── The field rule against that search ───────────────────────────────
*
* A field javadoc's field search finds binds only when its class is below
* every type outside the repository that the search for a METHOD passes
* before a type of the repository that declares one, and below that method's
* class. */
/* A field of the innermost type; the class AROUND it leaves the repository. */
TEST(doc_mentions_jvm_java_field_outer_outside) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jfout"));
const char *o = "src/main/java/ga/Outer.java";
jt_write(&r, o,
"package ga;\n"
"public class Outer extends java.io.InputStream {\n"
" public int read() { return 0; }\n"
" public class In {\n"
" public int available = 1;\n"
" /** {@link #available} */\n"
" public void m() {}\n"
" }\n"
"}\n");
jt_write(&r, "src/main/java/ga/Shut.java",
"package ga;\n"
"public class Shut {\n"
" public class In2 {\n"
" public int available = 1;\n"
" /** {@link #available} */\n"
" public void m() {}\n"
" }\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* the search passes In, then Outer and java.io.InputStream, whose
* available() would win: In is not below it */
ASSERT_EQ(jt_rows(db, o, "#available", "external"), 1);
ASSERT_EQ(dm_mentions_from(db, "ga.Outer.In.m"), 0);
/* decoy: the class around stays in the repository */
ASSERT_EQ(jt_edge(db, "ga.Shut.In2.m", "ga.Shut.In2.available"), 1);
jt_end(&r);
PASS();
}
/* The type's OWN field, the type leaving the repository itself and a class
* further out as well: the field is below the first, not the second. */
TEST(doc_mentions_jvm_java_field_own_far_outside) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jfown"));
const char *far_src = "src/main/java/gb/Far.java";
jt_write(&r, far_src,
"package gb;\n"
"public class Far extends java.io.InputStream {\n"
" public int read() { return 0; }\n"
" public abstract class Near extends java.io.Reader {\n"
" public int level = 1;\n"
" /** {@link #level} */\n"
" public void m() {}\n"
" }\n"
"}\n");
jt_write(&r, "src/main/java/gb/Far2.java",
"package gb;\n"
"public class Far2 {\n"
" public abstract class Near2 extends java.io.Reader {\n"
" public int level = 1;\n"
" /** {@link #level} */\n"
" public void m() {}\n"
" }\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
ASSERT_EQ(jt_rows(db, far_src, "#level", "external"), 1);
ASSERT_EQ(dm_mentions_from(db, "gb.Far.Near.m"), 0);
/* decoy: only the type's own supertype leaves the repository */
ASSERT_EQ(jt_edge(db, "gb.Far2.Near2.m", "gb.Far2.Near2.level"), 1);
jt_end(&r);
PASS();
}
/* Past a step outside the repository the field's class is below, a method of
* the repository the field's class is NOT below: javadoc links that method,
* or one the step outside holds (then the field): external. Without the step
* outside, javadoc links the method, and so does the reference. */
TEST(doc_mentions_jvm_java_field_method_past_outside) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jfpast"));
const char *top = "src/main/java/gc/Top.java";
jt_write(&r, top,
"package gc;\n"
"public class Top {\n"
" public int count() { return 0; }\n"
" public class Mid extends java.io.InputStream {\n"
" public int count = 1;\n"
" public int read() { return 0; }\n"
" public class Doc {\n"
" /** {@link #count} */\n"
" public void m() {}\n"
" }\n"
" }\n"
" public class Mid2 {\n"
" public int count = 1;\n"
" public class Doc2 {\n"
" /** {@link #count} */\n"
" public void m() {}\n"
" }\n"
" }\n"
"}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
ASSERT_EQ(jt_rows(db, top, "#count", "external"), 1);
ASSERT_EQ(dm_mentions_from(db, "gc.Top.Mid.Doc.m"), 0);
/* decoy: no step outside on the way: the method of Top */
ASSERT_EQ(jt_edge(db, "gc.Top.Mid2.Doc2.m", "gc.Top.count"), 1);
ASSERT_EQ(jt_rows(db, top, "#count", "ambiguous"), 0);
jt_end(&r);
PASS();
}
/* `Type#x`: javadoc's field search goes through the types around the type
* named, as its method search does. A field there comes first when its class
* is below the method's class. */
TEST(doc_mentions_jvm_java_field_named_around) {
jt_repo_t r;
ASSERT(jt_begin(&r, "jfnam"));
jt_write(&r, "src/main/java/ge/Base3.java",
"package ge;\n"
"public class Base3 {\n"
" public int x() { return 0; }\n"
"}\n");
jt_write(&r, "src/main/java/ge/E.java",
"package ge;\n"
"public class E extends Base3 {\n"
" public int x = 1;\n"
" public static class T {}\n"
"}\n");
jt_write(&r, "src/main/java/ge/User.java",
"package ge;\n"
"/** {@link E.T#x} {@link E#x} */\n"
"public class User {}\n");
ASSERT_EQ(dm_index(r.repo, r.db, NULL), 0);
const char *db = r.db;
/* the method search from E.T reaches Base3.x() through E; the field
* search reaches E.x first, and E is below Base3 */
ASSERT_EQ(jt_edge(db, "ge.User", "ge.E.x"), 1);
ASSERT_NOT_NULL(strstr(jt_props, "\"count\":2"));
ASSERT_EQ(jt_edge(db, "ge.User", "ge.Base3.x"), 0);
jt_end(&r);
PASS();
}
SUITE(doc_mentions_jvm) {
dm_ship_held_families();
RUN_TEST(doc_mentions_jvm_java_tokens);
RUN_TEST(doc_mentions_jvm_kotlin_tokens);
RUN_TEST(doc_mentions_jvm_malformed_and_oversized);
RUN_TEST(doc_mentions_jvm_java_norm_type);
RUN_TEST(doc_mentions_jvm_java_scope_blob);
RUN_TEST(doc_mentions_jvm_kotlin_scope_blob);
RUN_TEST(doc_mentions_jvm_scope_parse_errors);
RUN_TEST(doc_mentions_jvm_resolver_parse_errors);
RUN_TEST(doc_mentions_jvm_java_families_and_reasons);
RUN_TEST(doc_mentions_jvm_oversized_reference);
RUN_TEST(doc_mentions_jvm_java_type_levels);
RUN_TEST(doc_mentions_jvm_java_member_levels);
RUN_TEST(doc_mentions_jvm_kotlin_families_and_reasons);
RUN_TEST(doc_mentions_jvm_kotlin_levels);
RUN_TEST(doc_mentions_jvm_java_given_members);
RUN_TEST(doc_mentions_jvm_java_open_supertypes);
RUN_TEST(doc_mentions_jvm_java_uncompared_level);
RUN_TEST(doc_mentions_jvm_kotlin_given_members);
RUN_TEST(doc_mentions_jvm_kotlin_open_supertypes);
RUN_TEST(doc_mentions_jvm_kotlin_companion_inherits);
RUN_TEST(doc_mentions_jvm_kotlin_extension_guard);
RUN_TEST(doc_mentions_jvm_root_supertype_written);
RUN_TEST(doc_mentions_jvm_guard_incremental_equals_full);
RUN_TEST(doc_mentions_jvm_cross_language);
RUN_TEST(doc_mentions_jvm_node_ownership);
RUN_TEST(doc_mentions_jvm_kotlin_type_and_function);
RUN_TEST(doc_mentions_jvm_test_only_layouts);
RUN_TEST(doc_mentions_jvm_family_names);
RUN_TEST(doc_mentions_jvm_scope_delta_rules);
RUN_TEST(doc_mentions_jvm_incremental_equals_full);
RUN_TEST(doc_mentions_jvm_parallel_equals_sequential);
RUN_TEST(doc_mentions_jvm_platform_roots);
RUN_TEST(doc_mentions_jvm_java_field_before_level);
RUN_TEST(doc_mentions_jvm_java_field_at_open_level);
RUN_TEST(doc_mentions_jvm_java_search_superclass_around);
RUN_TEST(doc_mentions_jvm_java_search_outside_superclass);
RUN_TEST(doc_mentions_jvm_java_search_interface_order);
RUN_TEST(doc_mentions_jvm_java_search_interface_no_object);
RUN_TEST(doc_mentions_jvm_java_search_kotlin_superclass);
RUN_TEST(doc_mentions_jvm_java_field_outer_outside);
RUN_TEST(doc_mentions_jvm_java_field_own_far_outside);
RUN_TEST(doc_mentions_jvm_java_field_method_past_outside);
RUN_TEST(doc_mentions_jvm_java_field_named_around);
}