From 0c4a757972abc820ed28762ab124eb76455dba77 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 2 Nov 2026 18:09:22 +0100 Subject: [PATCH] feat(resolver): recover Java, Kotlin and C# edges reference assemblies and lost spellings had voided dotnet/runtime CALLS go from 392,440 to 773,575 and USAGE from 709,951 to 1,688,346; elasticsearch CALLS from 1,308,131 to 1,342,695 and Exposed from 20,357 to 23,603. The same type in ref/ and src/ now means the implementation stands for the contract. A reader means its own file's declaration or the only one in its build project, and .projitems counts as a project. A same-length parse view reads spellings the grammar loses: Vector128.cs goes from 0 to 172 methods. Lambdas take their parameters from the callee's single abstract method. A CALLS edge can now start from a Field node, and calls in C# conversion operators, indexers and property bodies belong to those members. Five wrong edge kinds found while sampling are fixed. Signed-off-by: Martin Vogel --- internal/cbm/cbm.c | 346 ++++- internal/cbm/extract_defs.c | 6 +- internal/cbm/extract_type_refs.c | 26 +- internal/cbm/extract_unified.c | 59 +- internal/cbm/helpers.c | 29 +- internal/cbm/helpers.h | 4 + internal/cbm/lang_specs.c | 15 +- internal/cbm/lsp/oo_core.c | 904 ++++++++++++- internal/cbm/lsp/oo_csharp.c | 739 ++++++++++- internal/cbm/lsp/oo_internal.h | 70 +- internal/cbm/lsp/oo_java.c | 350 ++++- internal/cbm/lsp/oo_kotlin.c | 223 +++- internal/cbm/lsp/oo_strict.h | 7 + internal/cbm/resolve/capabilities.c | 18 +- src/discover/language.c | 1 + src/pipeline/pass_lsp_cross.c | 158 ++- tests/test_doc_mentions.c | 22 +- tests/test_parallel.c | 13 +- tests/test_resolver_kotlin.c | 7 +- tests/test_resolver_oo.c | 1910 +++++++++++++++++++++++++++ 20 files changed, 4675 insertions(+), 232 deletions(-) diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index f3d9789d0..47d5a58f6 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -1099,11 +1099,349 @@ static bool csharp_line_starts(const char *line, size_t room, const char *word) return room >= length && !strncmp(line, word, length); } +/* True when what ends just before `at` lets a unary operator start an expression there: + * punctuation, or `return`, `ref` and `out`. An identifier or `)` before a star is multiplication. + */ +static bool csharp_expression_may_start(const char *view, uint32_t at) { + static const char *const WORDS[] = {"return", "ref", "out", NULL}; + uint32_t p = at; + while (p > 0 && isspace((unsigned char)view[p - 1])) + p--; + if (p == 0) + return true; + if (strchr("=(,>{;:?&|!+-*/%<[", view[p - 1])) + return true; + for (const char *const *word = WORDS; *word; word++) { + uint32_t size = (uint32_t)strlen(*word); + if (p >= size && !strncmp(view + p - size, *word, size) && + (p == size || + !(isalnum((unsigned char)view[p - size - 1]) || view[p - size - 1] == '_'))) + return true; + } + return false; +} + +static bool csharp_word_char(char c) { + return isalnum((unsigned char)c) || c == '_'; +} + +/* `=` or a compound assignment at `at`, after blanks. */ +static bool csharp_assignment_follows(const char *view, uint32_t length, uint32_t at) { + while (at < length && (view[at] == ' ' || view[at] == '\t')) + at++; + if (at + 2 >= length) + return false; + if (view[at] == '=') + return view[at + 1] != '=' && view[at + 1] != '>'; + if (view[at] && strchr("+-*/%&|^", view[at]) && view[at + 1] == '=') + return true; + return view[at + 2] == '=' && (!strncmp(view + at, "??", 2) || !strncmp(view + at, "<<", 2) || + !strncmp(view + at, ">>", 2)); +} + +/* Blanks `from` up to `to`, keeping the lines. */ +static void csharp_view_blank(char *view, uint32_t from, uint32_t to) { + for (uint32_t i = from; i < to; i++) + if (view[i] != '\n' && view[i] != '\r') + view[i] = ' '; +} + +/* `*(T*)p`, `*(p + 1)` and `*++p`: the grammar loses a whole file to a star in front of a + * parenthesis or an increment. Blank the star. `(T*)p` then reads as a cast to T, which is what the + * dereference yields. As an assignment's target no such form reads, and a write through a pointer + * names no member of its own, so the whole target becomes `_`. The scan stops at the statement's + * end, so the pass stays linear. */ +static void csharp_view_blank_dereference(char *view, uint32_t length) { + for (uint32_t i = 0; i + 2 < length; i++) { + if (view[i] != '*') + continue; + bool may_start = csharp_expression_may_start(view, i); + if (may_start && ((view[i + 1] == '+' && view[i + 2] == '+') || + (view[i + 1] == '-' && view[i + 2] == '-'))) { + view[i] = ' '; + continue; + } + if (view[i + 1] != '(') + continue; + uint32_t j = i + 2; + int depth = 1; + while (j < length && depth > 0 && view[j] != ';' && view[j] != '{' && view[j] != '}') { + depth += view[j] == '('; + depth -= view[j] == ')'; + j++; + } + if (depth != 0) + continue; + uint32_t end = j < length && view[j] == '&' ? j + 1 : j; + int operand_depth = 0; + for (; end < length && view[end] != ';' && view[end] != '\n'; end++) { + char c = view[end]; + operand_depth += c == '('; + operand_depth -= c == ')'; + if (operand_depth < 0 || + (operand_depth == 0 && !csharp_word_char(c) && c != '.' && c != ')')) + break; + } + if (csharp_assignment_follows(view, length, end)) { + csharp_view_blank(view, i, end); + view[i] = '_'; + continue; + } + if (!may_start) + continue; + view[i] = ' '; + for (uint32_t k = j - 2; k > i && view[k] == '*'; k--) + view[k] = ' '; + } +} + +/* `a?.b = c`, `a?[i] = c` (C# 14): the grammar has no null-conditional assignment, and what it + * loses can take the members after the statement with it. `a .b = c` assigns the same member. Each + * chain is read once, so the pass stays linear. */ +static void csharp_view_blank_conditional_target(char *view, uint32_t length) { + for (uint32_t i = 0; i + 1 < length; i++) { + if (view[i] != '?' || (view[i + 1] != '.' && view[i + 1] != '[')) + continue; + uint32_t end = i + 1; + int depth = 0; + for (; end < length && view[end] != '\n' && view[end] != ';'; end++) { + char c = view[end]; + if (c == '[' || c == '(') { + depth++; + } else if (c == ']' || c == ')') { + if (depth == 0) + break; + depth--; + } else if (depth == 0 && !csharp_word_char(c) && c != '.' && c != '?') { + break; + } + } + if (depth == 0 && csharp_assignment_follows(view, length, end)) { + for (uint32_t k = i; k < end; k++) { + depth += view[k] == '[' || view[k] == '('; + depth -= view[k] == ']' || view[k] == ')'; + if (depth == 0 && view[k] == '?' && (view[k + 1] == '.' || view[k + 1] == '[')) + view[k] = ' '; + } + } + i = end; + } +} + +/* The brace closing the one at `open`, or `length`. Comments, strings and characters hold none. */ +static uint32_t csharp_matching_brace(const char *view, uint32_t length, uint32_t open) { + int depth = 0; + for (uint32_t i = open; i < length; i++) { + char c = view[i]; + if (c == '/' && view[i + 1] == '/') { + const char *newline = memchr(view + i, '\n', length - i); + if (!newline) + return length; + i = (uint32_t)(newline - view); + } else if (c == '/' && view[i + 1] == '*') { + const char *close = strstr(view + i + 2, "*/"); + if (!close) + return length; + i = (uint32_t)(close - view) + 1; + } else if (c == '"') { + bool verbatim = i > 0 && (view[i - 1] == '@' || + (view[i - 1] == '$' && i > 1 && view[i - 2] == '@')); + if (!strncmp(view + i, "\"\"\"", 3)) { + const char *close = strstr(view + i + 3, "\"\"\""); + if (!close) + return length; + i = (uint32_t)(close - view) + 2; + continue; + } + for (i++; i < length; i++) { + if (!verbatim && view[i] == '\\') + i++; + else if (verbatim && view[i] == '"' && view[i + 1] == '"') + i++; + else if (view[i] == '"' || (!verbatim && view[i] == '\n')) + break; + } + } else if (c == '\'') { + if (view[i + 1] == '\\') { + const char *close = memchr(view + i + 2, '\'', length - i - 2); + if (!close) + return length; + i = (uint32_t)(close - view); + } else if (view[i + 1] && view[i + 2] == '\'') { + i += 2; + } + } else if (c == '{') { + depth++; + } else if (c == '}' && --depth == 0) { + return i; + } + } + return length; +} + +/* An extension block (C# 14) the grammar cannot read: `extension(Vector128) where ... { ... + * }`. It knows a block with a named, non-generic receiver as a constructor, so a named receiver + * only loses what is in the way. A block with no receiver name has none to lose: its braces go, and + * its members, which are the enclosing static class's in the assembly too, stand as that class's. + * Every search resumes past what it read, so the pass stays linear. */ +static void csharp_view_open_extension_blocks(char *view, uint32_t length) { + static const char WORD[] = "extension"; + enum { WORD_LENGTH = sizeof(WORD) - 1 }; + for (char *hit = view; (hit = strstr(hit, WORD)) != NULL; hit += WORD_LENGTH) { + uint32_t start = (uint32_t)(hit - view); + uint32_t p = start; + while (p > 0 && (view[p - 1] == ' ' || view[p - 1] == '\t')) + p--; + if (p > 0 && view[p - 1] != '\n') + continue; + uint32_t i = start + WORD_LENGTH; + uint32_t generic = i; + if (i < length && view[i] == '<') { + int angle = 0; + for (; i < length && view[i] != '\n'; i++) { + angle += view[i] == '<'; + angle -= view[i] == '>'; + if (angle == 0) + break; + } + if (i >= length || view[i] != '>') + continue; + i++; + } + uint32_t generic_end = i; + if (i >= length || view[i] != '(') + continue; + uint32_t open = i; + int depth = 0; + for (; i < length && view[i] != '\n'; i++) { + depth += view[i] == '('; + depth -= view[i] == ')'; + if (depth == 0) + break; + } + if (i >= length || view[i] != ')' || i == open + 1) + continue; + uint32_t close = i; + for (i = close + 1; i < length && !strchr("{};=", view[i]); i++) {} + if (i >= length || view[i] != '{') + continue; + /* `(Stream stream)` names its receiver: a word with something in front of it. */ + uint32_t word = close; + while (word > open + 1 && csharp_word_char(view[word - 1])) + word--; + uint32_t before = word; + while (before > open + 1 && view[before - 1] == ' ') + before--; + bool named = word < close && before > open + 1; + if (named) { + csharp_view_blank(view, generic, generic_end); + csharp_view_blank(view, close + 1, i); + continue; + } + uint32_t end = csharp_matching_brace(view, length, i); + if (end >= length) + continue; + csharp_view_blank(view, start, i + 1); + view[end] = ' '; + } +} + +static bool csharp_word_in(const char *word, size_t size, const char *const *set) { + for (; *set; set++) + if (strlen(*set) == size && !strncmp(word, *set, size)) + return true; + return false; +} + +/* `safe` (as in `safe extern`) and `closed` (on a class) are modifiers the grammar does not have: + * it took `safe` for the return type and `extern` for the method's name. Modifiers say nothing a + * name needs. Each is a modifier only between words that show it: another modifier before, a word + * after. */ +static void csharp_view_blank_new_modifiers(char *view, uint32_t length) { + static const char *const BEFORE[] = {"public", "private", "protected", "internal", "static", + "extern", "unsafe", "readonly", "partial", "new", + "override", "sealed", "abstract", "virtual", NULL}; + static const char *const CLASS_WORDS[] = {"class", "record", "partial", NULL}; + static const struct { + const char *word; + const char *const *after; /* NULL: any word. */ + } MODIFIERS[] = {{"safe", NULL}, {"closed", CLASS_WORDS}}; + for (size_t m = 0; m < sizeof(MODIFIERS) / sizeof(MODIFIERS[0]); m++) { + size_t size = strlen(MODIFIERS[m].word); + for (char *hit = view; (hit = strstr(hit, MODIFIERS[m].word)) != NULL; hit += size) { + if ((hit > view && csharp_word_char(hit[-1])) || csharp_word_char(hit[size])) + continue; + char *after = hit + size; + while (*after == ' ') + after++; + char *after_end = after; + while (csharp_word_char(*after_end)) + after_end++; + if (after_end == after || + (MODIFIERS[m].after && + !csharp_word_in(after, (size_t)(after_end - after), MODIFIERS[m].after))) + continue; + char *before_end = hit; + while (before_end > view && before_end[-1] == ' ') + before_end--; + char *before = before_end; + while (before > view && csharp_word_char(before[-1])) + before--; + if (csharp_word_in(before, (size_t)(before_end - before), BEFORE)) + memset(hit, ' ', size); + } + } + (void)length; +} + +/* `bool async`, `if (async)`, `async: true`: an identifier the grammar takes for the modifier. Only + * the parse sees this spelling: every name is read back from the source at the same span. */ +static void csharp_view_rename_async_identifier(char *view, uint32_t length) { + static const char WORD[] = "async"; + enum { WORD_LENGTH = sizeof(WORD) - 1 }; + for (char *at = view; (at = strstr(at, WORD)) != NULL; at += WORD_LENGTH) { + bool starts = at == view || !(isalnum((unsigned char)at[-1]) || at[-1] == '_'); + char *next = at + WORD_LENGTH; + if (!starts || isalnum((unsigned char)*next) || *next == '_') + continue; + while (next < view + length && (*next == ' ' || *next == '\t')) + next++; + if (*next && strchr("),:;?&|.]", *next)) + at[WORD_LENGTH - 1] = '_'; + else if (next[0] == '=' && next[1] != '>') + at[WORD_LENGTH - 1] = '_'; + } +} + +/* `$@"""{x}"""`: a doubled quote in a verbatim interpolated string is one quote of its text. */ +static void csharp_view_blank_doubled_quotes(char *view, uint32_t length) { + for (uint32_t i = 0; i + 2 < length; i++) { + bool opens = (view[i] == '$' && view[i + 1] == '@' && view[i + 2] == '"') || + (view[i] == '@' && view[i + 1] == '$' && view[i + 2] == '"'); + if (!opens) + continue; + uint32_t j = i + 3; + while (j < length) { + if (view[j] != '"') { + j++; + } else if (j + 1 < length && view[j + 1] == '"') { + view[j] = view[j + 1] = ' '; + j += 2; + } else { + break; + } + } + i = j; + } +} + /* C#: the grammar reads `#if` only where a whole statement or member can stand, and * does not know `ref partial struct`. Either can turn a whole file into one ERROR, * taking every type it declares with it. This view has the same length and the same * lines: conditional directives and the branches after a file's first one are blank, - * and so is that `ref`. Only a file the grammar could not otherwise read uses it. */ + * and so is that `ref`. So are the three spellings repaired below, seen to break whole files + * on dotnet/runtime (Task.cs, EventSource.cs, Vector2.cs, every test with a `bool async`). + * Only a file the grammar could not otherwise read uses it. */ static char *csharp_parse_view(const char *source, uint32_t length, bool *hid_branch) { enum { MAX_DEPTH = 64 }; char *view = cbm_alloc(CBM_MEM_CLASS_EXTRACT, (size_t)length + 1); @@ -1157,6 +1495,12 @@ static char *csharp_parse_view(const char *source, uint32_t length, bool *hid_br if (!strncmp(next, "struct", 6)) memset(ref, ' ', 3); } + csharp_view_blank_dereference(view, length); + csharp_view_rename_async_identifier(view, length); + csharp_view_blank_doubled_quotes(view, length); + csharp_view_blank_conditional_target(view, length); + csharp_view_open_extension_blocks(view, length); + csharp_view_blank_new_modifiers(view, length); return view; } diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index f2ec41f92..706ece72f 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -871,6 +871,9 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang) { if (lang == CBM_LANG_CSHARP && strcmp(kind, "operator_declaration") == 0) { return ts_node_child_by_field_name(node, TS_FIELD("operator")); } + if (lang == CBM_LANG_CSHARP && !ts_node_is_null(cbm_csharp_keyword_name(node))) { + return cbm_csharp_keyword_name(node); + } if (lang == CBM_LANG_HASKELL && strcmp(kind, "signature") == 0) { TSNode null_node = {0}; @@ -9823,7 +9826,8 @@ static TSNode resolve_method_name(TSNode child, CBMLanguage lang) { if (((lang == CBM_LANG_C || lang == CBM_LANG_CPP || lang == CBM_LANG_CUDA || lang == CBM_LANG_GLSL) && strcmp(ck, "function_definition") == 0) || - (lang == CBM_LANG_CSHARP && strcmp(ck, "operator_declaration") == 0)) { + (lang == CBM_LANG_CSHARP && (strcmp(ck, "operator_declaration") == 0 || + !ts_node_is_null(cbm_csharp_keyword_name(child))))) { return cbm_resolve_func_name(child, lang); } diff --git a/internal/cbm/extract_type_refs.c b/internal/cbm/extract_type_refs.c index b140d95b8..d8b59ca55 100644 --- a/internal/cbm/extract_type_refs.c +++ b/internal/cbm/extract_type_refs.c @@ -102,6 +102,24 @@ static const char *extract_type_text(CBMArena *a, TSNode node, const char *sourc return clean_type_name(a, cbm_node_text(a, node, source)); } +/* Java and C# spell a wrapper around the name: `ActionListener`, `Foo[]`, `Foo?`. The name + * is the leftmost leaf, so each level costs one child lookup. The wrapper itself when no leaf + * spells it exactly, as with an annotated type. */ +static TSNode jvm_clr_exact_type_site(const CBMExtractCtx *ctx, TSNode site, const char *name) { + enum { MAX_WRAPPERS = 8 }; + size_t length = strlen(name); + TSNode at = site; + for (int depth = 0; depth < MAX_WRAPPERS && !ts_node_is_null(at); depth++) { + uint32_t start = ts_node_start_byte(at), end = ts_node_end_byte(at); + if (end - start == length && end <= (uint32_t)ctx->source_len && + memcmp(ctx->source + start, name, length) == 0) { + return at; + } + at = ts_node_named_child_count(at) ? ts_node_named_child(at, 0) : (TSNode){0}; + } + return site; +} + // Add a type reference for a function. static void add_type_ref(CBMExtractCtx *ctx, const char *type_name, const char *func_qn, TSNode site) { @@ -115,6 +133,9 @@ static void add_type_ref(CBMExtractCtx *ctx, const char *type_name, const char * if (is_builtin_type(type_name)) { return; } + if (ctx->language == CBM_LANG_JAVA || ctx->language == CBM_LANG_CSHARP) { + site = jvm_clr_exact_type_site(ctx, site, type_name); + } CBMTypeRef tr = {0}; tr.type_name = type_name; @@ -231,8 +252,11 @@ static void extract_param_type_refs(CBMExtractCtx *ctx, TSNode params, const cha // Extract return type references. static void extract_return_type_refs(CBMExtractCtx *ctx, TSNode func_node, const char *func_qn) { - const char *fields[] = {"result", "return_type", "type", NULL}; + const char *fields[] = {"result", "return_type", "type", "returns", NULL}; for (const char **f = fields; *f; f++) { + if (strcmp(*f, "returns") == 0 && ctx->language != CBM_LANG_CSHARP) { + continue; + } TSNode rt = ts_node_child_by_field_name(func_node, *f, (uint32_t)strlen(*f)); if (!ts_node_is_null(rt) && ts_family(ctx->language)) { add_ts_annotation_refs(ctx, rt, func_qn, 0); diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index 44c840322..237fc2a2a 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -970,6 +970,48 @@ static bool jvm_anonymous_member(CBMLanguage language, TSNode function) { return strcmp(ts_node_type(ts_node_named_child(call, 0)), "object_literal") == 0; } +/* A local function has no node. Two C# shapes only look like one: a method of an extension block + * the grammar took for a constructor has a node of its own (#2071), and so may a member after a + * statement the grammar lost, which is why it can carry an access modifier no local function can. + */ +static bool local_function_without_node(CBMExtractCtx *ctx, TSNode node, const WalkState *state) { + const char *outer = state->enclosing_func_qn; + if (!outer) { + return false; + } + const char *kind = ts_node_type(node); + if (ctx->language == CBM_LANG_KOTLIN) { + TSNode parent = ts_node_parent(node); + return strcmp(kind, "function_declaration") == 0 && !ts_node_is_null(parent) && + strcmp(ts_node_type(parent), "statements") == 0; + } + if (ctx->language != CBM_LANG_CSHARP || strcmp(kind, "local_function_statement") != 0) { + return false; + } + static const char EXTENSION[] = ".extension"; + size_t outer_size = strlen(outer); + size_t suffix_size = sizeof(EXTENSION) - 1; + if (outer_size >= suffix_size && strcmp(outer + outer_size - suffix_size, EXTENSION) == 0) { + return false; + } + bool member = false; + TSTreeCursor cursor = ts_tree_cursor_new(node); + if (ts_tree_cursor_goto_first_child(&cursor)) { + do { + TSNode child = ts_tree_cursor_current_node(&cursor); + if (strcmp(ts_node_type(child), "modifier") != 0) { + continue; + } + const char *word = ctx->source + ts_node_start_byte(child); + member = member || strncmp(word, "public", 6) == 0 || + strncmp(word, "private", 7) == 0 || strncmp(word, "protected", 9) == 0 || + strncmp(word, "internal", 8) == 0; + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + ts_tree_cursor_delete(&cursor); + return !member; +} + static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) { /* Raw configured definitions already own the canonical QN, including @@ -982,7 +1024,9 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan /* A member of an anonymous class has no node of its own. Like a lambda's body, * its body belongs to the enclosing declaration; a QN of its own named a source * no node carries, and every call in it was dropped. */ - if (jvm_anonymous_member(ctx->language, node) && state->enclosing_func_qn) { + if ((jvm_anonymous_member(ctx->language, node) || + local_function_without_node(ctx, node, state)) && + state->enclosing_func_qn) { return state->enclosing_func_qn; } /* Nor has a Rust fn nested in a function body, or a method of an impl written there, and @@ -2799,6 +2843,19 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS state->os_type_map.class_base_count = 0; } } + } else if (ctx->language == CBM_LANG_CSHARP && + strcmp(ts_node_type(node), "property_declaration") == 0 && + state->enclosing_class_qn) { + /* A property has its node. What its accessors, its expression body and its initializer + * call belongs to it, not to the file. */ + TSNode name = ts_node_child_by_field_name(node, TS_FIELD("name")); + if (!ts_node_is_null(name)) { + (void)push_function_scope( + state, depth, + cbm_arena_sprintf(ctx->arena, "%s.%s", state->enclosing_class_qn, + cbm_node_text(ctx->arena, name, ctx->source)), + node); + } } else if (ctx->language == CBM_LANG_NIX && strcmp(ts_node_type(node), "binding") == 0 && cbm_nix_binding_is_attrset_scope(node)) { /* Nix: a binding whose value is an attribute set is a named scope, exactly diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index e180227ce..a2cde96f9 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -584,6 +584,19 @@ TSNode cbm_find_child_by_kind(TSNode parent, const char *kind) { return null_node; } +TSNode cbm_csharp_keyword_name(TSNode node) { + const char *kind = ts_node_type(node); + if (strcmp(kind, "indexer_declaration") == 0) { + return cbm_find_child_by_kind(node, "this"); + } + if (strcmp(kind, "conversion_operator_declaration") == 0) { + TSNode word = cbm_find_child_by_kind(node, "implicit"); + return ts_node_is_null(word) ? cbm_find_child_by_kind(node, "explicit") : word; + } + TSNode null_node = {0}; + return null_node; +} + int cbm_find_children_by_kind(TSNode parent, const char *kind, TSNode *out, int max) { int n = 0; uint32_t count = ts_node_child_count(parent); @@ -984,8 +997,9 @@ static const char *func_kinds_ocaml[] = {"value_definition", "let_binding", NULL static const char *func_kinds_zig[] = {"function_declaration", "test_declaration", NULL}; static const char *func_kinds_bash[] = {"function_definition", NULL}; static const char *func_kinds_erlang[] = {"function_clause", NULL}; -static const char *func_kinds_csharp[] = {"method_declaration", "constructor_declaration", - "operator_declaration", NULL}; +static const char *func_kinds_csharp[] = {"method_declaration", "constructor_declaration", + "operator_declaration", "conversion_operator_declaration", + "indexer_declaration", NULL}; static const char *func_kinds_matlab[] = {"function_definition", NULL}; static const char *func_kinds_lean[] = {"def", "theorem", "instance", "abbrev", NULL}; static const char *func_kinds_form[] = {"procedure_definition", NULL}; @@ -1327,8 +1341,9 @@ char *cbm_func_name_node_text(CBMArena *a, TSNode name_node, const char *source, /* C# `operator +` is named by its token alone, an anonymous node; any other C# name * is a named node. Spelled like C++'s `operator+` and `operator bool`, here so the * def name and the call-scope QN cannot disagree. */ - if (text && lang == CBM_LANG_CSHARP && !ts_node_is_named(name_node)) { - bool word = text[0] >= 'a' && text[0] <= 'z'; /* operator true, operator false */ + if (text && lang == CBM_LANG_CSHARP && !ts_node_is_named(name_node) && + strcmp(text, "this") != 0) { + bool word = text[0] >= 'a' && text[0] <= 'z'; /* operator true, implicit */ return cbm_arena_sprintf(a, "operator%s%s", word ? " " : "", text); } /* Nix quoted attrpath segment: `"kebab-case" = a: a;` and `services."my.svc" = …` @@ -1626,6 +1641,12 @@ static const char *func_node_name(CBMArena *a, TSNode func_node, const char *sou TSNode op = ts_node_child_by_field_name(func_node, TS_FIELD("operator")); return ts_node_is_null(op) ? NULL : cbm_func_name_node_text(a, op, source, lang); } + if (lang == CBM_LANG_CSHARP) { + TSNode word = cbm_csharp_keyword_name(func_node); + if (!ts_node_is_null(word)) { + return cbm_func_name_node_text(a, word, source, lang); + } + } // Arrow functions: check parent variable_declarator if (strcmp(ts_node_type(func_node), "arrow_function") == 0) { TSNode parent = ts_node_parent(func_node); diff --git a/internal/cbm/helpers.h b/internal/cbm/helpers.h index 5164bfec6..feb7e58f0 100644 --- a/internal/cbm/helpers.h +++ b/internal/cbm/helpers.h @@ -220,6 +220,10 @@ char *cbm_go_receiver_type_name(CBMArena *a, TSNode recv, const char *source); // Find a child node by kind string. TSNode cbm_find_child_by_kind(TSNode parent, const char *kind); +// C#: a conversion operator has no name, its `implicit` or `explicit` names it. An indexer's +// `this` does. Null for anything else. +TSNode cbm_csharp_keyword_name(TSNode node); + // Find every child node matching `kind` (unlike cbm_find_child_by_kind, // which stops at the first match). Writes up to `max` nodes into `out`; // returns how many were found. Repeated sibling nodes of the same kind are diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index 7576ea665..59993c340 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -369,10 +369,17 @@ static const char *cpp_assign_types[] = {"assignment_expression", "update_expres static const char *cpp_throw_types[] = {"throw_statement", NULL}; // ==================== C# ==================== -static const char *cs_func_types[] = { - "destructor_declaration", "local_function_statement", "function_pointer_type", - "constructor_declaration", "anonymous_method_expression", "lambda_expression", - "method_declaration", "operator_declaration", NULL}; +static const char *cs_func_types[] = {"destructor_declaration", + "local_function_statement", + "function_pointer_type", + "constructor_declaration", + "anonymous_method_expression", + "lambda_expression", + "method_declaration", + "operator_declaration", + "conversion_operator_declaration", + "indexer_declaration", + NULL}; static const char *cs_class_types[] = {"class_declaration", "struct_declaration", "enum_declaration", diff --git a/internal/cbm/lsp/oo_core.c b/internal/cbm/lsp/oo_core.c index 1d6fd730a..d3740ab0a 100644 --- a/internal/cbm/lsp/oo_core.c +++ b/internal/cbm/lsp/oo_core.c @@ -34,8 +34,9 @@ cbm_oo_index_t *cbm_oo_index_new(CBMArena *arena) { ix->functions = cbm_ht_create(1024); ix->props = cbm_ht_create(1024); ix->ext_static = cbm_ht_create(256); + ix->files = cbm_ht_create(256); if (!ix->types || !ix->methods || !ix->fields || !ix->functions || !ix->props || - !ix->ext_static) { + !ix->ext_static || !ix->files) { cbm_oo_index_free(ix); return NULL; } @@ -51,7 +52,9 @@ void cbm_oo_index_free(cbm_oo_index_t *ix) { cbm_ht_free(ix->functions); cbm_ht_free(ix->props); cbm_ht_free(ix->ext_static); + cbm_ht_free(ix->files); ix->types = ix->methods = ix->fields = ix->functions = ix->props = ix->ext_static = NULL; + ix->files = NULL; } static const char *oo_key(char *buffer, size_t capacity, const char *a, const char *b) { @@ -59,6 +62,50 @@ static const char *oo_key(char *buffer, size_t capacity, const char *a, const ch return length > 0 && (size_t)length < capacity ? buffer : NULL; } +static const char *file_rel_or(const char *rel) { + return rel ? rel : ""; +} + +/* Gradle and Maven: `src/test/...`, `src/internalClusterTest/...`, any source set named *Test. */ +static bool oo_jvm_test_source(const char *rel) { + for (const char *at = strstr(rel, "src/"); at; at = strstr(at + 1, "src/")) { + if (at != rel && at[-1] != '/') + continue; + const char *set = at + 4; + size_t length = strcspn(set, "/"); + if (set[length] != '/' || length < 4) + continue; + if (!strncmp(set + length - 4, "test", 4) || !strncmp(set + length - 4, "Test", 4)) + return true; + } + return false; +} + +/* .NET: a test project, by its own directory: `/tests`, `.Tests`. A helper library under + * a tests directory (dotnet/runtime's Common/tests/TestUtilities) is read by every test project. */ +static bool oo_csharp_test_project(const char *project) { + if (!project) + return false; + const char *slash = strrchr(project, '/'); + const char *at = slash ? slash + 1 : project; + size_t length = strlen(at); + return !strcmp(at, "tests") || !strcmp(at, "test") || + (length > 6 && !strcmp(at + length - 6, ".Tests")) || + (length > 5 && !strcmp(at + length - 5, ".Test")); +} + +/* Test sources are the leaves of a build: another project's code does not read them, unless it + * compiles the very file. So a free name never reaches a function or an extension method there from + * outside: `items.Concat(x)` in one test project does not mean another test project's helper in an + * enclosing namespace. A type named outright still binds, since a shared source file reads it. */ +static bool oo_file_outside_tests(const cbm_oo_index_t *ix, const oo_file_t *declared, + const oo_file_t *reader) { + if (!declared || !declared->test_source || !reader || !declared->project) + return false; + const oo_file_t *indexed = reader->project ? reader : cbm_ht_get(ix->files, reader->rel); + return indexed && indexed->project && strcmp(indexed->project, declared->project); +} + const oo_file_t *oo_index_file(cbm_oo_index_t *ix, CBMLanguage lang, const char *rel, const char *package, oo_import_t *imports, int import_count, bool has_error) { @@ -67,15 +114,109 @@ const oo_file_t *oo_index_file(cbm_oo_index_t *ix, CBMLanguage lang, const char ix->failed = true; return NULL; } - *file = (oo_file_t){.rel = cbm_arena_strdup(ix->arena, rel ? rel : ""), - .package = cbm_arena_strdup(ix->arena, package ? package : ""), - .language = lang, - .imports = imports, - .import_count = import_count, - .has_error = has_error}; + *file = + (oo_file_t){.rel = cbm_arena_strdup(ix->arena, rel ? rel : ""), + .package = cbm_arena_strdup(ix->arena, package ? package : ""), + .language = lang, + .imports = imports, + .import_count = import_count, + .has_error = has_error, + .reference = ix->reference_files, + .test_source = lang == CBM_LANG_CSHARP ? oo_csharp_test_project(ix->project) + : oo_jvm_test_source(file_rel_or(rel)), + .project = ix->project}; + if (ix->project) + cbm_ht_set(ix->files, file->rel, file); return file; } +void cbm_oo_index_set_project(cbm_oo_index_t *ix, const char *project_dir) { + if (ix) + ix->project = project_dir ? cbm_arena_strdup(ix->arena, project_dir) : NULL; +} + +void cbm_oo_index_begin_reference_files(cbm_oo_index_t *ix) { + if (ix) + ix->reference_files = true; +} + +bool oo_type_is_implemented_elsewhere(const oo_type_t *type, const oo_file_t *file) { + return file && file->reference && type && type->file && !type->file->reference; +} + +/* One more declaration of a name. The first keeps its members under the plain qualified name, and + * the index entry for the name becomes a holder of the declarations with no members of its own. + * Each later one is its own type, keyed by its file, so its members and nested types stay apart. */ +static oo_type_t *oo_index_variant(cbm_oo_index_t *ix, oo_type_t *prior, const char *qn, + const char *name, const oo_file_t *f, const char *ns, + const oo_type_t *owner, const char *graph_qn, uint32_t flags) { + oo_type_t *holder = prior; + if (!(prior->flags & OO_F_AMBIGUOUS)) { + holder = cbm_arena_calloc(ix->arena, sizeof(*holder)); + if (!holder) { + ix->failed = true; + return prior; + } + *holder = *prior; + holder->flags |= OO_F_AMBIGUOUS; + holder->graph_qn = NULL; + holder->graph_count = 0; + holder->variants = prior; + holder->next_in_index = NULL; + cbm_ht_set(ix->types, prior->qn, holder); + ix->has_variants = true; + } + /* Twice in one file, as in two `#if` branches, is one graph node. */ + for (const oo_type_t *v = holder->variants; v; v = v->next_variant) + if (v->file == f) + return (oo_type_t *)v; + const char *key = cbm_arena_sprintf(ix->arena, "%s\x1e%d", qn, ++ix->variant_serial); + oo_type_t *variant = cbm_arena_calloc(ix->arena, sizeof(*variant)); + const char *short_name = cbm_arena_strdup(ix->arena, name); + if (!variant || !key || !short_name) { + ix->failed = true; + return holder; + } + *variant = (oo_type_t){.qn = key, + .name = short_name, + .graph_qn = graph_qn ? cbm_arena_strdup(ix->arena, graph_qn) : NULL, + .graph_count = graph_qn ? 1 : 0, + .file = f, + .ns = ns ? cbm_arena_strdup(ix->arena, ns) : "", + .owner = owner, + .flags = flags, + .next_in_index = ix->all_types, + .next_variant = holder->variants}; + holder->variants = variant; + ix->all_types = variant; + cbm_ht_set(ix->types, key, variant); + return variant; +} + +/* The file named after a type (`Foo.cs` for `Foo` and `Foo`). */ +static bool oo_file_named_after(const char *rel, const char *name) { + const char *slash = rel ? strrchr(rel, '/') : NULL; + const char *base = slash ? slash + 1 : rel; + size_t length = strcspn(name, "`"); + return base && !strncmp(base, name, length) && !strcmp(base + length, ".cs"); +} + +/* A partial type is one type with a node per file. A mention of the type needs one of them, and + * every one is a declaration of it, so this picks by a pure function of the set: the file named + * after the type, then the smallest path. Its members keep their own nodes. */ +static void oo_partial_representative(cbm_oo_index_t *ix, oo_type_t *type, const oo_file_t *f, + const char *graph_qn) { + type->graph_count++; + if (!graph_qn || !f) + return; + bool named = oo_file_named_after(f->rel, type->name); + bool held = type->graph_rel && oo_file_named_after(type->graph_rel, type->name); + if (type->graph_qn && (held != named ? held : strcmp(type->graph_rel, f->rel) <= 0)) + return; + type->graph_qn = cbm_arena_strdup(ix->arena, graph_qn); + type->graph_rel = f->rel; +} + oo_type_t *oo_index_type(cbm_oo_index_t *ix, const char *qn, const char *name, const oo_file_t *f, const char *ns, const oo_type_t *owner, const char *graph_qn, uint32_t flags) { @@ -83,16 +224,17 @@ oo_type_t *oo_index_type(cbm_oo_index_t *ix, const char *qn, const char *name, c return NULL; oo_type_t *prior = cbm_ht_get(ix->types, qn); if (prior) { + if (oo_type_is_implemented_elsewhere(prior, f)) + return prior; /* C# partial declarations are one type with several graph nodes. Any * other repeated qualified name is two types the index cannot tell * apart (expect/actual, multi-release sources): never choose one. */ - if ((prior->flags & OO_F_PARTIAL) && (flags & OO_F_PARTIAL)) { - prior->graph_count++; - prior->graph_qn = NULL; - } else { - prior->flags |= OO_F_AMBIGUOUS; + if (!(prior->flags & OO_F_AMBIGUOUS) && (prior->flags & OO_F_PARTIAL) && + (flags & OO_F_PARTIAL)) { + oo_partial_representative(ix, prior, f, graph_qn); + return prior; } - return prior; + return oo_index_variant(ix, prior, qn, name, f, ns, owner, graph_qn, flags); } oo_type_t *type = cbm_arena_calloc(ix->arena, sizeof(*type)); const char *key = cbm_arena_strdup(ix->arena, qn); @@ -105,6 +247,7 @@ oo_type_t *oo_index_type(cbm_oo_index_t *ix, const char *qn, const char *name, c .name = short_name, .graph_qn = graph_qn ? cbm_arena_strdup(ix->arena, graph_qn) : NULL, .graph_count = graph_qn ? 1 : 0, + .graph_rel = f ? f->rel : NULL, .file = f, .ns = ns ? cbm_arena_strdup(ix->arena, ns) : "", .owner = owner, @@ -177,6 +320,81 @@ const oo_type_t *oo_lookup_type(const cbm_oo_index_t *ix, const char *qn) { return qn ? cbm_ht_get(ix->types, qn) : NULL; } +const oo_type_t *oo_lookup_declared(const cbm_oo_index_t *ix, const char *qn, const char *rel) { + const oo_type_t *type = oo_lookup_type(ix, qn); + if (!type || !(type->flags & OO_F_AMBIGUOUS) || !rel) + return type; + for (const oo_type_t *v = type->variants; v; v = v->next_variant) + if (v->file && !strcmp(v->file->rel, rel)) + return v; + return type; +} + +/* The declaration of a several-claimed name this file means, or NULL: its own file's, then the only + * one in its project (C# CS0436, and javac's and kotlinc's own sources: a project's declaration + * shadows one it could only read from elsewhere). Linear in that name's declarations. */ +static const oo_type_t *oo_variant_for(const cbm_oo_index_t *ix, const oo_type_t *holder, + const oo_file_t *file) { + if (!holder || !holder->variants || !file) + return NULL; + const oo_file_t *indexed = file->project ? file : cbm_ht_get(ix->files, file->rel); + const char *project = indexed ? indexed->project : NULL; + const oo_type_t *in_project = NULL; + int count = 0; + for (const oo_type_t *v = holder->variants; v; v = v->next_variant) { + if (!v->file) + continue; + if (!strcmp(v->file->rel, file->rel)) + return v; + if (project && v->file->project && !strcmp(project, v->file->project)) { + in_project = v; + count++; + } + } + return count == 1 ? in_project : NULL; +} + +const oo_type_t *oo_lookup_visible(const cbm_oo_index_t *ix, const char *qn, + const oo_file_t *file) { + const oo_type_t *exact = oo_lookup_type(ix, qn); + if (!qn || !ix->has_variants) + return exact; + if (exact && (exact->flags & OO_F_AMBIGUOUS)) { + const oo_type_t *variant = oo_variant_for(ix, exact, file); + return variant ? variant : exact; + } + /* A dotted name through a several-claimed outer type. Bounded by its dots. */ + char prefix[OO_KEY_MAX]; + size_t length = strlen(qn); + if (length >= sizeof(prefix)) + return exact; + memcpy(prefix, qn, length + 1); + for (char *dot = strchr(prefix, '.'); dot; dot = strchr(dot + 1, '.')) { + *dot = '\0'; + const oo_type_t *outer = cbm_ht_get(ix->types, prefix); + *dot = '.'; + if (!outer || !(outer->flags & OO_F_AMBIGUOUS)) + continue; + const oo_type_t *type = oo_variant_for(ix, outer, file); + if (!type) + return outer; + const char *rest = qn + (dot - prefix) + 1; + while (type && *rest) { + const char *next = strchr(rest, '.'); + size_t part = next ? (size_t)(next - rest) : strlen(rest); + char segment[256]; + if (part >= sizeof(segment)) + return NULL; + memcpy(segment, rest, part); + segment[part] = '\0'; + type = oo_find_nested(ix, type, segment); + rest = next ? next + 1 : rest + part; + } + return type; + } + return exact; +} + static void *oo_list_get(const CBMHashTable *table, const char *a, const char *b) { char buffer[OO_KEY_MAX]; const char *key = oo_key(buffer, sizeof(buffer), a, b); @@ -379,11 +597,15 @@ const oo_type_t *oo_find_nested(const cbm_oo_index_t *ix, const oo_type_t *type, /* ── Names ──────────────────────────────────────────────────────────── */ -static oo_name_t oo_name_type(const oo_type_t *type) { +static oo_name_t oo_name_type(const cbm_oo_index_t *ix, const oo_file_t *file, + const oo_type_t *type) { if (!type) return (oo_name_t){.status = OO_NAME_UNKNOWN}; - if (type->flags & OO_F_AMBIGUOUS) - return (oo_name_t){.status = OO_NAME_AMBIGUOUS}; + if (type->flags & OO_F_AMBIGUOUS) { + type = oo_variant_for(ix, type, file); + if (!type) + return (oo_name_t){.status = OO_NAME_AMBIGUOUS}; + } return (oo_name_t){.status = OO_NAME_TYPE, .type = type}; } @@ -421,12 +643,15 @@ static bool oo_lexical_name(const cbm_oo_index_t *ix, const oo_scope_t *scope, c } } if (!strcmp(type->name, segment)) { - *out = oo_name_type(type); + /* Inside its own body a type's name is this declaration. */ + *out = (oo_name_t){.status = OO_NAME_TYPE, .type = type}; + if (type->flags & OO_F_AMBIGUOUS) + *out = oo_name_type(ix, scope->file, type); return true; } const oo_type_t *nested = oo_find_nested(ix, type, segment); if (nested) { - *out = oo_name_type(nested); + *out = oo_name_type(ix, scope->file, nested); return true; } } @@ -438,18 +663,24 @@ static oo_name_t oo_star_name(const cbm_oo_index_t *ix, const oo_file_t *file, c const oo_type_t *found = NULL; for (int i = 0; i < file->import_count; i++) { const oo_import_t *import = &file->imports[i]; - if (!import->star || import->statik) + if (!import->star) continue; if (only_ns && strcmp(import->ns ? import->ns : "", only_ns)) continue; - const oo_type_t *candidate = oo_qualified(ix, import->path, segment); + /* A static import brings the type's nested types in as well (JLS 7.5.4, C# 14.5.4). */ + const oo_type_t *imported = + import->statik ? oo_lookup_visible(ix, import->path, file) : NULL; + const oo_type_t *candidate = !import->statik ? oo_qualified(ix, import->path, segment) + : imported && !(imported->flags & OO_F_AMBIGUOUS) + ? oo_find_nested(ix, imported, segment) + : NULL; if (!candidate) continue; if (found && found != candidate) return (oo_name_t){.status = OO_NAME_AMBIGUOUS}; found = candidate; } - return oo_name_type(found); + return oo_name_type(ix, file, found); } static oo_name_t oo_jvm_simple(const cbm_oo_index_t *ix, const oo_scope_t *scope, @@ -461,9 +692,9 @@ static oo_name_t oo_jvm_simple(const cbm_oo_index_t *ix, const oo_scope_t *scope const oo_import_t *import = &file->imports[i]; if (import->star || !import->local || strcmp(import->local, segment)) continue; - const oo_type_t *type = cbm_ht_get(ix->types, import->path); + const oo_type_t *type = oo_lookup_visible(ix, import->path, file); if (type) { - oo_name_t name = oo_name_type(type); + oo_name_t name = oo_name_type(ix, file, type); name.via_import = true; return name; } @@ -471,7 +702,7 @@ static oo_name_t oo_jvm_simple(const cbm_oo_index_t *ix, const oo_scope_t *scope } const oo_type_t *own = oo_qualified(ix, file->package, segment); if (own) - return oo_name_type(own); + return oo_name_type(ix, file, own); oo_name_t star = oo_star_name(ix, file, segment, NULL); if (star.status != OO_NAME_UNKNOWN) { star.via_import = true; @@ -491,16 +722,16 @@ static oo_name_t oo_cs_simple(const cbm_oo_index_t *ix, const oo_scope_t *scope, for (;;) { const oo_type_t *member = oo_qualified(ix, prefix, segment); if (member) - return oo_name_type(member); + return oo_name_type(ix, scope->file, member); const oo_file_t *file = scope->file; for (int i = 0; i < file->import_count; i++) { const oo_import_t *import = &file->imports[i]; if (import->star || !import->local || strcmp(import->local, segment) || strcmp(import->ns ? import->ns : "", prefix)) continue; - const oo_type_t *type = cbm_ht_get(ix->types, import->path); + const oo_type_t *type = oo_lookup_visible(ix, import->path, file); if (type) { - oo_name_t name = oo_name_type(type); + oo_name_t name = oo_name_type(ix, file, type); name.via_import = true; return name; } @@ -560,14 +791,14 @@ oo_name_t oo_resolve_name(const cbm_oo_index_t *ix, const oo_scope_t *scope, con return (oo_name_t){.status = OO_NAME_UNKNOWN}; rest = next ? next + 1 : NULL; } - return oo_name_type(type); + return oo_name_type(ix, scope->file, type); } if (head.status == OO_NAME_AMBIGUOUS) return head; /* Not a type in scope: the spelling can only be fully qualified. */ - const oo_type_t *exact = cbm_ht_get(ix->types, name); + const oo_type_t *exact = oo_lookup_visible(ix, name, scope->file); if (exact) - return oo_name_type(exact); + return oo_name_type(ix, scope->file, exact); return (oo_name_t){.status = OO_NAME_EXTERNAL, .external = name}; } @@ -656,6 +887,17 @@ oo_value_t oo_value_from_ref(oo_walk_t *w, const oo_typeref_t *ref, const oo_sco w->depth--; return value; } + if (!ref->name && ref->lower) { + oo_value_t value = oo_value_unknown(); + oo_value_t *lower = cbm_arena_alloc(w->arena, sizeof(*lower)); + if (lower) { + w->depth++; + *lower = oo_value_from_ref(w, ref->lower, scope, subst); + w->depth--; + value.lower = lower; + } + return value; + } if (!ref->name) return oo_value_unknown(); oo_name_t name = oo_resolve_name(w->ix, scope, ref->name); @@ -664,7 +906,7 @@ oo_value_t oo_value_from_ref(oo_walk_t *w, const oo_typeref_t *ref, const oo_sco switch (name.status) { case OO_NAME_TPARAM: { const oo_value_t *bound = oo_subst_lookup(subst, name.tparam); - if (bound) + if (bound && (oo_value_known(bound) || bound->lower)) value = *bound; else if (name.tparam->bound) value = oo_value_from_ref(w, name.tparam->bound, scope, subst); @@ -846,6 +1088,8 @@ static bool oo_name_in(const char *leaf, const char *const *names) { return false; } +static bool oo_same_value(const oo_value_t *a, const oo_value_t *b); + static const char *const OO_ONE_ARG_COLLECTIONS[] = {"Iterable", "MutableIterable", "Collection", @@ -887,6 +1131,20 @@ static const char *const OO_ONE_ARG_COLLECTIONS[] = {"Iterable", "ReadOnlyMemory", NULL}; +static const char *const OO_MAPS[] = {"Map", + "MutableMap", + "HashMap", + "LinkedHashMap", + "TreeMap", + "SortedMap", + "NavigableMap", + "ConcurrentHashMap", + "ConcurrentMap", + "Dictionary", + "IDictionary", + "IReadOnlyDictionary", + NULL}; + oo_value_t oo_element_of(oo_walk_t *w, const oo_value_t *value) { if (!value) return oo_value_unknown(); @@ -899,8 +1157,18 @@ oo_value_t oo_element_of(oo_walk_t *w, const oo_value_t *value) { char leaf[64]; if (value->external && value->arg_count == 1 && oo_plain_leaf(value->external, leaf, sizeof(leaf)) && - oo_name_in(leaf, OO_ONE_ARG_COLLECTIONS)) + oo_name_in(leaf, OO_ONE_ARG_COLLECTIONS)) { + /* `? super X` yields nothing known; `? extends X` already reads as X. */ return value->args[0]; + } + /* Kotlin and C# iterate a map's entries. Java does not compile this. */ + if (value->external && value->arg_count == 2 && + oo_plain_leaf(value->external, leaf, sizeof(leaf)) && oo_name_in(leaf, OO_MAPS)) { + oo_value_t entry = oo_value_external("Entry"); + entry.args = value->args; + entry.arg_count = 2; + return entry; + } if (!value->type) return oo_value_unknown(); /* A project type's own Iterable supertype, with its arguments substituted. */ @@ -970,10 +1238,12 @@ oo_value_t oo_external_member_return(oo_walk_t *w, const oo_value_t *value, cons "ElementAt", NULL}; static const char *const same[] = { - "iterator", "listIterator", "stream", "filter", "sorted", "distinct", - "limit", "skip", "toList", "toMutableList", "toSet", "asSequence", - "asIterable", "reversed", "subList", "take", "drop", "filterNotNull", - "Where", "ToList", "ToArray", "AsEnumerable", NULL}; + "iterator", "listIterator", "stream", "filter", "sorted", "distinct", + "limit", "skip", "toList", "toMutableList", "toSet", "asSequence", + "asIterable", "reversed", "subList", "take", "drop", "filterNotNull", + "peek", "takeWhile", "dropWhile", "parallelStream", "parallel", "sequential", + "Where", "ToList", "ToArray", "AsEnumerable", NULL}; + static const char *const optional_of[] = {"findFirst", "findAny", "min", "max", NULL}; if (value->arg_count == 1 && oo_name_in(leaf, OO_ONE_ARG_COLLECTIONS)) { if (oo_name_in(name, elements)) return value->args[0]; @@ -982,28 +1252,166 @@ oo_value_t oo_external_member_return(oo_walk_t *w, const oo_value_t *value, cons copy.classref = false; return copy; } + if (oo_name_in(name, optional_of) && !strcmp(leaf, "Stream")) { + oo_value_t optional = *value; + optional.classref = false; + optional.external = "Optional"; + return optional; + } + } + /* A map entry, or a pair. */ + if (value->arg_count == 2 && (!strcmp(leaf, "Entry") || !strcmp(leaf, "MutableEntry") || + !strcmp(leaf, "KeyValuePair") || !strcmp(leaf, "Pair"))) { + static const char *const keys[] = {"getKey", "key", "Key", "first", NULL}; + static const char *const vals[] = {"getValue", "value", "Value", "second", NULL}; + if (oo_name_in(name, keys)) + return value->args[0]; + if (oo_name_in(name, vals)) + return value->args[1]; } - static const char *const maps[] = {"Map", - "MutableMap", - "HashMap", - "LinkedHashMap", - "TreeMap", - "SortedMap", - "NavigableMap", - "ConcurrentHashMap", - "ConcurrentMap", - "Dictionary", - "IDictionary", - "IReadOnlyDictionary", - NULL}; static const char *const map_values[] = {"get", "getValue", "getOrDefault", "getOrPut", "getOrElse", "remove", "computeIfAbsent", NULL}; - if (value->arg_count == 2 && oo_name_in(leaf, maps) && oo_name_in(name, map_values)) + if (value->arg_count == 2 && oo_name_in(leaf, OO_MAPS) && oo_name_in(name, map_values)) return value->args[1]; - (void)w; + if (value->arg_count == 2 && oo_name_in(leaf, OO_MAPS)) { + /* The map's views: what iterating them, or their stream, yields. */ + oo_value_t view = oo_value_unknown(); + if (!strcmp(name, "keySet")) { + view = oo_value_external("Set"); + view.args = &value->args[0]; + view.arg_count = 1; + } else if (!strcmp(name, "values")) { + view = oo_value_external("Collection"); + view.args = &value->args[1]; + view.arg_count = 1; + } else if (!strcmp(name, "entrySet")) { + oo_value_t *entry = cbm_arena_alloc(w->arena, sizeof(*entry)); + if (entry) { + *entry = oo_value_external("Entry"); + entry->args = value->args; + entry->arg_count = 2; + view = oo_value_external("Set"); + view.args = entry; + view.arg_count = 1; + } + } + return view; + } return oo_value_unknown(); } +oo_value_t oo_external_with_element(oo_walk_t *w, const oo_value_t *value, oo_value_t element) { + oo_value_t *slot = cbm_arena_alloc(w->arena, sizeof(*slot)); + if (!slot || !value->external || !oo_value_known(&element)) + return oo_value_unknown(); + *slot = element; + slot->classref = false; + oo_value_t copy = *value; + copy.classref = false; + copy.args = slot; + copy.arg_count = 1; + return copy; +} + +oo_value_t oo_jdk_factory_result(oo_walk_t *w, const oo_value_t *receiver, const char *name, + const oo_value_t *args, int argc) { + char leaf[64]; + if (!receiver->classref || !receiver->external || argc < 1 || !args || + strncmp(receiver->external, "java.util.", 10) != 0 || + !oo_plain_leaf(receiver->external, leaf, sizeof(leaf))) + return oo_value_unknown(); + static const struct { + const char *owner, *factory, *result; + bool copies; /* The argument is a collection whose element carries over. */ + } FACTORIES[] = {{"Optional", "of", "Optional", false}, + {"Optional", "ofNullable", "Optional", false}, + {"List", "of", "List", false}, + {"Set", "of", "Set", false}, + {"List", "copyOf", "List", true}, + {"Set", "copyOf", "Set", true}, + {"Stream", "of", "Stream", false}, + {"Arrays", "asList", "List", false}, + {"Arrays", "stream", "Stream", false}, + {"Collections", "singletonList", "List", false}, + {"Collections", "singleton", "Set", false}, + {"Collections", "unmodifiableList", "List", true}, + {"Collections", "unmodifiableSet", "Set", true}, + {"Collections", "unmodifiableCollection", "Collection", true}}; + for (size_t i = 0; i < sizeof(FACTORIES) / sizeof(FACTORIES[0]); i++) { + if (strcmp(FACTORIES[i].owner, leaf) || strcmp(FACTORIES[i].factory, name)) + continue; + oo_value_t element = args[0]; + if (FACTORIES[i].copies) { + element = oo_element_of(w, &args[0]); + } else if (argc == 1 && element.dims > 0) { + element.dims--; + } else { + for (int k = 1; k < argc; k++) + if (!oo_same_value(&element, &args[k])) + return oo_value_unknown(); + } + oo_value_t made = oo_value_external(FACTORIES[i].result); + return oo_external_with_element(w, &made, element); + } + return oo_value_unknown(); +} + +int oo_stdlib_lambda_parameters(oo_walk_t *w, const oo_value_t *receiver, const char *name, + int position, int argc, oo_value_t *out, int capacity) { + static const char *const each[] = { + /* Java: Iterable, Collection, Stream, Optional. */ + "forEach", "forEachOrdered", "removeIf", "filter", "map", "mapToInt", "mapToLong", + "mapToDouble", "mapToObj", "flatMap", "anyMatch", "allMatch", "noneMatch", "peek", + "takeWhile", "dropWhile", "ifPresent", "ifPresentOrElse", + /* C#: List and Enumerable. */ + "ForEach", "Exists", "Find", "FindAll", "FindIndex", "FindLast", "FindLastIndex", + "RemoveAll", "TrueForAll", "Where", "Select", "SelectMany", "Any", "All", "First", + "FirstOrDefault", "Last", "LastOrDefault", "Single", "SingleOrDefault", "Count", + "LongCount", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "GroupBy", + "ToDictionary", "ToLookup", "Sum", "Min", "Max", "Average", "SkipWhile", "TakeWhile", + "DistinctBy", "MinBy", "MaxBy", "Sort", NULL}; + static const char *const pair[] = {"sorted", "sort", "min", "max", NULL}; + if (!receiver || capacity < 2 || position < 0) + return -1; + char leaf[64]; + if (receiver->external && receiver->arg_count == 2 && + oo_plain_leaf(receiver->external, leaf, sizeof(leaf)) && !strcmp(leaf, "Map")) { + if ((!strcmp(name, "forEach") || !strcmp(name, "replaceAll")) && position == 0) { + out[0] = receiver->args[0]; + out[1] = receiver->args[1]; + return 2; + } + if (!strcmp(name, "computeIfAbsent") && position == 1) { + out[0] = receiver->args[0]; + return 1; + } + if ((!strcmp(name, "computeIfPresent") || !strcmp(name, "compute")) && position == 1) { + out[0] = receiver->args[0]; + out[1] = receiver->args[1]; + return 2; + } + if (!strcmp(name, "merge") && position == 2) { + out[0] = receiver->args[1]; + out[1] = receiver->args[1]; + return 2; + } + return -1; + } + oo_value_t element = oo_element_of(w, receiver); + if (!oo_value_known(&element)) + return -1; + if (oo_name_in(name, pair) && argc == 1 && position == 0) { + out[0] = element; + out[1] = element; + return 2; + } + if (oo_name_in(name, each) && position == 0) { + out[0] = element; + return 1; + } + return -1; +} + /* ── Member selection ───────────────────────────────────────────────── */ typedef struct { @@ -1063,6 +1471,9 @@ static bool oo_private_visible(const oo_walk_t *w, const oo_type_t *owner) { } bool oo_callable_visible(const oo_walk_t *w, const oo_callable_t *callable) { + if (callable->file && callable->file->test_source && w->f && + oo_file_outside_tests(w->ix, callable->file, w->f)) + return false; if (!(callable->flags & OO_F_PRIVATE)) return true; if (callable->owner) @@ -1183,17 +1594,47 @@ static bool oo_scalar_mismatch(CBMLanguage language, const char *parameter, /* NO only when both sides are known to differ; an unknown keeps the candidate * so that it continues to compete. */ +/* C# 8.2.3: object, ValueType and Enum stand above every type of their kind, unspelled. */ +static bool oo_csharp_root_accepts(const oo_type_t *root, const oo_type_t *type) { + if (!strcmp(root->qn, "System.Object")) + return true; + if (!strcmp(root->qn, "System.ValueType")) + return (type->flags & (OO_F_VALUE | OO_F_ENUM)) != 0; + return !strcmp(root->qn, "System.Enum") && (type->flags & OO_F_ENUM); +} + +/* C#: `T[]` reaches `Span` and its kin, and no other element. */ +static bool oo_span_of_another_element(const oo_value_t *parameter, const oo_value_t *argument) { + static const char *const SPANS[] = {"Span`1", "ReadOnlySpan`1", "Memory`1", + "ReadOnlyMemory`1", "Span", "ReadOnlySpan", + "Memory", "ReadOnlyMemory", NULL}; + if (argument->dims != 1 || parameter->arg_count != 1 || parameter->dims) + return false; + const char *leaf = parameter->type ? parameter->type->name : oo_leaf(parameter->external); + if (!leaf || !oo_name_in(leaf, SPANS)) + return false; + const oo_value_t *element = ¶meter->args[0]; + if (element->dims) + return false; + if (element->type && argument->type) + return element->type != argument->type; + return element->external && argument->external && + strcmp(oo_leaf(element->external), oo_leaf(argument->external)) != 0; +} + static bool oo_argument_excludes(oo_walk_t *w, const oo_callable_t *callable, const oo_subst_t *subst, const oo_value_t *args, int argc) { oo_scope_t scope = oo_scope_of_callable(callable); for (int i = 0; i < argc && i < OO_ARG_SLOTS; i++) { int index = i < callable->param_count ? i : callable->param_count - 1; - /* A closing function literal lands on the last parameter, defaults between. */ - if (args[i].function_literal && i == argc - 1) + /* A Kotlin closing function literal lands on the last parameter, defaults between. */ + if (args[i].function_literal && i == argc - 1 && callable->file && + callable->file->language == CBM_LANG_KOTLIN) index = callable->param_count - 1; if (index < 0 || !callable->params || !callable->params[index]) continue; - if (args[i].by_ref != callable->params[index]->by_ref) + if (args[i].by_ref != callable->params[index]->by_ref && + !callable->params[index]->ref_optional) return true; if (callable->max_args < 0 && index == callable->param_count - 1) continue; /* Variadic element versus array conversion is not modelled. */ @@ -1206,14 +1647,44 @@ static bool oo_argument_excludes(oo_walk_t *w, const oo_callable_t *callable, oo_value_t parameter = oo_value_from_ref(w, callable->params[index], &scope, subst); const oo_value_t *argument = &args[i]; if (argument->function_literal) { - /* A literal converts to a function type or an interface, nothing else. */ - if (parameter.type && !(parameter.type->flags & OO_F_INTERFACE)) + /* A literal converts to a function type or an interface, nothing else. In C# + * it converts to a delegate, or to the expression tree of one. */ + bool csharp = callable->file && callable->file->language == CBM_LANG_CSHARP; + if (parameter.type && + !(parameter.type->flags & (csharp ? OO_F_DELEGATE : OO_F_INTERFACE)) && + !(csharp && !strcmp(parameter.type->name, "Expression`1"))) return true; - if (parameter.external && (oo_builtin_name(oo_leaf(parameter.external)) || - !strcmp(oo_leaf(parameter.external), "String"))) + char collection[64]; + if (parameter.external && + (oo_builtin_name(oo_leaf(parameter.external)) || + !strcmp(oo_leaf(parameter.external), "String") || + !strcmp(oo_leaf(parameter.external), "string") || + (oo_plain_leaf(parameter.external, collection, sizeof(collection)) && + (oo_name_in(collection, OO_ONE_ARG_COLLECTIONS) || + oo_name_in(collection, OO_MAPS))))) return true; + /* `x => ...` does not reach a delegate of two parameters. */ + if (argument->lambda_parameters) { + oo_value_t shape[OO_ARG_SLOTS]; + int fits = parameter.type + ? oo_functional_parameters(w, ¶meter, shape, OO_ARG_SLOTS) + : -1; + if (fits >= 0 && fits != argument->lambda_parameters - 1) + return true; + if (parameter.external && csharp) { + const char *leaf = oo_leaf(parameter.external); + int arity = parameter.arg_count; + if ((!strncmp(leaf, "Func`", 5) && + arity - 1 != argument->lambda_parameters - 1) || + (!strncmp(leaf, "Action`", 7) && arity != argument->lambda_parameters - 1)) + return true; + } + } continue; } + if (callable->file && callable->file->language == CBM_LANG_CSHARP && + oo_span_of_another_element(¶meter, argument)) + return true; /* Array is final: a project class is never one. */ if (parameter.external && !strcmp(oo_leaf(parameter.external), "Array") && argument->type && oo_hierarchy_complete(w->ix, argument->type)) @@ -1228,17 +1699,31 @@ static bool oo_argument_excludes(oo_walk_t *w, const oo_callable_t *callable, return true; continue; } + /* C# 10.2: a user-defined implicit conversion is declared on either side, and an + * array reaches the generic collection interfaces. Neither is on the type's hierarchy. */ + bool csharp = callable->file && callable->file->language == CBM_LANG_CSHARP; if (parameter.type && argument->type) { if (parameter.dims != argument->dims || !oo_is_subtype(w->ix, argument->type, parameter.type)) { + bool converts = + csharp && + (oo_csharp_root_accepts(parameter.type, argument->type) || + (parameter.type->flags & OO_F_CONVERTS) || + (argument->type->flags & OO_F_CONVERTS) || + (argument->dims > parameter.dims && (parameter.type->flags & OO_F_INTERFACE))); /* An unresolved supertype could still make it assignable. */ - if (oo_hierarchy_complete(w->ix, argument->type)) + if (!converts && oo_hierarchy_complete(w->ix, argument->type)) return true; } } else if (parameter.type && argument->external && !argument->fn_ref) { /* A literal or JDK value versus a project parameter: only a primitive - * or String literal is certainly not that class. */ - if (oo_scalar_of(oo_leaf(argument->external)) != OO_SCALAR_NONE) + * or String literal is certainly not that class. C#: an integer reaches an + * enum as a literal 0, and a project declaring the scalars themselves (a core + * library) has them implement its own interfaces. */ + bool converts = + csharp && ((parameter.type->flags & (OO_F_CONVERTS | OO_F_INTERFACE | OO_F_ENUM)) || + argument->dims); + if (!converts && oo_scalar_of(oo_leaf(argument->external)) != OO_SCALAR_NONE) return true; } else if (parameter.external && argument->external) { if (parameter.dims != argument->dims) { @@ -1251,7 +1736,8 @@ static bool oo_argument_excludes(oo_walk_t *w, const oo_callable_t *callable, parameter.external, argument)) { return true; } - } else if (parameter.external && argument->type && oo_builtin_name(parameter.external)) { + } else if (parameter.external && argument->type && oo_builtin_name(parameter.external) && + !(csharp && (argument->type->flags & OO_F_CONVERTS))) { return true; } } @@ -1292,11 +1778,134 @@ static bool oo_arguments_exact(oo_walk_t *w, const oo_callable_t *callable, cons return true; } +/* Which arguments have their own type as this declaration's parameter, one bit each. */ +static uint32_t oo_identical_arguments(oo_walk_t *w, const oo_callable_t *callable, + const oo_subst_t *subst, const oo_value_t *args, int argc) { + enum { MASK_BITS = 32 }; + uint32_t mask = 0; + if (!callable->params) + return 0; + oo_scope_t scope = oo_scope_of_callable(callable); + for (int i = 0; i < argc && i < callable->param_count && i < MASK_BITS; i++) { + const oo_typeref_t *ref = callable->params[i]; + if (!ref || (callable->max_args < 0 && i == callable->param_count - 1)) + continue; + /* C# infers T as the argument's own type: its conversion is an identity. */ + if (oo_ref_is_tparam(callable, ref)) { + if (oo_value_known(&args[i]) && callable->file && + callable->file->language == CBM_LANG_CSHARP) + mask |= 1u << i; + continue; + } + oo_value_t parameter = oo_value_from_ref(w, ref, &scope, subst); + if (args[i].type ? args[i].type == parameter.type && args[i].dims == parameter.dims + : oo_same_external(&args[i], ¶meter)) + mask |= 1u << i; + } + return mask; +} + typedef struct { const oo_callable_t *callable; oo_subst_t *subst; } oo_candidate_t; +static bool oo_same_graph_node(const oo_callable_t *a, const oo_callable_t *b) { + return a->graph_qn && b->graph_qn && !strcmp(a->graph_qn, b->graph_qn); +} + +/* Every argument certainly reaches this declaration's parameter: known, and of its type, a subtype, + * a scalar it widens to, or an array of a span's element. Not excluded is not the same thing: an + * argument from outside the project could still fail to convert to a project's parameter. */ +static bool oo_arguments_certainly_fit(oo_walk_t *w, const oo_callable_t *callable, + const oo_subst_t *subst, const oo_value_t *args, int argc) { + static const char *const SPANS[] = {"Span`1", "ReadOnlySpan`1", "Memory`1", + "ReadOnlyMemory`1", "Span", "ReadOnlySpan", + "Memory", "ReadOnlyMemory", NULL}; + static const char *const ROOTS[] = {"object", "Object", NULL}; + if (argc > callable->param_count || !callable->params || callable->max_args < 0) + return false; + oo_scope_t scope = oo_scope_of_callable(callable); + for (int i = 0; i < argc; i++) { + const oo_value_t *argument = &args[i]; + const oo_typeref_t *ref = callable->params[i]; + if (!ref) + return false; + if (argument->function_literal || oo_ref_is_tparam(callable, ref)) + continue; + oo_value_t parameter = oo_value_from_ref(w, ref, &scope, subst); + const char *to = parameter.type ? parameter.type->name : oo_leaf(parameter.external); + if (!to) + return false; + if (argument->null_literal) { + if (oo_scalar_of(to) != OO_SCALAR_NONE && oo_scalar_of(to) != OO_SCALAR_STRING) + return false; + continue; + } + if (!oo_value_known(argument)) + return false; + if (oo_name_in(to, ROOTS) && !parameter.dims) + continue; + bool fits = false; + if (argument->type && parameter.type) + fits = argument->dims == parameter.dims && + oo_is_subtype(w->ix, argument->type, parameter.type); + else if (argument->external && parameter.external && argument->dims == parameter.dims) + fits = !strcmp(oo_leaf(argument->external), to) || + (oo_scalar_of(to) != OO_SCALAR_NONE && + oo_scalar_of(oo_leaf(argument->external)) != OO_SCALAR_NONE); + /* An array reaches a span; a span of another element is excluded already. */ + if (!fits && argument->dims == 1 && oo_name_in(to, SPANS)) + fits = true; + if (!fits) + return false; + } + return true; +} + +/* An identical parameter is a better conversion than any other. A candidate with every identity + * any rival has, and one more, cannot lose an argument to it, and each rival loses one to it. The + * call compiles, so it has a best member, and no rival can be that. */ +static int oo_best_by_identity(oo_walk_t *w, const oo_candidate_t *candidates, int count, + const oo_value_t *args, int argc) { + uint32_t masks[OO_MAX_CANDIDATES]; + for (int i = 0; i < count; i++) + masks[i] = + oo_identical_arguments(w, candidates[i].callable, candidates[i].subst, args, argc); + for (int i = 0; i < count; i++) { + bool beats_all = + masks[i] != 0 && + oo_arguments_certainly_fit(w, candidates[i].callable, candidates[i].subst, args, argc); + for (int j = 0; j < count && beats_all; j++) + if (i != j && !oo_same_graph_node(candidates[i].callable, candidates[j].callable)) + beats_all = !(masks[j] & ~masks[i]) && masks[j] != masks[i]; + if (beats_all) + return i; + } + return -1; +} + +/* C# 12.8.10.2: a method declared in a base type leaves the set when an applicable one in a more + * derived type stands. */ +static int oo_keep_most_derived(oo_walk_t *w, oo_candidate_t *candidates, int count, + const oo_value_t *args, int argc) { + bool removed[OO_MAX_CANDIDATES] = {false}; + for (int j = 0; j < count; j++) { + if (!oo_arguments_certainly_fit(w, candidates[j].callable, candidates[j].subst, args, argc)) + continue; + for (int i = 0; i < count; i++) + removed[i] = + removed[i] || (candidates[i].callable->owner != candidates[j].callable->owner && + oo_is_subtype(w->ix, candidates[j].callable->owner, + candidates[i].callable->owner)); + } + int kept = 0; + for (int i = 0; i < count; i++) + if (!removed[i]) + candidates[kept++] = candidates[i]; + return kept; +} + static oo_select_t oo_select_finish(oo_walk_t *w, oo_candidate_t *candidates, int count, int argc, const oo_value_t *args) { oo_select_t out = {.status = OO_SEL_UNIQUE}; @@ -1326,6 +1935,15 @@ static oo_select_t oo_select_finish(oo_walk_t *w, oo_candidate_t *candidates, in } } } + bool csharp = + candidates[0].callable->file && candidates[0].callable->file->language == CBM_LANG_CSHARP; + if (out.status == OO_SEL_AMBIGUOUS && args && argc >= 0 && csharp) { + count = oo_keep_most_derived(w, candidates, count, args, argc); + out.status = OO_SEL_UNIQUE; + for (int i = 1; i < count; i++) + if (!oo_same_graph_node(candidates[0].callable, candidates[i].callable)) + out.status = OO_SEL_AMBIGUOUS; + } if (out.status == OO_SEL_AMBIGUOUS && args && argc > 0) { int exact = -1, matches = 0; for (int i = 0; i < count; i++) { @@ -1334,12 +1952,26 @@ static oo_select_t oo_select_finish(oo_walk_t *w, oo_candidate_t *candidates, in matches++; } } - if (matches == 1) { + bool one_node = matches > 1; + for (int i = 0; i < count && one_node; i++) + if (oo_arguments_exact(w, candidates[i].callable, candidates[i].subst, args, argc) && + (!candidates[i].callable->graph_qn || !candidates[exact].callable->graph_qn || + strcmp(candidates[i].callable->graph_qn, candidates[exact].callable->graph_qn))) + one_node = false; + if (matches == 1 || one_node) { candidates[0] = candidates[exact]; count = 1; out.status = OO_SEL_UNIQUE; } } + if (out.status == OO_SEL_AMBIGUOUS && args && argc > 0) { + int best = oo_best_by_identity(w, candidates, count, args, argc); + if (best >= 0) { + candidates[0] = candidates[best]; + count = 1; + out.status = OO_SEL_UNIQUE; + } + } if (out.status == OO_SEL_UNIQUE) { out.callable = candidates[0].callable; out.subst = candidates[0].subst; @@ -1364,6 +1996,11 @@ static oo_select_t oo_select_in_hierarchy(oo_walk_t *w, const oo_value_t *receiv bool seen = false; while (h->head < h->tail) { oo_step_t step = h->steps[h->head++]; + /* C# 12.5.1: the base types of a class or struct are its base classes. An interface's + * members are not among its members. */ + if ((step.type->flags & OO_F_INTERFACE) && !(receiver->type->flags & OO_F_INTERFACE) && + step.type->file && step.type->file->language == CBM_LANG_CSHARP) + continue; for (const oo_callable_t *c = oo_lookup_methods(w->ix, step.type->qn, name); c; c = c->next) { if (c->flags & OO_F_CTOR) @@ -1506,6 +2143,53 @@ const oo_field_t *oo_find_field(oo_walk_t *w, const oo_value_t *receiver, const return NULL; } +/* JLS 9.8: an interface with one abstract method, its own or inherited, counting a redeclaration + * once and leaving out one a subinterface implemented. An external supertype cannot add a second: + * the lambda would not compile. The walk is bounded by OO_MAX_HIERARCHY. */ +int oo_functional_parameters(oo_walk_t *w, const oo_value_t *function, oo_value_t *out, + int capacity) { + if (!function || !function->type || function->dims || + !(function->type->flags & (OO_F_INTERFACE | OO_F_DELEGATE))) + return -1; + oo_hierarchy_t walk, *h = &walk; + h->head = h->tail = 0; + h->w = w; + h->truncated = false; + oo_hierarchy_add(h, *function); + const oo_callable_t *only = NULL; + oo_subst_t *only_subst = NULL; + while (h->head < h->tail) { + oo_step_t step = h->steps[h->head++]; + if (step.type->abstract_count > 1) + return -1; + const oo_callable_t *declared = step.type->abstract_method; + if (declared) { + bool implemented = false; + for (int i = 0; i + 1 < h->head && !implemented; i++) + for (const oo_callable_t *m = + oo_lookup_methods(w->ix, h->steps[i].type->qn, declared->name); + m && !implemented; m = m->next) + implemented = m != h->steps[i].type->abstract_method; + if (!implemented && only && strcmp(only->name, declared->name)) + return -1; + if (!implemented && !only) { + only = declared; + only_subst = step.subst; + } + } + oo_hierarchy_expand(h, &step); + } + if (!only || h->truncated || only->param_count > capacity || + (only->param_count && !only->params)) + return -1; + oo_scope_t scope = oo_scope_of_callable(only); + for (int i = 0; i < only->param_count; i++) { + oo_value_t value = oo_value_from_ref(w, only->params[i], &scope, only_subst); + out[i] = !oo_value_known(&value) && value.lower ? *value.lower : value; + } + return only->param_count; +} + oo_select_t oo_select_from_list(oo_walk_t *w, const oo_callable_t *list, int argc, const oo_value_t *args, const oo_value_t *receiver) { oo_candidate_t candidates[OO_MAX_CANDIDATES]; @@ -1530,6 +2214,100 @@ oo_select_t oo_select_from_list(oo_walk_t *w, const oo_callable_t *list, int arg return (oo_select_t){.status = seen ? OO_SEL_MISMATCH : OO_SEL_NONE}; } +/* What one parameter says about one type parameter, given the value passed to it. */ +static oo_value_t oo_tparam_from(oo_walk_t *w, const oo_typeref_t *ref, const char *tparam, + const oo_value_t *value) { + if (!ref || !ref->name || !value) + return oo_value_unknown(); + if (!strcmp(ref->name, tparam)) { + if (ref->dims > value->dims) + return oo_value_unknown(); + oo_value_t element = *value; + element.dims -= ref->dims; + element.classref = false; + return element; + } + if (!ref->args || ref->dims) + return oo_value_unknown(); + int count = 0; + while (ref->args[count]) + count++; + for (int k = 0; k < count; k++) { + const oo_typeref_t *arg = ref->args[k]; + const oo_typeref_t *written = arg && arg->lower ? arg->lower : arg; + if (!written || !written->name || strcmp(written->name, tparam)) + continue; + /* The same generic type: positions match. Anything else only through a known element. */ + const char *declared = oo_leaf(ref->name); + const char *actual = value->type ? value->type->name : oo_leaf(value->external); + size_t stem = strcspn(declared, "`"); + if (actual && value->arg_count == count && !strncmp(declared, actual, stem) && + (actual[stem] == 0 || actual[stem] == 0x60)) + return value->args[k]; + if (count == 1) + return oo_element_of(w, value); + } + return oo_value_unknown(); +} + +static bool oo_same_value(const oo_value_t *a, const oo_value_t *b) { + return a->dims == b->dims && a->type == b->type && + (a->type || (a->external && b->external && !strcmp(a->external, b->external))); +} + +bool oo_ref_unsettled(const oo_callable_t *callee, const oo_typeref_t *ref, + const oo_subst_t *subst) { + if (!callee || !ref || !ref->name) + return false; + for (int i = 0; i < callee->tparam_count; i++) { + if (strcmp(callee->tparams[i].name, ref->name)) + continue; + const oo_value_t *bound = oo_subst_lookup(subst, &callee->tparams[i]); + return !bound || !oo_value_known(bound); + } + return false; +} + +const oo_subst_t *oo_infer_call_subst(oo_walk_t *w, const oo_callable_t *callee, + const oo_subst_t *subst, const oo_value_t *receiver, + const oo_value_t *args, int argc) { + if (!callee || !callee->tparam_count) + return subst; + oo_value_t *values = cbm_arena_calloc(w->arena, (size_t)callee->tparam_count * sizeof(*values)); + oo_subst_t *inferred = values ? cbm_arena_alloc(w->arena, sizeof(*inferred)) : NULL; + if (!inferred) + return subst; + bool any = false; + for (int t = 0; t < callee->tparam_count; t++) { + const char *name = callee->tparams[t].name; + bool conflict = false; + oo_value_t found = oo_value_unknown(); + for (int i = -1; i < argc && i < callee->param_count && !conflict; i++) { + const oo_typeref_t *ref = i < 0 ? callee->receiver + : callee->params ? callee->params[i] + : NULL; + const oo_value_t *value = i < 0 ? receiver : &args[i]; + oo_value_t one = oo_tparam_from(w, ref, name, value); + if (!oo_value_known(&one)) + continue; + conflict = oo_value_known(&found) && !oo_same_value(&found, &one); + found = one; + } + if (!conflict && oo_value_known(&found)) { + values[t] = found; + any = true; + } + } + if (!any) + return subst; + *inferred = (oo_subst_t){.tparams = callee->tparams, + .count = callee->tparam_count, + .args = values, + .arg_count = callee->tparam_count, + .next = (oo_subst_t *)subst}; + return inferred; +} + oo_value_t oo_kotlin_eval_summary(oo_walk_t *w, const oo_exprsum_t *summary, const oo_scope_t *scope, const oo_subst_t *subst); diff --git a/internal/cbm/lsp/oo_csharp.c b/internal/cbm/lsp/oo_csharp.c index c60899ef6..4bf8ea2b1 100644 --- a/internal/cbm/lsp/oo_csharp.c +++ b/internal/cbm/lsp/oo_csharp.c @@ -1,4 +1,5 @@ #include "oo_internal.h" +#include #include #include @@ -247,6 +248,11 @@ static bool cs_has_ref_modifier(const cs_text_t *t, TSNode node) { for (uint32_t i = 0; i < count; i++) { TSNode part = ts_node_child(node, i); const char *kind = ts_node_type(part); + /* `ref T value` can parse as a ref type; `ref readonly` also takes a plain argument. */ + if (!strcmp(kind, "ref_type")) { + const char *text = cs_text(t, part); + return !text || !strstr(text, "readonly"); + } if (!strcmp(kind, "ref") || !strcmp(kind, "out")) return true; if (!strcmp(kind, "modifier")) { @@ -258,6 +264,22 @@ static bool cs_has_ref_modifier(const cs_text_t *t, TSNode node) { return false; } +/* `in` and `ref readonly` parameters take a plain argument and a `ref` one. */ +static bool cs_ref_optional(const cs_text_t *t, TSNode node) { + uint32_t count = ts_node_child_count(node); + for (uint32_t i = 0; i < count; i++) { + TSNode part = ts_node_child(node, i); + const char *kind = ts_node_type(part); + const char *text = + !strcmp(kind, "modifier") || !strcmp(kind, "ref_type") ? cs_text(t, part) : NULL; + if (!strcmp(kind, "in") || !strcmp(kind, "readonly") || + (text && + (!strcmp(text, "in") || !strcmp(text, "readonly") || strstr(text, "readonly")))) + return true; + } + return false; +} + static void ch_parameters(const ch_t *h, oo_callable_t *callable, TSNode list, bool *extension) { uint32_t count = ts_node_is_null(list) ? 0 : ts_node_child_count(list); const oo_typeref_t **refs = @@ -275,11 +297,13 @@ static void ch_parameters(const ch_t *h, oo_callable_t *callable, TSNode list, b TSNode type = cs_field(child, "type"); if (refs) { refs[total] = cs_typeref(&h->text, type); - if (refs[total] && cs_has_ref_modifier(&h->text, child)) { + bool optional_ref = cs_ref_optional(&h->text, child); + if (refs[total] && (optional_ref || cs_has_ref_modifier(&h->text, child))) { oo_typeref_t *by_ref = cbm_arena_alloc(h->text.arena, sizeof(*by_ref)); if (by_ref) { *by_ref = *refs[total]; - by_ref->by_ref = true; + by_ref->by_ref = !optional_ref; + by_ref->ref_optional = optional_ref; refs[total] = by_ref; } } @@ -317,6 +341,8 @@ static void ch_parameters(const ch_t *h, oo_callable_t *callable, TSNode list, b callable->max_args = variadic ? -1 : total; } +static const char CW_INDEXER[] = "this"; + static const char *ch_graph(const ch_t *h, TSNode node, const char *name) { const CBMDefinition *def = oo_defs_find(&h->defs, 'C', cs_line(node), name); if (!def) @@ -359,6 +385,22 @@ static void ch_method(ch_t *h, oo_type_t *type, TSNode node) { } } +/* An indexer: the callable behind `x[i]`, which has a node named `this`. */ +static void ch_indexer(ch_t *h, oo_type_t *type, TSNode node) { + oo_callable_t *callable = cbm_arena_calloc(h->text.arena, sizeof(*callable)); + if (!callable) + return; + callable->name = CW_INDEXER; + callable->graph_qn = ch_graph(h, node, CW_INDEXER); + callable->owner = type; + callable->file = h->file; + callable->ns = type->ns; + callable->flags = cs_member_flags(cs_modifiers(&h->text, node), type); + ch_parameters(h, callable, cs_field(node, "parameters"), NULL); + callable->ret = cs_typeref(&h->text, cs_field(node, "type")); + oo_index_method(h->ix, type, callable); +} + static void ch_field(ch_t *h, oo_type_t *type, const char *name, TSNode node, TSNode type_node, unsigned modifiers, uint32_t forced) { if (!name) @@ -380,11 +422,23 @@ static void ch_type(ch_t *h, TSNode node, oo_type_t *owner, const char *ns); static void ch_members(ch_t *h, oo_type_t *type, TSNode body, const char *ns) { uint32_t count = ts_node_is_null(body) ? 0 : ts_node_named_child_count(body); + /* A contract's members are the implementation's; a nested type may still be its own. */ + bool contract = oo_type_is_implemented_elsewhere(type, h->file); for (uint32_t i = 0; i < count; i++) { TSNode member = ts_node_named_child(body, i); const char *kind = ts_node_type(member); + if (contract && strcmp(kind, "class_declaration") && strcmp(kind, "struct_declaration") && + strcmp(kind, "interface_declaration") && strcmp(kind, "enum_declaration") && + strcmp(kind, "record_declaration") && strcmp(kind, "record_struct_declaration") && + strcmp(kind, "delegate_declaration")) + continue; + /* C# 18.6.2: an explicit interface member implementation is no member of the type's. */ + if (!ts_node_is_null(cs_child(member, "explicit_interface_specifier"))) + continue; if (!strcmp(kind, "method_declaration") || !strcmp(kind, "constructor_declaration")) { ch_method(h, type, member); + } else if (!strcmp(kind, "indexer_declaration")) { + ch_indexer(h, type, member); } else if (!strcmp(kind, "property_declaration")) { ch_field(h, type, cs_name(&h->text, cs_field(member, "name")), member, cs_field(member, "type"), cs_modifiers(&h->text, member), 0); @@ -403,6 +457,10 @@ static void ch_members(ch_t *h, oo_type_t *type, TSNode body, const char *ns) { ch_field(h, type, cs_name(&h->text, cs_field(declarator, "name")), declarator, field_type, modifiers, forced); } + } else if (!strcmp(kind, "conversion_operator_declaration")) { + const char *text = cs_text(&h->text, member); + if (text && strstr(text, "implicit")) + type->flags |= OO_F_CONVERTS; } else if (!strcmp(kind, "enum_member_declaration")) { const char *name = cs_name(&h->text, cs_field(member, "name")); oo_field_t *field = name ? cbm_arena_calloc(h->text.arena, sizeof(*field)) : NULL; @@ -448,6 +506,10 @@ static void ch_type(ch_t *h, TSNode node, oo_type_t *owner, const char *ns) { flags |= OO_F_INTERFACE; if (cs_is(node, "enum_declaration")) flags |= OO_F_ENUM; + if (cs_is(node, "delegate_declaration")) + flags |= OO_F_DELEGATE; + if (cs_is(node, "struct_declaration") || cs_is(node, "record_struct_declaration")) + flags |= OO_F_VALUE; if (oo_members_broken(node)) flags |= OO_F_INCOMPLETE; const CBMDefinition *def = oo_defs_find(&h->defs, 'T', cs_line(node), name); @@ -455,6 +517,10 @@ static void ch_type(ch_t *h, TSNode node, oo_type_t *owner, const char *ns) { def ? def->qualified_name : NULL, flags); if (!type) return; + if (oo_type_is_implemented_elsewhere(type, h->file)) { + ch_members(h, type, cs_field(node, "body"), ns); + return; + } if (!type->tparams) type->tparams = cs_tparams(&h->text, node, &type->tparam_count); TSNode bases = cs_child(node, "base_list"); @@ -468,6 +534,22 @@ static void ch_type(ch_t *h, TSNode node, oo_type_t *owner, const char *ns) { continue; oo_type_add_super(h->ix, type, cs_typeref(&h->text, base), h->file, ns); } + /* A delegate's signature is its Invoke, which has no node of its own. */ + if (cs_is(node, "delegate_declaration")) { + oo_callable_t *invoke = cbm_arena_calloc(h->text.arena, sizeof(*invoke)); + if (invoke) { + invoke->name = "Invoke"; + invoke->owner = type; + invoke->file = h->file; + invoke->ns = ns; + ch_parameters(h, invoke, cs_field(node, "parameters"), NULL); + invoke->ret = cs_typeref(&h->text, cs_field(node, "type")); + oo_index_method(h->ix, type, invoke); + type->abstract_method = invoke; + type->abstract_count = 1; + } + return; + } /* A primary constructor: its parameters; a record's are also properties. */ TSNode primary = cs_child(node, "parameter_list"); if (!ts_node_is_null(primary)) { @@ -592,18 +674,34 @@ bool oo_csharp_harvest(cbm_oo_index_t *ix, const char *rel, const CBMFileResult /* ── Walk ───────────────────────────────────────────────────────────── */ +/* One call's lambda arguments. Their parameter types come from the declaration the call selects, + * so they are walked after selection, once each. */ +typedef struct { + TSNode *nodes; /* By argument position; a null node is no lambda. */ + int count; + bool walked; + oo_value_t last; /* The last lambda's expression body. */ +} cw_lambdas_t; + typedef struct { oo_walk_t *w; cs_text_t text; int depth; bool static_method; bool in_nameof; + cw_lambdas_t *lambdas; /* The call whose declaration is being selected. */ + const oo_value_t *type_args; /* What the call being selected spells: `Get()`. */ + int type_arg_count; + oo_value_t pattern_subject; /* What the pattern being walked is matched against. */ } cw_t; static oo_value_t cw_expr(cw_t *c, TSNode node); static void cw_members(cw_t *c, TSNode body, const oo_type_t *type); static void cw_type(cw_t *c, TSNode node, const oo_type_t *owner); +static void cw_attribute_lists(cw_t *c, TSNode owner); +static bool cw_lambda_to_declared(cw_t *c, TSNode lambda, TSNode type); + static void cw_children(cw_t *c, TSNode node) { uint32_t count = ts_node_is_null(node) ? 0 : ts_node_named_child_count(node); for (uint32_t i = 0; i < count; i++) @@ -623,8 +721,13 @@ static void cw_type_mentions(cw_t *c, TSNode node, unsigned depth) { oo_value_t value = cw_ref(c, mentioned); if (value.type && value.type->graph_qn && !(mentioned && oo_names_tparam(w, mentioned->name))) { - oo_emit_type_ref(w, node, value.type->graph_qn, "csharp-type-name-lookup"); - oo_emit_usage(w, node, value.type->graph_qn, CBM_RESOLVED_TYPE_REFERENCE, + /* `A.B` occurs as `B`; its USES_TYPE carrier may also span a plain `A.B`. */ + TSNode last = cs_is(node, "qualified_name") ? cs_field(node, "name") : node; + TSNode leaf = cs_is(last, "generic_name") ? cs_child(last, "identifier") : last; + oo_emit_type_ref(w, leaf, value.type->graph_qn, "csharp-type-name-lookup"); + if (!ts_node_eq(leaf, node)) + oo_emit_type_ref(w, node, value.type->graph_qn, "csharp-type-name-lookup"); + oo_emit_usage(w, leaf, value.type->graph_qn, CBM_RESOLVED_TYPE_REFERENCE, CBM_RESOLVE_RULE_DECLARED_TYPE, "csharp-type-name-lookup"); } TSNode list = cs_is(node, "generic_name") ? cs_child(node, "type_argument_list") @@ -653,6 +756,48 @@ static oo_value_t cw_declared(cw_t *c, TSNode type, oo_value_t initial) { /* Target-typed `new()`: the declared type names the class. */ static void cw_implicit_creation(cw_t *c, TSNode node, const oo_value_t *target); +/* `[Foo(Bar.Baz)]`: the class is Foo or FooAttribute (C# 23.3), and its arguments are expressions + * of the scope around the declaration. Both spellings existing is an error, so the first found is + * it. */ +static void cw_attribute(cw_t *c, TSNode attribute) { + oo_walk_t *w = c->w; + TSNode name = cs_field(attribute, "name"); + const oo_typeref_t *written = cs_typeref(&c->text, name); + oo_value_t value = cw_ref(c, written); + if (!value.type && written && written->name) { + oo_typeref_t suffixed = *written; + suffixed.name = cbm_arena_sprintf(w->arena, "%sAttribute", written->name); + if (suffixed.name) + value = cw_ref(c, &suffixed); + } + if (value.type && value.type->graph_qn) { + TSNode site = cs_is(name, "qualified_name") ? cs_field(name, "name") : name; + oo_emit_usage(w, site, value.type->graph_qn, CBM_RESOLVED_TYPE_REFERENCE, + CBM_RESOLVE_RULE_DECLARED_TYPE, "csharp-attribute-name-lookup"); + } + TSNode arguments = cs_child(attribute, "attribute_argument_list"); + uint32_t count = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments); + for (uint32_t i = 0; i < count; i++) { + TSNode argument = ts_node_named_child(arguments, i); + uint32_t parts = ts_node_named_child_count(argument); + if (parts) + (void)cw_expr(c, ts_node_named_child(argument, parts - 1)); + } +} + +static void cw_attribute_lists(cw_t *c, TSNode owner) { + uint32_t count = ts_node_is_null(owner) ? 0 : ts_node_named_child_count(owner); + for (uint32_t i = 0; i < count; i++) { + TSNode list = ts_node_named_child(owner, i); + if (!cs_is(list, "attribute_list")) + continue; + uint32_t attributes = ts_node_named_child_count(list); + for (uint32_t a = 0; a < attributes; a++) + if (cs_is(ts_node_named_child(list, a), "attribute")) + cw_attribute(c, ts_node_named_child(list, a)); + } +} + static void cw_variable_declaration(cw_t *c, TSNode node) { TSNode type = cs_field(node, "type"); uint32_t count = ts_node_named_child_count(node); @@ -671,7 +816,8 @@ static void cw_variable_declaration(cw_t *c, TSNode node) { } oo_value_t initial = oo_value_unknown(); bool implicit = cs_is(initializer, "implicit_object_creation_expression"); - if (!ts_node_is_null(initializer) && !implicit) + if (!ts_node_is_null(initializer) && !implicit && + !cw_lambda_to_declared(c, initializer, type)) initial = cw_expr(c, initializer); oo_value_t value = cw_declared(c, type, initial); if (implicit) @@ -688,6 +834,7 @@ static void cw_bind_parameters(cw_t *c, TSNode list) { if (cs_is(child, "parameter")) { TSNode type = cs_field(child, "type"); const char *name = cs_name(&c->text, cs_field(child, "name")); + cw_attribute_lists(c, child); cw_type_mentions(c, type, 0); oo_bind(c->w, name, ts_node_is_null(type) ? oo_value_unknown() @@ -717,7 +864,28 @@ static TSNode cw_argument_value(TSNode argument) { return parts ? ts_node_named_child(argument, parts - 1) : (TSNode){0}; } -static const oo_value_t *cw_arguments(cw_t *c, TSNode list, int *out_count) { +static bool cw_is_lambda(TSNode node) { + return cs_is(node, "lambda_expression") || cs_is(node, "anonymous_method_expression"); +} + +/* How many parameters a lambda declares, or -1 for `delegate { }`, which fits any. */ +static int cw_lambda_arity(TSNode lambda) { + TSNode parameters = cs_field(lambda, "parameters"); + if (cs_is(parameters, "implicit_parameter") || cs_is(parameters, "identifier")) + return 1; + if (ts_node_is_null(parameters)) + return -1; + int total = 0; + uint32_t count = ts_node_child_count(parameters); + for (uint32_t i = 0; i < count; i++) + total += cs_is(ts_node_child(parameters, i), "parameter"); + return total; +} + +/* A named argument (`isFinalBlock: false`) says nothing about its position, so its type does not + * take part in overload choice. A lambda waits for selection when the caller collects it. */ +static const oo_value_t *cw_arguments(cw_t *c, TSNode list, int *out_count, + cw_lambdas_t *deferred) { uint32_t children = ts_node_is_null(list) ? 0 : ts_node_named_child_count(list); int count = 0; for (uint32_t i = 0; i < children; i++) @@ -726,12 +894,31 @@ static const oo_value_t *cw_arguments(cw_t *c, TSNode list, int *out_count) { *out_count = count; oo_value_t *values = count ? cbm_arena_calloc(c->w->arena, (size_t)count * sizeof(*values)) : NULL; + if (deferred && count) { + deferred->nodes = cbm_arena_calloc(c->w->arena, (size_t)count * sizeof(TSNode)); + deferred->count = deferred->nodes ? count : 0; + } int at = 0; for (uint32_t i = 0; i < children; i++) { TSNode argument = ts_node_named_child(list, i); if (!cs_is(argument, "argument")) continue; - oo_value_t value = cw_expr(c, cw_argument_value(argument)); + TSNode expression = cw_argument_value(argument); + bool named = ts_node_named_child_count(argument) > 1 && + (cs_is(ts_node_named_child(argument, 0), "name_colon") || + !ts_node_is_null(cs_field(argument, "name"))); + bool lambda = cw_is_lambda(expression); + oo_value_t value = oo_value_unknown(); + if (lambda && !named && deferred && deferred->nodes) { + deferred->nodes[at] = expression; + } else { + value = cw_expr(c, expression); + if (named) + value = oo_value_unknown(); + } + value.function_literal = lambda && !named; + if (value.function_literal) + value.lambda_parameters = cw_lambda_arity(expression) + 1; value.by_ref = cs_has_ref_modifier(&c->text, argument); if (values) values[at] = value; @@ -740,14 +927,192 @@ static const oo_value_t *cw_arguments(cw_t *c, TSNode list, int *out_count) { return values; } -static bool cw_external_accepts(const char *declared, const oo_value_t *value) { - if (!value->external) +/* A lambda under the parameter types its target gave, `count` of them, or none known at -1. */ +static oo_value_t cw_lambda_typed(cw_t *c, TSNode node, const oo_value_t *types, int count) { + oo_walk_t *w = c->w; + oo_frame_t frame; + oo_frame_push(w, &frame); + TSNode parameters = cs_field(node, "parameters"); + if (cs_is(parameters, "implicit_parameter") || cs_is(parameters, "identifier")) { + oo_bind(w, cs_name(&c->text, parameters), count == 1 ? types[0] : oo_value_unknown()); + } else { + /* A written type wins; a name alone takes the target's. */ + uint32_t children = ts_node_is_null(parameters) ? 0 : ts_node_child_count(parameters); + int total = 0, at = 0; + for (uint32_t i = 0; i < children; i++) + total += cs_is(ts_node_child(parameters, i), "parameter"); + cw_bind_parameters(c, parameters); + for (uint32_t i = 0; i < children && total == count; i++) { + TSNode parameter = ts_node_child(parameters, i); + if (!cs_is(parameter, "parameter")) + continue; + if (ts_node_is_null(cs_field(parameter, "type"))) + oo_bind(w, cs_name(&c->text, cs_field(parameter, "name")), types[at]); + at++; + } + } + oo_value_t body = cw_expr(c, cs_field(node, "body")); + oo_frame_pop(w); + return body; +} + +static bool cw_imports_system(const oo_file_t *file) { + for (int i = 0; file && i < file->import_count; i++) + if (file->imports[i].star && !file->imports[i].statik && + !strcmp(file->imports[i].path, "System")) + return true; + return false; +} + +/* System's generic delegates when the project does not declare them: an identity-marked spelling + * (`Func`2`) that the declaring file reads from `using System`, or spells out. */ +static int cw_functional_parameters(cw_t *c, const oo_value_t *function, const oo_file_t *file, + oo_value_t *out, int capacity) { + if (function->type && !strcmp(function->type->name, "Expression`1") && function->arg_count == 1) + return cw_functional_parameters(c, &function->args[0], file, out, capacity); + if (function->type) + return oo_functional_parameters(c->w, function, out, capacity); + if (!function->external || function->dims) + return -1; + const char *leaf = strrchr(function->external, '.'); + if (leaf && strncmp(function->external, "System.", 7)) + return -1; + if (!leaf && !cw_imports_system(file)) + return -1; + leaf = leaf ? leaf + 1 : function->external; + int args = function->arg_count; + if (args > capacity) + return -1; + int count = -1; + if (!strncmp(leaf, "Action`", 7)) + count = args; + else if (!strncmp(leaf, "Func`", 5)) + count = args - 1; + else if (!strcmp(leaf, "Predicate`1") || !strcmp(leaf, "Converter`2")) + count = 1; + if (count >= 0) { + for (int i = 0; i < count; i++) + out[i] = function->args[i]; + return count; + } + if (!strcmp(leaf, "Comparison`1") && args == 1 && capacity >= 2) { + out[0] = function->args[0]; + out[1] = function->args[0]; + return 2; + } + return -1; +} + +enum { CW_LAMBDA_PARAMETERS = 8 }; + +/* `Func f = d => d.Go();`: the declared type is the target. */ +static bool cw_lambda_to_declared(cw_t *c, TSNode lambda, TSNode type) { + if (!cw_is_lambda(lambda) || ts_node_is_null(type) || cs_is(type, "implicit_type")) return false; + oo_value_t declared = cw_ref(c, cs_typeref(&c->text, type)); + oo_value_t types[CW_LAMBDA_PARAMETERS]; + int count = cw_functional_parameters(c, &declared, c->w->f, types, CW_LAMBDA_PARAMETERS); + (void)cw_lambda_typed(c, lambda, types, count); + return true; +} + +/* Walk a call's lambdas: under the selected declaration, under a standard collection method of + * `receiver` called `name`, or with unknown parameters when neither applies. */ +static void cw_finish_lambdas(cw_t *c, cw_lambdas_t *lambdas, const oo_callable_t *callee, + const oo_subst_t *subst, const oo_value_t *receiver, + const char *name) { + if (!lambdas || lambdas->walked) + return; + lambdas->walked = true; + oo_walk_t *w = c->w; + for (int i = 0; i < lambdas->count; i++) { + if (ts_node_is_null(lambdas->nodes[i])) + continue; + oo_value_t types[CW_LAMBDA_PARAMETERS]; + int count = -1; + if (callee && callee->params && callee->param_count) { + int index = i < callee->param_count ? i : callee->param_count - 1; + if (callee->params[index]) { + oo_scope_t scope = oo_scope_of_callable(callee); + oo_value_t function = oo_value_from_ref(w, callee->params[index], &scope, subst); + count = cw_functional_parameters(c, &function, callee->file, types, + CW_LAMBDA_PARAMETERS); + } + } else if (receiver && name) { + count = oo_stdlib_lambda_parameters(w, receiver, name, i, lambdas->count, types, + CW_LAMBDA_PARAMETERS); + } + lambdas->last = cw_lambda_typed(c, lambdas->nodes[i], types, count); + } +} + +static bool cw_word_in(const char *leaf, const char *const *names) { + size_t length = strcspn(leaf, "`"); + for (int i = 0; names[i]; i++) + if (strlen(names[i]) == length && !strncmp(leaf, names[i], length)) + return true; + return false; +} + +static const char *const CW_SEQUENCE_INTERFACES[] = { + "IEnumerable", "ICollection", "IReadOnlyCollection", "IList", "IReadOnlyList", NULL}; + +/* A BCL collection reaches the sequence interfaces it implements; an array reaches them all + * (C# 17.1.2). */ +static bool cw_external_accepts(const char *declared, const oo_value_t *value) { const char *a = strrchr(declared, '.'); a = a ? a + 1 : declared; + if (value->dims > 0) + return cw_word_in(a, CW_SEQUENCE_INTERFACES); + if (!value->external) + return false; const char *b = strrchr(value->external, '.'); b = b ? b + 1 : value->external; - return !strcmp(a, b); + if (!strcmp(a, b)) + return true; + static const char *const collections[] = {"List", + "HashSet", + "SortedSet", + "LinkedList", + "Queue", + "Stack", + "ImmutableArray", + "ImmutableList", + "ObservableCollection", + "Collection", + "ReadOnlyCollection", + NULL}; + return cw_word_in(a, CW_SEQUENCE_INTERFACES) && cw_word_in(b, collections); +} + +/* A project type that lists this outside interface among its supertypes, at any depth. */ +static bool cw_type_implements_external(const oo_type_t *type, const char *declared) { + enum { MAX_STEPS = 64 }; + const oo_type_t *queue[MAX_STEPS]; + int head = 0, tail = 0; + const char *wanted = strrchr(declared, '.'); + wanted = wanted ? wanted + 1 : declared; + queue[tail++] = type; + while (head < tail) { + const oo_type_t *current = queue[head++]; + for (int i = 0; i < current->super_count; i++) { + const oo_super_t *super = ¤t->supers[i]; + if (super->resolved) { + bool seen = false; + for (int j = 0; j < tail && !seen; j++) + seen = queue[j] == super->resolved; + if (!seen && tail < MAX_STEPS) + queue[tail++] = super->resolved; + continue; + } + const char *spelled = super->ref ? super->ref->name : NULL; + const char *leaf = spelled ? strrchr(spelled, '.') : NULL; + leaf = leaf ? leaf + 1 : spelled; + if (leaf && !strcmp(leaf, wanted)) + return true; + } + } + return false; } static bool cw_extension_accepts(oo_walk_t *w, const oo_callable_t *candidate, @@ -764,10 +1129,16 @@ static bool cw_extension_accepts(oo_walk_t *w, const oo_callable_t *candidate, return bound.type && value->type && oo_is_subtype(w->ix, value->type, bound.type); } oo_value_t receiver = oo_value_from_ref(w, ref, &scope, NULL); + /* An array reaches the sequence interfaces, here or in a core library. */ + if (value->dims > receiver.dims && !receiver.dims) + return receiver.type ? cw_word_in(receiver.type->name, CW_SEQUENCE_INTERFACES) + : receiver.external && cw_external_accepts(receiver.external, value); if (receiver.dims != value->dims) return false; if (receiver.type) return value->type && oo_is_subtype(w->ix, value->type, receiver.type); + if (receiver.external && value->type) + return cw_type_implements_external(value->type, receiver.external); return receiver.external && cw_external_accepts(receiver.external, value); } @@ -825,38 +1196,153 @@ static oo_select_t cw_select_extension(cw_t *c, const oo_value_t *value, const c } static oo_value_t cw_emit_selected(cw_t *c, TSNode site, const oo_select_t *selected, - const char *decision) { + const char *decision, const oo_value_t *receiver, int argc, + const oo_value_t *args) { oo_walk_t *w = c->w; + const oo_value_t *spelled = c->type_args; + int spelled_count = c->type_arg_count; + c->type_args = NULL; + c->type_arg_count = 0; if (selected->status != OO_SEL_UNIQUE || !selected->callable) return oo_value_unknown(); if (selected->callable->graph_qn) oo_emit_call(w, site, selected->callable->graph_qn, CBM_RESOLVE_RULE_TYPE_MEMBER, CBM_RESOLVE_FACT_RECEIVER_TYPED, decision); - return oo_callable_return(w, selected->callable, selected->subst); + const oo_subst_t *subst = + oo_infer_call_subst(w, selected->callable, selected->subst, receiver, args, argc); + oo_subst_t *given = spelled && spelled_count == selected->callable->tparam_count + ? cbm_arena_alloc(w->arena, sizeof(*given)) + : NULL; + if (given) { + *given = (oo_subst_t){.tparams = selected->callable->tparams, + .count = spelled_count, + .args = spelled, + .arg_count = spelled_count, + .next = selected->subst}; + subst = given; + } + cw_finish_lambdas(c, c->lambdas, selected->callable, subst, NULL, NULL); + return oo_callable_return(w, selected->callable, subst); +} + +/* The type arguments a generic call spells, read once its arguments are walked. */ +static void cw_spell_type_args(cw_t *c, TSNode name) { + c->type_args = NULL; + c->type_arg_count = 0; + if (!cs_is(name, "generic_name")) + return; + TSNode list = cs_child(name, "type_argument_list"); + int count = cs_type_args_count(list); + oo_value_t *values = + count ? cbm_arena_calloc(c->w->arena, (size_t)count * sizeof(*values)) : NULL; + if (!values) + return; + for (int i = 0; i < count; i++) + values[i] = cw_ref(c, cs_typeref(&c->text, ts_node_named_child(list, (uint32_t)i))); + c->type_args = values; + c->type_arg_count = count; +} + +/* C# 8.2: a keyword type is an alias of its System type. A project that declares that type (a core + * library) has it as a receiver; everywhere else it stays external. */ +static oo_value_t cw_keyword_receiver(const cw_t *c, const oo_value_t *value) { + static const struct { + const char *keyword, *type; + } ALIASES[] = { + {"string", "System.String"}, {"object", "System.Object"}, {"int", "System.Int32"}, + {"uint", "System.UInt32"}, {"long", "System.Int64"}, {"ulong", "System.UInt64"}, + {"short", "System.Int16"}, {"ushort", "System.UInt16"}, {"byte", "System.Byte"}, + {"sbyte", "System.SByte"}, {"bool", "System.Boolean"}, {"char", "System.Char"}, + {"float", "System.Single"}, {"double", "System.Double"}, {"decimal", "System.Decimal"}, + {"nint", "System.IntPtr"}, {"nuint", "System.UIntPtr"}}; + oo_value_t out = oo_value_unknown(); + if (value->dims > 0 && !value->classref) { + /* C# 17.2.2: every array type derives from System.Array. */ + const oo_type_t *array = oo_lookup_visible(c->w->ix, "System.Array", c->w->f); + return array && !(array->flags & OO_F_AMBIGUOUS) ? oo_value_of_type(array) : out; + } + if (!value->external || value->dims) + return out; + for (size_t i = 0; i < sizeof(ALIASES) / sizeof(ALIASES[0]); i++) { + if (strcmp(ALIASES[i].keyword, value->external)) + continue; + const oo_type_t *type = oo_lookup_type(c->w->ix, ALIASES[i].type); + if (type && !(type->flags & OO_F_AMBIGUOUS)) { + out = oo_value_of_type(type); + out.classref = value->classref; + } + break; + } + return out; +} + +/* C# 8.2.3: object's members are every class's, and ValueType's and Enum's stand in front of them + * for a struct and an enum. A complete hierarchy that found nothing means them, where the project + * declares them all. */ +static oo_select_t cw_select_object_member(cw_t *c, const oo_type_t *from, const char *name, + int argc, const oo_value_t *args) { + static const char *const CHAINS[][4] = { + {"System.Object", NULL, NULL, NULL}, + {"System.ValueType", "System.Object", NULL, NULL}, + {"System.Enum", "System.ValueType", "System.Object", NULL}}; + oo_select_t selected = {.status = OO_SEL_NONE}; + if (!from) + return selected; + int kind = (from->flags & OO_F_ENUM) ? 2 : (from->flags & OO_F_VALUE) ? 1 : 0; + for (const char *const *root = CHAINS[kind]; *root; root++) { + const oo_type_t *type = oo_lookup_visible(c->w->ix, *root, c->w->f); + if (!type || (type->flags & OO_F_AMBIGUOUS)) + return (oo_select_t){.status = OO_SEL_NONE}; + } + for (const char *const *root = CHAINS[kind]; *root; root++) { + const oo_type_t *type = oo_lookup_visible(c->w->ix, *root, c->w->f); + if (type == from) + return (oo_select_t){.status = OO_SEL_NONE}; + oo_value_t base = oo_value_of_type(type); + selected = oo_select_method(c->w, &base, name, argc, args, false); + if (selected.status != OO_SEL_NONE) + return selected; + } + return selected; } static oo_value_t cw_member_call(cw_t *c, TSNode site, const oo_value_t *receiver, const char *name, int argc, const oo_value_t *args) { oo_walk_t *w = c->w; - if (receiver->type) { - oo_select_t member = oo_select_method(w, receiver, name, argc, args, receiver->classref); + oo_value_t keyword = cw_keyword_receiver(c, receiver); + const oo_value_t *lookup = keyword.type ? &keyword : receiver; + if (lookup->type) { + oo_select_t member = oo_select_method(w, lookup, name, argc, args, lookup->classref); + if (member.status == OO_SEL_NONE && !lookup->classref && !lookup->dims) + member = cw_select_object_member(c, lookup->type, name, argc, args); /* C# 12.8.7: a static member is not reached through an instance. */ - if (member.status == OO_SEL_UNIQUE && !receiver->classref && + if (member.status == OO_SEL_UNIQUE && !lookup->classref && (member.callable->flags & OO_F_STATIC)) return oo_value_unknown(); if (member.status == OO_SEL_UNIQUE) return cw_emit_selected(c, site, &member, - receiver->classref ? "csharp-static-member" - : "csharp-declared-receiver-member"); + lookup->classref ? "csharp-static-member" + : "csharp-declared-receiver-member", + NULL, argc, args); if (member.status == OO_SEL_AMBIGUOUS) return oo_value_unknown(); } if (receiver->classref || (!receiver->type && !receiver->external)) return oo_value_unknown(); oo_select_t extension = cw_select_extension(c, receiver, name, argc, args); - if (extension.status != OO_SEL_UNIQUE) + if (extension.status != OO_SEL_UNIQUE) { + /* A standard collection method from outside the project. */ + cw_finish_lambdas(c, c->lambdas, NULL, NULL, receiver, name); + if (!strcmp(name, "Select") && receiver->external && c->lambdas && + oo_value_known(&c->lambdas->last)) + return oo_external_with_element(w, receiver, c->lambdas->last); + if (!strcmp(name, "SelectMany") && receiver->external && c->lambdas) { + oo_value_t held = oo_element_of(w, &c->lambdas->last); + return oo_external_with_element(w, receiver, held); + } return oo_external_member_return(w, receiver, name); - return cw_emit_selected(c, site, &extension, "csharp-extension-method"); + } + return cw_emit_selected(c, site, &extension, "csharp-extension-method", receiver, argc, args); } static oo_value_t cw_bare_call(cw_t *c, TSNode site, const char *name, int argc, @@ -880,7 +1366,8 @@ static oo_value_t cw_bare_call(cw_t *c, TSNode site, const char *name, int argc, if (c->static_method && selected.status == OO_SEL_UNIQUE && !(selected.callable->flags & OO_F_STATIC)) return oo_value_unknown(); - return cw_emit_selected(c, site, &selected, "csharp-enclosing-type-member"); + return cw_emit_selected(c, site, &selected, "csharp-enclosing-type-member", NULL, argc, + args); } /* A delegate-typed member of this name is the member lookup's answer. */ oo_subst_t *field_subst = NULL; @@ -890,6 +1377,12 @@ static oo_value_t cw_bare_call(cw_t *c, TSNode site, const char *name, int argc, if (!oo_hierarchy_complete(w->ix, r->value.type)) complete = false; } + if (complete && w->receiver) { + oo_select_t object = cw_select_object_member(c, w->receiver->value.type, name, argc, args); + if (object.status == OO_SEL_UNIQUE && + (!c->static_method || (object.callable->flags & OO_F_STATIC))) + return cw_emit_selected(c, site, &object, "csharp-object-member", NULL, argc, args); + } if (!complete || oo_closed_member(w, name)) return oo_value_unknown(); /* `using static`: every imported type's members compete. */ @@ -914,7 +1407,7 @@ static oo_value_t cw_bare_call(cw_t *c, TSNode site, const char *name, int argc, return oo_value_unknown(); oo_value_t receiver = oo_value_of_type(found); oo_select_t selected = oo_select_method(w, &receiver, name, argc, args, true); - return cw_emit_selected(c, site, &selected, "csharp-using-static"); + return cw_emit_selected(c, site, &selected, "csharp-using-static", NULL, argc, args); } static const char *cw_generic_or_name(cw_t *c, TSNode node) { @@ -955,10 +1448,10 @@ static oo_value_t cw_base_call(cw_t *c, TSNode site, const char *name, int argc, return oo_value_unknown(); oo_value_t receiver = oo_value_of_type(only); oo_select_t selected = oo_select_method(w, &receiver, name, argc, args, false); - return cw_emit_selected(c, site, &selected, "csharp-base-member"); + return cw_emit_selected(c, site, &selected, "csharp-base-member", NULL, argc, args); } -static oo_value_t cw_invocation(cw_t *c, TSNode node) { +static oo_value_t cw_invocation_with(cw_t *c, TSNode node, cw_lambdas_t *lambdas) { TSNode function = cs_field(node, "function"); TSNode arguments = cs_field(node, "arguments"); int argc = 0; @@ -967,37 +1460,59 @@ static oo_value_t cw_invocation(cw_t *c, TSNode node) { /* nameof yields a string; a method it names is mentioned, not taken. */ bool saved_nameof = c->in_nameof; c->in_nameof = saved_nameof || (name && !strcmp(name, "nameof")); - const oo_value_t *args = cw_arguments(c, arguments, &argc); + const oo_value_t *args = cw_arguments(c, arguments, &argc, lambdas); + c->lambdas = lambdas; bool was_nameof = c->in_nameof && !saved_nameof; c->in_nameof = saved_nameof; if (was_nameof) return oo_value_external("string"); - return name ? cw_bare_call(c, node, name, argc, args) : oo_value_unknown(); + cw_spell_type_args(c, function); + oo_value_t result = name ? cw_bare_call(c, node, name, argc, args) : oo_value_unknown(); + c->type_args = NULL; + c->type_arg_count = 0; + return result; } if (cs_is(function, "member_access_expression")) { TSNode object = cs_field(function, "expression"); const char *name = cw_generic_or_name(c, cs_field(function, "name")); if (cs_is(object, "base")) { - const oo_value_t *args = cw_arguments(c, arguments, &argc); + const oo_value_t *args = cw_arguments(c, arguments, &argc, lambdas); + c->lambdas = lambdas; return name ? cw_base_call(c, node, name, argc, args) : oo_value_unknown(); } oo_value_t receiver = cw_expr(c, object); - const oo_value_t *args = cw_arguments(c, arguments, &argc); - return name ? cw_member_call(c, node, &receiver, name, argc, args) : oo_value_unknown(); + const oo_value_t *args = cw_arguments(c, arguments, &argc, lambdas); + c->lambdas = lambdas; + cw_spell_type_args(c, cs_field(function, "name")); + oo_value_t result = + name ? cw_member_call(c, node, &receiver, name, argc, args) : oo_value_unknown(); + c->type_args = NULL; + c->type_arg_count = 0; + return result; } if (cs_is(function, "conditional_access_expression")) { oo_value_t receiver = cw_expr(c, cs_field(function, "condition")); receiver.classref = false; TSNode binding = cs_child(function, "member_binding_expression"); const char *name = cw_generic_or_name(c, cs_field(binding, "name")); - const oo_value_t *args = cw_arguments(c, arguments, &argc); + const oo_value_t *args = cw_arguments(c, arguments, &argc, lambdas); + c->lambdas = lambdas; return name ? cw_member_call(c, node, &receiver, name, argc, args) : oo_value_unknown(); } (void)cw_expr(c, function); - (void)cw_arguments(c, arguments, &argc); + (void)cw_arguments(c, arguments, &argc, lambdas); return oo_value_unknown(); } +static oo_value_t cw_invocation(cw_t *c, TSNode node) { + cw_lambdas_t lambdas = {0}; + cw_lambdas_t *outer = c->lambdas; + oo_value_t result = cw_invocation_with(c, node, &lambdas); + cw_finish_lambdas(c, &lambdas, NULL, NULL, NULL, NULL); + c->lambdas = outer; + return result; +} + static void cw_emit_field(cw_t *c, TSNode site, const oo_field_t *field) { if (!field->graph_qn) return; @@ -1056,7 +1571,7 @@ static oo_value_t cw_identifier(cw_t *c, TSNode node) { oo_value_t value = cw_ref(c, &ref); if (value.type || value.external) value.classref = true; - if (value.type && value.type->graph_qn) + if (value.type && value.type->graph_qn && !oo_names_tparam(w, name)) oo_emit_usage(w, node, value.type->graph_qn, CBM_RESOLVED_TYPE_REFERENCE, CBM_RESOLVE_RULE_DECLARED_TYPE, "csharp-type-name-lookup"); return value; @@ -1068,7 +1583,10 @@ static oo_value_t cw_member_access(cw_t *c, TSNode node) { TSNode name_node = cs_field(node, "name"); const char *name = cw_generic_or_name(c, name_node); /* A dotted chain can be a namespace-qualified type. */ + static const char ROOT[] = "global::"; const char *spelled = cs_text(&c->text, node); + if (spelled && !strncmp(spelled, ROOT, sizeof(ROOT) - 1)) + spelled += sizeof(ROOT) - 1; if (spelled && strspn(spelled, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.") == strlen(spelled)) { @@ -1085,9 +1603,14 @@ static oo_value_t cw_member_access(cw_t *c, TSNode node) { oo_typeref_t ref = {.name = spelled}; oo_name_t type_name = oo_resolve_name(w->ix, &w->scope, spelled); if (type_name.status == OO_NAME_TYPE) { - oo_value_t value = oo_value_of_type(oo_unalias(w->ix, type_name.type)); + const oo_type_t *named = oo_unalias(w->ix, type_name.type); + oo_value_t value = oo_value_of_type(named); (void)ref; value.classref = true; + (void)cw_expr(c, object); + if (named && named->graph_qn) + oo_emit_usage(w, name_node, named->graph_qn, CBM_RESOLVED_TYPE_REFERENCE, + CBM_RESOLVE_RULE_DECLARED_TYPE, "csharp-type-name-lookup"); return value; } } @@ -1100,6 +1623,12 @@ static oo_value_t cw_member_access(cw_t *c, TSNode node) { if (self.type && self.type->super_count && self.type->supers[0].resolved) receiver = oo_value_of_type(self.type->supers[0].resolved); } + oo_value_t keyword = cw_keyword_receiver(c, &receiver); + if (keyword.type) + receiver = keyword; + /* `pair.Value` on a dictionary's entry. */ + if (name && !receiver.type && receiver.external) + return oo_external_member_return(w, &receiver, name); if (!name || !receiver.type) return oo_value_unknown(); oo_subst_t *subst = NULL; @@ -1116,6 +1645,9 @@ static oo_value_t cw_member_access(cw_t *c, TSNode node) { if (nested && !(nested->flags & OO_F_AMBIGUOUS)) { oo_value_t value = oo_value_of_type(nested); value.classref = true; + if (nested->graph_qn) + oo_emit_usage(w, name_node, nested->graph_qn, CBM_RESOLVED_TYPE_REFERENCE, + CBM_RESOLVE_RULE_DECLARED_TYPE, "csharp-nested-type-name"); return value; } } @@ -1126,6 +1658,8 @@ static void cw_emit_constructor(cw_t *c, TSNode site, const oo_value_t *value, i const oo_value_t *args) { oo_walk_t *w = c->w; oo_select_t selected = oo_select_constructor(w, value, argc, args); + if (selected.status == OO_SEL_UNIQUE) + cw_finish_lambdas(c, c->lambdas, selected.callable, selected.subst, NULL, NULL); if (selected.status == OO_SEL_UNIQUE && selected.callable->graph_qn) oo_emit_call(w, site, selected.callable->graph_qn, CBM_RESOLVE_RULE_LANGUAGE_BINDING, CBM_RESOLVE_FACT_LANGUAGE_BOUND, "csharp-constructor-selection"); @@ -1145,13 +1679,16 @@ static void cw_initializer(cw_t *c, TSNode initializer, const oo_value_t *create TSNode entry = ts_node_named_child(initializer, i); if (cs_is(entry, "assignment_expression")) { TSNode left = cs_field(entry, "left"); - (void)cw_expr(c, cs_field(entry, "right")); + TSNode right = cs_field(entry, "right"); + (void)cw_expr(c, right); const char *name = cs_is(left, "identifier") ? cs_name(&c->text, left) : NULL; if (name && created->type) { oo_subst_t *subst = NULL; bool incomplete = false; const oo_field_t *field = oo_find_field(w, created, name, &subst, &incomplete); - if (field) + /* C# 12.8.17.3: `Items = { a, b }` reads Items and adds to it. It writes nothing, + * and an assignment's target has no read site to say so at. */ + if (field && !cs_is(right, "initializer_expression")) cw_emit_field(c, left, field); } } else { @@ -1165,9 +1702,14 @@ static oo_value_t cw_object_creation(cw_t *c, TSNode node) { cw_type_mentions(c, type, 0); oo_value_t value = cw_ref(c, cs_typeref(&c->text, type)); int argc = 0; - const oo_value_t *args = cw_arguments(c, cs_field(node, "arguments"), &argc); + cw_lambdas_t lambdas = {0}; + const oo_value_t *args = cw_arguments(c, cs_field(node, "arguments"), &argc, &lambdas); + cw_lambdas_t *outer = c->lambdas; + c->lambdas = &lambdas; if (value.type) cw_emit_constructor(c, node, &value, argc, args); + cw_finish_lambdas(c, &lambdas, NULL, NULL, NULL, NULL); + c->lambdas = outer; TSNode initializer = cs_field(node, "initializer"); if (ts_node_is_null(initializer)) initializer = cs_child(node, "initializer_expression"); @@ -1177,9 +1719,14 @@ static oo_value_t cw_object_creation(cw_t *c, TSNode node) { static void cw_implicit_creation(cw_t *c, TSNode node, const oo_value_t *target) { int argc = 0; - const oo_value_t *args = cw_arguments(c, cs_child(node, "argument_list"), &argc); + cw_lambdas_t lambdas = {0}; + const oo_value_t *args = cw_arguments(c, cs_child(node, "argument_list"), &argc, &lambdas); + cw_lambdas_t *outer = c->lambdas; + c->lambdas = &lambdas; if (target->type) cw_emit_constructor(c, node, target, argc, args); + cw_finish_lambdas(c, &lambdas, NULL, NULL, NULL, NULL); + c->lambdas = outer; cw_initializer(c, cs_child(node, "initializer_expression"), target); } @@ -1193,6 +1740,19 @@ static oo_value_t cw_declaration_pattern(cw_t *c, TSNode node) { return oo_value_unknown(); } +/* A local function's scope is its whole block: a call above its declaration means it too. */ +static void cw_bind_local_functions(cw_t *c, TSNode block) { + TSTreeCursor cursor = ts_tree_cursor_new(block); + if (ts_tree_cursor_goto_first_child(&cursor)) { + do { + TSNode child = ts_tree_cursor_current_node(&cursor); + if (cs_is(child, "local_function_statement")) + oo_bind(c->w, cs_name(&c->text, cs_field(child, "name")), oo_value_unknown()); + } while (ts_tree_cursor_goto_next_sibling(&cursor)); + } + ts_tree_cursor_delete(&cursor); +} + static oo_value_t cw_foreach(cw_t *c, TSNode node) { oo_walk_t *w = c->w; oo_value_t iterable = cw_expr(c, cs_field(node, "right")); @@ -1244,6 +1804,7 @@ static void cw_bind_query_names(cw_t *c, TSNode node, unsigned depth) { static oo_value_t cw_block_like(cw_t *c, TSNode node) { oo_frame_t frame; oo_frame_push(c->w, &frame); + cw_bind_local_functions(c, node); cw_children(c, node); oo_frame_pop(c->w); return oo_value_unknown(); @@ -1306,27 +1867,38 @@ static oo_value_t cw_expr_kind(cw_t *c, TSNode node, const char *kind) { } if (!strcmp(kind, "recursive_pattern") || !strcmp(kind, "positional_pattern_clause") || !strcmp(kind, "property_pattern_clause") || !strcmp(kind, "subpattern")) { - /* A trailing designation binds; a member name inside is not a value. */ + /* A trailing designation binds; a member name inside is not a value. What it binds is the + * type the pattern spells, or else the type of what it is matched against. */ + oo_value_t subject = c->pattern_subject; + c->pattern_subject = oo_value_unknown(); + bool recursive = !strcmp(kind, "recursive_pattern"); + TSNode spelled = cs_field(node, "type"); + oo_value_t bound = + ts_node_is_null(spelled) ? subject : cw_declared(c, spelled, oo_value_unknown()); uint32_t parts = ts_node_named_child_count(node); for (uint32_t i = 0; i < parts; i++) { TSNode part = ts_node_named_child(node, i); - if (cs_is(part, "identifier") && !strcmp(kind, "recursive_pattern")) - oo_bind(w, cs_name(&c->text, part), oo_value_unknown()); - else if (!cs_is(part, "identifier") && !cs_is(part, "name_colon") && - !cs_is(part, "expression_colon")) + if (!cs_is(part, "identifier") && !cs_is(part, "name_colon") && + !cs_is(part, "expression_colon") && !ts_node_eq(part, spelled)) (void)cw_expr(c, part); } + if (recursive) + oo_bind(w, cs_name(&c->text, cs_field(node, "name")), bound); return oo_value_unknown(); } if (!strcmp(kind, "declaration_expression")) { TSNode type = cs_field(node, "type"); const char *name = cs_name(&c->text, cs_field(node, "name")); - oo_bind(w, name, cw_declared(c, type, oo_value_unknown())); - return oo_value_unknown(); + oo_value_t declared = cw_declared(c, type, oo_value_unknown()); + oo_bind(w, name, declared); + return declared; } if (!strcmp(kind, "is_pattern_expression")) { - (void)cw_expr(c, cs_field(node, "expression")); + oo_value_t subject = cw_expr(c, cs_field(node, "expression")); + oo_value_t outer = c->pattern_subject; + c->pattern_subject = subject; (void)cw_expr(c, cs_field(node, "pattern")); + c->pattern_subject = outer; return oo_value_unknown(); } if (!strcmp(kind, "catch_clause")) { @@ -1398,6 +1970,10 @@ static oo_value_t cw_expr_kind(cw_t *c, TSNode node, const char *kind) { if (!strcmp(leaf, "Task`1") || !strcmp(leaf, "ValueTask`1")) return value.args[0]; } + /* A core library's own. */ + if (value.type && value.arg_count == 1 && + (!strcmp(value.type->name, "Task`1") || !strcmp(value.type->name, "ValueTask`1"))) + return value.args[0]; return oo_value_unknown(); } if (!strcmp(kind, "conditional_expression")) { @@ -1418,6 +1994,11 @@ static oo_value_t cw_expr_kind(cw_t *c, TSNode node, const char *kind) { const char *op = cs_text(&c->text, cs_field(node, "operator")); if (op && !strcmp(op, "??")) return left.type && (left.type == right.type || !oo_value_known(&right)) ? left : right; + /* C# 12.12.5: a string on either side of `+` makes the result a string. */ + if (op && !strcmp(op, "+") && + ((left.external && !strcmp(left.external, "string")) || + (right.external && !strcmp(right.external, "string")))) + return oo_value_external("string"); return oo_value_unknown(); } if (!strcmp(kind, "assignment_expression")) { @@ -1427,12 +2008,20 @@ static oo_value_t cw_expr_kind(cw_t *c, TSNode node, const char *kind) { } if (!strcmp(kind, "element_access_expression")) { oo_value_t array = cw_expr(c, cs_field(node, "expression")); - (void)cw_expr(c, cs_field(node, "subscript")); + int argc = 0; + const oo_value_t *args = cw_arguments(c, cs_field(node, "subscript"), &argc, NULL); if (array.dims > 0) { array.dims--; array.classref = false; return array; } + /* A project type's indexer says what `x[i]` is. */ + if (array.type && !array.classref) { + oo_select_t indexer = oo_select_method(w, &array, CW_INDEXER, argc, args, false); + return indexer.status == OO_SEL_UNIQUE + ? oo_callable_return(w, indexer.callable, indexer.subst) + : oo_value_unknown(); + } /* An indexer of a known collection reads its element. */ return oo_external_member_return(w, &array, "get"); } @@ -1461,10 +2050,20 @@ static oo_value_t cw_expr_kind(cw_t *c, TSNode node, const char *kind) { (void)cw_expr(c, cs_child(node, "initializer_expression")); return cw_ref(c, cs_typeref(&c->text, type)); } - if (!strcmp(kind, "typeof_expression") || !strcmp(kind, "sizeof_expression") || - !strcmp(kind, "default_expression")) { + if (!strcmp(kind, "typeof_expression")) { cw_type_mentions(c, cs_field(node, "type"), 0); - return oo_value_unknown(); + const oo_type_t *type = oo_lookup_visible(w->ix, "System.Type", w->f); + return type && !(type->flags & OO_F_AMBIGUOUS) ? oo_value_of_type(type) + : oo_value_unknown(); + } + if (!strcmp(kind, "default_expression")) { + TSNode type = cs_field(node, "type"); + cw_type_mentions(c, type, 0); + return cw_ref(c, cs_typeref(&c->text, type)); + } + if (!strcmp(kind, "sizeof_expression")) { + cw_type_mentions(c, cs_field(node, "type"), 0); + return oo_value_external("int"); } if (!strcmp(kind, "string_literal") || !strcmp(kind, "verbatim_string_literal") || !strcmp(kind, "raw_string_literal") || !strcmp(kind, "interpolated_string_expression")) { @@ -1474,20 +2073,33 @@ static oo_value_t cw_expr_kind(cw_t *c, TSNode node, const char *kind) { } if (!strcmp(kind, "integer_literal")) { /* The suffix decides: 1L is a long, and does not reach an int parameter. */ - const char *digits = ts_node_start_byte(node) < ts_node_end_byte(node) - ? c->w->source + ts_node_end_byte(node) - 1 - : ""; - return oo_value_external(*digits == 'l' || *digits == 'L' ? "long" : "int"); + uint32_t start = ts_node_start_byte(node), end = ts_node_end_byte(node); + char last = end > start ? (char)tolower((unsigned char)c->w->source[end - 1]) : 0; + char before = end > start + 1 ? (char)tolower((unsigned char)c->w->source[end - 2]) : 0; + if ((last == 'l' && before == 'u') || (last == 'u' && before == 'l')) + return oo_value_external("ulong"); + return oo_value_external(last == 'l' ? "long" : last == 'u' ? "uint" : "int"); } if (!strcmp(kind, "boolean_literal")) return oo_value_external("bool"); - if (!strcmp(kind, "real_literal")) - return oo_value_external("double"); + if (!strcmp(kind, "real_literal")) { + char last = (char)tolower((unsigned char)c->w->source[ts_node_end_byte(node) - 1]); + return oo_value_external(last == 'f' ? "float" : last == 'm' ? "decimal" : "double"); + } if (!strcmp(kind, "character_literal")) return oo_value_external("char"); - if (!strcmp(kind, "null_literal") || !strcmp(kind, "comment") || - !strcmp(kind, "attribute_list") || !strcmp(kind, "discard") || - !strcmp(kind, "predefined_type")) + if (!strcmp(kind, "predefined_type")) { + const char *keyword = cs_text(&c->text, node); + oo_value_t value = keyword ? oo_value_external(keyword) : oo_value_unknown(); + value.classref = keyword != NULL; + return value; + } + if (!strcmp(kind, "null_literal")) { + oo_value_t nothing = oo_value_unknown(); + nothing.null_literal = true; + return nothing; + } + if (!strcmp(kind, "comment") || !strcmp(kind, "attribute_list") || !strcmp(kind, "discard")) return oo_value_unknown(); /* A generic name in expression position: a type used as a receiver. */ if (!strcmp(kind, "generic_name") || !strcmp(kind, "qualified_name")) { @@ -1549,6 +2161,10 @@ static void cw_members(cw_t *c, TSNode body, const oo_type_t *type) { for (uint32_t i = 0; i < count; i++) { TSNode member = ts_node_named_child(body, i); const char *kind = ts_node_type(member); + if (strcmp(kind, "class_declaration") && strcmp(kind, "struct_declaration") && + strcmp(kind, "interface_declaration") && strcmp(kind, "enum_declaration") && + strcmp(kind, "record_declaration") && strcmp(kind, "record_struct_declaration")) + cw_attribute_lists(c, member); if (!strcmp(kind, "method_declaration") || !strcmp(kind, "constructor_declaration") || !strcmp(kind, "destructor_declaration") || !strcmp(kind, "operator_declaration") || !strcmp(kind, "conversion_operator_declaration")) { @@ -1585,7 +2201,7 @@ static void cw_members(cw_t *c, TSNode body, const oo_type_t *type) { continue; if (cs_is(piece, "implicit_object_creation_expression")) cw_implicit_creation(c, piece, &declared); - else + else if (!cw_lambda_to_declared(c, piece, field_type)) (void)cw_expr(c, piece); } } @@ -1593,6 +2209,7 @@ static void cw_members(cw_t *c, TSNode body, const oo_type_t *type) { !strcmp(kind, "interface_declaration") || !strcmp(kind, "enum_declaration") || !strcmp(kind, "record_declaration") || !strcmp(kind, "record_struct_declaration")) { + /* cw_type reads the attributes of a type. */ cw_type(c, member, type); } else if (!strcmp(kind, "enum_member_declaration")) { uint32_t parts = ts_node_named_child_count(member); @@ -1614,7 +2231,9 @@ static void cw_type(cw_t *c, TSNode node, const oo_type_t *owner) { const char *identity = cs_arity_name(w->arena, name, arity); const char *qn = owner ? cs_join(w->arena, owner->qn, identity) : cs_join(w->arena, w->scope.ns, identity); - const oo_type_t *type = qn ? oo_lookup_type(w->ix, qn) : NULL; + const oo_type_t *type = qn ? oo_lookup_declared(w->ix, qn, w->rel_path) : NULL; + /* A type's attributes read the scope around it. */ + cw_attribute_lists(c, node); oo_scope_t saved = w->scope; if (type) w->scope.owner = type; @@ -1633,7 +2252,7 @@ static void cw_type(cw_t *c, TSNode node, const oo_type_t *owner) { TSNode base = ts_node_named_child(bases, i); if (cs_is(base, "argument_list")) { int argc = 0; - (void)cw_arguments(c, base, &argc); + (void)cw_arguments(c, base, &argc, NULL); } else { cw_type_mentions(c, base, 0); } diff --git a/internal/cbm/lsp/oo_internal.h b/internal/cbm/lsp/oo_internal.h index a3bc02c7b..d4b5cc410 100644 --- a/internal/cbm/lsp/oo_internal.h +++ b/internal/cbm/lsp/oo_internal.h @@ -25,7 +25,11 @@ enum { OO_F_COMPANION = 1u << 11, OO_F_INCOMPLETE = 1u << 12, /* Syntax errors hid part of this declaration. */ OO_F_PROPERTY_GET = 1u << 13, - OO_F_INNER = 1u << 14 + OO_F_INNER = 1u << 14, + OO_F_DELEGATE = 1u << 15, /* C#: its Invoke is the type's one callable shape. */ + OO_F_CONVERTS = 1u << 16, /* C#: declares a user-defined implicit conversion. */ + OO_F_VALUE = 1u << 18, /* C#: a struct. Its object members come through ValueType. */ + OO_F_DATA = 1u << 17 /* Kotlin data class: its primary constructor gives componentN. */ }; typedef struct oo_typeref { @@ -35,7 +39,9 @@ typedef struct oo_typeref { bool function; /* Kotlin function type: args are its parameters. */ const struct oo_typeref *fn_receiver; const struct oo_typeref *fn_return; - bool by_ref; /* C# `ref` or `out` parameter. */ + bool by_ref; /* C# `ref` or `out` parameter. */ + bool ref_optional; /* C# `in` or `ref readonly`: takes `ref` and a plain argument. */ + const struct oo_typeref *lower; /* Java `? super X`: reads as nothing, takes an X. */ } oo_typeref_t; typedef struct { @@ -56,6 +62,9 @@ typedef struct oo_file { oo_import_t *imports; int import_count; bool has_error; + bool reference; /* C#: a reference assembly's source. */ + bool test_source; /* Its project's tests: nothing outside that project reads them. */ + const char *project; /* The build project this file compiles in, or NULL. */ } oo_file_t; typedef struct oo_type oo_type_t; @@ -70,8 +79,9 @@ typedef struct { struct oo_type { const char *qn, *name; - const char *graph_qn; /* NULL when no single declaration node represents it. */ + const char *graph_qn; /* A partial type's node for the whole type; see oo_index_type. */ int graph_count; + const char *graph_rel; /* The file of graph_qn. */ const oo_file_t *file; const char *ns; const oo_type_t *owner; @@ -84,6 +94,13 @@ struct oo_type { const oo_typeref_t *alias_of; const oo_type_t *companion; /* Kotlin companion object. */ oo_type_t *next_in_index; + const struct oo_callable *abstract_method; /* The last one declared here, for a lambda. */ + int abstract_count; + /* A name several declarations claim. The index holds one OO_F_AMBIGUOUS entry for the name; + * `variants` lists the declarations, each with its own members. A variant's qn is unique. */ + const oo_type_t *variants; + const oo_type_t *next_variant; + const struct oo_callable *primary; /* Kotlin primary constructor. */ }; typedef struct oo_exprsum oo_exprsum_t; @@ -96,6 +113,7 @@ typedef struct oo_callable { int min_args, max_args; /* max_args -1: variadic. */ int vararg_position; /* Kotlin: 1 + the vararg's index; it need not be last. */ const oo_typeref_t **params; + const char **param_names; /* Kotlin: for a named argument. */ int param_count; const oo_typeref_t *ret; const oo_exprsum_t *ret_expr; /* Kotlin expression body without an annotation. */ @@ -147,6 +165,11 @@ struct cbm_oo_index { CBMHashTable *ext_static; /* C#: ns \x1f name -> extension methods */ oo_type_t *all_types; bool sealed, failed; + bool reference_files; /* C#: the files being added are reference assemblies. */ + const char *project; /* The build project of the files being added. */ + CBMHashTable *files; /* rel -> oo_file_t, for a file with a project. */ + bool has_variants; + int variant_serial; /* Keys each later declaration of a several-claimed name. */ }; /* Members an anonymous object declares itself: they have no index entry and @@ -154,6 +177,7 @@ struct cbm_oo_index { typedef struct { const char **names; int count; + struct oo_value *values; /* A property's type as its declaration gave it; unknown otherwise. */ } oo_hidden_t; typedef struct oo_value { @@ -163,14 +187,17 @@ typedef struct oo_value { int arg_count; int dims; bool classref; /* Denotes the type itself rather than an instance. */ + bool null_literal; /* C#: `null`, which reaches any reference parameter. */ bool function_literal; /* An argument spelled as a lambda: its own type is not read. */ bool small_literal; /* An integer literal: it fits a narrower or wider parameter. */ bool by_ref; /* C#: passed with `ref` or `out`. */ + int lambda_parameters; /* A function literal's parameter count plus one; 0 when not known. */ const oo_hidden_t *hidden; /* Kotlin function value. */ const struct oo_value *fn_receiver; const struct oo_value *fn_return; const oo_typeref_t *fn_ref; + const struct oo_value *lower; /* `? super X`: unknown as a value, X as a lambda parameter. */ } oo_value_t; typedef enum { @@ -238,8 +265,11 @@ typedef struct oo_receiver { oo_value_t value; bool unknown; /* A receiver exists whose member set cannot be listed. */ bool anonymous; /* Own members have no index entry. */ + bool object_super; /* A supertype of the anonymous object being walked. */ const char **own_names; /* Anonymous body member names. */ int own_count; + const bool *own_function; /* Kotlin: each own name is a function, not a property. */ + const struct oo_value *own_values; /* Kotlin: what each own property holds, once walked. */ bool static_context; bool extension; struct oo_receiver *parent; @@ -285,7 +315,17 @@ void oo_index_function(cbm_oo_index_t *ix, const char *ns, oo_callable_t *callab void oo_index_prop(cbm_oo_index_t *ix, const char *ns, oo_field_t *field); void oo_index_ext_static(cbm_oo_index_t *ix, const char *ns, oo_callable_t *callable); +/* A reference assembly declares this type's name, and an implementation has it: + * the declaration in `file` adds nothing to the type. */ +bool oo_type_is_implemented_elsewhere(const oo_type_t *type, const oo_file_t *file); + const oo_type_t *oo_lookup_type(const cbm_oo_index_t *ix, const char *qn); +/* The declaration this file is lexically inside of, when `qn` has several. */ +const oo_type_t *oo_lookup_declared(const cbm_oo_index_t *ix, const char *qn, const char *rel); +/* The type a dotted name means from this file: one declaration of a name several claim is meant + * when it is in the file itself, or is the only one in the file's project. Otherwise the entry + * comes back with OO_F_AMBIGUOUS. */ +const oo_type_t *oo_lookup_visible(const cbm_oo_index_t *ix, const char *qn, const oo_file_t *file); const oo_callable_t *oo_lookup_methods(const cbm_oo_index_t *ix, const char *qn, const char *name); const oo_field_t *oo_lookup_fields(const cbm_oo_index_t *ix, const char *qn, const char *name); const oo_callable_t *oo_lookup_functions(const cbm_oo_index_t *ix, const char *ns, @@ -331,6 +371,21 @@ bool oo_names_tparam(const oo_walk_t *w, const char *name); oo_value_t oo_element_of(oo_walk_t *w, const oo_value_t *value); /* The declared result of a standard collection accessor, or unknown. */ oo_value_t oo_external_member_return(oo_walk_t *w, const oo_value_t *value, const char *name); +/* The parameters a lambda takes for a standard collection method on a known element type: how many, + * or -1 when the method is not known to take one here. `position` is the lambda's argument index. + */ +int oo_stdlib_lambda_parameters(oo_walk_t *w, const oo_value_t *receiver, const char *name, + int position, int argc, oo_value_t *out, int capacity); +/* The same collection with these element types: `map` on a stream or an optional. */ +oo_value_t oo_external_with_element(oo_walk_t *w, const oo_value_t *value, oo_value_t element); +/* `Optional.of(x)`, `List.of(a, b)`, `Arrays.stream(items)`: what a JDK factory holds is spelled by + * its arguments. Arguments that disagree settle nothing: that is a least upper bound. */ +oo_value_t oo_jdk_factory_result(oo_walk_t *w, const oo_value_t *receiver, const char *name, + const oo_value_t *args, int argc); +/* A lambda's parameters when the target is a project interface with one abstract method, or a C# + * delegate: how many, or -1 when it has no such single shape. */ +int oo_functional_parameters(oo_walk_t *w, const oo_value_t *function, oo_value_t *out, + int capacity); /* Follows a type's aliases to the declared type. */ const oo_type_t *oo_unalias(const cbm_oo_index_t *ix, const oo_type_t *type); @@ -359,6 +414,15 @@ oo_select_t oo_select_from_list(oo_walk_t *w, const oo_callable_t *list, int arg const oo_value_t *args, const oo_value_t *receiver); bool oo_callable_visible(const oo_walk_t *w, const oo_callable_t *callable); oo_value_t oo_callable_return(oo_walk_t *w, const oo_callable_t *callable, const oo_subst_t *subst); +/* `subst` plus the callee's own type parameters that one argument (or the receiver of an + * extension) settles. Two arguments that disagree settle nothing: that is a least upper bound. */ +/* `ref` names one of the callee's type parameters that nothing settled: its value is only a bound, + * which says what the type at least has, not what it lacks. */ +bool oo_ref_unsettled(const oo_callable_t *callee, const oo_typeref_t *ref, + const oo_subst_t *subst); +const oo_subst_t *oo_infer_call_subst(oo_walk_t *w, const oo_callable_t *callee, + const oo_subst_t *subst, const oo_value_t *receiver, + const oo_value_t *args, int argc); /* Walk state. */ bool oo_walk_init(oo_walk_t *w, const cbm_oo_index_t *ix, CBMArena *arena, const char *source, diff --git a/internal/cbm/lsp/oo_java.c b/internal/cbm/lsp/oo_java.c index 2594cc8fe..fcd36fefc 100644 --- a/internal/cbm/lsp/oo_java.c +++ b/internal/cbm/lsp/oo_java.c @@ -113,10 +113,11 @@ static const oo_typeref_t *j_typeref(const j_text_t *t, TSNode node) { } else if (!strcmp(kind, "wildcard")) { const char *text = j_text(t, node); /* `? extends X` reads as X; `? super X` and `?` read as nothing. */ - if (text && strstr(text, "extends")) { - uint32_t count = ts_node_named_child_count(node); - return count ? j_typeref(t, ts_node_named_child(node, count - 1)) : ref; - } + uint32_t count = ts_node_named_child_count(node); + if (text && count && strstr(text, "extends")) + return j_typeref(t, ts_node_named_child(node, count - 1)); + if (text && count && strstr(text, "super")) + ref->lower = j_typeref(t, ts_node_named_child(node, count - 1)); } return ref; } @@ -411,6 +412,12 @@ static void jh_method(jh_t *h, oo_type_t *type, TSNode node, bool is_interface) callable->tparams = j_tparams(&h->text, j_field(node, "type_parameters"), &callable->tparam_count); oo_index_method(h->ix, type, callable); + /* JLS 9.8: Object's public methods do not count. */ + if (is_interface && !ctor && ts_node_is_null(j_field(node, "body")) && strcmp(name, "equals") && + strcmp(name, "hashCode") && strcmp(name, "toString")) { + type->abstract_method = callable; + type->abstract_count++; + } } static bool j_type_decl(TSNode node) { @@ -567,11 +574,22 @@ bool oo_java_harvest(cbm_oo_index_t *ix, const char *rel, const CBMFileResult *f /* ── Walk ───────────────────────────────────────────────────────────── */ +/* One call's lambda arguments. Their parameter types come from the declaration the call selects, + * so they are walked after selection, once each. */ +typedef struct { + TSNode *nodes; /* By argument position; a null node is no lambda. */ + int count; + bool walked; + oo_value_t last; /* The last lambda's expression body. */ +} jw_lambdas_t; + typedef struct { oo_walk_t *w; j_text_t text; int depth; bool static_method; + jw_lambdas_t *lambdas; /* The call whose declaration is being selected. */ + TSNode returns; /* The enclosing method's declared result. */ } jw_t; static oo_value_t jw_expr(jw_t *j, TSNode node); @@ -611,6 +629,7 @@ static const oo_typeref_t *jw_type_at(jw_t *j, TSNode node) { } static void jw_emit_type_mentions(jw_t *j, TSNode node, unsigned depth); +static void jw_annotations(jw_t *j, TSNode owner); /* Every type mention in a type expression: the identifier leaves at their own * spans, resolved in the current scope. */ @@ -621,8 +640,16 @@ static void jw_type_leaf(jw_t *j, TSNode node) { return; oo_typeref_t ref = {.name = text}; oo_value_t value = jw_ref(j, &ref); - if (value.type && value.type->graph_qn && !oo_names_tparam(w, text)) - oo_emit_type_ref(w, node, value.type->graph_qn, "java-type-name-lookup"); + if (!value.type || !value.type->graph_qn || oo_names_tparam(w, text)) + return; + oo_emit_type_ref(w, node, value.type->graph_qn, "java-type-name-lookup"); + /* A dotted spelling's USAGE occurrence is its last segment. */ + TSNode leaf = node; + uint32_t parts = ts_node_named_child_count(node); + if (j_is(node, "scoped_type_identifier") && parts) + leaf = ts_node_named_child(node, parts - 1); + oo_emit_usage(w, leaf, value.type->graph_qn, CBM_RESOLVED_TYPE_REFERENCE, + CBM_RESOLVE_RULE_DECLARED_TYPE, "java-type-name-lookup"); } static void jw_emit_type_mentions(jw_t *j, TSNode node, unsigned depth) { @@ -630,6 +657,9 @@ static void jw_emit_type_mentions(jw_t *j, TSNode node, unsigned depth) { return; if (j_is(node, "type_identifier") || j_is(node, "scoped_type_identifier")) { jw_type_leaf(j, node); + /* `Outer.Inner` also mentions Outer. */ + if (j_is(node, "scoped_type_identifier") && ts_node_named_child_count(node)) + jw_emit_type_mentions(j, ts_node_named_child(node, 0), depth + 1); return; } uint32_t count = ts_node_named_child_count(node); @@ -637,6 +667,45 @@ static void jw_emit_type_mentions(jw_t *j, TSNode node, unsigned depth) { jw_emit_type_mentions(j, ts_node_named_child(node, i), depth + 1); } +/* `@Nullable`, `@Foo(Bar.BAZ)`: the annotation's type and its value expressions. */ +static void jw_annotation(jw_t *j, TSNode node) { + oo_walk_t *w = j->w; + TSNode name = j_field(node, "name"); + const char *spelled = ts_node_is_null(name) ? NULL : j_compact(&j->text, name); + if (spelled) { + oo_typeref_t ref = {.name = spelled}; + oo_value_t type = jw_ref(j, &ref); + uint32_t parts = ts_node_named_child_count(name); + TSNode leaf = + j_is(name, "scoped_identifier") && parts ? ts_node_named_child(name, parts - 1) : name; + if (type.type && type.type->graph_qn) + oo_emit_usage(w, leaf, type.type->graph_qn, CBM_RESOLVED_TYPE_REFERENCE, + CBM_RESOLVE_RULE_DECLARED_TYPE, "java-annotation-type"); + } + TSNode arguments = j_field(node, "arguments"); + uint32_t count = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments); + for (uint32_t i = 0; i < count; i++) { + TSNode argument = ts_node_named_child(arguments, i); + /* `name = value`: the name is the annotation's element. */ + TSNode value = j_is(argument, "element_value_pair") ? j_field(argument, "value") : argument; + if (j_is(value, "annotation") || j_is(value, "marker_annotation")) + jw_annotation(j, value); + else + (void)jw_expr(j, value); + } +} + +/* The annotations in a declaration's modifiers. */ +static void jw_annotations(jw_t *j, TSNode owner) { + TSNode modifiers = j_child(owner, "modifiers"); + uint32_t count = ts_node_is_null(modifiers) ? 0 : ts_node_named_child_count(modifiers); + for (uint32_t i = 0; i < count; i++) { + TSNode child = ts_node_named_child(modifiers, i); + if (j_is(child, "annotation") || j_is(child, "marker_annotation")) + jw_annotation(j, child); + } +} + static void jw_bind_declared(jw_t *j, const char *name, TSNode type, oo_value_t initial, int dims) { oo_walk_t *w = j->w; oo_value_t value; @@ -656,6 +725,7 @@ static void jw_parameters(jw_t *j, TSNode parameters) { uint32_t count = ts_node_is_null(parameters) ? 0 : ts_node_named_child_count(parameters); for (uint32_t i = 0; i < count; i++) { TSNode parameter = ts_node_named_child(parameters, i); + jw_annotations(j, parameter); if (j_is(parameter, "formal_parameter")) { jw_bind_declared(j, j_text(&j->text, j_field(parameter, "name")), j_field(parameter, "type"), oo_value_unknown(), @@ -690,12 +760,19 @@ static int jw_argument_count(TSNode arguments) { return count; } -/* Arguments are ordinary expressions at this site: evaluate each exactly once. */ -static const oo_value_t *jw_arguments(jw_t *j, TSNode arguments, int *out_count) { +/* Arguments are ordinary expressions at this site: evaluate each exactly once. A lambda waits for + * selection when the caller collects it in `deferred`; either way it reaches only a function type. + */ +static const oo_value_t *jw_arguments(jw_t *j, TSNode arguments, int *out_count, + jw_lambdas_t *deferred) { int count = jw_argument_count(arguments); *out_count = count; oo_value_t *values = count ? cbm_arena_calloc(j->w->arena, (size_t)count * sizeof(*values)) : NULL; + if (deferred && count) { + deferred->nodes = cbm_arena_calloc(j->w->arena, (size_t)count * sizeof(TSNode)); + deferred->count = deferred->nodes ? count : 0; + } int at = 0; uint32_t children = ts_node_is_null(arguments) ? 0 : ts_node_named_child_count(arguments); for (uint32_t i = 0; i < children; i++) { @@ -703,7 +780,20 @@ static const oo_value_t *jw_arguments(jw_t *j, TSNode arguments, int *out_count) const char *kind = ts_node_type(child); if (!strcmp(kind, "line_comment") || !strcmp(kind, "block_comment")) continue; - oo_value_t value = jw_expr(j, child); + bool lambda = !strcmp(kind, "lambda_expression"); + oo_value_t value = oo_value_unknown(); + if (lambda && deferred && deferred->nodes) + deferred->nodes[at] = child; + else + value = jw_expr(j, child); + if (lambda || !strcmp(kind, "method_reference")) + value.function_literal = true; + if (lambda) { + TSNode parameters = j_field(child, "parameters"); + value.lambda_parameters = + 1 + + (j_is(parameters, "identifier") ? 1 : (int)ts_node_named_child_count(parameters)); + } if (values) values[at] = value; at++; @@ -711,6 +801,138 @@ static const oo_value_t *jw_arguments(jw_t *j, TSNode arguments, int *out_count) return values; } +/* JDK functional interfaces: the positions of their type arguments a lambda's parameters take. + * A name counts only as what the declaring file imports it from. */ +typedef struct { + const char *leaf, *package; + int count; + int positions[2]; +} jw_functional_t; + +static const jw_functional_t JW_FUNCTIONAL[] = { + {"Consumer", "java.util.function", 1, {0, 0}}, + {"BiConsumer", "java.util.function", 2, {0, 1}}, + {"Function", "java.util.function", 1, {0, 0}}, + {"BiFunction", "java.util.function", 2, {0, 1}}, + {"UnaryOperator", "java.util.function", 1, {0, 0}}, + {"BinaryOperator", "java.util.function", 2, {0, 0}}, + {"Predicate", "java.util.function", 1, {0, 0}}, + {"BiPredicate", "java.util.function", 2, {0, 1}}, + {"ToIntFunction", "java.util.function", 1, {0, 0}}, + {"ToLongFunction", "java.util.function", 1, {0, 0}}, + {"ToDoubleFunction", "java.util.function", 1, {0, 0}}, + {"ToIntBiFunction", "java.util.function", 2, {0, 1}}, + {"ToLongBiFunction", "java.util.function", 2, {0, 1}}, + {"ToDoubleBiFunction", "java.util.function", 2, {0, 1}}, + {"Comparator", "java.util", 2, {0, 0}}, +}; + +static bool jw_imports_star(const oo_file_t *file, const char *package) { + for (int i = 0; file && i < file->import_count; i++) + if (file->imports[i].star && !file->imports[i].statik && + !strcmp(file->imports[i].path, package)) + return true; + return false; +} + +static int jw_functional_parameters(jw_t *j, const oo_value_t *function, const oo_file_t *file, + oo_value_t *out, int capacity) { + if (function->type) + return oo_functional_parameters(j->w, function, out, capacity); + if (!function->external || function->dims || capacity < 2) + return -1; + const char *dot = strrchr(function->external, '.'); + const char *leaf = dot ? dot + 1 : function->external; + for (size_t t = 0; t < sizeof(JW_FUNCTIONAL) / sizeof(JW_FUNCTIONAL[0]); t++) { + const jw_functional_t *known = &JW_FUNCTIONAL[t]; + if (strcmp(known->leaf, leaf)) + continue; + size_t package = strlen(known->package); + bool named = dot ? (size_t)(dot - function->external) == package && + !strncmp(function->external, known->package, package) + : jw_imports_star(file, known->package); + if (!named) + return -1; + for (int i = 0; i < known->count; i++) { + int at = known->positions[i]; + oo_value_t value = at < function->arg_count ? function->args[at] : oo_value_unknown(); + out[i] = !oo_value_known(&value) && value.lower ? *value.lower : value; + } + return known->count; + } + return -1; +} + +/* A lambda under the parameter types its target gave, `count` of them, or none known at -1. */ +static oo_value_t jw_lambda_typed(jw_t *j, TSNode node, const oo_value_t *types, int count) { + oo_walk_t *w = j->w; + oo_frame_t frame; + oo_frame_push(w, &frame); + TSNode parameters = j_field(node, "parameters"); + if (j_is(parameters, "identifier")) { + oo_bind(w, j_text(&j->text, parameters), count == 1 ? types[0] : oo_value_unknown()); + } else if (j_is(parameters, "inferred_parameters")) { + uint32_t names = ts_node_named_child_count(parameters); + for (uint32_t i = 0; i < names; i++) + oo_bind(w, j_text(&j->text, ts_node_named_child(parameters, i)), + count == (int)names ? types[i] : oo_value_unknown()); + } else { + jw_parameters(j, parameters); + } + TSNode saved_returns = j->returns; + j->returns = (TSNode){0}; + oo_value_t body = jw_expr(j, j_field(node, "body")); + j->returns = saved_returns; + oo_frame_pop(w); + return body; +} + +enum { JW_LAMBDA_PARAMETERS = 8 }; + +/* `Consumer c = d -> d.go();`, `return d -> d.go();`: the declared type is the target. */ +static bool jw_lambda_to_declared(jw_t *j, TSNode lambda, TSNode type) { + if (!j_is(lambda, "lambda_expression") || ts_node_is_null(type)) + return false; + oo_value_t declared = jw_ref(j, j_typeref(&j->text, type)); + oo_value_t types[JW_LAMBDA_PARAMETERS]; + int count = jw_functional_parameters(j, &declared, j->w->f, types, JW_LAMBDA_PARAMETERS); + (void)jw_lambda_typed(j, lambda, types, count); + return true; +} + +/* Walk a call's lambdas: under the selected declaration, under a standard collection method of + * `receiver` called `name`, or with unknown parameters when neither applies. */ +static void jw_finish_lambdas(jw_t *j, jw_lambdas_t *lambdas, const oo_callable_t *callee, + const oo_subst_t *subst, const oo_value_t *receiver, + const char *name) { + if (!lambdas || lambdas->walked) + return; + lambdas->walked = true; + oo_walk_t *w = j->w; + for (int i = 0; i < lambdas->count; i++) { + if (ts_node_is_null(lambdas->nodes[i])) + continue; + oo_value_t types[JW_LAMBDA_PARAMETERS]; + int count = -1; + if (callee && callee->params && callee->param_count) { + int index = i < callee->param_count ? i : callee->param_count - 1; + if (callee->params[index]) { + oo_scope_t scope = oo_scope_of_callable(callee); + oo_value_t function = oo_value_from_ref(w, callee->params[index], &scope, subst); + /* `Consumer... each`: the element. */ + if (callee->max_args < 0 && index == callee->param_count - 1 && function.dims) + function.dims--; + count = jw_functional_parameters(j, &function, callee->file, types, + JW_LAMBDA_PARAMETERS); + } + } else if (receiver && name) { + count = oo_stdlib_lambda_parameters(w, receiver, name, i, lambdas->count, types, + JW_LAMBDA_PARAMETERS); + } + lambdas->last = jw_lambda_typed(j, lambdas->nodes[i], types, count); + } +} + static oo_value_t jw_select_emit(jw_t *j, TSNode site, const oo_value_t *receiver, const char *name, int argc, const oo_value_t *args, bool static_only, const char *decision) { @@ -721,7 +943,10 @@ static oo_value_t jw_select_emit(jw_t *j, TSNode site, const oo_value_t *receive if (selected.callable->graph_qn) oo_emit_call(w, site, selected.callable->graph_qn, CBM_RESOLVE_RULE_TYPE_MEMBER, CBM_RESOLVE_FACT_RECEIVER_TYPED, decision); - return oo_callable_return(w, selected.callable, selected.subst); + const oo_subst_t *subst = + oo_infer_call_subst(w, selected.callable, selected.subst, NULL, args, argc); + jw_finish_lambdas(j, j->lambdas, selected.callable, subst, NULL, NULL); + return oo_callable_return(w, selected.callable, subst); } static bool jw_receiver_owns(const oo_receiver_t *receiver, const char *name) { @@ -743,7 +968,7 @@ static oo_value_t jw_static_import_call(jw_t *j, TSNode site, const char *name, const char *dot = strrchr(import->path, '.'); const char *owner_qn = dot ? cbm_arena_strndup(w->arena, import->path, (size_t)(dot - import->path)) : NULL; - const oo_type_t *owner = owner_qn ? oo_lookup_type(w->ix, owner_qn) : NULL; + const oo_type_t *owner = owner_qn ? oo_lookup_visible(w->ix, owner_qn, w->f) : NULL; if (!owner || (owner->flags & OO_F_AMBIGUOUS)) return oo_value_unknown(); oo_value_t receiver = oo_value_of_type(owner); @@ -757,7 +982,7 @@ static oo_value_t jw_static_import_call(jw_t *j, TSNode site, const char *name, const oo_import_t *import = &file->imports[i]; if (!import->statik || !import->star) continue; - const oo_type_t *owner = oo_lookup_type(w->ix, import->path); + const oo_type_t *owner = oo_lookup_visible(w->ix, import->path, w->f); /* Members of an unindexed class take part in the same overload set. */ if (!owner || (owner->flags & OO_F_AMBIGUOUS)) return oo_value_unknown(); @@ -871,8 +1096,8 @@ static oo_value_t jw_identifier(jw_t *j, TSNode node) { ? import->path : cbm_arena_strndup(w->arena, import->path, strlen(import->path) - strlen(name) - 1); - const oo_type_t *owner = owner_qn ? oo_lookup_type(w->ix, owner_qn) : NULL; - if (!owner) + const oo_type_t *owner = owner_qn ? oo_lookup_visible(w->ix, owner_qn, w->f) : NULL; + if (!owner || (owner->flags & OO_F_AMBIGUOUS)) continue; oo_value_t receiver = oo_value_of_type(owner); oo_subst_t *subst = NULL; @@ -926,7 +1151,7 @@ static oo_value_t jw_field_access(jw_t *j, TSNode node) { (oo_find_field(w, &r->value, head, &subst, &incomplete) || incomplete)); } } - const oo_type_t *exact = variable ? NULL : oo_lookup_type(w->ix, text); + const oo_type_t *exact = variable ? NULL : oo_lookup_visible(w->ix, text, w->f); if (exact && !(exact->flags & OO_F_AMBIGUOUS)) { oo_value_t value = oo_value_of_type(exact); value.classref = true; @@ -947,13 +1172,16 @@ static oo_value_t jw_field_access(jw_t *j, TSNode node) { if (nested && !(nested->flags & OO_F_AMBIGUOUS)) { oo_value_t value = oo_value_of_type(nested); value.classref = true; + if (nested->graph_qn) + oo_emit_usage(w, field_node, nested->graph_qn, CBM_RESOLVED_TYPE_REFERENCE, + CBM_RESOLVE_RULE_DECLARED_TYPE, "java-nested-type-name"); return value; } } return oo_value_unknown(); } -static oo_value_t jw_method_invocation(jw_t *j, TSNode node) { +static oo_value_t jw_method_invocation_with(jw_t *j, TSNode node, jw_lambdas_t *lambdas) { oo_walk_t *w = j->w; TSNode object = j_field(node, "object"); TSNode name_node = j_field(node, "name"); @@ -965,12 +1193,14 @@ static oo_value_t jw_method_invocation(jw_t *j, TSNode node) { } if (ts_node_is_null(object)) { int argc = 0; - const oo_value_t *args = jw_arguments(j, arguments, &argc); + const oo_value_t *args = jw_arguments(j, arguments, &argc, lambdas); + j->lambdas = lambdas; return jw_bare_call(j, node, name, argc, args); } if (j_is(object, "super")) { int argc = 0; - const oo_value_t *args = jw_arguments(j, arguments, &argc); + const oo_value_t *args = jw_arguments(j, arguments, &argc, lambdas); + j->lambdas = lambdas; /* super.m() selects from the direct supertypes of the enclosing class. */ const oo_value_t self = jw_this_value(w); if (!self.type) @@ -996,12 +1226,39 @@ static oo_value_t jw_method_invocation(jw_t *j, TSNode node) { if (j_is(object, "field_access") && j_is(j_field(object, "field"), "super")) receiver.classref = false; int argc = 0; - const oo_value_t *args = jw_arguments(j, arguments, &argc); - if (!receiver.type) + const oo_value_t *args = jw_arguments(j, arguments, &argc, lambdas); + j->lambdas = lambdas; + if (!receiver.type) { + jw_finish_lambdas(j, lambdas, NULL, NULL, &receiver, name); + /* `map` yields what its lambda's expression produced, in the same kind of container, and + * `flatMap` what that holds. */ + if ((!strcmp(name, "map") || !strcmp(name, "mapToObj")) && receiver.external && + oo_value_known(&lambdas->last)) + return oo_external_with_element(w, &receiver, lambdas->last); + if (!strcmp(name, "flatMap") && receiver.external) { + oo_value_t held = oo_element_of(w, &lambdas->last); + return oo_external_with_element(w, &receiver, held); + } + oo_value_t made = oo_jdk_factory_result(w, &receiver, name, args, argc); + if (oo_value_known(&made)) + return made; return oo_external_member_return(w, &receiver, name); - return jw_select_emit(j, node, &receiver, name, argc, args, receiver.classref, - receiver.classref ? "java-static-member" - : "java-declared-receiver-member"); + } + oo_value_t result = + jw_select_emit(j, node, &receiver, name, argc, args, receiver.classref, + receiver.classref ? "java-static-member" : "java-declared-receiver-member"); + /* A project collection's `forEach` comes from the JDK supertype. */ + jw_finish_lambdas(j, lambdas, NULL, NULL, &receiver, name); + return result; +} + +static oo_value_t jw_method_invocation(jw_t *j, TSNode node) { + jw_lambdas_t lambdas = {0}; + jw_lambdas_t *outer = j->lambdas; + oo_value_t result = jw_method_invocation_with(j, node, &lambdas); + jw_finish_lambdas(j, &lambdas, NULL, NULL, NULL, NULL); + j->lambdas = outer; + return result; } static void jw_anonymous_body(jw_t *j, TSNode body, oo_value_t super_value) { @@ -1055,12 +1312,15 @@ static oo_value_t jw_object_creation(jw_t *j, TSNode node) { jw_emit_type_mentions(j, type_node, 0); } int argc = 0; - const oo_value_t *args = jw_arguments(j, arguments, &argc); + jw_lambdas_t lambdas = {0}; + const oo_value_t *args = jw_arguments(j, arguments, &argc, &lambdas); TSNode body = j_child(node, "class_body"); if (value.type && !ts_node_is_null(body) && (value.type->flags & OO_F_INTERFACE)) { /* An anonymous implementation of an interface has only Object's constructor. */ } else if (value.type) { oo_select_t selected = oo_select_constructor(w, &value, argc, args); + if (selected.status == OO_SEL_UNIQUE) + jw_finish_lambdas(j, &lambdas, selected.callable, selected.subst, NULL, NULL); if (selected.status == OO_SEL_UNIQUE && selected.callable->graph_qn) oo_emit_call(w, node, selected.callable->graph_qn, CBM_RESOLVE_RULE_LANGUAGE_BINDING, CBM_RESOLVE_FACT_LANGUAGE_BOUND, "java-constructor-selection"); @@ -1070,6 +1330,7 @@ static oo_value_t jw_object_creation(jw_t *j, TSNode node) { CBM_RESOLVE_FACT_LANGUAGE_BOUND | CBM_RESOLVE_FACT_CLASS_CALLABLE, "java-implicit-default-constructor"); } + jw_finish_lambdas(j, &lambdas, NULL, NULL, NULL, NULL); if (!ts_node_is_null(body)) jw_anonymous_body(j, body, value); return value; @@ -1083,8 +1344,9 @@ static oo_value_t jw_local_variable(jw_t *j, TSNode node) { if (!j_is(declarator, "variable_declarator")) continue; TSNode value_node = j_field(declarator, "value"); - oo_value_t initial = - ts_node_is_null(value_node) ? oo_value_unknown() : jw_expr(j, value_node); + oo_value_t initial = oo_value_unknown(); + if (!ts_node_is_null(value_node) && !jw_lambda_to_declared(j, value_node, type)) + initial = jw_expr(j, value_node); jw_bind_declared(j, j_text(&j->text, j_field(declarator, "name")), type, initial, j_dimensions(j_field(declarator, "dimensions"))); } @@ -1140,7 +1402,10 @@ static oo_value_t jw_lambda(jw_t *j, TSNode node) { oo_bind(w, j_text(&j->text, parameters), oo_value_unknown()); else jw_parameters(j, parameters); + TSNode saved_returns = j->returns; + j->returns = (TSNode){0}; (void)jw_expr(j, j_field(node, "body")); + j->returns = saved_returns; oo_frame_pop(w); return oo_value_unknown(); } @@ -1279,6 +1544,14 @@ static void jw_local_class(jw_t *j, TSNode node) { static oo_value_t jw_expr_kind(jw_t *j, TSNode node, const char *kind) { oo_walk_t *w = j->w; + /* `Doc.class` is a Class, which is how ` T get(Class type)` gets its T. */ + if (!strcmp(kind, "class_literal") && ts_node_named_child_count(node) == 1) { + jw_children(j, node); + oo_value_t of = jw_ref(j, j_typeref(&j->text, ts_node_named_child(node, 0))); + of.classref = false; + oo_value_t klass = oo_value_external("Class"); + return oo_external_with_element(w, &klass, of); + } switch (kind[0]) { case 'i': if (!strcmp(kind, "identifier")) @@ -1432,6 +1705,9 @@ static oo_value_t jw_expr_kind(jw_t *j, TSNode node, const char *kind) { return oo_value_unknown(); break; case 'r': + if (!strcmp(kind, "return_statement") && ts_node_named_child_count(node) == 1 && + jw_lambda_to_declared(j, ts_node_named_child(node, 0), j->returns)) + return oo_value_unknown(); if (!strcmp(kind, "resource")) { TSNode name = j_field(node, "name"); if (ts_node_is_null(name)) @@ -1477,7 +1753,7 @@ static oo_value_t jw_expr_kind(jw_t *j, TSNode node, const char *kind) { /* super(...) and this(...) only need their arguments walked. */ (void)jw_expr(j, j_field(node, "object")); int argc = 0; - (void)jw_arguments(j, j_field(node, "arguments"), &argc); + (void)jw_arguments(j, j_field(node, "arguments"), &argc, NULL); return oo_value_unknown(); } jw_children(j, node); @@ -1510,6 +1786,9 @@ static void jw_method(jw_t *j, TSNode node) { j_tparams(&j->text, j_field(node, "type_parameters"), &w->scope.tparam_count); oo_frame_t frame; oo_frame_push(w, &frame); + TSNode saved_returns = j->returns; + j->returns = j_field(node, "type"); + jw_annotations(j, node); jw_emit_type_mentions(j, j_field(node, "type"), 0); if (j_is(node, "compact_constructor_declaration")) { /* The header's components are this constructor's parameters. */ @@ -1519,18 +1798,23 @@ static void jw_method(jw_t *j, TSNode node) { jw_parameters(j, j_field(node, "parameters")); } (void)jw_expr(j, j_field(node, "body")); + j->returns = saved_returns; oo_frame_pop(w); w->scope = saved; j->static_method = saved_static; } static void jw_field_declaration(jw_t *j, TSNode node) { + jw_annotations(j, node); jw_emit_type_mentions(j, j_field(node, "type"), 0); uint32_t count = ts_node_named_child_count(node); for (uint32_t i = 0; i < count; i++) { TSNode declarator = ts_node_named_child(node, i); - if (j_is(declarator, "variable_declarator")) - (void)jw_expr(j, j_field(declarator, "value")); + if (!j_is(declarator, "variable_declarator")) + continue; + TSNode value = j_field(declarator, "value"); + if (!jw_lambda_to_declared(j, value, j_field(node, "type"))) + (void)jw_expr(j, value); } } @@ -1553,7 +1837,7 @@ static void jw_class_body(jw_t *j, TSNode body, const oo_type_t *type) { oo_frame_pop(j->w); } else if (j_is(member, "enum_constant")) { int argc = 0; - (void)jw_arguments(j, j_field(member, "arguments"), &argc); + (void)jw_arguments(j, j_field(member, "arguments"), &argc, NULL); TSNode constant_body = j_field(member, "body"); if (!ts_node_is_null(constant_body)) jw_anonymous_body(j, constant_body, oo_value_of_type(type)); @@ -1573,7 +1857,9 @@ static void jw_type_decl(jw_t *j, TSNode node, const oo_type_t *owner) { return; const char *qn = owner ? j_join(w->arena, owner->qn, name) : j_join(w->arena, w->f->package, name); - const oo_type_t *type = qn ? oo_lookup_type(w->ix, qn) : NULL; + const oo_type_t *type = qn ? oo_lookup_declared(w->ix, qn, w->rel_path) : NULL; + /* A class's annotations read the scope around it. */ + jw_annotations(j, node); oo_scope_t saved = w->scope; if (type) w->scope.owner = type; @@ -1602,7 +1888,7 @@ void oo_java_resolve(oo_walk_t *w, TSNode root) { oo_file_t *file = cbm_arena_alloc(w->arena, sizeof(*file)); if (!file) return; - *file = (oo_file_t){.rel = "", + *file = (oo_file_t){.rel = w->rel_path ? w->rel_path : "", .package = package, .language = CBM_LANG_JAVA, .imports = imports, diff --git a/internal/cbm/lsp/oo_kotlin.c b/internal/cbm/lsp/oo_kotlin.c index c5761964e..d36bd373b 100644 --- a/internal/cbm/lsp/oo_kotlin.c +++ b/internal/cbm/lsp/oo_kotlin.c @@ -210,7 +210,13 @@ static oo_tparam_t *kt_tparams(const kt_text_t *t, TSNode owner, int *out_count) return items; } -enum { KT_MOD_PRIVATE = 1, KT_MOD_OVERRIDE = 2, KT_MOD_INNER = 4, KT_MOD_ABSTRACT = 8 }; +enum { + KT_MOD_PRIVATE = 1, + KT_MOD_OVERRIDE = 2, + KT_MOD_INNER = 4, + KT_MOD_ABSTRACT = 8, + KT_MOD_DATA = 16 +}; static unsigned kt_modifiers(const kt_text_t *t, TSNode node) { unsigned flags = 0; @@ -232,6 +238,8 @@ static unsigned kt_modifiers(const kt_text_t *t, TSNode node) { flags |= KT_MOD_INNER; else if (!strcmp(text, "abstract")) flags |= KT_MOD_ABSTRACT; + else if (!strcmp(text, "data")) + flags |= KT_MOD_DATA; } return flags; } @@ -249,7 +257,8 @@ static TSNode kt_after_colon(TSNode node, const char *stop_a, const char *stop_b } if (kt_is(child, stop_a) || (stop_b && kt_is(child, stop_b))) break; - if (seen) + /* `suspend` and annotations stand in front of the type itself. */ + if (seen && !kt_is(child, "type_modifiers")) return child; } return (TSNode){0}; @@ -346,6 +355,7 @@ static void kh_parameters(const kh_t *h, oo_callable_t *callable, TSNode paramet uint32_t count = ts_node_is_null(parameters) ? 0 : ts_node_child_count(parameters); const oo_typeref_t **refs = count ? cbm_arena_calloc(h->text.arena, count * sizeof(*refs)) : NULL; + const char **names = count ? cbm_arena_calloc(h->text.arena, count * sizeof(*names)) : NULL; int total = 0, optional = 0; bool vararg = false, pending_vararg = false; for (uint32_t i = 0; i < count; i++) { @@ -361,6 +371,8 @@ static void kh_parameters(const kh_t *h, oo_callable_t *callable, TSNode paramet type = ts_node_named_child(child, parts - 1); if (refs) refs[total] = kt_typeref(&h->text, type); + if (names) + names[total] = kt_name(&h->text, kt_child(child, "simple_identifier")); total++; if (pending_vararg && !vararg) callable->vararg_position = total; @@ -376,6 +388,7 @@ static void kh_parameters(const kh_t *h, oo_callable_t *callable, TSNode paramet } } callable->params = refs; + callable->param_names = names; callable->param_count = total; callable->min_args = total - optional - (vararg ? 1 : 0); if (callable->min_args < 0) @@ -543,6 +556,7 @@ static void kh_primary_constructor(kh_t *h, oo_type_t *type, TSNode primary) { callable->flags = OO_F_CTOR; kh_parameters(h, callable, primary); oo_index_method(h->ix, type, callable); + type->primary = callable; uint32_t count = ts_node_named_child_count(primary); for (uint32_t i = 0; i < count; i++) { TSNode parameter = ts_node_named_child(primary, i); @@ -616,6 +630,8 @@ static void kh_type(kh_t *h, TSNode node, oo_type_t *owner) { flags |= OO_F_PRIVATE; if (modifiers & KT_MOD_INNER) flags |= OO_F_INNER; + if (modifiers & KT_MOD_DATA) + flags |= OO_F_DATA; if (oo_members_broken(node)) flags |= OO_F_INCOMPLETE; const CBMDefinition *def = oo_defs_find(&h->defs, 'T', kt_line(node), name); @@ -886,6 +902,15 @@ static bool kt_hidden(const oo_value_t *value, const char *name) { return false; } +/* What an anonymous object's own property holds, seen from outside. Unknown for a function. */ +static oo_value_t kt_hidden_value(const oo_value_t *value, const char *name) { + if (value->hidden && value->hidden->values) + for (int i = 0; i < value->hidden->count; i++) + if (!strcmp(value->hidden->names[i], name)) + return value->hidden->values[i]; + return oo_value_unknown(); +} + static oo_select_t kt_select_list(oo_walk_t *w, const kt_list_t *list, int argc, const oo_value_t *args, const oo_value_t *receiver, bool extensions_only) { @@ -1157,6 +1182,18 @@ static oo_value_t kt_summary_first(oo_walk_t *w, const oo_sumstep_t *step, const if (incomplete) return oo_value_unknown(); } + /* `val at = datetime("at")` on an extension of the enclosing type. */ + for (int level = 0; step->argc >= 0 && level < KT_LEVEL_COUNT; level++) { + kt_list_t list = {.count = 0}; + kt_level_functions(w, scope, step->name, level, &list); + if (list.overflow) + return oo_value_unknown(); + oo_select_t extension = kt_select_list(w, &list, step->argc, NULL, &receiver, true); + if (extension.status == OO_SEL_UNIQUE) + return oo_callable_return(w, extension.callable, extension.subst); + if (extension.status == OO_SEL_AMBIGUOUS) + return oo_value_unknown(); + } if (pass == 0 && type->companion) receiver = oo_value_of_type(type->companion); else @@ -1241,6 +1278,7 @@ typedef struct { kt_text_t text; int depth; kw_args_t *free_args; + oo_hidden_t *object_members; /* The anonymous object whose members are being walked. */ } kw_t; static oo_value_t kw_expr(kw_t *k, TSNode node); @@ -1270,6 +1308,29 @@ static oo_value_t kw_ref(kw_t *k, const oo_typeref_t *ref) { return ref ? oo_value_from_ref(w, ref, &w->scope, NULL) : oo_value_unknown(); } +static void kw_type_mentions(kw_t *k, TSNode node, unsigned depth); + +/* `@InternalApi`, `@OptIn(InternalApi::class)`, in a declaration's modifiers. */ +static void kw_annotations(kw_t *k, TSNode owner) { + TSNode modifiers = kt_child(owner, "modifiers"); + uint32_t count = ts_node_is_null(modifiers) ? 0 : ts_node_named_child_count(modifiers); + for (uint32_t i = 0; i < count; i++) { + TSNode annotation = ts_node_named_child(modifiers, i); + if (!kt_is(annotation, "annotation")) + continue; + uint32_t parts = ts_node_named_child_count(annotation); + for (uint32_t p = 0; p < parts; p++) { + TSNode part = ts_node_named_child(annotation, p); + if (kt_is(part, "user_type")) { + kw_type_mentions(k, part, 0); + } else if (kt_is(part, "constructor_invocation")) { + kw_type_mentions(k, kt_child(part, "user_type"), 0); + kw_children(k, kt_child(part, "value_arguments")); + } + } + } +} + /* Every type mention inside an annotation, at the identifier's own span. */ static void kw_type_mentions(kw_t *k, TSNode node, unsigned depth) { if (ts_node_is_null(node) || depth > 32) @@ -1467,16 +1528,52 @@ static void kt_stdlib_lambda(oo_walk_t *w, const char *name, const oo_value_t *r } } -static void kw_bind_destructured(kw_t *k, TSNode node) { +/* `componentN` of a destructured value (Kotlin spec 8.4): a pair, a triple or a map entry from the + * standard library, a list's element, a data class's primary constructor, or the operator itself. + */ +static oo_value_t kt_component(oo_walk_t *w, const oo_value_t *value, int position) { + if (!value || value->dims) + return oo_value_unknown(); + if (value->external) { + const char *dot = strrchr(value->external, '.'); + const char *leaf = dot ? dot + 1 : value->external; + bool tuple = (!strcmp(leaf, "Pair") && value->arg_count == 2) || + (!strcmp(leaf, "Triple") && value->arg_count == 3) || + (!strcmp(leaf, "Entry") && value->arg_count == 2) || + (!strcmp(leaf, "MutableEntry") && value->arg_count == 2); + if (tuple) + return position < value->arg_count ? value->args[position] : oo_value_unknown(); + return oo_element_of(w, value); + } + if (!value->type) + return oo_value_unknown(); + oo_subst_t *subst = oo_subst_of(w, value); + char method[32]; + snprintf(method, sizeof(method), "component%d", position + 1); + oo_select_t selected = oo_select_method(w, value, method, 0, NULL, false); + if (selected.status == OO_SEL_UNIQUE) + return oo_callable_return(w, selected.callable, selected.subst); + const oo_callable_t *primary = value->type->primary; + if ((value->type->flags & OO_F_DATA) && primary && primary->params && + position < primary->param_count && primary->params[position]) { + oo_scope_t scope = oo_scope_of_callable(primary); + return oo_value_from_ref(w, primary->params[position], &scope, subst); + } + return oo_value_unknown(); +} + +static void kw_bind_destructured(kw_t *k, TSNode node, const oo_value_t *source) { uint32_t count = ts_node_named_child_count(node); + int position = 0; for (uint32_t i = 0; i < count; i++) { TSNode child = ts_node_named_child(node, i); if (kt_is(child, "variable_declaration")) { const char *name = kt_name(&k->text, kt_child(child, "simple_identifier")); TSNode declared = kt_after_colon(child, "=", NULL); oo_bind(k->w, name, - ts_node_is_null(declared) ? oo_value_unknown() - : kw_ref(k, kt_typeref(&k->text, declared))); + !ts_node_is_null(declared) ? kw_ref(k, kt_typeref(&k->text, declared)) + : kt_component(k->w, source, position)); + position++; } } } @@ -1489,7 +1586,9 @@ static oo_value_t kw_lambda(kw_t *k, TSNode lambda, const oo_value_t *expected, oo_frame_push(w, &frame); bool pushed = false; const oo_value_t *function = expected && expected->fn_ref ? expected : NULL; - if (function && function->fn_receiver && oo_value_known(function->fn_receiver)) { + bool settled = function && !(ctx && oo_ref_unsettled(ctx->callee, function->fn_ref->fn_receiver, + ctx->subst)); + if (settled && function->fn_receiver && oo_value_known(function->fn_receiver)) { pushed = oo_receiver_push(w, *function->fn_receiver, false) != NULL; } else if (function && function->fn_ref->fn_receiver) { /* A receiver exists whose type could not be read. */ @@ -1548,7 +1647,7 @@ static oo_value_t kw_lambda(kw_t *k, TSNode lambda, const oo_value_t *expected, oo_bind(w, name, value); index++; } else if (kt_is(parameter, "multi_variable_declaration")) { - kw_bind_destructured(k, parameter); + kw_bind_destructured(k, parameter, index == 0 ? &first_parameter : NULL); index++; } } @@ -1572,6 +1671,7 @@ struct kw_args { TSNode nodes[32]; oo_value_t values[32]; bool lambda[32]; + const char *names[32]; /* A named argument lands on that parameter, not at its position. */ int count; bool overflow; kw_args_t *next_free; @@ -1621,16 +1721,18 @@ static void kw_collect_args(kw_t *k, TSNode suffix, kw_args_t *args) { continue; } TSNode lambda = kw_lambda_of(argument); + uint32_t parts = ts_node_named_child_count(argument); + bool named = parts >= 2 && kt_is(ts_node_named_child(argument, 0), "simple_identifier"); args->nodes[args->count] = argument; args->lambda[args->count] = !ts_node_is_null(lambda); + args->names[args->count] = + named ? kt_name(&k->text, ts_node_named_child(argument, 0)) : NULL; args->values[args->count] = oo_value_unknown(); - args->values[args->count].function_literal = !ts_node_is_null(lambda); + args->values[args->count].function_literal = !ts_node_is_null(lambda) && !named; if (ts_node_is_null(lambda)) { - uint32_t parts = ts_node_named_child_count(argument); TSNode value = parts ? ts_node_named_child(argument, parts - 1) : (TSNode){0}; oo_value_t read = kw_expr(k, value); - /* A named argument lands on a parameter of that name, not this position. */ - if (parts < 2 || !kt_is(ts_node_named_child(argument, 0), "simple_identifier")) + if (!named) args->values[args->count] = read; } args->count++; @@ -1642,6 +1744,7 @@ static void kw_collect_args(kw_t *k, TSNode suffix, kw_args_t *args) { continue; args->nodes[args->count] = child; args->lambda[args->count] = true; + args->names[args->count] = NULL; args->values[args->count] = oo_value_unknown(); args->values[args->count].function_literal = true; args->count++; @@ -1650,9 +1753,15 @@ static void kw_collect_args(kw_t *k, TSNode suffix, kw_args_t *args) { /* The parameter a positional or trailing argument lands on. */ static const oo_typeref_t *kw_param_for(const oo_callable_t *callee, int index, int total, - bool trailing) { + bool trailing, const char *named) { if (!callee || !callee->params || !callee->param_count) return NULL; + if (named) { + for (int i = 0; callee->param_names && i < callee->param_count; i++) + if (callee->param_names[i] && !strcmp(callee->param_names[i], named)) + return callee->params[i]; + return NULL; + } if (trailing && index == total - 1) return callee->params[callee->param_count - 1]; return index < callee->param_count @@ -1675,7 +1784,7 @@ static oo_value_t kw_finish_lambdas(kw_t *k, const kw_args_t *args, const oo_cal ctx.subst = subst; oo_value_t expected = oo_value_unknown(); bool trailing = kt_is(args->nodes[i], "annotated_lambda"); - const oo_typeref_t *ref = kw_param_for(callee, i, args->count, trailing); + const oo_typeref_t *ref = kw_param_for(callee, i, args->count, trailing, args->names[i]); if (ref && callee) { oo_scope_t scope = oo_scope_of_callable(callee); expected = oo_value_from_ref(w, ref, &scope, subst); @@ -1728,6 +1837,7 @@ static kw_implicit_t kw_implicit_member(kw_t *k, TSNode name_site, const char *n oo_walk_t *w = k->w; kw_implicit_t out = {0}; bool no_outer_instance = false; + bool own_property = false; for (const oo_receiver_t *r = w->receiver; r; r = r->parent) { bool blocked = no_outer_instance && !(r->value.type && (r->value.type->flags & OO_F_OBJECT)); @@ -1735,10 +1845,13 @@ static kw_implicit_t kw_implicit_member(kw_t *k, TSNode name_site, const char *n if (blocked) continue; for (int i = 0; i < r->own_count; i++) { - if (!strcmp(r->own_names[i], name)) { + if (strcmp(r->own_names[i], name)) + continue; + if (!r->own_function || r->own_function[i]) { out.stop = true; return out; } + own_property = true; } if (r->unknown) { out.unknown_receiver = true; @@ -1796,6 +1909,8 @@ static kw_implicit_t kw_implicit_member(kw_t *k, TSNode name_site, const char *n return out; } } + /* No function of the name anywhere: the call is the own property's invoke. */ + out.stop = out.stop || own_property; return out; } @@ -1807,6 +1922,14 @@ static oo_select_t kw_free_call(kw_t *k, const char *name, int argc, const oo_va *type_value = oo_value_unknown(); oo_typeref_t ref = {.name = name}; oo_name_t type_name = oo_resolve_name(w->ix, &w->scope, name); + for (const oo_receiver_t *r = w->receiver; r; r = r->parent) { + const oo_type_t *nested = + r->object_super && r->value.type ? oo_find_nested(w->ix, r->value.type, name) : NULL; + if (nested && !(nested->flags & OO_F_AMBIGUOUS)) { + type_name = (oo_name_t){.status = OO_NAME_TYPE, .type = nested}; + break; + } + } bool shadowed_type = oo_lookup_local(w, cbm_arena_sprintf(w->arena, "#type:%s", name)) != NULL; if (shadowed_type) return (oo_select_t){.status = OO_SEL_AMBIGUOUS}; @@ -2124,7 +2247,12 @@ static oo_value_t kw_member_call_with(kw_t *k, TSNode node, TSNode navigation, T kw_lambda_ctx_t ctx = {0}; if (!target) kt_stdlib_lambda(w, name, &receiver, &ctx); - oo_value_t lambda_value = kw_finish_lambdas(k, args, target, subst, &ctx); + /* `Users.insert { ... }`: the receiver settles a `T.() -> Unit`'s T. */ + const oo_subst_t *lambda_subst = + target && !args->overflow + ? oo_infer_call_subst(w, target, subst, &receiver, kw_arg_values(args), args->count) + : subst; + oo_value_t lambda_value = kw_finish_lambdas(k, args, target, lambda_subst, &ctx); if (!target) { if (ctx.result_is_lambda) return lambda_value; @@ -2192,6 +2320,7 @@ static oo_value_t kw_object_body(kw_t *k, TSNode object, TSNode members) { oo_receiver_t *receiver = oo_receiver_push(w, value, false); if (receiver) { receiver->unknown = !value.type; + receiver->object_super = true; pushed++; } } @@ -2204,28 +2333,41 @@ static oo_value_t kw_object_body(kw_t *k, TSNode object, TSNode members) { uint32_t members_count = ts_node_is_null(members) ? 0 : ts_node_named_child_count(members); const char **names = members_count ? cbm_arena_calloc(w->arena, members_count * sizeof(*names)) : NULL; + bool *functions = + members_count ? cbm_arena_calloc(w->arena, members_count * sizeof(*functions)) : NULL; + oo_value_t *values = + members_count ? cbm_arena_calloc(w->arena, members_count * sizeof(*values)) : NULL; int n = 0; for (uint32_t i = 0; i < members_count; i++) { TSNode member = ts_node_named_child(members, i); const char *name = NULL; - if (kt_is(member, "function_declaration")) + bool function = kt_is(member, "function_declaration"); + if (function) name = kt_name(&k->text, kt_child(member, "simple_identifier")); else if (kt_is(member, "property_declaration")) name = kt_name(&k->text, kt_child(kt_child(member, "variable_declaration"), "simple_identifier")); - if (name && names) + if (name && names && functions) { + functions[n] = function; names[n++] = name; + } } own->own_names = names; own->own_count = n; + own->own_function = functions; + own->own_values = values; if (hidden) { hidden->names = names; hidden->count = n; + hidden->values = values; } } oo_frame_t frame; oo_frame_push(w, &frame); + oo_hidden_t *outer_members = k->object_members; + k->object_members = hidden; kw_members(k, members, NULL); + k->object_members = outer_members; oo_frame_pop(w); for (int i = 0; i < pushed; i++) oo_receiver_pop(w); @@ -2285,13 +2427,16 @@ static oo_value_t kw_identifier(kw_t *k, TSNode node) { continue; for (int i = 0; i < r->own_count; i++) if (!strcmp(r->own_names[i], name)) - return oo_value_unknown(); + return r->own_values ? r->own_values[i] : oo_value_unknown(); if (r->unknown) { unknown = true; continue; } if (!r->value.type) continue; + /* An anonymous object's own member, which has no node. */ + if (kt_hidden(&r->value, name)) + return kt_hidden_value(&r->value, name); oo_subst_t *subst = NULL; bool incomplete = false; const oo_field_t *field = oo_find_field(w, &r->value, name, &subst, &incomplete); @@ -2358,7 +2503,7 @@ static oo_value_t kw_navigation(kw_t *k, TSNode node) { size_t head_length = strcspn(text, "."); char *head = cbm_arena_strndup(w->arena, text, head_length); const oo_type_t *exact = - head && !oo_lookup_local(w, head) ? oo_lookup_type(w->ix, text) : NULL; + head && !oo_lookup_local(w, head) ? oo_lookup_visible(w->ix, text, w->f) : NULL; if (exact && !(exact->flags & OO_F_AMBIGUOUS)) { bool shadow = false; for (const oo_receiver_t *r = w->receiver; r && !shadow; r = r->parent) @@ -2371,7 +2516,9 @@ static oo_value_t kw_navigation(kw_t *k, TSNode node) { } } oo_value_t receiver = kw_expr(k, receiver_node); - if (!name || (!receiver.type && !receiver.external) || kt_hidden(&receiver, name)) + if (name && kt_hidden(&receiver, name)) + return kt_hidden_value(&receiver, name); + if (!name || (!receiver.type && !receiver.external)) return oo_value_unknown(); if (receiver.type) { oo_subst_t *subst = NULL; @@ -2413,7 +2560,8 @@ static oo_value_t kw_navigation(kw_t *k, TSNode node) { return kt_field_type(w, extension, NULL); } } - return oo_value_unknown(); + /* `entry.value`, `pair.first`. */ + return receiver.external ? oo_external_member_return(w, &receiver, name) : oo_value_unknown(); } /* The operator method a desugared expression calls, at the expression's span. */ @@ -2682,7 +2830,7 @@ static oo_value_t kw_property(kw_t *k, TSNode node) { initial = value; } if (!ts_node_is_null(multi)) { - kw_bind_destructured(k, multi); + kw_bind_destructured(k, multi, &initial); } else if (!ts_node_is_null(variable)) { const char *name = kt_name(&k->text, kt_child(variable, "simple_identifier")); TSNode declared = kt_after_colon(variable, "=", NULL); @@ -2715,7 +2863,8 @@ static oo_value_t kw_for(kw_t *k, TSNode node) { } oo_frame_push(w, &frame); if (!ts_node_is_null(multi)) { - kw_bind_destructured(k, multi); + oo_value_t element = oo_element_of(w, &iterable); + kw_bind_destructured(k, multi, &element); } else if (!ts_node_is_null(variable)) { const char *name = kt_name(&k->text, kt_child(variable, "simple_identifier")); TSNode declared = kt_after_colon(variable, "=", NULL); @@ -3171,8 +3320,10 @@ static void kw_members(kw_t *k, TSNode body, const oo_type_t *owner) { TSNode member = ts_node_named_child(body, i); const char *kind = ts_node_type(member); if (!strcmp(kind, "function_declaration")) { + kw_annotations(k, member); kw_function(k, member); } else if (!strcmp(kind, "property_declaration")) { + kw_annotations(k, member); /* A member property: initializer, accessors, annotation. */ oo_frame_t frame; oo_frame_push(w, &frame); @@ -3189,7 +3340,9 @@ static void kw_members(kw_t *k, TSNode body, const oo_type_t *owner) { } } TSNode variable = kt_child(member, "variable_declaration"); - kw_type_mentions(k, kt_after_colon(variable, "=", NULL), 0); + TSNode declared = kt_after_colon(variable, "=", NULL); + kw_type_mentions(k, declared, 0); + oo_value_t held = oo_value_unknown(); uint32_t parts = ts_node_named_child_count(member); for (uint32_t p = 0; p < parts; p++) { TSNode part = ts_node_named_child(member, p); @@ -3220,7 +3373,19 @@ static void kw_members(kw_t *k, TSNode body, const oo_type_t *owner) { kw_children(k, part); continue; } - (void)kw_expr(k, part); + oo_value_t value = kw_expr(k, part); + if (!ts_node_eq(part, declared)) + held = value; + } + oo_hidden_t *members = k->object_members; + if (members && members->values) { + if (!ts_node_is_null(declared)) + held = kw_ref(k, kt_typeref(&k->text, declared)); + held.classref = false; + const char *held_name = kt_name(&k->text, kt_child(variable, "simple_identifier")); + for (int m = 0; held_name && m < members->count; m++) + if (!strcmp(members->names[m], held_name)) + members->values[m] = held; } if (pushed) oo_receiver_pop(w); @@ -3228,7 +3393,10 @@ static void kw_members(kw_t *k, TSNode body, const oo_type_t *owner) { oo_frame_pop(w); } else if (!strcmp(kind, "class_declaration") || !strcmp(kind, "object_declaration") || !strcmp(kind, "companion_object")) { + oo_hidden_t *outer_members = k->object_members; + k->object_members = NULL; kw_class_like(k, member, owner); + k->object_members = outer_members; } else if (!strcmp(kind, "anonymous_initializer")) { (void)kw_block(k, kt_child(member, "statements")); } else if (!strcmp(kind, "secondary_constructor")) { @@ -3284,7 +3452,9 @@ static void kw_class_like(kw_t *k, TSNode node, const oo_type_t *owner) { return; const char *qn = owner ? kt_join(w->arena, owner->qn, name) : kt_join(w->arena, w->f->package, name); - const oo_type_t *type = qn ? oo_lookup_type(w->ix, qn) : NULL; + const oo_type_t *type = qn ? oo_lookup_declared(w->ix, qn, w->rel_path) : NULL; + /* A class's annotations read the scope around it. */ + kw_annotations(k, node); bool usable = type && !(type->flags & OO_F_AMBIGUOUS); oo_scope_t saved = w->scope; if (type) @@ -3359,6 +3529,7 @@ static void kw_top_level(kw_t *k, TSNode parent, bool in_error) { } else if (!strcmp(kind, "class_declaration") || !strcmp(kind, "object_declaration")) { kw_class_like(k, child, NULL); } else if (!strcmp(kind, "function_declaration")) { + kw_annotations(k, child); kw_function(k, child); } else if (!strcmp(kind, "property_declaration") || !strcmp(kind, "anonymous_initializer") || diff --git a/internal/cbm/lsp/oo_strict.h b/internal/cbm/lsp/oo_strict.h index 3d4551bcb..827081493 100644 --- a/internal/cbm/lsp/oo_strict.h +++ b/internal/cbm/lsp/oo_strict.h @@ -23,6 +23,13 @@ cbm_oo_index_t *cbm_oo_index_new(CBMArena *arena); /* Coordinator only: one file at a time, before seal. */ bool cbm_oo_index_add_file(cbm_oo_index_t *index, CBMLanguage language, const char *rel_path, const CBMFileResult *file, const char *source, int source_len); +/* Coordinator only, between files: every later file belongs to the build project in this + * directory (a .csproj, build.gradle or pom.xml beside it or above it). NULL: no known project. */ +void cbm_oo_index_set_project(cbm_oo_index_t *index, const char *project_dir); +/* Coordinator only, between files: every later file is a reference assembly's + * source. Such a declaration is the contract of the implementation of its name, + * which stands for it. Call it after the implementation files are in. */ +void cbm_oo_index_begin_reference_files(cbm_oo_index_t *index); /* Resolves supertypes. After this the index is immutable and shared by readers. */ void cbm_oo_index_seal(cbm_oo_index_t *index); void cbm_oo_index_free(cbm_oo_index_t *index); diff --git a/internal/cbm/resolve/capabilities.c b/internal/cbm/resolve/capabilities.c index 64dd8021e..b69b994a6 100644 --- a/internal/cbm/resolve/capabilities.c +++ b/internal/cbm/resolve/capabilities.c @@ -414,12 +414,26 @@ bool cbm_resolve_capabilities(CBMLanguage language, cbm_resolve_capabilities_t * out->limitation = "The declaration static rules select: lexical scopes, import precedence, inherited " "members over resolved supertypes, arity, known argument types and exact overload " - "matches. Competing overloads on different nodes, lambdas with an unreadable " + "matches. A lambda's parameters come from the selected declaration's functional type, " + "and a generic method's type arguments from what one argument settles. A name several " + "declarations claim means the one in the file itself or the only one in the file's " + "build project; any other reader abstains. A C# reference assembly's declaration or " + "platform stub stands behind the implementation of its name, and a partial type's " + "mentions target the node of the file named after it. A function or extension method " + "in " + "another project's test sources is no candidate. Competing overloads on different " + "nodes (one signature in two platform files among them), lambdas with an unreadable " "receiver, members hidden by an unindexed supertype and unmodelled inference abstain. " "A type-shaped name is assumed not to be a field of an unindexed supertype. Implicit " "iterator and componentN calls share one span and are not emitted. A bare call inside " "an extension on an external type abstains, because that type's members are unlisted. " - "A C# file the grammar cannot read is read without its later conditional branches."; + "A C# file the grammar cannot read is read without its later conditional branches, and " + "with the spellings it loses members to made readable at the same spans. A C# local " + "function's body belongs to the member around it, a property's to the property, and a " + "conversion operator and an indexer have nodes of their own. C# member lookup: an " + "explicit interface implementation and an interface's members are no class's, an " + "applicable method hides the base type's, and an identical parameter is the better " + "conversion."; break; case CBM_LANG_RUST: out->adapter = "rust"; diff --git a/src/discover/language.c b/src/discover/language.c index bfd76a070..595d70ac7 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -337,6 +337,7 @@ static const ext_entry_t EXT_TABLE[] = { * .NET repository's package references, target frameworks, build hooks and * project layout — none of which were reachable while these were unmapped. */ {".csproj", CBM_LANG_XML}, + {".projitems", CBM_LANG_XML}, {".vbproj", CBM_LANG_XML}, {".fsproj", CBM_LANG_XML}, {".props", CBM_LANG_XML}, diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index fd2bef9a0..788e1078a 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -2494,6 +2494,129 @@ void cbm_pxc_dispatch_file_with_php_bindings( rust_shared_ctx, bindings, NULL); } +/* The .NET convention for a reference assembly: /ref holds a project of its own beside + * a /src tree. Both compile one assembly name, so a type they share is one type: the + * reference declares its contract, the implementation (there, or forwarded to another project) + * its members. Returns the set of such `ref` directories; each path adds its own segments, + * so the cost is the length of the file list. NULL when the repository has none. */ +static CBMHashTable *pxc_csharp_reference_dirs(CBMArena *arena, const cbm_file_info_t *files, + int file_count) { + CBMHashTable *libraries = NULL, *references = NULL; + for (int i = 0; i < file_count; i++) { + const char *rel = files[i].rel_path; + if (files[i].language != CBM_LANG_CSHARP || !rel) + continue; + for (const char *at = strstr(rel, "/src/"); at; at = strstr(at + 1, "/src/")) { + if (!libraries) + libraries = cbm_ht_create(64); + const char *library = + libraries ? cbm_arena_strndup(arena, rel, (size_t)(at - rel)) : NULL; + if (library && !cbm_ht_get(libraries, library)) + cbm_ht_set(libraries, library, (void *)library); + } + } + for (int i = 0; libraries && i < file_count; i++) { + const char *rel = files[i].rel_path; + size_t length = rel ? strlen(rel) : 0; + if (length < sizeof(".csproj") || strcmp(rel + length - (sizeof(".csproj") - 1), ".csproj")) + continue; + const char *slash = strrchr(rel, '/'); + const char *dir = slash ? cbm_arena_strndup(arena, rel, (size_t)(slash - rel)) : NULL; + const char *base = dir ? strrchr(dir, '/') : NULL; + if (!base || strcmp(base, "/ref")) + continue; + const char *library = cbm_arena_strndup(arena, dir, (size_t)(base - dir)); + if (!library || !cbm_ht_get(libraries, library)) + continue; + if (!references) + references = cbm_ht_create(64); + if (references && !cbm_ht_get(references, dir)) + cbm_ht_set(references, dir, (void *)dir); + } + cbm_ht_free(libraries); + return references; +} + +/* A declaration that is the contract of an implementation: a reference assembly's, or one in a + * `.PlatformNotSupported.cs`, the .NET convention for what a target without the feature + * compiles instead, with every body throwing. A reference directory's ancestry is bounded by the + * path's depth. */ +static bool pxc_in_reference_dir(CBMArena *arena, const CBMHashTable *references, const char *rel) { + static const char STUB[] = ".PlatformNotSupported.cs"; + size_t length = rel ? strlen(rel) : 0; + if (length >= sizeof(STUB) && !strcmp(rel + length - (sizeof(STUB) - 1), STUB)) + return true; + if (!references || !rel) + return false; + char *dir = cbm_arena_strdup(arena, rel); + for (char *slash = dir ? strrchr(dir, '/') : NULL; slash; slash = strrchr(dir, '/')) { + *slash = '\0'; + if (cbm_ht_get(references, dir)) + return true; + } + return false; +} + +/* The directories holding a build project's manifest. One pass over the file list. "." is the root. + */ +static CBMHashTable *pxc_project_dirs(CBMArena *arena, const cbm_file_info_t *files, + int file_count) { + CBMHashTable *dirs = cbm_ht_create(64); + for (int i = 0; dirs && i < file_count; i++) { + const char *rel = files[i].rel_path; + const char *slash = rel ? strrchr(rel, '/') : NULL; + const char *base = slash ? slash + 1 : rel; + size_t length = base ? strlen(base) : 0; + bool manifest = base && (!strcmp(base, "build.gradle") || + !strcmp(base, "build.gradle.kts") || !strcmp(base, "pom.xml") || + (length > 7 && !strcmp(base + length - 7, ".csproj")) || + (length > 10 && !strcmp(base + length - 10, ".projitems"))); + if (!manifest) + continue; + const char *dir = slash ? cbm_arena_strndup(arena, rel, (size_t)(slash - rel)) : "."; + if (dir && !cbm_ht_get(dirs, dir)) + cbm_ht_set(dirs, dir, (void *)dir); + } + return dirs; +} + +/* The nearest manifest's directory above this file. Bounded by the path's depth. */ +static const char *pxc_project_of(CBMArena *arena, const CBMHashTable *dirs, const char *rel) { + if (!dirs || !rel) + return NULL; + char *dir = cbm_arena_strdup(arena, rel); + for (char *slash = dir ? strrchr(dir, '/') : NULL; slash; slash = strrchr(dir, '/')) { + *slash = '\0'; + const char *project = cbm_ht_get(dirs, dir); + if (project) + return project; + } + return cbm_ht_get(dirs, "."); +} + +static void pxc_oo_add_file(const cbm_pipeline_ctx_t *ctx, cbm_oo_index_t *index, + CBMFileResult **cache, const cbm_file_info_t *files, int i, + CBMArena *scratch, const CBMHashTable *projects) { + bool loaded = false; + CBMFileResult *result = cbm_pipeline_result_acquire(ctx, cache, i, NULL, &loaded); + if (!result) + return; + int source_len = result->source_len; + const char *source = result->source; + char *owned = NULL; + if (!source || source_len <= 0) { + owned = pxc_read_file(files[i].path, &source_len); + source = owned; + } + if (source && source_len > 0) { + cbm_oo_index_set_project(index, pxc_project_of(scratch, projects, files[i].rel_path)); + (void)cbm_oo_index_add_file(index, files[i].language, files[i].rel_path, result, source, + source_len); + } + cbm_free(CBM_MEM_CLASS_EXTRACT, owned); + cbm_pipeline_result_release(result, loaded); +} + struct cbm_oo_index *cbm_pxc_build_oo_index(const cbm_pipeline_ctx_t *ctx, CBMArena *arena, CBMFileResult **cache, const cbm_file_info_t *files, int file_count) { @@ -2505,26 +2628,33 @@ struct cbm_oo_index *cbm_pxc_build_oo_index(const cbm_pipeline_ctx_t *ctx, CBMAr cbm_oo_index_t *index = cbm_oo_index_new(arena); if (!index) return NULL; + CBMArena scratch; + cbm_arena_init(&scratch); + CBMHashTable *references = pxc_csharp_reference_dirs(&scratch, files, file_count); + CBMHashTable *projects = pxc_project_dirs(&scratch, files, file_count); + bool *deferred = cbm_arena_calloc(&scratch, (size_t)file_count * sizeof(bool)); + bool any_deferred = false; for (int i = 0; i < file_count; i++) { if (!cbm_oo_language(files[i].language)) continue; - bool loaded = false; - CBMFileResult *result = cbm_pipeline_result_acquire(ctx, cache, i, NULL, &loaded); - if (!result) + if (deferred && files[i].language == CBM_LANG_CSHARP && + pxc_in_reference_dir(&scratch, references, files[i].rel_path)) { + deferred[i] = true; + any_deferred = true; continue; - int source_len = result->source_len; - const char *source = result->source; - char *owned = NULL; - if (!source || source_len <= 0) { - owned = pxc_read_file(files[i].path, &source_len); - source = owned; } - if (source && source_len > 0) - (void)cbm_oo_index_add_file(index, files[i].language, files[i].rel_path, result, source, - source_len); - cbm_free(CBM_MEM_CLASS_EXTRACT, owned); - cbm_pipeline_result_release(result, loaded); + pxc_oo_add_file(ctx, index, cache, files, i, &scratch, projects); } + /* An implementation is in before any contract asks whether it exists. */ + if (any_deferred) { + cbm_oo_index_begin_reference_files(index); + for (int i = 0; i < file_count; i++) + if (deferred[i]) + pxc_oo_add_file(ctx, index, cache, files, i, &scratch, projects); + } + cbm_ht_free(references); + cbm_ht_free(projects); + cbm_arena_destroy(&scratch); cbm_oo_index_seal(index); return index; } diff --git a/tests/test_doc_mentions.c b/tests/test_doc_mentions.c index 727c866d6..7354f0426 100644 --- a/tests/test_doc_mentions.c +++ b/tests/test_doc_mentions.c @@ -235,7 +235,7 @@ static const struct { static bool dm_namespace_source(char *out, size_t cap, size_t index, bool file_scoped) { const char *recovery = dm_namespace_cases[index].recovery ? "public class ParseEdge { unsafe void M(void* p) { " - "_r = ref *(int*)p; } }\n" + "_r = ref * (int*)p; } }\n" : ""; int n = snprintf(out, cap, "namespace %s%s" @@ -509,7 +509,7 @@ TEST(doc_mentions_cs_namespace_boundaries) { const char *id, *header, *footer, *name; } cases[] = { {"bare_keyword", "namespace class{\n", - "public class ParseEdge { unsafe void M(void* p) { _r = ref *(int*)p; } }\n}\n", NULL}, + "public class ParseEdge { unsafe void M(void* p) { _r = ref * (int*)p; } }\n}\n", NULL}, {"adjacent_file", "namespace@Boundary;\n", "", "Boundary"}, {"adjacent_block", "namespace@Boundary {\n", "}\n", "Boundary"}, {"verbatim_keyword_file", "namespace @class;\n", "", "class"}, @@ -1001,14 +1001,15 @@ TEST(doc_mentions_cs_utf8_surfaces) { * recovery closes blocks early and late, which would otherwise move the * declarations after the error into another namespace or outer type. */ TEST(doc_mentions_cs_scope_parse_errors) { - /* `ref *(int*)p` is beyond the vendored grammar: the method swallows the + /* `ref * (int*)p` is beyond the vendored grammar, and the parse view reads it + * only without the space: the method swallows the * class's closing brace, `P` becomes a local, `Two` a nested class of * `One`, and the namespace an error node. */ const char *late = "namespace A\n" /* 1 */ "{\n" /* 2 */ " public class One\n" /* 3 */ " {\n" /* 4 */ - " unsafe void M(void* p) { _r = ref *(int*)p; }\n" /* 5 */ + " unsafe void M(void* p) { _r = ref * (int*)p; }\n" /* 5 */ " public int P;\n" /* 6 */ " }\n" /* 7 */ " public class Two { }\n" /* 8 */ @@ -1121,7 +1122,8 @@ TEST(doc_mentions_cs_scope_parse_errors) { s = r->doc_scope; ASSERT_NOT_NULL(s); ASSERT_NOT_NULL(strstr(s, "R\t1\t0\t1\t9\tNet\n")); - ASSERT_NOT_NULL(strstr(s, "T\t1\t3\t7\tc!\t-\tVerb\t\t")); + /* The parse view blanks the doubled quotes, so nothing of Verb is hidden. */ + ASSERT_NOT_NULL(strstr(s, "T\t1\t3\t7\tc\t-\tVerb\t\t")); ASSERT_NOT_NULL(strstr(s, "T\t1\t8\t8\tc")); ASSERT_NOT_NULL(strstr(s, "\tAfter\t\t")); ASSERT_NULL(strstr(s, "\nX\t")); @@ -1171,12 +1173,12 @@ TEST(doc_mentions_cs_scope_parse_errors) { cbm_free_result(r); /* A member whose header did not parse is hidden, and its type says so: - * `safe extern` comes back as a method named `extern`. */ + * an unknown modifier comes back as a return type, and `extern` as the method. */ const char *hidden = "namespace C\n" "{\n" " public class Holey\n" /* 3 */ " {\n" - " public safe extern int Hidden();\n" /* 5 */ + " public fancy extern int Hidden();\n" /* 5 */ " public int Seen;\n" /* 6 */ " }\n" /* 7 */ "}\n"; @@ -3917,7 +3919,7 @@ static void dm_write_resolver_fixture(const char *tmp) { " }\n" " public class Holey\n" " {\n" - " public safe extern int Hidden();\n" + " public fancy extern int Hidden();\n" " /// \n" " public int Seen;\n" " }\n" @@ -6037,7 +6039,7 @@ TEST(doc_mentions_cs_scan_nested_holes) { "{\n" " public class Deep\n" /* 3 */ " {\n" - " unsafe void E(void* p) { _r = ref *(int*)p; }\n" + " unsafe void E(void* p) { _r = ref * (int*)p; }\n" " void M() { s = $\"a{$\"b{$\"c{1}\"}\"}\"; }\n" " public int Q;\n" /* 7 */ " }\n" /* 8 */ @@ -6060,7 +6062,7 @@ TEST(doc_mentions_cs_scan_nested_holes) { "{\n" " public class Deep\n" " {\n" - " unsafe void E(void* p) { _r = ref *(int*)p; }\n" + " unsafe void E(void* p) { _r = ref * (int*)p; }\n" " void M() { s = ", "$\"{", DM_DEEP, closers); free(closers); diff --git a/tests/test_parallel.c b/tests/test_parallel.c index c9df869e1..eeebc3d1a 100644 --- a/tests/test_parallel.c +++ b/tests/test_parallel.c @@ -3511,14 +3511,11 @@ TEST(parallel_csharp_same_name_class_binds_by_namespace) { .site = {.file = rels[11], .caller = "PartUser.Use", .needle = "new Thing()"}, .relation = "CALLS", .target = {.file = rels[9], + .suffix = "cbm_cs_same_name.src.Parts.Thing.A.Thing", + .label = "Class", .declaration_line = 2, .declaration_text = "public partial class Thing { public void Alpha() { } }"}, - .mode = RQ_KNOWN_GAP, - .gap_id = "csharp-partial-type-identity", - .why = "Each partial piece of Thing has a node of its own (its QN carries its file), so " - "no one class node is the type this constructor call selects.", - .tried = "The members bind to the piece that declares them. The type itself has no single " - "identity to bind to, and choosing a piece would be a guess."}; + .mode = RQ_REQUIRED}; RQ_Report quality[2] = {0}; bool ok[2][N_CHECKS]; bool all = true; @@ -3528,8 +3525,8 @@ TEST(parallel_csharp_same_name_class_binds_by_namespace) { g ? "csharp-members-parallel" : "csharp-members-sequential", sources, N_FILES, expected, N_EXPECTED, &quality[g])); ASSERT_EQ(quality[g].genuine, N_EXPECTED); - ASSERT_EQ(quality[g].found, N_EXPECTED - 1); - ASSERT_EQ(quality[g].missing, 1); + ASSERT_EQ(quality[g].found, N_EXPECTED); + ASSERT_EQ(quality[g].missing, 0); ok[g][0] = has_edge_from_callable_to_qn(gb, "Handler.Handle", bo, "CALLS"); ok[g][1] = !has_edge_from_callable_to_qn(gb, "Handler.Handle", pf, "CALLS"); ok[g][2] = has_edge_from_callable_to_qn(gb, "PlatformHandler.Handle", pf, "CALLS"); diff --git a/tests/test_resolver_kotlin.c b/tests/test_resolver_kotlin.c index e3f50f36d..9ccb487a4 100644 --- a/tests/test_resolver_kotlin.c +++ b/tests/test_resolver_kotlin.c @@ -630,7 +630,9 @@ static const char KOTLIN_DIRECT_8[] = "package duplicate\n" "class Duplicate {\n" " fun same(value: Int): Int = value\n" - "}\n" + "}\n"; +static const char KOTLIN_DIRECT_8_USE[] = + "package duplicate\n" "fun invalid(value: Duplicate, number: Int): Int = value.same(number)\n"; static const char KOTLIN_DIRECT_9[] = "package duplicate\n" "class Duplicate {\n" @@ -976,7 +978,7 @@ static const RQ_Forbidden KOTLIN_DIRECT_FORBIDDEN[] = { .relation = "CALLS", .any_target = true}, {.case_id = "duplicate-class-domain", - .site = {.file = "Duplicate.kt", + .site = {.file = "DuplicateUse.kt", .caller = "invalid", .needle = "value.same(number)", .occurrence_index = 0}, @@ -1000,6 +1002,7 @@ TEST(resolver_kotlin_primitive_arguments_and_lexical_members_require_proofs) { {.path = "Shadow.kt", .text = KOTLIN_DIRECT_6}, {.path = "Alias.kt", .text = KOTLIN_DIRECT_7}, {.path = "Duplicate.kt", .text = KOTLIN_DIRECT_8}, + {.path = "DuplicateUse.kt", .text = KOTLIN_DIRECT_8_USE}, {.path = "DuplicateOther.kt", .text = KOTLIN_DIRECT_9}, {.path = "Decoy.kt", .text = KOTLIN_DIRECT_10}, }; diff --git a/tests/test_resolver_oo.c b/tests/test_resolver_oo.c index 343447f5f..9b4733837 100644 --- a/tests/test_resolver_oo.c +++ b/tests/test_resolver_oo.c @@ -875,6 +875,1878 @@ TEST(resolver_oo_csharp_repaired_parse_and_exact_overloads_parallel) { PASS(); } +/* A .NET reference assembly: /ref is a project of its own beside /src, and + * declares one assembly's public surface with `throw null` bodies (dotnet/runtime). A caller + * means the type, whichever of the two the compiler read. The decoys are a `ref` directory + * that is no project, and a `ref` project with nothing beside it: both are plain duplicates. */ +static const OOFile REFERENCE_FILES[] = { + {"src/libraries/System.Demo/ref/System.Demo.csproj", + "" + "\n"}, + {"src/libraries/System.Demo/ref/System.Demo.cs", + "namespace System.Demo {\n" + " public partial class Widget {\n" + " public Widget() { }\n" + " public int Count { get { throw null; } }\n" + " public void Add(int value) { }\n" + " public static System.Demo.Widget Create() { throw null; }\n" + " public partial class Nested { public void Inner() { } }\n" + " }\n" + " public static partial class WidgetExtensions {\n" + " public static void Twist(this System.Demo.Widget widget) { }\n" + " }\n" + " public partial class OnlyInReference { public void Solo() { } }\n" + "}\n"}, + {"src/libraries/System.Demo/src/System.Demo.csproj", + "" + "\n"}, + {"src/libraries/System.Demo/src/System/Demo/Widget.cs", + "namespace System.Demo {\n" + " public class Widget {\n" + " public Widget() { }\n" + " public int Count => 0;\n" + " public void Add(int value) { }\n" + " public static Widget Create() { return new Widget(); }\n" + " public class Nested { public void Inner() { } }\n" + " }\n" + " public static class WidgetExtensions {\n" + " public static void Twist(this Widget widget) { }\n" + " }\n" + "}\n"}, + {"src/libraries/System.Demo/tests/WidgetTests.cs", + "using System.Demo;\n" + "namespace Acme.Tests {\n" + " public class WidgetTests {\n" + " void Constructs() { var made = new Widget(); }\n" + " void Calls(Widget widget) { widget.Add(1); }\n" + " void Static() { Widget.Create(); }\n" + " void Reads(Widget widget) { var count = widget.Count; }\n" + " void Extension(Widget widget) { widget.Twist(); }\n" + " void Inner(Widget.Nested nested) { nested.Inner(); }\n" + " void Solo(OnlyInReference only) { only.Solo(); }\n" + " }\n" + "}\n"}, + /* A directory that merely has this name. */ + {"plain/ref/Thing.cs", "namespace Acme.Plain { public class Thing { public void Go() { } } }\n"}, + {"plain/src/Thing.cs", "namespace Acme.Plain { public class Thing { public void Go() { } } }\n"}, + {"plain/Use.cs", + "using Acme.Plain;\n" + "namespace Acme.Use { public class Use { void Plain(Thing thing) { thing.Go(); } } }\n"}, + /* A project called ref with no implementation tree beside it. */ + {"lonely/ref/Lonely.csproj", + "\n"}, + {"lonely/ref/Thing.cs", + "namespace Acme.Lonely { public class Thing { public void Go() { } } }\n"}, + {"other/Thing.cs", "namespace Acme.Lonely { public class Thing { public void Go() { } } }\n"}, + {"lonely/Use.cs", + "using Acme.Lonely;\n" + "namespace Acme.Use { public class UseLonely { void Lonely(Thing thing) { thing.Go(); } } }\n"}, +}; + +static int oo_reference_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, REFERENCE_FILES, + (int)(sizeof(REFERENCE_FILES) / sizeof(REFERENCE_FILES[0])), parallel, "cs")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "WidgetTests.Calls", "src.System.Demo.Widget.Widget.Add", 1); + OO_EXPECT("CALLS", "WidgetTests.Calls", "ref.System.Demo.Widget.Add", 0); + OO_EXPECT("CALLS", "WidgetTests.Static", "src.System.Demo.Widget.Widget.Create", 1); + OO_EXPECT("CALLS", "WidgetTests.Static", "ref.System.Demo.Widget.Create", 0); + OO_EXPECT("CALLS", "WidgetTests.Constructs", "src.System.Demo.Widget.Widget.Widget", 1); + OO_EXPECT("CALLS", "WidgetTests.Constructs", "ref.System.Demo.Widget.Widget", 0); + OO_EXPECT("CALLS", "WidgetTests.Extension", "src.System.Demo.Widget.WidgetExtensions.Twist", 1); + OO_EXPECT("CALLS", "WidgetTests.Extension", "ref.System.Demo.WidgetExtensions.Twist", 0); + OO_EXPECT("CALLS", "WidgetTests.Inner", "src.System.Demo.Widget.Widget.Nested.Inner", 1); + OO_EXPECT("USAGE", "WidgetTests.Reads", "src.System.Demo.Widget.Widget.Count", 1); + OO_EXPECT("USAGE", "WidgetTests.Reads", "ref.System.Demo.Widget.Count", 0); + /* The sole declaration of its name is the target, stub or not. */ + OO_EXPECT("CALLS", "WidgetTests.Solo", "ref.System.Demo.OnlyInReference.Solo", 1); + /* Neither of these is a reference assembly, so nothing tells the two apart. */ + OO_EXPECT("CALLS", "Use.Plain", "ref.Thing.Thing.Go", 0); + OO_EXPECT("CALLS", "Use.Plain", "src.Thing.Thing.Go", 0); + OO_EXPECT("CALLS", "UseLonely.Lonely", "lonely.ref.Thing.Thing.Go", 0); + OO_EXPECT("CALLS", "UseLonely.Lonely", "other.Thing.Thing.Go", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_csharp_reference_assembly_stands_behind) { + ASSERT_EQ(oo_reference_check(false), 0); + PASS(); +} + +TEST(resolver_oo_csharp_reference_assembly_stands_behind_parallel) { + ASSERT_EQ(oo_reference_check(true), 0); + PASS(); +} + +/* `suspend` is a type modifier in front of the function type, as on every lambda parameter of + * Exposed's coroutine API: `statement: suspend R2dbcTransaction.(row: Row) -> Unit`. The lambda + * still has that receiver and those parameters. */ +static const OOFile SUSPEND_FILES[] = { + {"acme/Scopes.kt", + "package acme\n" + "class Tx { fun commit() {} }\n" + "class Other { fun commit() {} }\n" + "class Row { fun cell() {} }\n" + "fun Tx.helper() {}\n" + "fun Other.helper() {}\n" + "fun withSuspend(statement: suspend Tx.(row: Row) -> Unit) {}\n" + "fun withBare(statement: suspend (Row) -> Unit) {}\n" + "fun withDefaulted(limit: Int = 0, statement: suspend Tx.(Row) -> Unit) {}\n" + "class Annotated(val handler: @Marker Row)\n" + "annotation class Marker\n"}, + {"acme/Main.kt", + "package acme\n" + "class Main {\n" + " fun commit() {}\n" + " fun viaReceiver() { withSuspend { row -> commit() } }\n" + " fun viaParameter() { withSuspend { row -> row.cell() } }\n" + " fun viaExtension() { withSuspend { row -> helper() } }\n" + " fun viaIt() { withBare { it.cell() } }\n" + " fun viaDefaulted() { withDefaulted { it.cell() } }\n" + " fun viaModifiedProperty(a: Annotated) { a.handler.cell() }\n" + " fun plainLambdaHasNoReceiver() { withBare { commit() } }\n" + "}\n"}, +}; + +static int oo_suspend_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, SUSPEND_FILES, (int)(sizeof(SUSPEND_FILES) / sizeof(SUSPEND_FILES[0])), + parallel, "kt")) { + oo_close(&index); + return 1; + } + /* The function type's receiver is the innermost one, ahead of the enclosing class. */ + OO_EXPECT("CALLS", "Main.viaReceiver", "Scopes.Tx.commit", 1); + OO_EXPECT("CALLS", "Main.viaReceiver", "Main.Main.commit", 0); + OO_EXPECT("CALLS", "Main.viaReceiver", "Scopes.Other.commit", 0); + OO_EXPECT("CALLS", "Main.viaParameter", "Scopes.Row.cell", 1); + /* An extension applies to the receiver the lambda has, not to a namesake's. */ + OO_EXPECT("CALLS", "Main.viaExtension", "Scopes.helper", 1); + OO_EXPECT("CALLS", "Main.viaIt", "Scopes.Row.cell", 1); + OO_EXPECT("CALLS", "Main.viaDefaulted", "Scopes.Row.cell", 1); + OO_EXPECT("CALLS", "Main.viaModifiedProperty", "Scopes.Row.cell", 1); + /* No receiver in the function type: the enclosing class answers. */ + OO_EXPECT("CALLS", "Main.plainLambdaHasNoReceiver", "Main.Main.commit", 1); + OO_EXPECT("CALLS", "Main.plainLambdaHasNoReceiver", "Scopes.Tx.commit", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_kotlin_suspend_function_type) { + ASSERT_EQ(oo_suspend_check(false), 0); + PASS(); +} + +TEST(resolver_oo_kotlin_suspend_function_type_parallel) { + ASSERT_EQ(oo_suspend_check(true), 0); + PASS(); +} + +/* A type mention at class level and a generic head, in Java and C#. Forms from Elasticsearch + * (`private final RemoteConnectionManager m;`, `ActionListener listener`) and + * System.Text.Json. A type parameter of the same name is not the class. */ +static const OOFile JAVA_TYPE_FILES[] = { + {"acme/fields/Main.java", + "package acme.fields;\n" + "import acme.other.Imported;\n" + "import java.util.List;\n" + "public class Main {\n" + " private final Service service = null;\n" + " private Holder generic;\n" + " static Imported imported;\n" + " Holder signature(Holder parameter, List external) {\n" + " Holder local = parameter;\n" + " return local;\n" + " }\n" + "}\n" + "class Service {}\n" + "class Holder {}\n" + "class Box {\n" + " Service held;\n" + " Service give(Service value) { return value; }\n" + "}\n"}, + {"acme/other/Imported.java", "package acme.other;\npublic class Imported {}\n"}, + {"acme/other/Service.java", "package acme.other;\npublic class Service {}\n"}, + {"acme/plain/Bare.java", + "package acme.plain;\n" + "class Bare {\n" + " private final OnlyHere here = null;\n" + "}\n" + "class OnlyHere {}\n"}, +}; + +static int oo_java_type_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, JAVA_TYPE_FILES, + (int)(sizeof(JAVA_TYPE_FILES) / sizeof(JAVA_TYPE_FILES[0])), parallel, "java")) { + oo_close(&index); + return 1; + } + OO_EXPECT("USAGE", "Bare.java.__file__", "acme.plain.OnlyHere", 1); + OO_EXPECT("USAGE", "Main.java.__file__", "acme.fields.Holder", 1); + OO_EXPECT("USAGE", "Main.java.__file__", "acme.other.Imported", 1); + OO_EXPECT("USAGE", "Main.java.__file__", "acme.other.Service", 0); + OO_EXPECT("USES_TYPE", "Main.signature", "acme.fields.Holder", 1); + OO_EXPECT("USES_TYPE", "Main.signature", "acme.fields.Service", 1); + OO_EXPECT("USAGE", "Main.signature", "acme.fields.Holder", 1); + /* `Service` inside Box is Box's own type parameter. */ + OO_EXPECT("USES_TYPE", "Box.give", "acme.fields.Service", 0); + OO_EXPECT("USAGE", "Box.give", "acme.fields.Service", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_java_class_level_and_generic_type_mentions) { + ASSERT_EQ(oo_java_type_check(false), 0); + PASS(); +} + +TEST(resolver_oo_java_class_level_and_generic_type_mentions_parallel) { + ASSERT_EQ(oo_java_type_check(true), 0); + PASS(); +} + +static const OOFile CS_TYPE_FILES[] = { + {"Gen/Main.cs", + "using Acme.Other;\n" + "namespace Acme.Gen {\n" + " public class Service {}\n" + " public class Holder {}\n" + " public class Main {\n" + " Holder Signature(Holder parameter) {\n" + " Holder local = parameter;\n" + " return local;\n" + " }\n" + " System.Collections.Generic.List External() { return null; }\n" + " Imported Elsewhere() { return null; }\n" + " }\n" + " public class Box {\n" + " Service Give(Service value) { return value; }\n" + " }\n" + " public class Twin {\n" + " Holder Arity() { return null; }\n" + " }\n" + "}\n"}, + {"Gen/Imported.cs", "namespace Acme.Other { public class Imported {} }\n"}, + {"Gen/Plain.cs", "namespace Acme.Gen { public class Holder {} }\n"}, +}; + +static int oo_csharp_type_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, CS_TYPE_FILES, (int)(sizeof(CS_TYPE_FILES) / sizeof(CS_TYPE_FILES[0])), + parallel, "cs")) { + oo_close(&index); + return 1; + } + OO_EXPECT("USES_TYPE", "Main.Signature", "Main.Holder", 1); + OO_EXPECT("USAGE", "Main.Signature", "Main.Holder", 1); + OO_EXPECT("USAGE", "Main.Signature", "Main.Service", 1); + /* `Holder` and `Holder` are two types: the arity selects. */ + OO_EXPECT("USES_TYPE", "Main.Signature", "Plain.Holder", 0); + OO_EXPECT("USES_TYPE", "Twin.Arity", "Plain.Holder", 1); + OO_EXPECT("USES_TYPE", "Twin.Arity", "Main.Holder", 0); + OO_EXPECT("USES_TYPE", "Main.Elsewhere", "Imported.Imported", 1); + OO_EXPECT("USES_TYPE", "Box.Give", "Main.Service", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_csharp_generic_type_mentions) { + ASSERT_EQ(oo_csharp_type_check(false), 0); + PASS(); +} + +TEST(resolver_oo_csharp_generic_type_mentions_parallel) { + ASSERT_EQ(oo_csharp_type_check(true), 0); + PASS(); +} + +/* A lambda's parameters take their types from the declaration the call selects: a project functional + * interface with one abstract method, or a JDK one the declaring file imports. Forms from Elasticsearch: + * `checker.register("x", b -> b.field(...))`, `listener.delegateFailure((l, r) -> l.onResponse(r))`, + * `map.forEach((k, v) -> v.go())`, `entry.getValue().go()`. */ +static const OOFile JAVA_LAMBDA_FILES[] = { + {"acme/lam/Types.java", + "package acme.lam;\n" + "import java.util.List;\n" + "import java.util.Map;\n" + "import java.util.function.Consumer;\n" + "import java.util.function.BiConsumer;\n" + "import acme.fake.Handler;\n" + "public class Types {\n" + " public interface Check {\n" + " void accept(T value) throws E;\n" + " default Check then() { return this; }\n" + " boolean equals(Object other);\n" + " }\n" + " public interface TwoMethods { void one(T a); void two(T a); }\n" + " public interface Inherited extends Check {}\n" + " public interface Replaced extends Check {\n" + " default void accept(T value) {}\n" + " void other(Doc doc);\n" + " }\n" + " public static abstract class NotFunctional { abstract void run(T a); }\n" + " public static class Doc { public void go() {} public Doc next() { return this; } }\n" + " public static class Other { public void go() {} }\n" + " public void check(Check c) {}\n" + " public void jdk(Consumer c) {}\n" + " public void jdkSuper(Consumer c) {}\n" + " public void jdkTwo(BiConsumer c) {}\n" + " public void two(TwoMethods t) {}\n" + " public void abstractClass(NotFunctional t) {}\n" + " public void inherited(Inherited i) {}\n" + " public void replaced(Replaced r) {}\n" + " public void fake(Handler h) {}\n" + " public void generic(Consumer c) {}\n" + " public void each(List items, Consumer c) {}\n" + " public U first(List items) { return null; }\n" + " public void pick(U one, U two, Consumer c) {}\n" + " public Consumer makes() { return d -> d.go(); }\n" + " public Consumer kept = d -> d.next();\n" + " public void overload(Consumer c) {}\n" + " public void overload(String notAFunction) {}\n" + " public void varargs(Consumer... all) {}\n" + " public Types(Consumer c) {}\n" + " public Types() {}\n" + "}\n"}, + {"acme/lam/Use.java", + "package acme.lam;\n" + "import java.util.List;\n" + "import java.util.Map;\n" + "import java.util.Optional;\n" + "import acme.lam.Types.Doc;\n" + "import acme.lam.Types.Other;\n" + "public class Use {\n" + " Types t;\n" + " List docs;\n" + " Map byName;\n" + " void viaCheck() { t.check(d -> d.go()); }\n" + " void viaBlock() { t.check(d -> { d.next().go(); }); }\n" + " void viaJdk() { t.jdk(d -> d.go()); }\n" + " void viaJdkSuper() { t.jdkSuper(d -> d.go()); }\n" + " void viaJdkTwo() { t.jdkTwo((d, o) -> o.go()); }\n" + " void viaInherited() { t.inherited(d -> d.go()); }\n" + " void viaOverload() { t.overload(d -> d.go()); }\n" + " void viaVarargs() { t.varargs(d -> d.go()); }\n" + " void viaConstructor() { new Types(d -> d.go()); }\n" + " void viaWritten() { t.jdk((Doc d) -> d.go()); }\n" + " void viaForEach() { docs.forEach(d -> d.go()); }\n" + " void viaStream() { docs.stream().filter(d -> d.next() != null).forEach(d -> d.go()); }\n" + " void viaMap() { docs.stream().map(d -> d.next()).forEach(n -> n.go()); }\n" + " void viaMapForEach() { byName.forEach((k, v) -> v.go()); }\n" + " void viaOptional(Optional o) { o.ifPresent(d -> d.go()); }\n" + " void viaEntries() { for (Map.Entry e : byName.entrySet()) { e.getValue().go(); } }\n" + " void viaValues() { for (var d : byName.values()) { d.go(); } }\n" + " void viaInference() { t.each(docs, d -> d.go()); }\n" + " void viaInferredResult() { t.first(docs).go(); }\n" + " void disagreeing(Doc d1, Other o1) { t.pick(d1, o1, x -> x.go()); }\n" + " void viaLocal() { java.util.function.Consumer c = d -> d.go(); }\n" + " void twoAbstract() { t.two(d -> d.go()); }\n" + " void classTarget() { t.abstractClass(d -> d.go()); }\n" + " void replacedTarget() { t.replaced(d -> d.go()); }\n" + " void lookalike() { t.fake(d -> d.go()); }\n" + " void unboundParameter() { t.generic(d -> d.go()); }\n" + " void shadowedParameter(Other d) { t.check(d2 -> d.go()); }\n" + "}\n"}, + {"acme/fake/Handler.java", + "package acme.fake;\n" + "public abstract class Handler { public abstract void handle(Object o); }\n"}, + /* A library's own Consumer takes whatever it takes: nothing here says it is the JDK's. */ + {"acme/lam/Foreign.java", + "package acme.lam;\n" + "import org.other.Consumer;\n" + "public class Foreign {\n" + " public void take(Consumer c) {}\n" + " void foreignConsumer() { take(d -> d.go()); }\n" + "}\n"}, +}; + +static int oo_java_lambda_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, JAVA_LAMBDA_FILES, + (int)(sizeof(JAVA_LAMBDA_FILES) / sizeof(JAVA_LAMBDA_FILES[0])), parallel, + "java")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "Use.viaCheck", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaBlock", "Types.Doc.next", 1); + OO_EXPECT("CALLS", "Use.viaBlock", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaJdk", "Types.Doc.go", 1); + /* JLS 9.9: `? super Doc` gives the lambda a Doc. */ + OO_EXPECT("CALLS", "Use.viaJdkSuper", "Types.Doc.go", 1); + /* The second parameter is the second argument's type. */ + OO_EXPECT("CALLS", "Use.viaJdkTwo", "Types.Other.go", 1); + OO_EXPECT("CALLS", "Use.viaJdkTwo", "Types.Doc.go", 0); + OO_EXPECT("CALLS", "Use.viaInherited", "Types.Doc.go", 1); + /* A lambda is no String, so that overload is out. */ + OO_EXPECT("CALLS", "Use.viaOverload", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaVarargs", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaConstructor", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaWritten", "Types.Doc.go", 1); + /* JDK collections on a known element type. */ + OO_EXPECT("CALLS", "Use.viaForEach", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaStream", "Types.Doc.next", 1); + OO_EXPECT("CALLS", "Use.viaStream", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaMap", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaMapForEach", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaOptional", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaEntries", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaValues", "Types.Doc.go", 1); + /* One argument settles the method's type parameter. Two that disagree settle nothing. */ + OO_EXPECT("CALLS", "Use.viaInference", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaInferredResult", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.disagreeing", "Types.Doc.go", 0); + OO_EXPECT("CALLS", "Use.disagreeing", "Types.Other.go", 0); + /* A declared target needs no call. */ + OO_EXPECT("CALLS", "Use.viaLocal", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Types.makes", "Types.Doc.go", 1); + /* No single abstract method, or not an interface: the parameter stays unknown. */ + OO_EXPECT("CALLS", "Use.twoAbstract", "Types.Doc.go", 0); + OO_EXPECT("CALLS", "Use.classTarget", "Types.Doc.go", 0); + /* A subinterface implemented `accept`, so its one abstract method is `other`, taking a Doc + * where the type argument says Other. */ + OO_EXPECT("CALLS", "Use.replacedTarget", "Types.Doc.go", 1); + OO_EXPECT("CALLS", "Use.replacedTarget", "Types.Other.go", 0); + /* A project class that is called Handler is not a JDK interface. */ + OO_EXPECT("CALLS", "Use.lookalike", "Types.Doc.go", 0); + OO_EXPECT("CALLS", "Foreign.foreignConsumer", "Foreign.take", 1); + OO_EXPECT("CALLS", "Foreign.foreignConsumer", "Types.Doc.go", 0); + /* The method's own type parameter is settled by nothing here. */ + OO_EXPECT("CALLS", "Use.unboundParameter", "Types.Doc.go", 0); + OO_EXPECT("CALLS", "Use.unboundParameter", "Types.Other.go", 0); + /* The lambda's parameter is d2. `d` is still the method's parameter, an Other. */ + OO_EXPECT("CALLS", "Use.shadowedParameter", "Types.Other.go", 1); + OO_EXPECT("CALLS", "Use.shadowedParameter", "Types.Doc.go", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_java_lambda_parameters_from_the_selected_declaration) { + ASSERT_EQ(oo_java_lambda_check(false), 0); + PASS(); +} + +TEST(resolver_oo_java_lambda_parameters_from_the_selected_declaration_parallel) { + ASSERT_EQ(oo_java_lambda_check(true), 0); + PASS(); +} + +/* C# overload choice sees user-defined conversions, arrays reaching collection interfaces, and + * named arguments; lambdas take their parameters from a delegate; a keyword type is its System type where + * the project declares it (a core library). Forms from System.Text.Json and System.Linq. */ +static const OOFile CS_CORE_FILES[] = { + {"Corelib/String.cs", + "namespace System {\n" + " public class Object { public virtual string ToString() { return null; } }\n" + " public sealed partial class String { public int Length => 0; public string Trim() { return this; } " + "public static bool IsNullOrEmpty(string value) { return true; } }\n" + " public delegate void Action(T obj);\n" + " public delegate TResult Func(T arg);\n" + " public readonly ref struct Span { public int Length => 0; }\n" + " public readonly ref struct ReadOnlySpan {\n" + " public int Length => 0;\n" + " public static implicit operator ReadOnlySpan(Span span) { return default; }\n" + " public static implicit operator ReadOnlySpan(T[] array) { return default; }\n" + " }\n" + "}\n" + "namespace System.Collections.Generic { public interface IEnumerable {} }\n"}, + {"Corelib/Client.cs", + "using System;\n" + "using System.Collections.Generic;\n" + "namespace Acme.Conv {\n" + " public enum Mode { One }\n" + " public class Doc { public void Go() {} public Doc Next() { return this; } }\n" + " public class Other { public void Go() {} }\n" + " public class Writer {\n" + " public static void Read(ReadOnlySpan data) {}\n" + " public static void Sized(int length, bool last, Doc doc) {}\n" + " public static void Each(IEnumerable items) {}\n" + " public static void Apply(Action act) {}\n" + " public static void Map(Func map) {}\n" + " public static void NotADelegate(Doc doc) {}\n" + " public static void Only(Action act) {}\n" + " public static void Only(int notDelegate) {}\n" + " public static void Ref(ref int value) {}\n" + " public static void Plain(Other other) {}\n" + " public static void Each(IEnumerable items, Action act) {}\n" + " }\n" + " public static class Seq {\n" + " public static IEnumerable Keep(this IEnumerable items, Func keep) { return items; }\n" + " public static IEnumerable Keep(this IEnumerable items, Func keep) { return items; }\n" + " public static T Head(this IEnumerable items) { return default; }\n" + " }\n" + " public class Main {\n" + " void FromInference(Doc[] docs) { Writer.Each(docs, d => d.Go()); }\n" + " void FromReceiver(Doc[] docs) { docs.Keep(d => { d.Go(); return true; }); }\n" + " void ChainResult(Doc[] docs) { docs.Keep(d => true).Head().Go(); }\n" + " void DeclaredLambda() { Action a = d => d.Go(); }\n" + " void FromSpan(Span span) { Writer.Read(span); }\n" + " void FromArray(byte[] array) { Writer.Read(array); }\n" + " void ArrayToEnumerable(int[] items) { Writer.Each(items); }\n" + " void Named(Doc doc) { Writer.Sized(last: true, length: 1, doc: doc); }\n" + " void Lambda() { Writer.Apply(d => d.Go()); }\n" + " void LambdaBlock() { Writer.Apply(d => { d.Next().Go(); }); }\n" + " void LambdaParens() { Writer.Apply((d) => d.Go()); }\n" + " void LambdaFunc() { Writer.Map(d => { d.Go(); return null; }); }\n" + " void LambdaOverload() { Writer.Only(d => d.Go()); }\n" + " void RefArgument(int v) { Writer.Ref(ref v); }\n" + " void WrongType(Doc doc) { Writer.Plain(doc); }\n" + " void Keyword(string text) { text.Trim(); var n = text.Length; string.IsNullOrEmpty(text); }\n" + " void Literal() { \"abc\".Trim(); }\n" + " }\n" + " public class Typed where TValue : Doc {\n" + " void Bound(TValue value) { value.Go(); }\n" + " }\n" + " public class Statics where TSelf : IMaker {\n" + " void Make() { TSelf.Create(); }\n" + " }\n" + " public interface IMaker { static abstract void Create(); }\n" + "}\n"}, +}; + +static int oo_csharp_core_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, CS_CORE_FILES, (int)(sizeof(CS_CORE_FILES) / sizeof(CS_CORE_FILES[0])), + parallel, "cs")) { + oo_close(&index); + return 1; + } + /* Span and arrays reach ReadOnlySpan through its implicit operators. */ + OO_EXPECT("CALLS", "Main.FromSpan", "Writer.Read", 1); + OO_EXPECT("CALLS", "Main.FromArray", "Writer.Read", 1); + OO_EXPECT("CALLS", "Main.ArrayToEnumerable", "Writer.Each", 1); + /* Named arguments can come in any order. */ + OO_EXPECT("CALLS", "Main.Named", "Writer.Sized", 1); + OO_EXPECT("CALLS", "Main.Lambda", "Client.Doc.Go", 1); + OO_EXPECT("CALLS", "Main.LambdaBlock", "Client.Doc.Next", 1); + OO_EXPECT("CALLS", "Main.LambdaParens", "Client.Doc.Go", 1); + OO_EXPECT("CALLS", "Main.LambdaFunc", "Client.Doc.Go", 1); + /* A lambda is no int. */ + OO_EXPECT("CALLS", "Main.LambdaOverload", "Client.Doc.Go", 1); + OO_EXPECT("CALLS", "Main.RefArgument", "Writer.Ref", 1); + OO_EXPECT("CALLS", "Main.FromInference", "Client.Doc.Go", 1); + OO_EXPECT("CALLS", "Main.FromReceiver", "Client.Doc.Go", 1); + /* `d => ...` takes one parameter: the two-parameter overload is out, and both share a node. */ + OO_EXPECT("CALLS", "Main.ChainResult", "Client.Seq.Keep", 1); + OO_EXPECT("CALLS", "Main.ChainResult", "Client.Doc.Go", 1); + OO_EXPECT("CALLS", "Main.DeclaredLambda", "Client.Doc.Go", 1); + /* A class with no conversion still does not fit. */ + OO_EXPECT("CALLS", "Main.WrongType", "Writer.Plain", 0); + OO_EXPECT("CALLS", "Main.Keyword", "String.String.Trim", 1); + OO_EXPECT("USAGE", "Main.Keyword", "String.String.Length", 1); + OO_EXPECT("CALLS", "Main.Keyword", "String.String.IsNullOrEmpty", 1); + OO_EXPECT("CALLS", "Main.Literal", "String.String.Trim", 1); + /* A type parameter's receiver reads its bound for members. */ + OO_EXPECT("CALLS", "Typed.Bound", "Client.Doc.Go", 1); + OO_EXPECT("CALLS", "Statics.Make", "Client.IMaker.Create", 1); + /* `TSelf` spells the parameter, never the constraint's declaration. */ + OO_EXPECT("USAGE", "Statics.Make", "Client.IMaker", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_csharp_conversions_lambdas_and_keyword_types) { + ASSERT_EQ(oo_csharp_core_check(false), 0); + PASS(); +} + +TEST(resolver_oo_csharp_conversions_lambdas_and_keyword_types_parallel) { + ASSERT_EQ(oo_csharp_core_check(true), 0); + PASS(); +} + +/* Elsewhere a keyword is only a keyword, and the System delegates come from `using System`. */ +static const OOFile CS_PLAIN_FILES[] = { + {"Plain/Client.cs", + "using System;\n" + "using System.Collections.Generic;\n" + "namespace Acme.Plain {\n" + " public class Doc { public void Go() {} public string Trim() { return null; } }\n" + " public class Writer {\n" + " public static void Apply(Action act) {}\n" + " public static void Map(Func map) {}\n" + " }\n" + " public class Main {\n" + " List docs;\n" + " void Lambda() { Writer.Apply(d => d.Go()); }\n" + " void LambdaFunc() { Writer.Map(d => { d.Go(); return 1; }); }\n" + " void Linq() { docs.ForEach(d => d.Go()); }\n" + " void Keyword(string text) { text.Trim(); }\n" + " }\n" + "}\n"}, + {"Plain/Lookalike.cs", + "namespace Acme.Other {\n" + " public class Doc { public void Go() {} }\n" + " public class Writer { public static void Apply(Action act) {} }\n" + " public class Main { void NoUsing() { Writer.Apply(d => d.Go()); } }\n" + "}\n"}, +}; + +static int oo_csharp_plain_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, CS_PLAIN_FILES, (int)(sizeof(CS_PLAIN_FILES) / sizeof(CS_PLAIN_FILES[0])), + parallel, "cs")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "Client.Main.Lambda", "Client.Doc.Go", 1); + OO_EXPECT("CALLS", "Client.Main.LambdaFunc", "Client.Doc.Go", 1); + OO_EXPECT("CALLS", "Client.Main.Linq", "Client.Doc.Go", 1); + /* A string is no Doc, and no project type declares what a keyword is. */ + OO_EXPECT("CALLS", "Client.Main.Keyword", "Client.Doc.Trim", 0); + /* `Action` without `using System` could be any type of that name. */ + OO_EXPECT("CALLS", "Lookalike.Main.NoUsing", "Lookalike.Doc.Go", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_csharp_system_delegates_without_a_core_library) { + ASSERT_EQ(oo_csharp_plain_check(false), 0); + PASS(); +} + +TEST(resolver_oo_csharp_system_delegates_without_a_core_library_parallel) { + ASSERT_EQ(oo_csharp_plain_check(true), 0); + PASS(); +} + +/* One qualified name, several declarations: documentation snippets that are each a Gradle project + * (Exposed: `org.example.tables.CitiesTable`), a type's platform files (dotnet: `Foo.Unix.cs`, `Foo.Windows.cs`). A + * type's own body means that declaration. A project's own declaration shadows another's. Nothing + * else tells them apart, so a caller outside abstains. */ +static const OOFile KOTLIN_VARIANT_FILES[] = { + {"lib/build.gradle.kts", "plugins { kotlin(\"jvm\") }\n"}, + {"lib/src/main/kotlin/base/Table.kt", + "package base\n" + "open class Table { fun varchar(name: String) {} }\n"}, + {"snippets/one/build.gradle.kts", "plugins { kotlin(\"jvm\") }\n"}, + {"snippets/one/src/main/kotlin/example/Cities.kt", + "package example\n" + "import base.Table\n" + "object Cities : Table() {\n" + " val name = varchar(\"name\")\n" + " fun count() {}\n" + " fun own() { count() }\n" + "}\n"}, + {"snippets/one/src/main/kotlin/example/Use.kt", + "package example\n" + "fun viaProjectOne() { Cities.count() }\n"}, + {"snippets/two/build.gradle.kts", "plugins { kotlin(\"jvm\") }\n"}, + {"snippets/two/src/main/kotlin/example/Cities.kt", + "package example\n" + "import base.Table\n" + "object Cities : Table() {\n" + " val name = varchar(\"name\")\n" + " fun count() {}\n" + " fun own() { count() }\n" + "}\n"}, + {"snippets/two/src/main/kotlin/example/Use.kt", + "package example\n" + "fun viaProjectTwo() { Cities.count() }\n"}, + {"tools/build.gradle.kts", "plugins { kotlin(\"jvm\") }\n"}, + {"tools/src/main/kotlin/other/Elsewhere.kt", + "package other\n" + "import example.Cities\n" + "fun fromOutside() { Cities.count() }\n"}, +}; + +static int oo_kotlin_variant_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, KOTLIN_VARIANT_FILES, + (int)(sizeof(KOTLIN_VARIANT_FILES) / sizeof(KOTLIN_VARIANT_FILES[0])), parallel, + "kt")) { + oo_close(&index); + return 1; + } + /* Each body reaches its own member, and the one supertype. */ + OO_EXPECT("CALLS", "one.src.main.kotlin.example.Cities.Cities.own", + "one.src.main.kotlin.example.Cities.Cities.count", 1); + OO_EXPECT("CALLS", "one.src.main.kotlin.example.Cities.Cities.own", + "two.src.main.kotlin.example.Cities.Cities.count", 0); + OO_EXPECT("CALLS", "two.src.main.kotlin.example.Cities.Cities.own", + "two.src.main.kotlin.example.Cities.Cities.count", 1); + OO_EXPECT("CALLS", "one.src.main.kotlin.example.Cities", "base.Table.Table.varchar", 1); + /* Each project's caller reaches its own project's declaration. */ + OO_EXPECT("CALLS", "one.src.main.kotlin.example.Use.viaProjectOne", + "one.src.main.kotlin.example.Cities.Cities.count", 1); + OO_EXPECT("CALLS", "one.src.main.kotlin.example.Use.viaProjectOne", + "two.src.main.kotlin.example.Cities.Cities.count", 0); + OO_EXPECT("CALLS", "two.src.main.kotlin.example.Use.viaProjectTwo", + "two.src.main.kotlin.example.Cities.Cities.count", 1); + /* Another project reads one of them, and nothing here says which. */ + OO_EXPECT("CALLS", "Elsewhere.fromOutside", "one.src.main.kotlin.example.Cities.Cities.count", + 0); + OO_EXPECT("CALLS", "Elsewhere.fromOutside", "two.src.main.kotlin.example.Cities.Cities.count", + 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_kotlin_same_name_in_two_projects) { + ASSERT_EQ(oo_kotlin_variant_check(false), 0); + PASS(); +} + +TEST(resolver_oo_kotlin_same_name_in_two_projects_parallel) { + ASSERT_EQ(oo_kotlin_variant_check(true), 0); + PASS(); +} + +static const OOFile CS_VARIANT_FILES[] = { + {"src/Pal/Pal.csproj", "\n"}, + {"src/Pal/Pal.Unix.cs", + "namespace Acme.Pal {\n" + " internal static class Native {\n" + " public static void Open() { Close(); }\n" + " public static void Close() {}\n" + " public class Nested { public void Inner() { Close(); } }\n" + " }\n" + "}\n"}, + {"src/Pal/Pal.Windows.cs", + "namespace Acme.Pal {\n" + " internal static class Native {\n" + " public static void Open() { Close(); }\n" + " public static void Close() {}\n" + " public class Nested { public void Inner() { Close(); } }\n" + " }\n" + "}\n"}, + {"src/Pal/Caller.cs", + "namespace Acme.Pal {\n" + " public class Caller { void Both() { Native.Open(); } }\n" + "}\n"}, + /* The same internal helper in two assemblies, each with its own copy: System.ThrowHelper. */ + {"src/One/One.csproj", "\n"}, + {"src/One/Helper.cs", + "namespace System { internal static class Helper { public static void Fail() {} } }\n"}, + {"src/One/Use.cs", + "namespace Acme.One { public class UseOne { void Run() { System.Helper.Fail(); } } }\n"}, + {"src/Two/Two.csproj", "\n"}, + {"src/Two/Helper.cs", + "namespace System { internal static class Helper { public static void Fail() {} } }\n"}, + {"src/Two/Use.cs", + "using System;\n" + "namespace Acme.Two { public class UseTwo { void Run() { Helper.Fail(); } } }\n"}, + {"src/Three/Three.csproj", "\n"}, + {"src/Three/Use.cs", + "using System;\n" + "namespace Acme.Three { public class UseThree { void Run() { Helper.Fail(); } } }\n"}, +}; + +static int oo_csharp_variant_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, CS_VARIANT_FILES, + (int)(sizeof(CS_VARIANT_FILES) / sizeof(CS_VARIANT_FILES[0])), parallel, "cs")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "Pal.Unix.Native.Open", "Pal.Unix.Native.Close", 1); + OO_EXPECT("CALLS", "Pal.Unix.Native.Open", "Pal.Windows.Native.Close", 0); + OO_EXPECT("CALLS", "Pal.Windows.Native.Open", "Pal.Windows.Native.Close", 1); + OO_EXPECT("CALLS", "Pal.Unix.Native.Nested.Inner", "Pal.Unix.Native.Close", 1); + /* Two in one project: nothing says which this configuration compiles. */ + OO_EXPECT("CALLS", "Caller.Both", "Pal.Unix.Native.Open", 0); + OO_EXPECT("CALLS", "Caller.Both", "Pal.Windows.Native.Open", 0); + OO_EXPECT("CALLS", "UseOne.Run", "One.Helper.Helper.Fail", 1); + OO_EXPECT("CALLS", "UseOne.Run", "Two.Helper.Helper.Fail", 0); + OO_EXPECT("CALLS", "UseTwo.Run", "Two.Helper.Helper.Fail", 1); + OO_EXPECT("CALLS", "UseTwo.Run", "One.Helper.Helper.Fail", 0); + OO_EXPECT("CALLS", "UseThree.Run", "One.Helper.Helper.Fail", 0); + OO_EXPECT("CALLS", "UseThree.Run", "Two.Helper.Helper.Fail", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_csharp_same_name_in_one_file_and_one_project) { + ASSERT_EQ(oo_csharp_variant_check(false), 0); + PASS(); +} + +TEST(resolver_oo_csharp_same_name_in_one_file_and_one_project_parallel) { + ASSERT_EQ(oo_csharp_variant_check(true), 0); + PASS(); +} + +static const OOFile JAVA_VARIANT_FILES[] = { + {"a/pom.xml", "\n"}, + {"a/src/main/java/org/x/Util.java", + "package org.x;\npublic class Util { public static void go() {} public static class In { public static void run() {} } }\n"}, + {"a/src/main/java/org/y/Client.java", + "package org.y;\n" + "import org.x.Util;\n" + "import static org.x.Util.go;\n" + "public class Client {\n" + " void imported() { Util.go(); }\n" + " void qualified() { org.x.Util.go(); }\n" + " void nested() { org.x.Util.In.run(); }\n" + " void statically() { go(); }\n" + "}\n"}, + {"b/pom.xml", "\n"}, + {"b/src/main/java/org/x/Util.java", + "package org.x;\npublic class Util { public static void go() {} public static class In { public static void run() {} } }\n"}, + {"c/pom.xml", "\n"}, + {"c/src/main/java/org/z/Reader.java", + "package org.z;\n" + "import org.x.Util;\n" + "public class Reader {\n" + " void imported() { Util.go(); }\n" + " void qualified() { org.x.Util.go(); }\n" + " void nested() { org.x.Util.In.run(); }\n" + "}\n"}, +}; + +static int oo_java_variant_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, JAVA_VARIANT_FILES, + (int)(sizeof(JAVA_VARIANT_FILES) / sizeof(JAVA_VARIANT_FILES[0])), parallel, + "java")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "Client.imported", "a.src.main.java.org.x.Util.go", 1); + OO_EXPECT("CALLS", "Client.imported", "b.src.main.java.org.x.Util.go", 0); + OO_EXPECT("CALLS", "Client.qualified", "a.src.main.java.org.x.Util.go", 1); + OO_EXPECT("CALLS", "Client.nested", "a.src.main.java.org.x.Util.In.run", 1); + OO_EXPECT("CALLS", "Client.statically", "a.src.main.java.org.x.Util.go", 1); + /* A third project could read either. A nested name inherits its outer type's doubt. */ + OO_EXPECT("CALLS", "Reader.imported", "a.src.main.java.org.x.Util.go", 0); + OO_EXPECT("CALLS", "Reader.imported", "b.src.main.java.org.x.Util.go", 0); + OO_EXPECT("CALLS", "Reader.qualified", "a.src.main.java.org.x.Util.go", 0); + OO_EXPECT("CALLS", "Reader.nested", "a.src.main.java.org.x.Util.In.run", 0); + OO_EXPECT("CALLS", "Reader.nested", "b.src.main.java.org.x.Util.In.run", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_java_same_name_in_two_projects) { + ASSERT_EQ(oo_java_variant_check(false), 0); + PASS(); +} + +TEST(resolver_oo_java_same_name_in_two_projects_parallel) { + ASSERT_EQ(oo_java_variant_check(true), 0); + PASS(); +} + +/* Exposed's tests are built on local anonymous tables: `val t = object : Table() { val c = varchar("c") }` + * then `t.c.isNotNull()`, `PrimaryKey(c)` inside, `val text = text("x")` beside the inherited function, + * `Words.upsert(onUpdate = { ... }) { ... }`, `for ((k, v) in map)`. */ +static const OOFile KOTLIN_OBJECT_FILES[] = { + {"acme/Lib.kt", + "package acme\n" + "class Op { fun and() {} }\n" + "class Column { fun isNotNull(): Op = Op()\n fun defaulted(x: Int): Column = this }\n" + "open class Table {\n" + " inner class PrimaryKey(val col: Column)\n" + " fun text(name: String): Column = Column()\n" + " fun varchar(name: String): Column = Column()\n" + "}\n" + "data class Row(val left: Column, val right: Op)\n" + "class Plain(val left: Column)\n" + "fun T.upsert(vararg keys: Column, onUpdate: (T.() -> Unit)? = null, body: T.(Int) -> Unit) {}\n" + "fun helper(first: Int = 0, onRow: (Row) -> Unit, onOp: (Op) -> Unit) {}\n" + "fun Table.stamp(name: String): Column = Column()\n" + "fun Column.day(): Op = Op()\n" + "object Cities : Table() { val at = stamp(\"at\")\n val plain = varchar(\"p\") }\n"}, + {"acme/Main.kt", + "package acme\n" + "class Main {\n" + " fun anonymousProperty() {\n" + " val t = object : Table() { val c = varchar(\"c\")\n val d = text(\"d\").defaulted(1) }\n" + " t.c.isNotNull()\n" + " t.d.isNotNull()\n" + " t.varchar(\"x\")\n" + " }\n" + " fun declaredProperty() {\n" + " val t = object : Table() { val c: Op = Op() }\n" + " t.c.and()\n" + " }\n" + " fun ownMemberRead() {\n" + " val t = object : Table() { val c = varchar(\"c\")\n val e = c.isNotNull() }\n" + " }\n" + " fun sameNameFunction() {\n" + " val t = object : Table() { val text = text(\"x\") }\n" + " t.text.isNotNull()\n" + " }\n" + " fun ownFunctionHides() {\n" + " val t = object : Table() { fun text(name: String): Op = Op()\n val z = text(\"q\") }\n" + " }\n" + " fun nestedOfSuper() {\n" + " val t = object : Table() { val c = varchar(\"c\")\n val pk = PrimaryKey(c) }\n" + " }\n" + " fun pairParts(p: Pair) { val (a, b) = p\n a.isNotNull()\n b.and() }\n" + " fun pairNames(p: Pair) { p.first.isNotNull()\n p.second.and() }\n" + " fun mapLoop(m: Map) { for ((k, v) in m) { k.isNotNull()\n v.and() } }\n" + " fun entryLoop(m: Map) { for (e in m) { e.value.and()\n e.key.isNotNull() } }\n" + " fun dataParts(r: Row) { val (l, o) = r\n l.isNotNull()\n o.and() }\n" + " fun notData(r: Plain) { val (l) = r\n l.isNotNull() }\n" + " fun lambdaParts(m: List>) { m.forEach { (c, o) -> c.isNotNull()\n o.and() } }\n" + " fun swappedParts(r: Row) { val (first, second) = r\n second.and() }\n" + " fun namedLambda(t: Table) { t.upsert(onUpdate = { varchar(\"a\") }) { text(\"b\") } }\n" + " fun namedByName() { helper(onOp = { it.and() }, onRow = { it.left.isNotNull() }) }\n" + " fun extensionInitializer() { Cities.at.day()\n Cities.plain.day() }\n" + "}\n"}, +}; + +static int oo_kotlin_object_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, KOTLIN_OBJECT_FILES, + (int)(sizeof(KOTLIN_OBJECT_FILES) / sizeof(KOTLIN_OBJECT_FILES[0])), parallel, + "kt")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "Main.anonymousProperty", "Lib.Column.isNotNull", 1); + OO_EXPECT("CALLS", "Main.anonymousProperty", "Lib.Table.varchar", 1); + OO_EXPECT("CALLS", "Main.declaredProperty", "Lib.Op.and", 1); + OO_EXPECT("CALLS", "Main.ownMemberRead", "Lib.Column.isNotNull", 1); + /* A property does not hide a function of its name: the function comes first. */ + OO_EXPECT("CALLS", "Main.sameNameFunction", "Lib.Table.text", 1); + OO_EXPECT("CALLS", "Main.sameNameFunction", "Lib.Column.isNotNull", 1); + /* A function the object declares itself does. */ + OO_EXPECT("CALLS", "Main.ownFunctionHides", "Lib.Table.text", 0); + OO_EXPECT("CALLS", "Main.nestedOfSuper", "Lib.Table.PrimaryKey", 1); + OO_EXPECT("CALLS", "Main.pairParts", "Lib.Column.isNotNull", 1); + OO_EXPECT("CALLS", "Main.pairParts", "Lib.Op.and", 1); + OO_EXPECT("CALLS", "Main.pairNames", "Lib.Column.isNotNull", 1); + OO_EXPECT("CALLS", "Main.pairNames", "Lib.Op.and", 1); + OO_EXPECT("CALLS", "Main.mapLoop", "Lib.Column.isNotNull", 1); + OO_EXPECT("CALLS", "Main.mapLoop", "Lib.Op.and", 1); + OO_EXPECT("CALLS", "Main.entryLoop", "Lib.Op.and", 1); + OO_EXPECT("CALLS", "Main.entryLoop", "Lib.Column.isNotNull", 1); + OO_EXPECT("CALLS", "Main.dataParts", "Lib.Column.isNotNull", 1); + OO_EXPECT("CALLS", "Main.dataParts", "Lib.Op.and", 1); + /* A class that is not `data` has no componentN to read. */ + OO_EXPECT("CALLS", "Main.notData", "Lib.Column.isNotNull", 0); + OO_EXPECT("CALLS", "Main.lambdaParts", "Lib.Column.isNotNull", 1); + OO_EXPECT("CALLS", "Main.lambdaParts", "Lib.Op.and", 1); + /* By position, whatever the local is called. */ + OO_EXPECT("CALLS", "Main.swappedParts", "Lib.Op.and", 1); + OO_EXPECT("CALLS", "Main.swappedParts", "Lib.Column.isNotNull", 0); + /* A named lambda is no positional vararg, and has the receiver of the parameter it names. */ + OO_EXPECT("CALLS", "Main.namedLambda", "Lib.upsert", 1); + OO_EXPECT("CALLS", "Main.namedLambda", "Lib.Table.varchar", 1); + OO_EXPECT("CALLS", "Main.namedLambda", "Lib.Table.text", 1); + OO_EXPECT("CALLS", "Main.namedByName", "Lib.Op.and", 1); + OO_EXPECT("CALLS", "Main.namedByName", "Lib.Column.isNotNull", 1); + OO_EXPECT("CALLS", "Main.extensionInitializer", "Lib.day", 1); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_kotlin_anonymous_objects_destructuring_named_lambdas) { + ASSERT_EQ(oo_kotlin_object_check(false), 0); + PASS(); +} + +TEST(resolver_oo_kotlin_anonymous_objects_destructuring_named_lambdas_parallel) { + ASSERT_EQ(oo_kotlin_object_check(true), 0); + PASS(); +} + +/* Three spellings on which the C# grammar loses a whole file, and with it every type the file declares: + * a dereference of a parenthesis (Task.cs, Vector2.cs), `async` as a name (every `bool async` test), + * a doubled quote in a verbatim interpolated string. The caller's site spans stay exact. */ +static const OOFile CS_PARSE_FILES[] = { + {"Parse/Deref.cs", + "namespace Acme.Parse {\n" + " public unsafe struct Vec {\n" + " public static Vec Load(float* source) => *(Vec*)source;\n" + " public static float Next(float* p) { return *(p + 1); }\n" + " public void Store(float* destination) => *(Vec*)destination = this;\n" + " public ref Vec Self(void* p) { return ref *(Vec*)p; }\n" + " public int Width() { return 1; }\n" + " public int Twice() { return Width() * (2); }\n" + " }\n" + " public class Use { void Run(float* p) { var v = Vec.Load(p); v.Width(); } }\n" + "}\n"}, + {"Parse/AsyncName.cs", + "using System.Threading.Tasks;\n" + "namespace Acme.Parse {\n" + " public class Worker {\n" + " public async Task Go(bool async) {\n" + " if (async) { Done(); }\n" + " await Run(async: true);\n" + " var other = async ? 1 : 2;\n" + " }\n" + " public Task Run(bool async) { return null; }\n" + " public void Done() {}\n" + " public async Task Lambda() { await Task.Run(async () => { Done(); }); }\n" + " }\n" + "}\n"}, + {"Parse/Quotes.cs", + "namespace Acme.Parse {\n" + " public class Text {\n" + " public string Quoted(string name) { Note(); return $@\"\"\"{name}\"\"\"; }\n" + " public void Note() {}\n" + " public void After() { Note(); }\n" + " }\n" + "}\n"}, +}; + +static int oo_csharp_parse_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, CS_PARSE_FILES, (int)(sizeof(CS_PARSE_FILES) / sizeof(CS_PARSE_FILES[0])), + parallel, "cs")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "Use.Run", "Deref.Vec.Load", 1); + OO_EXPECT("CALLS", "Use.Run", "Deref.Vec.Width", 1); + /* A star after a name is multiplication, and stays. */ + OO_EXPECT("CALLS", "Vec.Twice", "Deref.Vec.Width", 1); + OO_EXPECT("CALLS", "Worker.Go", "AsyncName.Worker.Done", 1); + OO_EXPECT("CALLS", "Worker.Go", "AsyncName.Worker.Run", 1); + /* The modifier is still the modifier. */ + OO_EXPECT("CALLS", "Worker.Lambda", "AsyncName.Worker.Done", 1); + OO_EXPECT("CALLS", "Text.Quoted", "Quotes.Text.Note", 1); + OO_EXPECT("CALLS", "Text.After", "Quotes.Text.Note", 1); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_csharp_syntax_that_loses_a_whole_file) { + ASSERT_EQ(oo_csharp_parse_check(false), 0); + PASS(); +} + +TEST(resolver_oo_csharp_syntax_that_loses_a_whole_file_parallel) { + ASSERT_EQ(oo_csharp_parse_check(true), 0); + PASS(); +} + +/* An array or a BCL collection reaches a project's extension on a sequence interface. A partial type has + * one node that stands for it. `using static` brings nested types in. Attributes and annotations name their + * class and read their arguments. An extension in another test project is not in scope. */ +static const OOFile CS_MORE_FILES[] = { + {"More/Ext.cs", + "using System.Collections.Generic;\n" + "namespace Acme.More {\n" + " public class Bag : IEnumerable { }\n" + " public class Plain { }\n" + " public static class Sequences {\n" + " public static int Tally(this IEnumerable items) { return 0; }\n" + " public static int Lone(this Plain plain) { return 0; }\n" + " public static int OnlyList(this IList items) { return 0; }\n" + " }\n" + " public class Use {\n" + " void FromArray(int[] values) { values.Tally(); }\n" + " void FromList(List values) { values.Tally(); }\n" + " void FromBag(Bag bag) { bag.Tally(); }\n" + " void FromPlain(Plain plain) { plain.Tally(); plain.Lone(); }\n" + " void Dictionary(Dictionary map) { map.OnlyList(); }\n" + " }\n" + "}\n"}, + {"More/Value.Overloads.cs", + "namespace Acme.More { public partial class Value {\n" + " public static Value Create(int value) { return null; }\n" + " public static Value Create(int? value) { return null; }\n" + "} }\n"}, + {"More/Value.cs", + "namespace Acme.More { public partial class Value {\n" + " public static Value Create(T value, string note) { return null; }\n" + " public static class Kinds { public const int One = 1; }\n" + "} }\n"}, + {"More/UseValue.cs", + "namespace Acme.More { public class UseValue {\n" + " void Make() { Value.Create(10); }\n" + " int Nested() { return Value.Kinds.One; }\n" + "} }\n"}, + {"More/Part.A.cs", "namespace Acme.More { public partial class Whole { public void One() {} } }\n"}, + {"More/Whole.cs", "namespace Acme.More { public partial class Whole { public void Two() {} } }\n"}, + {"More/Part.Z.cs", "namespace Acme.More { public partial class Whole { public void Three() {} } }\n"}, + {"More/UseWhole.cs", + "namespace Acme.More {\n" + " public class UseWhole {\n" + " Whole Make() { return new Whole(); }\n" + " void Call(Whole whole) { whole.One(); whole.Two(); whole.Three(); }\n" + " }\n" + "}\n"}, + {"More/Static.cs", + "using static Acme.More.Helpers;\n" + "namespace Acme.More {\n" + " public static class Helpers { public class Inner { public void Go() {} } public static void Free() {} }\n" + " public class UseStatic { void Run(Inner inner) { inner.Go(); Free(); } }\n" + "}\n"}, + {"More/Attr.cs", + "using System;\n" + "namespace Acme.More {\n" + " public enum Level { High }\n" + " public class TagAttribute : Attribute { public TagAttribute(Level level) {} }\n" + " public class Marked {}\n" + " [Tag(Level.High)] public class Tagged { [Tag(Level.High)] public void Method() {} }\n" + " [Marked] public class NotAnAttributeName {}\n" + " public class Param { public void Take([Tag(Level.High)] int value) {} }\n" + "}\n"}, + {"proj/Lib.Tests/Lib.Tests.csproj", "\n"}, + {"proj/Lib.Tests/Helpers.cs", + "namespace System { public static class TestHelpers {\n" + " public static int Blend(this System.Collections.Generic.IEnumerable a, T b) { return 0; }\n" + "} }\n"}, + {"proj/Lib.Tests/Own.cs", + "using System.Collections.Generic;\n" + "namespace System.Own { public class OwnUse { void Run(List items) { items.Blend(1); } } }\n"}, + {"proj/Other.Tests/Other.Tests.csproj", "\n"}, + {"proj/Other.Tests/Use.cs", + "using System.Collections.Generic;\n" + "namespace System.Other { public class ElseUse { void Run(List items) { items.Blend(1); } } }\n"}, +}; + +static int oo_csharp_more_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, CS_MORE_FILES, (int)(sizeof(CS_MORE_FILES) / sizeof(CS_MORE_FILES[0])), + parallel, "cs")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "Use.FromArray", "Ext.Sequences.Tally", 1); + OO_EXPECT("CALLS", "Use.FromList", "Ext.Sequences.Tally", 1); + OO_EXPECT("CALLS", "Use.FromBag", "Ext.Sequences.Tally", 1); + /* Plain implements nothing: only its own extension applies. */ + OO_EXPECT("CALLS", "Use.FromPlain", "Ext.Sequences.Tally", 0); + OO_EXPECT("CALLS", "Use.FromPlain", "Ext.Sequences.Lone", 1); + OO_EXPECT("CALLS", "Use.Dictionary", "Ext.Sequences.OnlyList", 0); + /* The file named after the type stands for it, wherever the first path would sort. */ + OO_EXPECT("CALLS", "UseWhole.Make", "More.Whole.Whole", 1); + OO_EXPECT("USES_TYPE", "UseWhole.Make", "More.Whole.Whole", 1); + OO_EXPECT("USAGE", "UseWhole.Call", "More.Whole.Whole", 1); + OO_EXPECT("USAGE", "UseWhole.Call", "Part.A.Whole", 0); + /* A member keeps its own file's node. */ + OO_EXPECT("CALLS", "UseWhole.Call", "Part.A.Whole.One", 1); + OO_EXPECT("CALLS", "UseWhole.Call", "More.Whole.Whole.Two", 1); + OO_EXPECT("CALLS", "UseWhole.Call", "Part.Z.Whole.Three", 1); + OO_EXPECT("CALLS", "UseValue.Make", "Value.Overloads.Value.Create", 1); + OO_EXPECT("USAGE", "UseValue.Nested", "More.Value.Value.Kinds", 1); + OO_EXPECT("CALLS", "UseStatic.Run", "Static.Helpers.Inner.Go", 1); + OO_EXPECT("CALLS", "UseStatic.Run", "Static.Helpers.Free", 1); + /* `[Tag]` is TagAttribute, and its argument an expression. */ + OO_EXPECT("USAGE", "More.Attr", "Attr.TagAttribute", 1); + OO_EXPECT("USAGE", "More.Attr", "Attr.Level", 1); + OO_EXPECT("DECORATES", "Attr.Tagged", "Attr.TagAttribute", 1); + OO_EXPECT("USAGE", "Tagged.Method", "Attr.TagAttribute", 1); + OO_EXPECT("USAGE", "More.Attr", "Attr.Marked", 1); + /* A test project's helper from outside it is no candidate. */ + OO_EXPECT("CALLS", "OwnUse.Run", "Helpers.TestHelpers.Blend", 1); + OO_EXPECT("CALLS", "ElseUse.Run", "Helpers.TestHelpers.Blend", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_csharp_extension_receivers_partial_types_attributes) { + ASSERT_EQ(oo_csharp_more_check(false), 0); + PASS(); +} + +TEST(resolver_oo_csharp_extension_receivers_partial_types_attributes_parallel) { + ASSERT_EQ(oo_csharp_more_check(true), 0); + PASS(); +} + +/* Annotations name their type and read their arguments, in Java and Kotlin. */ +static const OOFile JAVA_ANNOTATION_FILES[] = { + {"acme/anno/Nullable.java", "package acme.anno;\npublic @interface Nullable {}\n"}, + {"acme/anno/Scope.java", "package acme.anno;\npublic enum Scope { INTERNAL }\n"}, + {"acme/anno/Serverless.java", + "package acme.anno;\npublic @interface Serverless { Scope value(); }\n"}, + {"acme/anno/Outer.java", + "package acme.anno;\n" + "public class Outer { public enum Kind { ONE } }\n"}, + {"acme/anno/Nested.java", + "package acme.anno;\n" + "class Nested { Object read() { return Outer.Kind.ONE; } }\n"}, + {"acme/anno/Use.java", + "package acme.anno;\n" + "import java.lang.Override;\n" + "@Serverless(Scope.INTERNAL)\n" + "public class Use {\n" + " @Nullable private String field;\n" + " @Nullable public String method(@Nullable String parameter) { return parameter; }\n" + " @Override public String toString() { return null; }\n" + "}\n"}, +}; + +static const OOFile KOTLIN_ANNOTATION_FILES[] = { + {"acme/Anno.kt", + "package acme\n" + "annotation class InternalApi\n" + "annotation class Tagged(val why: String)\n" + "const val REASON = \"r\"\n" + "@InternalApi\n" + "fun top() {}\n" + "@Tagged(REASON)\n" + "class Holder {\n" + " @InternalApi\n" + " fun member() {}\n" + " @InternalApi\n" + " val field = 1\n" + " @Suppress(\"x\")\n" + " fun outside() {}\n" + "}\n"}, +}; + +static int oo_annotation_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, JAVA_ANNOTATION_FILES, + (int)(sizeof(JAVA_ANNOTATION_FILES) / sizeof(JAVA_ANNOTATION_FILES[0])), parallel, + "java")) { + oo_close(&index); + return 1; + } + OO_EXPECT("USAGE", "Use.java.__file__", "acme.anno.Serverless", 1); + OO_EXPECT("USAGE", "Use.java.__file__", "acme.anno.Scope", 1); + OO_EXPECT("USAGE", "Use.java.__file__", "acme.anno.Nullable", 1); + OO_EXPECT("USAGE", "Use.method", "acme.anno.Nullable", 1); + OO_EXPECT("USAGE", "Nested.read", "acme.anno.Outer", 1); + OO_EXPECT("USAGE", "Nested.read", "Outer.Kind", 1); + oo_close(&index); + if (!oo_open(&index, KOTLIN_ANNOTATION_FILES, + (int)(sizeof(KOTLIN_ANNOTATION_FILES) / sizeof(KOTLIN_ANNOTATION_FILES[0])), + parallel, "kt")) { + oo_close(&index); + return failed + 1; + } + OO_EXPECT("USAGE", "Anno.top", "Anno.InternalApi", 1); + OO_EXPECT("USAGE", "Holder.member", "Anno.InternalApi", 1); + OO_EXPECT("USAGE", "acme.Anno", "Anno.Tagged", 1); + OO_EXPECT("DECORATES", "Anno.Holder", "Anno.Tagged", 1); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_annotations_name_their_type) { + ASSERT_EQ(oo_annotation_check(false), 0); + PASS(); +} + +TEST(resolver_oo_annotations_name_their_type_parallel) { + ASSERT_EQ(oo_annotation_check(true), 0); + PASS(); +} + +/* Spellings of .NET's own sources the grammar cannot read, each of which lost nodes or whole files. */ +static const OOFile CSHARP_SYNTAX_FILES[] = { + {"Intrinsics/Vector4x.cs", + "namespace Intrinsics {\n" + " public interface IConst { }\n" + " public readonly struct Vector4x { }\n" + " public static class Vector4x {\n" + " public static int Create(int x) { return x; }\n" + " /// The type.\n" + " extension(Vector4x)\n" + " where T : IConst\n" + " {\n" + " public static int Pi\n" + " {\n" + " get => Create(1);\n" + " }\n" + " public static int Tau() { return Create(2); }\n" + " }\n" + " public static int After() { return Create(3); }\n" + " }\n" + " public static class UseVector {\n" + " public static int Run() { return Vector4x.After() + Vector4x.Tau(); }\n" + " }\n" + "}\n"}, + {"Extensions/Bags.cs", + "namespace Extensions {\n" + " public class Bag { public int Count() { return 0; } }\n" + " public static class Bags {\n" + " extension(Bag bag) where T : class\n" + " {\n" + " public int Size() { return bag.Count(); }\n" + " }\n" + " public static int After(Bag bag) { return bag.Count(); }\n" + " }\n" + "}\n"}, + {"Links/Lists.cs", + "namespace Links {\n" + " public class Node { public Node Next; public Node Prev; public void Link() {} }\n" + " public class Lists {\n" + " public void Add(Node node) {\n" + " node.Next?.Prev = node;\n" + " node.Link();\n" + " }\n" + " public bool Read(Node node) { return node?.Prev == node; }\n" + " }\n" + "}\n"}, + {"Pointers/Reader.cs", + "namespace Pointers {\n" + " public static class Helper {\n" + " public static int One() { return 1; }\n" + " public static int Two() { return 2; }\n" + " }\n" + " public unsafe class Reader {\n" + " public int Next(char* p) {\n" + " char c = *p;\n" + " c = *++p;\n" + " if (c == 'x') { c = *++p; }\n" + " return Helper.One();\n" + " }\n" + " public int Store(byte* p) {\n" + " if (p != null)\n" + " *(byte*)(p + 1) = (byte)Helper.One();\n" + " *(int*)p = 0;\n" + " return Helper.Two();\n" + " }\n" + " public void After() { Helper.Two(); }\n" + " public int Scale() { return Helper.One() *(Helper.Two()); }\n" + " void extension(int value) {}\n" + " void Plain(char* p) {\n" + " char c = *++p;\n" + " extension(1);\n" + " }\n" + " }\n" + "}\n"}, + {"Natives/Native.cs", + "namespace Natives {\n" + " public static class Native {\n" + " public static safe extern double Acos(double d);\n" + " }\n" + " public closed class Shape { public void Draw() {} }\n" + " public class UseNative {\n" + " public double Run(Shape shape) {\n" + " bool safe = true;\n" + " bool closed = safe;\n" + " shape.Draw();\n" + " return Native.Acos(1.0);\n" + " }\n" + " }\n" + "}\n"}, +}; + +static int oo_csharp_syntax_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, CSHARP_SYNTAX_FILES, + (int)(sizeof(CSHARP_SYNTAX_FILES) / sizeof(CSHARP_SYNTAX_FILES[0])), parallel, + "cs")) { + oo_close(&index); + return 1; + } + /* A block with no receiver name: its members stand as the static class's. */ + OO_EXPECT("CALLS", "Vector4x.After", "Vector4x.Create", 1); + OO_EXPECT("CALLS", "Vector4x.Tau", "Vector4x.Create", 1); + OO_EXPECT("CALLS", "UseVector.Run", "Vector4x.After", 1); + OO_EXPECT("CALLS", "UseVector.Run", "Vector4x.Tau", 1); + /* A named receiver stays bound in the block. */ + OO_EXPECT("CALLS", "Bags.Size", "Bag.Count", 1); + OO_EXPECT("CALLS", "Bags.After", "Bag.Count", 1); + /* `a?.b = c` writes b. `a?.b == c` still does not. */ + OO_EXPECT("WRITES", "Lists.Add", "Node.Prev", 1); + OO_EXPECT("WRITES", "Lists.Read", "Node.Prev", 0); + OO_EXPECT("CALLS", "Lists.Add", "Node.Link", 1); + /* A dereference is read, written through, or a multiplication. */ + OO_EXPECT("CALLS", "Reader.Next", "Helper.One", 1); + OO_EXPECT("CALLS", "Reader.Store", "Helper.One", 1); + OO_EXPECT("CALLS", "Reader.Store", "Helper.Two", 1); + OO_EXPECT("CALLS", "Reader.After", "Helper.Two", 1); + OO_EXPECT("CALLS", "Reader.Scale", "Helper.One", 1); + OO_EXPECT("CALLS", "Reader.Scale", "Helper.Two", 1); + /* A method named extension is called like any other. */ + OO_EXPECT("CALLS", "Reader.Plain", "Reader.extension", 1); + /* `safe` and `closed` are modifiers only between words that show it. */ + OO_EXPECT("CALLS", "UseNative.Run", "Native.Acos", 1); + OO_EXPECT("CALLS", "UseNative.Run", "Shape.Draw", 1); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_csharp_syntax_the_grammar_lacks) { + ASSERT_EQ(oo_csharp_syntax_check(false), 0); + PASS(); +} + +TEST(resolver_oo_csharp_syntax_the_grammar_lacks_parallel) { + ASSERT_EQ(oo_csharp_syntax_check(true), 0); + PASS(); +} + +/* What runs in a member with no node of its own belongs to the nearest declaration that has one. */ +static const OOFile CSHARP_CALLER_FILES[] = { + {"Client/Lib.cs", + "namespace Client {\n" + " public static class Lib {\n" + " public static int Implicit(int x) { return x; }\n" + " public static int Explicit(int x) { return x; }\n" + " public static int Indexed(int x) { return x; }\n" + " public static int Arrow() { return 0; }\n" + " public static int Getter() { return 0; }\n" + " public static int Setter(int x) { return x; }\n" + " public static int Init() { return 0; }\n" + " public static int Local(int x) { return x; }\n" + " public static int Misread() { return 0; }\n" + " }\n" + "}\n"}, + {"Client/Main.cs", + "namespace Client {\n" + " public class Main {\n" + " public static implicit operator Main(int v) { Lib.Implicit(v); return null; }\n" + " public static explicit operator int(Main m) => Lib.Explicit(1);\n" + " public int this[int i] { get { return Lib.Indexed(i); } }\n" + " public int Prop => Lib.Arrow();\n" + " public int Body {\n" + " get { return Lib.Getter(); }\n" + " set => Lib.Setter(value);\n" + " }\n" + " public int Auto { get; set; } = Lib.Init();\n" + " public void Run() {\n" + " Hoisted();\n" + " int Inner(int x) { return Lib.Local(x); }\n" + " Inner(1);\n" + " void Hoisted() {}\n" + " }\n" + " public void Hoisted(int unused = 0) {}\n" + " public void Outer() {\n" + " public void Swallowed() { Lib.Misread(); }\n" + " }\n" + " }\n" + "}\n"}, +}; + +static const OOFile KOTLIN_CALLER_FILES[] = { + {"lib/Worker.kt", "package lib\nobject Worker {\n fun go() {}\n}\n"}, + {"app/Main.kt", + "package app\n" + "import lib.Worker\n" + "fun outer() {\n" + " fun inner() { Worker.go() }\n" + " inner()\n" + "}\n" + "class Box {\n" + " fun member() {\n" + " fun helper() { Worker.go() }\n" + " helper()\n" + " }\n" + "}\n"}, +}; + +static int oo_caller_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, CSHARP_CALLER_FILES, + (int)(sizeof(CSHARP_CALLER_FILES) / sizeof(CSHARP_CALLER_FILES[0])), parallel, + "cs")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "Main.operator implicit", "Lib.Implicit", 1); + OO_EXPECT("CALLS", "Main.operator explicit", "Lib.Explicit", 1); + OO_EXPECT("CALLS", "Main.this", "Lib.Indexed", 1); + OO_EXPECT("CALLS", "Main.Prop", "Lib.Arrow", 1); + OO_EXPECT("CALLS", "Main.Body", "Lib.Getter", 1); + OO_EXPECT("CALLS", "Main.Body", "Lib.Setter", 1); + OO_EXPECT("CALLS", "Main.Auto", "Lib.Init", 1); + OO_EXPECT("CALLS", "Main.Run", "Lib.Local", 1); + /* A local function hides a member of its name from the whole block, above it too. */ + OO_EXPECT("CALLS", "Main.Run", "Main.Hoisted", 0); + /* An access modifier: not a local function, so not a part of the method around it. */ + OO_EXPECT("CALLS", "Main.Outer", "Lib.Misread", 0); + oo_close(&index); + if (!oo_open(&index, KOTLIN_CALLER_FILES, + (int)(sizeof(KOTLIN_CALLER_FILES) / sizeof(KOTLIN_CALLER_FILES[0])), parallel, + "kt")) { + oo_close(&index); + return failed + 1; + } + OO_EXPECT("CALLS", "Main.outer", "Worker.go", 1); + OO_EXPECT("CALLS", "Box.member", "Worker.go", 1); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_callers_without_a_node_of_their_own) { + ASSERT_EQ(oo_caller_check(false), 0); + PASS(); +} + +TEST(resolver_oo_callers_without_a_node_of_their_own_parallel) { + ASSERT_EQ(oo_caller_check(true), 0); + PASS(); +} + +/* The C# language's own member lookup, as .NET's core library leans on it. */ +static const OOFile CSHARP_LOOKUP_FILES[] = { + {"System/Core.cs", + "namespace System {\n" + " public class Object {\n" + " public virtual string ToString() { return null; }\n" + " public Type GetType() { return null; }\n" + " public static bool ReferenceEquals(object a, object b) { return false; }\n" + " }\n" + " public abstract class ValueType { public override string ToString() { return null; } }\n" + " public abstract class Enum { public bool HasFlag(Enum flag) { return false; } }\n" + " public abstract class Type { public object GetMethod(string name) { return null; } }\n" + " public abstract class Array {\n" + " public object GetValue(int index) { return null; }\n" + " public int Length { get { return 0; } }\n" + " }\n" + "}\n"}, + {"Client/Types.cs", + "namespace Client {\n" + " public class Doc { public void Go() {} }\n" + " public class Other { public void Go() {} }\n" + " public class Stream {}\n" + " public class FileStream : Stream {}\n" + " public class Plain {}\n" + " public struct Point {}\n" + " public interface IOld { void Put(object key, object value); Other Size { get; } }\n" + " public class Dict : IOld {\n" + " void IOld.Put(object key, object value) {}\n" + " public void Put(string key, string value) {}\n" + " Other IOld.Size => null;\n" + " public Doc Size => null;\n" + " }\n" + " public class Base { public void Log(object o) {} }\n" + " public class Derived : Base { public void Log(Stream s) {} }\n" + " public class Bag {\n" + " public Doc this[string key] { get { return null; } }\n" + " public Other this[int index] { get { return null; } }\n" + " }\n" + " public static class Box { public static T Cast(object o) { return default; } }\n" + " public static class Lib { public static void Run() {} }\n" + " public class Span {}\n" + " public enum Color { Red }\n" + " public class Task {}\n" + " public class Later { public Task Fetch() { return null; } }\n" + " public static class Text { public static void Show(this string s) {} }\n" + " public class Holder { public object Field; }\n" + " public class Node2 { public static implicit operator Node2(bool value) { return null; } }\n" + " public partial class Sink {\n" + " public void Add(Node2 value) {}\n" + " }\n" + " public class Bytes {}\n" + " public partial class Spans {\n" + " public void Write(System.ReadOnlySpan utf8) {}\n" + " }\n" + "}\n"}, + {"Client/Sink.Generic.cs", + "namespace Client {\n" + " public partial class Sink {\n" + " public void Add(T value) {}\n" + " }\n" + "}\n"}, + {"Client/Spans.Chars.cs", + "namespace Client {\n" + " public partial class Spans {\n" + " public void Write(System.ReadOnlySpan chars) {}\n" + " }\n" + "}\n"}, + {"Client/Writer.Float.cs", + "namespace Client {\n" + " public partial class Writer {\n" + " public void Put(Span name, float value) {}\n" + " public bool Parse(Span text, out bool value) { value = false; return true; }\n" + " }\n" + "}\n"}, + {"Client/Writer.Double.cs", + "namespace Client {\n" + " public partial class Writer {\n" + " public void Put(Span name, double value) {}\n" + " public bool Parse(Span text, out int value) { value = 0; return true; }\n" + " }\n" + "}\n"}, + {"Client/Use.cs", + "namespace Client {\n" + " public class Use {\n" + " void ViaClass(Dict d, Mystery m) { d.Put(m.X, m.Y); }\n" + " void ViaInterface(IOld i, Mystery m) { i.Put(m.X, m.Y); }\n" + " void Explicit(Dict d) { d.Size.Go(); }\n" + " void Derives(Derived d, FileStream f) { d.Log(f); }\n" + " void BaseOnly(Derived d, Plain p) { d.Log(p); }\n" + " void UnknownArg(Derived d, Mystery m) { d.Log(m.X); }\n" + " void Indexed(Bag b) { b[\"k\"].Go(); }\n" + " void IndexedByInt(Bag b) { b[1].Go(); }\n" + " void Spelled(object o) { Box.Cast(o).Go(); }\n" + " void Rooted() { global::Client.Lib.Run(); }\n" + " void FloatLiteral(Writer w, Span s) { w.Put(s, 10.1f); }\n" + " void DoubleLiteral(Writer w, Span s) { w.Put(s, 10.1); }\n" + " void UnknownLiteral(Writer w, Span s, Mystery m) { w.Put(s, m.X); }\n" + " void OutBool(Writer w, Span s) { w.Parse(s, out bool v); }\n" + " void OutInt(Writer w, Span s) { w.Parse(s, out int v); }\n" + " void TypeOf() { typeof(Doc).GetMethod(\"Go\"); }\n" + " void ArrayMember(int[] items) { items.GetValue(0); }\n" + " void ObjectMember(Plain p) { p.GetType(); }\n" + " void BareObject(Plain p) { ReferenceEquals(p, p); }\n" + " void StructMember(Point p) { p.ToString(); }\n" + " void EnumMember(Color c) { c.HasFlag(Color.Red); }\n" + " async void Awaited(Later l) { (await l.Fetch()).Go(); }\n" + " void Concatenated(Mystery m) { (\"a\" + m.X).Show(); }\n" + " void Sums(int a, int b) { (a + b).Show(); }\n" + " void GenericWins(Sink s) { s.Add(false); }\n" + " void BytesToSpan(Spans s, byte[] bytes) { s.Write(bytes); }\n" + " void CharsToSpan(Spans s, char[] chars) { s.Write(chars); }\n" + " void EmptyPattern(Doc d) { if (d is { } bound) { bound.Go(); } }\n" + " void TypedPattern(object o) { if (o is Doc { } bound) { bound.Go(); } }\n" + " void TypeNotShadowed(object o) { if (o is Doc { } bound) { Doc made = null; made.Go(); } }\n" + " }\n" + "}\n"}, + {"Core/src/Core.projitems", "\n"}, + {"Core/src/ThrowHelper.cs", + "namespace Shared { public static class ThrowHelper { public static void Fail() {} } }\n"}, + {"Core/src/Array2.cs", + "namespace Shared { public class Array2 { void Run() { ThrowHelper.Fail(); } } }\n"}, + {"Memory/src/Memory.csproj", "\n"}, + {"Memory/src/ThrowHelper.cs", + "namespace Shared { public static class ThrowHelper { public static void Fail() {} } }\n"}, + {"Memory/src/Slice.cs", + "namespace Shared { public class Slice { void Run() { ThrowHelper.Fail(); } } }\n"}, + {"Floating/Loose.cs", + "namespace Shared { public class Loose { void Run() { ThrowHelper.Fail(); } } }\n"}, +}; + +static int oo_csharp_lookup_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, CSHARP_LOOKUP_FILES, + (int)(sizeof(CSHARP_LOOKUP_FILES) / sizeof(CSHARP_LOOKUP_FILES[0])), parallel, + "cs")) { + oo_close(&index); + return 1; + } + /* An interface's members are no class's. Through the interface they are the only ones. */ + OO_EXPECT("CALLS", "Use.ViaClass", "Dict.Put", 1); + OO_EXPECT("CALLS", "Use.ViaClass", "IOld.Put", 0); + OO_EXPECT("CALLS", "Use.ViaInterface", "IOld.Put", 1); + OO_EXPECT("CALLS", "Use.ViaInterface", "Dict.Put", 0); + /* An explicit implementation is no member: Size is the public one. */ + OO_EXPECT("CALLS", "Use.Explicit", "Doc.Go", 1); + OO_EXPECT("CALLS", "Use.Explicit", "Other.Go", 0); + /* A more derived applicable method hides the base's. Not applicable, or not known to be: */ + OO_EXPECT("CALLS", "Use.Derives", "Derived.Log", 1); + OO_EXPECT("CALLS", "Use.Derives", "Base.Log", 0); + OO_EXPECT("CALLS", "Use.BaseOnly", "Base.Log", 1); + OO_EXPECT("CALLS", "Use.BaseOnly", "Derived.Log", 0); + OO_EXPECT("CALLS", "Use.UnknownArg", "Derived.Log", 0); + OO_EXPECT("CALLS", "Use.UnknownArg", "Base.Log", 0); + /* An indexer's declared result, by its argument. */ + OO_EXPECT("CALLS", "Use.Indexed", "Doc.Go", 1); + OO_EXPECT("CALLS", "Use.Indexed", "Other.Go", 0); + OO_EXPECT("CALLS", "Use.IndexedByInt", "Other.Go", 1); + OO_EXPECT("CALLS", "Use.IndexedByInt", "Doc.Go", 0); + /* Type arguments a call spells, and the namespace root. */ + OO_EXPECT("CALLS", "Use.Spelled", "Doc.Go", 1); + OO_EXPECT("CALLS", "Use.Rooted", "Lib.Run", 1); + /* An identical parameter is the better conversion, across a partial type's files. */ + OO_EXPECT("CALLS", "Use.FloatLiteral", "Writer.Float.Writer.Put", 1); + OO_EXPECT("CALLS", "Use.FloatLiteral", "Writer.Double.Writer.Put", 0); + OO_EXPECT("CALLS", "Use.DoubleLiteral", "Writer.Double.Writer.Put", 1); + OO_EXPECT("CALLS", "Use.DoubleLiteral", "Writer.Float.Writer.Put", 0); + OO_EXPECT("CALLS", "Use.UnknownLiteral", "Writer.Float.Writer.Put", 0); + OO_EXPECT("CALLS", "Use.UnknownLiteral", "Writer.Double.Writer.Put", 0); + OO_EXPECT("CALLS", "Use.OutBool", "Writer.Float.Writer.Parse", 1); + OO_EXPECT("CALLS", "Use.OutInt", "Writer.Double.Writer.Parse", 1); + /* The core library's own types, where the project declares them. */ + OO_EXPECT("CALLS", "Use.TypeOf", "Type.GetMethod", 1); + OO_EXPECT("CALLS", "Use.ArrayMember", "Array.GetValue", 1); + OO_EXPECT("CALLS", "Use.ObjectMember", "Object.GetType", 1); + OO_EXPECT("CALLS", "Use.BareObject", "Object.ReferenceEquals", 1); + OO_EXPECT("CALLS", "Use.StructMember", "Object.ToString", 0); + OO_EXPECT("CALLS", "Use.StructMember", "ValueType.ToString", 1); + OO_EXPECT("CALLS", "Use.EnumMember", "Enum.HasFlag", 1); + OO_EXPECT("CALLS", "Use.Awaited", "Doc.Go", 1); + /* A string on either side of `+` is a string. Two ints are not. */ + OO_EXPECT("CALLS", "Use.Concatenated", "Text.Show", 1); + OO_EXPECT("CALLS", "Use.Sums", "Text.Show", 0); + /* A generic parameter takes the argument as it is; a user conversion does not. */ + OO_EXPECT("CALLS", "Use.GenericWins", "Sink.Generic.Sink.Add", 1); + OO_EXPECT("CALLS", "Use.GenericWins", "Types.Sink.Add", 0); + /* An array reaches a span of its own element only. */ + OO_EXPECT("CALLS", "Use.BytesToSpan", "Types.Spans.Write", 1); + OO_EXPECT("CALLS", "Use.BytesToSpan", "Spans.Chars.Spans.Write", 0); + OO_EXPECT("CALLS", "Use.CharsToSpan", "Spans.Chars.Spans.Write", 1); + OO_EXPECT("CALLS", "Use.CharsToSpan", "Types.Spans.Write", 0); + /* A pattern binds the type it spells, or what it is matched against. */ + OO_EXPECT("CALLS", "Use.EmptyPattern", "Doc.Go", 1); + OO_EXPECT("CALLS", "Use.TypedPattern", "Doc.Go", 1); + OO_EXPECT("CALLS", "Use.TypeNotShadowed", "Doc.Go", 1); + /* A shared project is a project: each reader means the one its project compiles. */ + OO_EXPECT("CALLS", "Array2.Run", "Core.src.ThrowHelper.ThrowHelper.Fail", 1); + OO_EXPECT("CALLS", "Array2.Run", "Memory.src.ThrowHelper.ThrowHelper.Fail", 0); + OO_EXPECT("CALLS", "Slice.Run", "Memory.src.ThrowHelper.ThrowHelper.Fail", 1); + OO_EXPECT("CALLS", "Slice.Run", "Core.src.ThrowHelper.ThrowHelper.Fail", 0); + /* In no project: two declarations, no way to choose. */ + OO_EXPECT("CALLS", "Loose.Run", "Core.src.ThrowHelper.ThrowHelper.Fail", 0); + OO_EXPECT("CALLS", "Loose.Run", "Memory.src.ThrowHelper.ThrowHelper.Fail", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_csharp_member_lookup) { + ASSERT_EQ(oo_csharp_lookup_check(false), 0); + PASS(); +} + +TEST(resolver_oo_csharp_member_lookup_parallel) { + ASSERT_EQ(oo_csharp_lookup_check(true), 0); + PASS(); +} + +/* What a JDK factory holds is spelled by its arguments, and Class by its class literal. */ +static const OOFile JAVA_FACTORY_FILES[] = { + {"acme/Doc.java", "package acme;\npublic class Doc { public void go() {} }\n"}, + {"acme/Other.java", "package acme;\npublic class Other { public void go() {} }\n"}, + {"acme/Holder.java", + "package acme;\nimport java.util.List;\n" + "public class Holder { public List docs() { return null; } }\n"}, + {"acme/Registry.java", + "package acme;\nimport java.util.stream.Stream;\n" + "public class Registry {\n" + " public T find(Class type) { return null; }\n" + " public Stream all(Class type) { return null; }\n" + "}\n"}, + {"acme/Use.java", + "package acme;\n" + "import java.util.Arrays;\nimport java.util.List;\nimport java.util.Optional;\n" + "public class Use {\n" + " void viaOptional(Doc d) { Optional.ofNullable(d).ifPresent(x -> x.go()); }\n" + " void viaListOf(Doc d) { List.of(d).forEach(x -> x.go()); }\n" + " void viaArrays(Doc[] items) { Arrays.stream(items).forEach(x -> x.go()); }\n" + " void disagreeing(Doc d, Other o) { List.of(d, o).forEach(x -> x.go()); }\n" + " void viaFlatMap(List holders) {\n" + " holders.stream().flatMap(h -> h.docs().stream()).forEach(x -> x.go());\n" + " }\n" + " void viaClass(Registry r) { r.find(Doc.class).go(); }\n" + " void viaClassStream(Registry r) { r.all(Other.class).forEach(x -> x.go()); }\n" + "}\n"}, + {"plain/Optional.java", + "package plain;\npublic class Optional { public static Other ofNullable(Object o) { return null; } }\n"}, + {"plain/Other.java", "package plain;\npublic class Other { public void go() {} }\n"}, + {"plain/Use2.java", + "package plain;\n" + "public class Use2 { void own(acme.Doc d) { Optional.ofNullable(d).go(); } }\n"}, +}; + +static int oo_java_factory_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, JAVA_FACTORY_FILES, + (int)(sizeof(JAVA_FACTORY_FILES) / sizeof(JAVA_FACTORY_FILES[0])), parallel, + "java")) { + oo_close(&index); + return 1; + } + OO_EXPECT("CALLS", "Use.viaOptional", "acme.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaListOf", "acme.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaArrays", "acme.Doc.go", 1); + OO_EXPECT("CALLS", "Use.disagreeing", "acme.Doc.go", 0); + OO_EXPECT("CALLS", "Use.disagreeing", "acme.Other.go", 0); + OO_EXPECT("CALLS", "Use.viaFlatMap", "acme.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaClass", "acme.Doc.go", 1); + OO_EXPECT("CALLS", "Use.viaClassStream", "acme.Other.go", 1); + /* A project's own Optional is itself, not the JDK's. */ + OO_EXPECT("CALLS", "Use2.own", "plain.Other.go", 1); + OO_EXPECT("CALLS", "Use2.own", "acme.Doc.go", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_java_factories_and_class_literals) { + ASSERT_EQ(oo_java_factory_check(false), 0); + PASS(); +} + +TEST(resolver_oo_java_factories_and_class_literals_parallel) { + ASSERT_EQ(oo_java_factory_check(true), 0); + PASS(); +} + +/* A lambda's receiver is what settles it. A type parameter nothing settled says nothing about what + * the receiver lacks, so a name it does not show may still be its own. */ +static const OOFile KOTLIN_RECEIVER_FILES[] = { + {"lib/Lib.kt", + "package lib\n" + "open class Table\n" + "class Stmt\n" + "class Tx { val id = 1 }\n" + "object Users : Table() { val id = 2 }\n" + "fun T.insert(body: T.(Stmt) -> Unit) {}\n" + "fun make(body: T.() -> Unit) {}\n" + "fun T.push(query: Stmt, columns: List? = null): Int = 0\n" + "fun T.push(body: T.(Stmt) -> Unit): Stmt = Stmt()\n" + "fun tx(body: Tx.() -> Unit) {}\n"}, + {"app/Main.kt", + "package app\n" + "import lib.*\n" + "fun settled() { tx { Users.insert { println(id) } } }\n" + "fun unsettled() { tx { make { println(id) } } }\n" + "fun plain() { tx { println(id) } }\n" + "fun overloaded() { tx { Users.push { println(id) } } }\n" + "fun anonymous() {\n" + " val tester = object : Table() { val id = 3 }\n" + " tx { tester.insert { println(id) } }\n" + "}\n"}, +}; + +static const OOFile CSHARP_INITIALIZER_FILES[] = { + {"Init/Options.cs", + "namespace Init {\n" + " public class Item {}\n" + " public class Options { public System.Collections.Generic.List Items; public string Name; }\n" + " public class Use {\n" + " Options Make() { return new Options { Items = { new Item() }, Name = \"n\" }; }\n" + " }\n" + "}\n"}, +}; + +static int oo_receiver_check(bool parallel) { + OOIndex index; + int failed = 0; + if (!oo_open(&index, KOTLIN_RECEIVER_FILES, + (int)(sizeof(KOTLIN_RECEIVER_FILES) / sizeof(KOTLIN_RECEIVER_FILES[0])), parallel, + "kt")) { + oo_close(&index); + return 1; + } + OO_EXPECT("USAGE", "Main.settled", "Users.id", 1); + OO_EXPECT("USAGE", "Main.settled", "Tx.id", 0); + OO_EXPECT("USAGE", "Main.unsettled", "Tx.id", 0); + OO_EXPECT("USAGE", "Main.plain", "Tx.id", 1); + /* A closing lambda is no List: of the overloads on one node, the function type is the one. */ + OO_EXPECT("USAGE", "Main.overloaded", "Users.id", 1); + OO_EXPECT("USAGE", "Main.overloaded", "Tx.id", 0); + /* An anonymous object's own id: not the transaction's, though the object's supertype lacks it. */ + OO_EXPECT("USAGE", "Main.anonymous", "Tx.id", 0); + oo_close(&index); + if (!oo_open(&index, CSHARP_INITIALIZER_FILES, + (int)(sizeof(CSHARP_INITIALIZER_FILES) / sizeof(CSHARP_INITIALIZER_FILES[0])), + parallel, "cs")) { + oo_close(&index); + return failed + 1; + } + OO_EXPECT("WRITES", "Use.Make", "Options.Name", 1); + OO_EXPECT("WRITES", "Use.Make", "Options.Items", 0); + oo_close(&index); + return failed; +} + +TEST(resolver_oo_lambda_receivers_and_collection_initializers) { + ASSERT_EQ(oo_receiver_check(false), 0); + PASS(); +} + +TEST(resolver_oo_lambda_receivers_and_collection_initializers_parallel) { + ASSERT_EQ(oo_receiver_check(true), 0); + PASS(); +} + static const OOFile MIXED_FILES[] = { {"native/Cargo.toml", "[package]\nname = \"native\"\nversion = \"0.1.0\"\n"}, {"native/src/lib.rs", "pub fn f() -> i32 { 1 }\n"}, @@ -968,4 +2840,42 @@ SUITE(resolver_oo) { RUN_TEST(resolver_oo_large_file_binds_its_last_site); RUN_TEST(resolver_oo_csharp_repaired_parse_and_exact_overloads); RUN_TEST(resolver_oo_csharp_repaired_parse_and_exact_overloads_parallel); + RUN_TEST(resolver_oo_csharp_reference_assembly_stands_behind); + RUN_TEST(resolver_oo_csharp_reference_assembly_stands_behind_parallel); + RUN_TEST(resolver_oo_kotlin_suspend_function_type); + RUN_TEST(resolver_oo_kotlin_suspend_function_type_parallel); + RUN_TEST(resolver_oo_java_class_level_and_generic_type_mentions); + RUN_TEST(resolver_oo_java_class_level_and_generic_type_mentions_parallel); + RUN_TEST(resolver_oo_csharp_generic_type_mentions); + RUN_TEST(resolver_oo_csharp_generic_type_mentions_parallel); + RUN_TEST(resolver_oo_java_lambda_parameters_from_the_selected_declaration); + RUN_TEST(resolver_oo_java_lambda_parameters_from_the_selected_declaration_parallel); + RUN_TEST(resolver_oo_csharp_conversions_lambdas_and_keyword_types); + RUN_TEST(resolver_oo_csharp_conversions_lambdas_and_keyword_types_parallel); + RUN_TEST(resolver_oo_csharp_system_delegates_without_a_core_library); + RUN_TEST(resolver_oo_csharp_system_delegates_without_a_core_library_parallel); + RUN_TEST(resolver_oo_kotlin_same_name_in_two_projects); + RUN_TEST(resolver_oo_kotlin_same_name_in_two_projects_parallel); + RUN_TEST(resolver_oo_csharp_same_name_in_one_file_and_one_project); + RUN_TEST(resolver_oo_csharp_same_name_in_one_file_and_one_project_parallel); + RUN_TEST(resolver_oo_java_same_name_in_two_projects); + RUN_TEST(resolver_oo_java_same_name_in_two_projects_parallel); + RUN_TEST(resolver_oo_kotlin_anonymous_objects_destructuring_named_lambdas); + RUN_TEST(resolver_oo_kotlin_anonymous_objects_destructuring_named_lambdas_parallel); + RUN_TEST(resolver_oo_csharp_syntax_that_loses_a_whole_file); + RUN_TEST(resolver_oo_csharp_syntax_that_loses_a_whole_file_parallel); + RUN_TEST(resolver_oo_csharp_extension_receivers_partial_types_attributes); + RUN_TEST(resolver_oo_csharp_extension_receivers_partial_types_attributes_parallel); + RUN_TEST(resolver_oo_annotations_name_their_type); + RUN_TEST(resolver_oo_annotations_name_their_type_parallel); + RUN_TEST(resolver_oo_csharp_syntax_the_grammar_lacks); + RUN_TEST(resolver_oo_csharp_syntax_the_grammar_lacks_parallel); + RUN_TEST(resolver_oo_callers_without_a_node_of_their_own); + RUN_TEST(resolver_oo_callers_without_a_node_of_their_own_parallel); + RUN_TEST(resolver_oo_csharp_member_lookup); + RUN_TEST(resolver_oo_csharp_member_lookup_parallel); + RUN_TEST(resolver_oo_java_factories_and_class_literals); + RUN_TEST(resolver_oo_java_factories_and_class_literals_parallel); + RUN_TEST(resolver_oo_lambda_receivers_and_collection_initializers); + RUN_TEST(resolver_oo_lambda_receivers_and_collection_initializers_parallel); }