test(resolver): attest class mentions and cover C# operator scopes

A matrix row on a class target brings that class's other roles into the harness
audit. `make(): Widget` and `anon(): User` name the class they also construct, so
the signature's USAGE, and USES_TYPE in Scala and Swift, become required rows at
that exact token.

mkc_c2_csharp_operator_overloads pins the operator scope for overloads stored as
variants, for a spaced `operator -` and for the word form `operator true`.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-01 20:24:05 +01:00
1 parent 2840080f69
commit 852cb8859f
2 files changed
+126 -19

No files matched your search

+107 -19
View File
@@ -183,12 +183,6 @@ static const MKC_Golden mkc_goldens[] = {
" public float Dot(Vec2 o) { return X*o.X + Y*o.Y; }", "Method", 8, 0, false},
{"mkc_c2_csharp_operator_plus", "operator+", "new Vec2(a.X + b.X, a.Y + b.Y)", "Vec2",
" public Vec2(float x, float y) { X = x; Y = y; }", "Method", 4, 0, true},
{"mkc_c2_csharp_operator_overloads", "operator+", "new Vec2(a.X + b.X)", "Vec2",
" public Vec2(float x) { X = x; }", "Method", 4, 0, true},
{"mkc_c2_csharp_operator_overloads", "operator+", "new Vec2(a.X + b)", "Vec2",
" public Vec2(float x) { X = x; }", "Method", 4, 0, true},
{"mkc_c2_csharp_operator_overloads", "operator-", "new Vec2(a.X - b.X)", "Vec2",
" public Vec2(float x) { X = x; }", "Method", 4, 0, true},
{"mkc_c2_scala_symbolic_method", "run", "a + b", "+",
" def +(other: Vec2): Vec2 = Vec2(x + other.x, y + other.y)", "Method", 2, 0, false},
{"mkc_c2_scala_symbolic_method", "run", "(a + b).dot(b)", "dot",
@@ -271,6 +265,23 @@ static const MKC_Golden mkc_goldens[] = {
{"mkc_c8_kotlin_constructor_samefile", "run", "make(name).label()", "label",
" fun label(): String = name", "Method", 2, 0, true},
};
/* The harness audits a known target under every role. A class named in a signature is
* referenced there, in these languages as USAGE and as USES_TYPE, apart from the call. */
typedef struct {
const char *test, *caller, *needle, *relation, *target, *declaration;
int line;
unsigned occurrence;
} MKC_TypeMention;
static const MKC_TypeMention mkc_type_mentions[] = {
{"mkc_c1_scala_constructor_new", "make", "Widget", "USAGE", "Widget",
"class Widget(val name: String) {", 1, 1},
{"mkc_c1_scala_constructor_new", "make", "Widget", "USES_TYPE", "Widget",
"class Widget(val name: String) {", 1, 1},
{"mkc_c1_swift_initializer", "make", "Widget", "USAGE", "Widget", "class Widget {", 1, 1},
{"mkc_c1_swift_initializer", "make", "Widget", "USES_TYPE", "Widget", "class Widget {", 1, 1},
{"mkc_c8_kotlin_constructor_samefile", "make", "Widget", "USAGE", "Widget",
"class Widget(val name: String) {", 1, 1},
};
typedef struct {
const char *test, *caller, *needle;
unsigned occurrence;
@@ -346,6 +357,28 @@ static bool mkc_quality(const MKC_File *original, int count, const char *test) {
required += g->required;
n++;
}
for (size_t i = 0; i < sizeof(mkc_type_mentions) / sizeof(mkc_type_mentions[0]); i++) {
const MKC_TypeMention *g = mkc_type_mentions + i;
if (strcmp(test, g->test))
continue;
if (n == sizeof(expected) / sizeof(expected[0]))
return false;
snprintf(ids[n], sizeof(ids[n]), "%s-%zu", test, n);
expected[n] = (RQ_Expected){.case_id = ids[n],
.site = {.file = original[0].name,
.caller = g->caller,
.needle = g->needle,
.occurrence_index = g->occurrence},
.relation = g->relation,
.target = {.file = original[0].name,
.suffix = g->target,
.label = "Class",
.declaration_line = g->line,
.declaration_text = g->declaration},
.mode = RQ_REQUIRED};
required++;
n++;
}
for (size_t i = 0; i < sizeof(mkc_forbidden) / sizeof(mkc_forbidden[0]); i++) {
const MKC_Forbidden *g = mkc_forbidden + i;
if (strcmp(test, g->test))
@@ -661,20 +694,75 @@ TEST(mkc_c2_csharp_operator_plus) {
PASS();
}
/* A C# operator's body has its own node, also when the operator is overloaded. A call in it
* belongs to that operator, never to the file. */
/* A C# operator's body has its own node, also when the operator is overloaded or spelled
* with a space or a word. A call in it belongs to that operator, never to the file. */
TEST(mkc_c2_csharp_operator_overloads) {
static const MKC_File f[] = {
{"Vec.cs", "namespace App {\n"
" struct Vec2 {\n"
" public float X;\n"
" public Vec2(float x) { X = x; }\n"
" public static Vec2 operator+(Vec2 a, Vec2 b) { return new Vec2(a.X + b.X); }\n"
" public static Vec2 operator+(Vec2 a, float b) { return new Vec2(a.X + b); }\n"
" public static Vec2 operator -(Vec2 a, Vec2 b) { return new Vec2(a.X - b.X); }\n"
" }\n"
"}\n"}};
ASSERT_TRUE(mkc_quality(f, 1, __func__));
static const char source[] =
"namespace App {\n"
" struct Vec2 {\n"
" public float X;\n"
" public Vec2(float x) { X = x; }\n"
" public static Vec2 operator+(Vec2 a, Vec2 b) { return new Vec2(a.X + b.X); }\n"
" public static Vec2 operator+(Vec2 a, float b) { return new Vec2(a.X + b); }\n"
" public static Vec2 operator -(Vec2 a, Vec2 b) { return new Vec2(a.X - b.X); }\n"
" public static bool operator true(Vec2 a) { return Check(a); }\n"
" static bool Check(Vec2 a) { return true; }\n"
" }\n"
"}\n";
const MKC_File files[] = {{"Vec.cs", source}};
const RQ_Source sources[] = {{.path = "Vec.cs", .text = source}};
const RQ_Target ctor = {.file = "Vec.cs",
.suffix = "Vec2.Vec2",
.label = "Method",
.declaration_line = 4,
.declaration_text = " public Vec2(float x) { X = x; }"};
const RQ_Target check = {.file = "Vec.cs",
.suffix = "Vec2.Check",
.label = "Method",
.declaration_line = 9,
.declaration_text =
" static bool Check(Vec2 a) { return true; }"};
const RQ_Expected expected[] = {
{.case_id = "first-plus",
.site = {.file = "Vec.cs",
.caller = "Vec2.operator+",
.needle = "new Vec2(a.X + b.X)",
.caller_variant_start_line = 5,
.caller_variant_end_line = 5,
.caller_variant_declaration = " public static Vec2 operator+(Vec2 a, Vec2 "
"b) { return new Vec2(a.X + b.X); }"},
.relation = "CALLS",
.target = ctor,
.mode = RQ_REQUIRED},
{.case_id = "second-plus",
.site = {.file = "Vec.cs",
.caller = "Vec2.operator+",
.needle = "new Vec2(a.X + b)",
.caller_variant_start_line = 6,
.caller_variant_end_line = 6,
.caller_variant_declaration = " public static Vec2 operator+(Vec2 a, float "
"b) { return new Vec2(a.X + b); }"},
.relation = "CALLS",
.target = ctor,
.mode = RQ_REQUIRED},
{.case_id = "minus",
.site = {.file = "Vec.cs", .caller = "Vec2.operator-", .needle = "new Vec2(a.X - b.X)"},
.relation = "CALLS",
.target = ctor,
.mode = RQ_REQUIRED},
{.case_id = "word",
.site = {.file = "Vec.cs", .caller = "Vec2.operator true", .needle = "Check(a)"},
.relation = "CALLS",
.target = check,
.mode = RQ_REQUIRED}};
MKC_Proj project;
cbm_store_t *store = mkc_index(&project, files, 1);
RQ_Report report = {0};
bool ok = store && rq_check_project_declarations(store, project.project, __func__, sources, 1,
expected, 4, NULL, 0, &report, stderr);
mkc_cleanup(&project, store);
ASSERT_TRUE(ok);
ASSERT_EQ(report.found, 4);
PASS();
}
+19
View File
@@ -414,6 +414,25 @@ static bool mn_quality(const char *test, const char *filename, const char *conte
}
if (n == 1)
return false;
/* The harness audits a known target under every role: `anon(): User` names the class
* it also constructs, and that signature is a USAGE of it. */
if (!strcmp(test, "mn_record_kotlin_data_class")) {
if (n == sizeof(expected) / sizeof(expected[0]))
return false;
snprintf(ids[n], sizeof(ids[n]), "%s-%zu", test, n);
expected[n] = (RQ_Expected){
.case_id = ids[n],
.site = {.file = filename, .caller = "anon", .needle = "User", .occurrence_index = 1},
.relation = "USAGE",
.target = {.file = filename,
.suffix = "User",
.label = "Class",
.declaration_line = 1,
.declaration_text = "data class User(val name: String, val age: Int) {"},
.mode = RQ_REQUIRED};
required++;
n++;
}
MN_LangProj lp;
cbm_store_t *store = mn_index_files(&lp, files, 2);
RQ_Report report = {0};