mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
The project-file scan wrote the "could not be read" blob for a *.csproj that is no readable MSBuild <Project>, and for a *.props or *.targets file only once its <Project> root was seen. A *.props or *.targets file that is malformed before its root, or ends before one, got no blob at all: an import of it counted as outside the repository with a closed scope, and as the nearest Directory.Build.* it was passed over for the one above, whose usings were then applied although MSBuild does not read that file. Such a file now gets the "could not be read" blob too, so the projects that evaluate it have an open scope. A *.props or *.targets file whose root element is not <Project> is still no MSBuild file and has no blob. Test: msbuild_malformed_props_opens (a malformed nearest Directory.Build.props under a usable one: a name found nowhere is external, and a type only the upper file's using reaches is not bound; decoy: a project without a nearer file binds through the upper file and keeps a closed scope). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
3791 lines
131 KiB
C
3791 lines
131 KiB
C
/*
|
|
* doclink_cs.c — C# doc-comment references and the C# doc-link scope.
|
|
*
|
|
* Tokens: the cref of <see>, <seealso>, <exception> and <inheritdoc>, and the
|
|
* href of <see>/<seealso> (external). <paramref>/<typeparamref> name the
|
|
* definition's own parameters and are not references; <include> pulls doc
|
|
* text from another file.
|
|
*
|
|
* Scope blob (one record per line, tab-separated, first line "cs1"; records
|
|
* in document order, so a member follows its type):
|
|
* X from to lines whose declarations could not be
|
|
* placed (the braces stop pairing, a
|
|
* namespace cannot be named, a block nests
|
|
* past a limit): a definition there has no
|
|
* known scope
|
|
* R id parent start end name
|
|
* namespace region: `name` as its
|
|
* declaration writes it (`A.B`), inside the
|
|
* region `parent` (0 = the file)
|
|
* U region kind alias target
|
|
* using: n namespace, s static, a alias;
|
|
* a `g` after it for a `global using`
|
|
* T region start end kind outer name tparams bases
|
|
* type: c class, s struct, i interface, e
|
|
* enum, r record class, t record struct, d
|
|
* delegate; then `p` when it is declared
|
|
* partial, then `!` when a parse error hides
|
|
* some of its members. `outer` is the ordinal
|
|
* of the enclosing type's T record (its
|
|
* position among the file's T records), `-`
|
|
* for none. tparams ','-joined, bases
|
|
* '|'-joined as written
|
|
* M start kind explicit type name tparams sig
|
|
* member of the type with ordinal `type`: c
|
|
* callable (method, constructor, primary
|
|
* constructor), v field (constant, enum
|
|
* member), p property (record parameter), e
|
|
* event, o operator (name: its token, or
|
|
* `implicit` / `explicit`), x indexer (name
|
|
* `this`); then `s` for what `using static`
|
|
* brings in (static or const, an enum's
|
|
* member, no extension method; a static
|
|
* constructor too). sig: '|'-joined normalized
|
|
* parameter types, '-' for v, p and e
|
|
* Q name a type that is declared, but whose
|
|
* namespace or outer type could not be
|
|
* established
|
|
* A record names its outer type and its owner by ordinal, never by a path, so
|
|
* the blob grows with the file and not with the nesting. Everything is
|
|
* derived from the tree alone, so the blob is a pure function of the file's
|
|
* bytes. Nesting is the tree's while the tree has no parse error, and the
|
|
* braces' otherwise (see "Braces" below).
|
|
*/
|
|
#include "doclink.h"
|
|
|
|
#include "arena.h"
|
|
#include "foundation/constants.h"
|
|
#include "foundation/mem_core.h"
|
|
#include "tree_sitter/api.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* ── Small arena string builder ──────────────────────────────────── */
|
|
|
|
typedef struct {
|
|
CBMArena *a;
|
|
char *buf;
|
|
size_t len;
|
|
size_t cap;
|
|
bool failed;
|
|
} cs_sb_t;
|
|
|
|
enum { CS_SB_INIT = 1024, CS_UINT_DIGITS = 16, CS_NAME_MAX = 512 };
|
|
|
|
static void sb_reserve(cs_sb_t *sb, size_t extra) {
|
|
if (sb->failed || sb->len + extra + SKIP_ONE <= sb->cap) {
|
|
return;
|
|
}
|
|
size_t ncap = sb->cap ? sb->cap : CS_SB_INIT;
|
|
while (ncap < sb->len + extra + SKIP_ONE) {
|
|
ncap *= PAIR_LEN;
|
|
}
|
|
char *grown = (char *)cbm_arena_alloc(sb->a, ncap);
|
|
if (!grown) {
|
|
sb->failed = true;
|
|
return;
|
|
}
|
|
if (sb->len > 0) {
|
|
memcpy(grown, sb->buf, sb->len);
|
|
}
|
|
sb->buf = grown;
|
|
sb->cap = ncap;
|
|
}
|
|
|
|
static void sb_putn(cs_sb_t *sb, const char *s, size_t n) {
|
|
sb_reserve(sb, n);
|
|
if (sb->failed) {
|
|
return;
|
|
}
|
|
memcpy(sb->buf + sb->len, s, n);
|
|
sb->len += n;
|
|
sb->buf[sb->len] = '\0';
|
|
}
|
|
|
|
static void sb_puts(cs_sb_t *sb, const char *s) {
|
|
sb_putn(sb, s, strlen(s));
|
|
}
|
|
|
|
static void sb_putc(cs_sb_t *sb, char c) {
|
|
sb_putn(sb, &c, SKIP_ONE);
|
|
}
|
|
|
|
static void sb_putu(cs_sb_t *sb, uint32_t v) {
|
|
char tmp[CS_UINT_DIGITS];
|
|
int n = snprintf(tmp, sizeof(tmp), "%u", v);
|
|
if (n > 0) {
|
|
sb_putn(sb, tmp, (size_t)n);
|
|
}
|
|
}
|
|
|
|
/* ── Well-formed text ─────────────────────────────────────────────
|
|
*
|
|
* A scope blob is stored as a JSON string, and a JSON writer refuses a
|
|
* string that is not UTF-8: every byte the scans write is well-formed UTF-8.
|
|
* A name or a text of a C# source that is not is not kept (a name: the
|
|
* declaration is not placed; a text: "?"); project-file text gets U+FFFD for
|
|
* every byte that is no part of a well-formed sequence. Both are a function
|
|
* of the file's bytes alone. */
|
|
|
|
static const char CS_REPLACEMENT[] = "\xEF\xBF\xBD"; /* U+FFFD */
|
|
|
|
enum {
|
|
CS_UTF8_TAIL_MASK = 0xC0,
|
|
CS_UTF8_TAIL = 0x80,
|
|
};
|
|
|
|
/* Length of the well-formed UTF-8 sequence at s[0, n), 1 to 4; 0 when there
|
|
* is none: a stray continuation byte, an overlong form, a surrogate, a code
|
|
* point past U+10FFFF, a sequence cut short. */
|
|
static size_t cs_utf8_len(const unsigned char *s, size_t n) {
|
|
if (n == 0) {
|
|
return 0;
|
|
}
|
|
unsigned char c = s[0];
|
|
if (c < 0x80) {
|
|
return SKIP_ONE;
|
|
}
|
|
size_t len = 0;
|
|
if (c >= 0xC2 && c <= 0xDF) {
|
|
len = PAIR_LEN;
|
|
} else if (c >= 0xE0 && c <= 0xEF) {
|
|
len = 3;
|
|
} else if (c >= 0xF0 && c <= 0xF4) {
|
|
len = 4;
|
|
}
|
|
if (len == 0 || n < len) {
|
|
return 0;
|
|
}
|
|
unsigned char c1 = s[1];
|
|
if ((c == 0xE0 && c1 < 0xA0) || (c == 0xED && c1 > 0x9F) || (c == 0xF0 && c1 < 0x90) ||
|
|
(c == 0xF4 && c1 > 0x8F)) {
|
|
return 0; /* overlong, surrogate, or past U+10FFFF */
|
|
}
|
|
for (size_t k = SKIP_ONE; k < len; k++) {
|
|
if ((s[k] & CS_UTF8_TAIL_MASK) != CS_UTF8_TAIL) {
|
|
return 0;
|
|
}
|
|
}
|
|
return len;
|
|
}
|
|
|
|
/* True when s[0, n) is well-formed UTF-8. */
|
|
static bool cs_utf8_ok(const char *s, size_t n) {
|
|
for (size_t i = 0; i < n;) {
|
|
size_t l = cs_utf8_len((const unsigned char *)s + i, n - i);
|
|
if (l == 0) {
|
|
return false;
|
|
}
|
|
i += l;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ── Doc-comment references ──────────────────────────────────────── */
|
|
|
|
static int cs_tag_syntax(const char *name, size_t len) {
|
|
static const struct {
|
|
const char *name;
|
|
int syntax;
|
|
} tags[] = {
|
|
{"see", CBM_DOCLINK_CS_SEE},
|
|
{"seealso", CBM_DOCLINK_CS_SEEALSO},
|
|
{"exception", CBM_DOCLINK_CS_EXCEPTION},
|
|
{"inheritdoc", CBM_DOCLINK_CS_INHERITDOC},
|
|
};
|
|
for (size_t i = 0; i < sizeof(tags) / sizeof(tags[0]); i++) {
|
|
if (strlen(tags[i].name) == len && memcmp(tags[i].name, name, len) == 0) {
|
|
return tags[i].syntax;
|
|
}
|
|
}
|
|
return CBM_DOCLINK_NONE;
|
|
}
|
|
|
|
static bool cs_attr_name_char(char c) {
|
|
return isalnum((unsigned char)c) || c == '_' || c == ':' || c == '.' || c == '-';
|
|
}
|
|
|
|
/* Decode the five XML entities, drop the comment prefix (`///`, ` * `) that
|
|
* follows a line break inside a value, collapse whitespace, trim. */
|
|
static const char *cs_clean_value(CBMArena *a, const char *v, size_t n) {
|
|
char *out;
|
|
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
|
if (cbm_doclink_test_fail_alloc(CBM_DOCLINK_ALLOC_VALUE)) {
|
|
out = NULL;
|
|
} else
|
|
#endif
|
|
{
|
|
out = (char *)cbm_arena_alloc(a, n + SKIP_ONE);
|
|
}
|
|
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
|
if (out) {
|
|
cbm_doclink_test_note_doc_work(0, 0, n + SKIP_ONE);
|
|
}
|
|
#endif
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
static const struct {
|
|
const char *ent;
|
|
char ch;
|
|
} ents[] = {{"<", '<'}, {">", '>'}, {"&", '&'}, {""", '"'}, {"'", '\''}};
|
|
size_t w = 0;
|
|
bool space = false;
|
|
size_t i = 0;
|
|
while (i < n) {
|
|
char c = v[i];
|
|
if (c == '\n' || c == '\r') {
|
|
i++;
|
|
while (i < n && (v[i] == ' ' || v[i] == '\t' || v[i] == '\r' || v[i] == '\n')) {
|
|
i++;
|
|
}
|
|
static const char line_doc[] = "///";
|
|
size_t ld = sizeof(line_doc) - SKIP_ONE;
|
|
if (i + ld <= n && memcmp(v + i, line_doc, ld) == 0) {
|
|
i += ld; /* the next `///` line of the same comment */
|
|
} else if (i < n && v[i] == '*' && !(i + SKIP_ONE < n && v[i + SKIP_ONE] == '/')) {
|
|
i++; /* a block comment's leading star */
|
|
}
|
|
space = true;
|
|
continue;
|
|
}
|
|
if (c == ' ' || c == '\t') {
|
|
space = true;
|
|
i++;
|
|
continue;
|
|
}
|
|
if (space && w > 0) {
|
|
out[w++] = ' ';
|
|
}
|
|
space = false;
|
|
if (c == '&') {
|
|
bool decoded = false;
|
|
for (size_t e = 0; e < sizeof(ents) / sizeof(ents[0]); e++) {
|
|
size_t el = strlen(ents[e].ent);
|
|
if (i + el <= n && memcmp(v + i, ents[e].ent, el) == 0) {
|
|
out[w++] = ents[e].ch;
|
|
i += el;
|
|
decoded = true;
|
|
break;
|
|
}
|
|
}
|
|
if (decoded) {
|
|
continue;
|
|
}
|
|
}
|
|
out[w++] = c;
|
|
i++;
|
|
}
|
|
out[w] = '\0';
|
|
return out;
|
|
}
|
|
|
|
bool cbm_doclink_cs_parse_doc_checked(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
|
|
uint32_t doc_line) {
|
|
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
|
cbm_doclink_test_note_doc_work(0, strlen(doc), 0);
|
|
#endif
|
|
bool complete = true;
|
|
const char *p = doc;
|
|
const char *counted = doc;
|
|
uint32_t line = doc_line;
|
|
while ((p = strchr(p, '<')) != NULL) {
|
|
/* what stands in an XML comment or a CDATA section is text, not
|
|
* markup: a cref written there is no reference */
|
|
static const char comment_open[] = "<!--";
|
|
static const char cdata_open[] = "<![CDATA[";
|
|
bool comment = strncmp(p, comment_open, sizeof(comment_open) - SKIP_ONE) == 0;
|
|
if (comment || strncmp(p, cdata_open, sizeof(cdata_open) - SKIP_ONE) == 0) {
|
|
const char *end = strstr(p, comment ? "-->" : "]]>");
|
|
if (!end) {
|
|
break; /* it does not end: the rest of the text is inside it */
|
|
}
|
|
p = end + 3;
|
|
continue;
|
|
}
|
|
const char *tag = p;
|
|
const char *q = p + SKIP_ONE;
|
|
while (*q == ' ' || *q == '\t') {
|
|
q++;
|
|
}
|
|
const char *name = q;
|
|
while (isalpha((unsigned char)*q)) {
|
|
q++;
|
|
}
|
|
int syntax = cs_tag_syntax(name, (size_t)(q - name));
|
|
if (syntax == CBM_DOCLINK_NONE ||
|
|
!(*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r' || *q == '/' || *q == '>')) {
|
|
p = tag + SKIP_ONE;
|
|
continue;
|
|
}
|
|
const char *cref = NULL;
|
|
size_t cref_len = 0;
|
|
const char *href = NULL;
|
|
size_t href_len = 0;
|
|
bool closed = false;
|
|
while (*q) {
|
|
if (*q == '>') {
|
|
closed = true;
|
|
q++;
|
|
break;
|
|
}
|
|
if (*q == '<') {
|
|
break; /* malformed: another tag starts first */
|
|
}
|
|
if (*q == '"' || *q == '\'') {
|
|
const char *end = strchr(q + SKIP_ONE, *q);
|
|
if (!end) {
|
|
break;
|
|
}
|
|
q = end + SKIP_ONE;
|
|
continue;
|
|
}
|
|
if (!cs_attr_name_char(*q)) {
|
|
q++;
|
|
continue;
|
|
}
|
|
const char *an = q;
|
|
while (cs_attr_name_char(*q)) {
|
|
q++;
|
|
}
|
|
size_t an_len = (size_t)(q - an);
|
|
const char *r = q;
|
|
while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') {
|
|
r++;
|
|
}
|
|
if (*r != '=') {
|
|
continue;
|
|
}
|
|
r++;
|
|
while (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') {
|
|
r++;
|
|
}
|
|
if (*r != '"' && *r != '\'') {
|
|
q = r;
|
|
continue;
|
|
}
|
|
const char *vend = strchr(r + SKIP_ONE, *r);
|
|
if (!vend) {
|
|
break;
|
|
}
|
|
const char *val = r + SKIP_ONE;
|
|
size_t val_len = (size_t)(vend - val);
|
|
if (an_len == 4 && memcmp(an, "cref", 4) == 0) {
|
|
cref = val;
|
|
cref_len = val_len;
|
|
} else if (an_len == 4 && memcmp(an, "href", 4) == 0) {
|
|
href = val;
|
|
href_len = val_len;
|
|
}
|
|
q = vend + SKIP_ONE;
|
|
}
|
|
if (!closed) {
|
|
p = tag + SKIP_ONE;
|
|
continue;
|
|
}
|
|
for (; counted < tag; counted++) {
|
|
if (*counted == '\n') {
|
|
line++;
|
|
}
|
|
}
|
|
const char *raw = NULL;
|
|
int tok_syntax = CBM_DOCLINK_NONE;
|
|
if (cref) {
|
|
raw = cs_clean_value(ctx->arena, cref, cref_len);
|
|
tok_syntax = syntax;
|
|
} else if (href && (syntax == CBM_DOCLINK_CS_SEE || syntax == CBM_DOCLINK_CS_SEEALSO)) {
|
|
raw = cs_clean_value(ctx->arena, href, href_len);
|
|
tok_syntax = CBM_DOCLINK_HREF;
|
|
}
|
|
if (tok_syntax != CBM_DOCLINK_NONE && !raw) {
|
|
complete = false;
|
|
}
|
|
if (raw && raw[0]) {
|
|
CBMDocLink link = {
|
|
.source_qn = def->qualified_name,
|
|
.raw = raw,
|
|
.line = line,
|
|
.def_line = def->start_line,
|
|
.syntax = (uint16_t)tok_syntax,
|
|
};
|
|
int before = ctx->result->doc_links.count;
|
|
cbm_doclinks_push(&ctx->result->doc_links, ctx->arena, link);
|
|
if (ctx->result->doc_links.count == before) {
|
|
complete = false;
|
|
}
|
|
}
|
|
p = q;
|
|
}
|
|
return complete;
|
|
}
|
|
|
|
void cbm_doclink_cs_parse_doc(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
|
|
uint32_t doc_line) {
|
|
(void)cbm_doclink_cs_parse_doc_checked(ctx, def, doc, doc_line);
|
|
}
|
|
|
|
/* ── Parameter type normalization ────────────────────────────────── */
|
|
|
|
typedef struct {
|
|
char *s;
|
|
size_t len;
|
|
} cs_str_t;
|
|
|
|
static bool cs_ident_start(char c) {
|
|
return isalpha((unsigned char)c) || c == '_' || c == '@';
|
|
}
|
|
|
|
static bool cs_ident_char(char c) {
|
|
return isalnum((unsigned char)c) || c == '_';
|
|
}
|
|
|
|
/* Remove [start, end) from s. */
|
|
static void cs_cut(cs_str_t *t, size_t start, size_t end) {
|
|
memmove(t->s + start, t->s + end, t->len - end + SKIP_ONE);
|
|
t->len -= end - start;
|
|
}
|
|
|
|
static void cs_trim(cs_str_t *t) {
|
|
size_t b = 0;
|
|
while (b < t->len && isspace((unsigned char)t->s[b])) {
|
|
b++;
|
|
}
|
|
if (b > 0) {
|
|
cs_cut(t, 0, b);
|
|
}
|
|
while (t->len > 0 && isspace((unsigned char)t->s[t->len - SKIP_ONE])) {
|
|
t->s[--t->len] = '\0';
|
|
}
|
|
}
|
|
|
|
/* Index just past the bracket group opened at s[i] (one of < { [ ( ),
|
|
* matching nested groups of any of those kinds; t->len when unbalanced. */
|
|
static size_t cs_group_end(const cs_str_t *t, size_t i) {
|
|
int depth = 0;
|
|
for (size_t k = i; k < t->len; k++) {
|
|
char c = t->s[k];
|
|
if (c == '<' || c == '{' || c == '[' || c == '(') {
|
|
depth++;
|
|
} else if (c == '>' || c == '}' || c == ']' || c == ')') {
|
|
depth--;
|
|
if (depth == 0) {
|
|
return k + SKIP_ONE;
|
|
}
|
|
}
|
|
}
|
|
return t->len;
|
|
}
|
|
|
|
/* Leading parameter attribute lists: [NotNull] [In] T x. */
|
|
static void cs_drop_attributes(cs_str_t *t) {
|
|
cs_trim(t);
|
|
while (t->len > 0 && t->s[0] == '[') {
|
|
size_t e = cs_group_end(t, 0);
|
|
cs_cut(t, 0, e);
|
|
cs_trim(t);
|
|
}
|
|
}
|
|
|
|
/* Nullable<X> / Nullable{X} (optionally System.- or global::System.-
|
|
* qualified) -> X. */
|
|
static void cs_unwrap_nullable(cs_str_t *t) {
|
|
static const char *const prefixes[] = {"global::System.Nullable", "System.Nullable",
|
|
"Nullable"};
|
|
/* one pass: what an unwrapped group leaves at its place is looked at
|
|
* again (Nullable<Nullable<X>>), the text before it never is */
|
|
for (size_t i = 0; i < t->len;) {
|
|
bool unwrapped = false;
|
|
bool boundary = i == 0 || !(cs_ident_char(t->s[i - SKIP_ONE]) ||
|
|
t->s[i - SKIP_ONE] == '.' || t->s[i - SKIP_ONE] == ':');
|
|
for (size_t pi = 0; boundary && !unwrapped && pi < sizeof(prefixes) / sizeof(prefixes[0]);
|
|
pi++) {
|
|
size_t pl = strlen(prefixes[pi]);
|
|
if (i + pl > t->len || memcmp(t->s + i, prefixes[pi], pl) != 0) {
|
|
continue;
|
|
}
|
|
size_t j = i + pl;
|
|
while (j < t->len && t->s[j] == ' ') {
|
|
j++;
|
|
}
|
|
if (j >= t->len || (t->s[j] != '<' && t->s[j] != '{')) {
|
|
continue;
|
|
}
|
|
size_t e = cs_group_end(t, j);
|
|
if (e > t->len || e <= j + SKIP_ONE) {
|
|
continue;
|
|
}
|
|
/* keep the inner text */
|
|
size_t inner_len = e - j - PAIR_LEN;
|
|
memmove(t->s + i, t->s + j + SKIP_ONE, inner_len);
|
|
memmove(t->s + i + inner_len, t->s + e, t->len - e + SKIP_ONE);
|
|
t->len = i + inner_len + (t->len - e);
|
|
unwrapped = true;
|
|
}
|
|
if (!unwrapped) {
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool cs_word_at(const cs_str_t *t, size_t i, const char *w) {
|
|
size_t wl = strlen(w);
|
|
if (i + wl > t->len || memcmp(t->s + i, w, wl) != 0) {
|
|
return false;
|
|
}
|
|
if (i > 0 && cs_ident_char(t->s[i - SKIP_ONE])) {
|
|
return false;
|
|
}
|
|
return i + wl < t->len && isspace((unsigned char)t->s[i + wl]);
|
|
}
|
|
|
|
static void cs_drop_modifiers(cs_str_t *t) {
|
|
static const char *const mods[] = {"ref", "out", "in", "params",
|
|
"this", "scoped", "readonly", "final"};
|
|
for (size_t i = 0; i < t->len;) {
|
|
bool cut = false;
|
|
for (size_t m = 0; m < sizeof(mods) / sizeof(mods[0]); m++) {
|
|
if (cs_word_at(t, i, mods[m])) {
|
|
size_t e = i + strlen(mods[m]);
|
|
while (e < t->len && isspace((unsigned char)t->s[e])) {
|
|
e++;
|
|
}
|
|
cs_cut(t, i, e);
|
|
cut = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!cut) {
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Doc-ID arity markers: `1, ``0. */
|
|
static void cs_drop_backtick_arity(cs_str_t *t) {
|
|
for (size_t i = 0; i < t->len;) {
|
|
if (t->s[i] != '`') {
|
|
i++;
|
|
continue;
|
|
}
|
|
size_t e = i;
|
|
while (e < t->len && t->s[e] == '`') {
|
|
e++;
|
|
}
|
|
size_t d = e;
|
|
while (d < t->len && isdigit((unsigned char)t->s[d])) {
|
|
d++;
|
|
}
|
|
if (d > e) {
|
|
cs_cut(t, i, d);
|
|
} else {
|
|
i = e;
|
|
}
|
|
}
|
|
}
|
|
|
|
enum { CS_NORM_WORK = 512 };
|
|
|
|
/* Remove every <...> and {...} group. An opener pairs with the next closer of
|
|
* its own kind that no unpaired opener stands before (`<` with `>`, `{` with
|
|
* `}`), so groups nest; what does not pair stays. Two passes over the text:
|
|
* pair, then copy what is outside. */
|
|
static void cs_drop_type_args(cs_str_t *t) {
|
|
uint16_t past[CS_NORM_WORK]; /* for a paired opener: index just past its closer */
|
|
uint16_t open[CS_NORM_WORK];
|
|
size_t depth = 0;
|
|
if (t->len >= CS_NORM_WORK) {
|
|
return;
|
|
}
|
|
for (size_t i = 0; i < t->len; i++) {
|
|
char c = t->s[i];
|
|
past[i] = 0;
|
|
if (c == '<' || c == '{') {
|
|
open[depth++] = (uint16_t)i;
|
|
} else if (depth > 0 && ((c == '>' && t->s[open[depth - SKIP_ONE]] == '<') ||
|
|
(c == '}' && t->s[open[depth - SKIP_ONE]] == '{'))) {
|
|
past[open[--depth]] = (uint16_t)(i + SKIP_ONE);
|
|
}
|
|
}
|
|
size_t w = 0;
|
|
for (size_t i = 0; i < t->len;) {
|
|
if (past[i]) {
|
|
i = past[i];
|
|
} else {
|
|
t->s[w++] = t->s[i++];
|
|
}
|
|
}
|
|
t->s[w] = '\0';
|
|
t->len = w;
|
|
}
|
|
|
|
static const char *cs_bcl_alias(const char *base, size_t len) {
|
|
static const struct {
|
|
const char *bcl;
|
|
const char *kw;
|
|
} map[] = {
|
|
{"Int32", "int"}, {"Int64", "long"}, {"Int16", "short"}, {"Byte", "byte"},
|
|
{"SByte", "sbyte"}, {"UInt32", "uint"}, {"UInt64", "ulong"}, {"UInt16", "ushort"},
|
|
{"Single", "float"}, {"Double", "double"}, {"Decimal", "decimal"}, {"Boolean", "bool"},
|
|
{"Char", "char"}, {"String", "string"}, {"Object", "object"}, {"IntPtr", "nint"},
|
|
{"UIntPtr", "nuint"}, {"Void", "void"},
|
|
};
|
|
for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) {
|
|
if (strlen(map[i].bcl) == len && memcmp(map[i].bcl, base, len) == 0) {
|
|
return map[i].kw;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
size_t cbm_doclink_cs_norm_type(const char *in, size_t len, char *out, size_t cap) {
|
|
if (!out || cap == 0) {
|
|
return 0;
|
|
}
|
|
out[0] = '\0';
|
|
char work[CS_NORM_WORK];
|
|
if (!in || len == 0 || len >= sizeof(work)) {
|
|
snprintf(out, cap, "?");
|
|
return strlen(out);
|
|
}
|
|
memcpy(work, in, len);
|
|
work[len] = '\0';
|
|
cs_str_t t = {work, len};
|
|
cs_drop_attributes(&t);
|
|
cs_unwrap_nullable(&t);
|
|
cs_drop_modifiers(&t);
|
|
cs_drop_backtick_arity(&t);
|
|
cs_drop_type_args(&t);
|
|
/* "..." -> "[]" (varargs spelling) */
|
|
for (size_t i = 0; i + 2 < t.len; i++) {
|
|
if (t.s[i] == '.' && t.s[i + 1] == '.' && t.s[i + 2] == '.') {
|
|
t.s[i] = '[';
|
|
t.s[i + 1] = ']';
|
|
cs_cut(&t, i + 2, i + 3);
|
|
}
|
|
}
|
|
while (t.len > 0 && t.s[t.len - SKIP_ONE] == '@') {
|
|
t.s[--t.len] = '\0';
|
|
}
|
|
cs_trim(&t);
|
|
/* A trailing identifier after whitespace is the parameter name. */
|
|
size_t last_ws = 0;
|
|
bool have_ws = false;
|
|
for (size_t i = 0; i < t.len; i++) {
|
|
if (isspace((unsigned char)t.s[i])) {
|
|
last_ws = i;
|
|
have_ws = true;
|
|
}
|
|
}
|
|
if (have_ws) {
|
|
size_t s0 = last_ws + SKIP_ONE;
|
|
bool ident = s0 < t.len && cs_ident_start(t.s[s0]);
|
|
for (size_t i = s0 + SKIP_ONE; ident && i < t.len; i++) {
|
|
ident = cs_ident_char(t.s[i]);
|
|
}
|
|
if (ident) {
|
|
t.s[last_ws] = '\0';
|
|
t.len = last_ws;
|
|
}
|
|
}
|
|
/* Drop all whitespace and trailing ?/! markers. */
|
|
size_t w = 0;
|
|
for (size_t i = 0; i < t.len; i++) {
|
|
if (!isspace((unsigned char)t.s[i])) {
|
|
t.s[w++] = t.s[i];
|
|
}
|
|
}
|
|
t.s[w] = '\0';
|
|
t.len = w;
|
|
while (t.len > 0 && (t.s[t.len - SKIP_ONE] == '?' || t.s[t.len - SKIP_ONE] == '!')) {
|
|
t.s[--t.len] = '\0';
|
|
}
|
|
/* base, then array ([] [,]) and pointer (*) suffixes */
|
|
size_t suf = t.len;
|
|
while (suf > 0 && t.s[suf - SKIP_ONE] == '*') {
|
|
suf--;
|
|
}
|
|
for (;;) {
|
|
if (suf > 0 && t.s[suf - SKIP_ONE] == ']') {
|
|
size_t k = suf - SKIP_ONE;
|
|
while (k > 0 && t.s[k - SKIP_ONE] == ',') {
|
|
k--;
|
|
}
|
|
if (k > 0 && t.s[k - SKIP_ONE] == '[') {
|
|
suf = k - SKIP_ONE;
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
size_t base_end = suf;
|
|
while (base_end > 0 && t.s[base_end - SKIP_ONE] == '?') {
|
|
base_end--;
|
|
}
|
|
size_t base_start = 0;
|
|
for (size_t i = 0; i < base_end; i++) {
|
|
if (t.s[i] == '.') {
|
|
base_start = i + SKIP_ONE;
|
|
} else if (t.s[i] == ':' && i + SKIP_ONE < base_end && t.s[i + SKIP_ONE] == ':') {
|
|
base_start = i + PAIR_LEN;
|
|
}
|
|
}
|
|
if (base_start < base_end && t.s[base_start] == '@') {
|
|
base_start++;
|
|
}
|
|
size_t blen = base_end > base_start ? base_end - base_start : 0;
|
|
if (blen == 0) {
|
|
snprintf(out, cap, "?");
|
|
return strlen(out);
|
|
}
|
|
const char *kw = cs_bcl_alias(t.s + base_start, blen);
|
|
int n = kw ? snprintf(out, cap, "%s%.*s", kw, (int)(t.len - suf), t.s + suf)
|
|
: snprintf(out, cap, "%.*s%.*s", (int)blen, t.s + base_start, (int)(t.len - suf),
|
|
t.s + suf);
|
|
if (n < 0 || (size_t)n >= cap) {
|
|
/* it does not fit: an unknown type, never a cut one (two long names
|
|
* cut to one prefix would compare equal) */
|
|
snprintf(out, cap, "?");
|
|
}
|
|
return strlen(out);
|
|
}
|
|
|
|
/* ── Scope scan ──────────────────────────────────────────────────── */
|
|
|
|
/* An entry of the brace list: a structural brace, or the nesting depth a
|
|
* preprocessor branch starts from. */
|
|
typedef struct {
|
|
uint32_t pos; /* byte offset */
|
|
uint32_t row; /* 0-based */
|
|
int match; /* index of the paired brace, CBM_NOT_FOUND when unpaired */
|
|
int alias; /* the first branch's brace this one stands in for, or CBM_NOT_FOUND */
|
|
int depth; /* nesting depth just after this entry */
|
|
char kind; /* '{', '}', or '#' for a branch mark */
|
|
} cs_brace_t;
|
|
|
|
/* A declaration keyword (namespace, class, struct ...). */
|
|
typedef struct {
|
|
uint32_t end; /* first byte after the keyword */
|
|
uint32_t row; /* 0-based */
|
|
char kind; /* N namespace; c s i e r as for types */
|
|
bool partial; /* the word before it is `partial` */
|
|
} cs_head_t;
|
|
|
|
/* An open brace while the braces are paired. The open braces form a stack
|
|
* that is never copied: every open brace is one entry that names the one
|
|
* below it, so "the stack as it stood at the #if" is a single index, however
|
|
* deep the nesting, and going back to it costs nothing. */
|
|
typedef struct {
|
|
int brace; /* its entry in the brace list */
|
|
int below; /* the open brace under it, or CBM_NOT_FOUND */
|
|
} cs_open_t;
|
|
|
|
/* An open #if while the braces are paired. */
|
|
typedef struct {
|
|
int at_if; /* the top open brace where the #if stands (CBM_NOT_FOUND: none) */
|
|
int n_if;
|
|
int end1; /* ... and where its first branch ended; valid once has_end1 */
|
|
int n_end1;
|
|
int first_open; /* first open-brace entry allocated in the current branch */
|
|
bool has_end1; /* an #else was seen */
|
|
int mark; /* its entry in the brace list */
|
|
} cs_pp_t;
|
|
|
|
enum {
|
|
CS_OWNER_NONE = -1, /* not in a type */
|
|
CS_OWNER_LEXICAL = -2, /* found in an error node: whichever type's braces hold it */
|
|
CS_ITEM_TEXT = -3, /* frame of a declaration read from the text */
|
|
CS_PP_IF = 1,
|
|
CS_PP_ELSE,
|
|
CS_PP_ENDIF,
|
|
CS_PP_MAX = 32, /* nested #if */
|
|
CS_CHAR_LITERAL_MAX = 12, /* '\U0010FFFF' */
|
|
CS_RAW_QUOTES = 3, /* """ */
|
|
CS_LEX_MAX_NEST = 64, /* interpolated strings inside interpolation holes */
|
|
/* How deep a file may nest what the scope records. A block past either
|
|
* limit is not placed: its types are named in Q records, what is
|
|
* documented inside has no scope (an X range). Deeper than any program;
|
|
* the limits keep a lookup's walk over the enclosing types and namespaces
|
|
* bounded. */
|
|
CS_MAX_TYPE_NEST = 64, /* types inside types */
|
|
CS_MAX_NS_SEGMENTS = 64, /* segments of a namespace's full name */
|
|
};
|
|
|
|
/* One declaration of the file. `node` is the declaration (a field's
|
|
* declaration for each of its declarators); a declaration read from the
|
|
* text, where the tree has no node for it, has none. */
|
|
typedef struct {
|
|
TSNode node;
|
|
TSNode name;
|
|
TSNode tparams;
|
|
TSNode params;
|
|
const char *text_name; /* text declarations; M: an operator's or indexer's name */
|
|
const char *text_tparams; /* text declarations: "T,U" */
|
|
uint32_t start; /* byte offset: document order */
|
|
uint32_t line; /* 1-based */
|
|
int brace; /* text declarations: the brace opening the block, or CBM_NOT_FOUND */
|
|
int tree_idx; /* position among the tree's items; CS_ITEM_TEXT for a text one */
|
|
int owner; /* M: tree_idx of the type the tree nests it in, or CS_OWNER_* */
|
|
char tag; /* N namespace block, F file-scoped namespace, U using, T type,
|
|
M member */
|
|
char kind; /* T: c s i e r t d; M: c v p e o x */
|
|
bool explicit_impl;
|
|
bool is_static; /* M: what `using static` brings in */
|
|
bool partial; /* T: declared `partial` */
|
|
bool broken; /* T: a parse error sits among its members */
|
|
bool from_text; /* read from the text, not from a declaration node */
|
|
} cs_item_t;
|
|
|
|
typedef struct {
|
|
CBMExtractCtx *ctx;
|
|
CBMArena *tmp; /* names, paths, items, tokens: nothing of it outlives the scan */
|
|
cs_sb_t sb;
|
|
cs_item_t *items;
|
|
int nitems;
|
|
int cap_items;
|
|
cs_brace_t *braces;
|
|
int nbraces;
|
|
int cap_braces;
|
|
cs_head_t *heads;
|
|
int nheads;
|
|
int cap_heads;
|
|
cs_open_t *open; /* brace pairing: every brace that was ever open */
|
|
int nopen;
|
|
int cap_open;
|
|
int top; /* the innermost open brace (index into open), or CBM_NOT_FOUND */
|
|
int sp; /* how many are open */
|
|
cs_pp_t pp[CS_PP_MAX];
|
|
int npp;
|
|
uint32_t row_pos; /* row cursor: the row of byte row_pos is `row` */
|
|
uint32_t row;
|
|
uint64_t cost_steps; /* text positions and brace-stack entries visited */
|
|
uint64_t cost_bytes; /* bytes taken from the scratch arena */
|
|
bool failed; /* out of memory */
|
|
bool lexical; /* the tree has parse errors: nesting is read from the braces */
|
|
uint32_t untrusted; /* byte offset from which the braces do not pair up */
|
|
uint32_t untrusted_row; /* its 0-based row */
|
|
int next_region;
|
|
int types_out; /* T records written: a type's ordinal is its position among them */
|
|
uint32_t root_end_byte;
|
|
uint32_t root_end_line;
|
|
} cs_scan_t;
|
|
|
|
enum {
|
|
CS_ITEMS_INIT = 256,
|
|
CS_BRACES_INIT = 1024,
|
|
CS_HEADS_INIT = 64,
|
|
CS_HEADER_SCAN_MAX = 4096, /* bytes from a type's name to its `{` or `;` */
|
|
CS_TPARAMS_SCAN_MAX = 1024,
|
|
};
|
|
|
|
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
|
static _Atomic uint64_t cs_cost_text_steps;
|
|
static _Atomic uint64_t cs_cost_scratch_bytes;
|
|
static _Atomic uint64_t cs_cost_scope_bytes;
|
|
|
|
void cbm_doclink_cs_test_cost_reset(void) {
|
|
atomic_store(&cs_cost_text_steps, 0);
|
|
atomic_store(&cs_cost_scratch_bytes, 0);
|
|
atomic_store(&cs_cost_scope_bytes, 0);
|
|
}
|
|
|
|
void cbm_doclink_cs_test_cost(uint64_t *text_steps, uint64_t *scratch_bytes) {
|
|
*text_steps = atomic_load(&cs_cost_text_steps);
|
|
*scratch_bytes = atomic_load(&cs_cost_scratch_bytes);
|
|
}
|
|
|
|
uint64_t cbm_doclink_cs_test_scope_bytes(void) {
|
|
return atomic_load(&cs_cost_scope_bytes);
|
|
}
|
|
#endif
|
|
|
|
/* Hand a cost to the test seam (nothing in a product build). */
|
|
static void cs_cost_add(uint64_t steps, uint64_t bytes) {
|
|
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
|
atomic_fetch_add(&cs_cost_text_steps, steps);
|
|
atomic_fetch_add(&cs_cost_scratch_bytes, bytes);
|
|
#else
|
|
(void)steps;
|
|
(void)bytes;
|
|
#endif
|
|
}
|
|
|
|
/* Hand a finished scan's cost to the test seam. */
|
|
static void cs_cost_publish(const cs_scan_t *s) {
|
|
cs_cost_add(s->cost_steps, s->cost_bytes);
|
|
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
|
if (!s->failed && !s->sb.failed && s->sb.buf) {
|
|
atomic_fetch_add(&cs_cost_scope_bytes, s->sb.len);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static bool cs_kind_is(TSNode n, const char *kind) {
|
|
return strcmp(ts_node_type(n), kind) == 0;
|
|
}
|
|
|
|
static TSNode cs_field(TSNode n, const char *field) {
|
|
return ts_node_child_by_field_name(n, field, (uint32_t)strlen(field));
|
|
}
|
|
|
|
/* The children of a node, in order. ts_node_child(n, i) walks from the first
|
|
* child on every call, so a loop over i costs the square of the child count
|
|
* (a parameter list, a base list and a class body are as long as the file
|
|
* makes them); a cursor steps from one child to the next. */
|
|
typedef struct {
|
|
TSTreeCursor cur;
|
|
bool started;
|
|
bool done;
|
|
} cs_kids_t;
|
|
|
|
static cs_kids_t cs_kids(TSNode parent) {
|
|
return (cs_kids_t){.cur = ts_tree_cursor_new(parent)};
|
|
}
|
|
|
|
/* The next child, named or not; false after the last one. */
|
|
static bool cs_kids_next(cs_kids_t *k, TSNode *out) {
|
|
if (k->done) {
|
|
return false;
|
|
}
|
|
bool moved = k->started ? ts_tree_cursor_goto_next_sibling(&k->cur)
|
|
: ts_tree_cursor_goto_first_child(&k->cur);
|
|
k->started = true;
|
|
if (!moved) {
|
|
k->done = true;
|
|
return false;
|
|
}
|
|
*out = ts_tree_cursor_current_node(&k->cur);
|
|
return true;
|
|
}
|
|
|
|
/* The next NAMED child; false after the last one. */
|
|
static bool cs_kids_next_named(cs_kids_t *k, TSNode *out) {
|
|
while (cs_kids_next(k, out)) {
|
|
if (ts_node_is_named(*out)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void cs_kids_end(cs_kids_t *k) {
|
|
ts_tree_cursor_delete(&k->cur);
|
|
}
|
|
|
|
/* The first named child of `n` of kind `kind`; a null node when it has none. */
|
|
static TSNode cs_child_of_kind(TSNode n, const char *kind) {
|
|
TSNode found = {0};
|
|
cs_kids_t k = cs_kids(n);
|
|
TSNode c;
|
|
while (cs_kids_next_named(&k, &c)) {
|
|
if (strcmp(ts_node_type(c), kind) == 0) {
|
|
found = c;
|
|
break;
|
|
}
|
|
}
|
|
cs_kids_end(&k);
|
|
return found;
|
|
}
|
|
|
|
/* A declaration's type_parameter_list: a named field in some grammar
|
|
* versions, an unnamed child in others. */
|
|
static TSNode cs_type_params(TSNode decl) {
|
|
TSNode tp = cs_field(decl, "type_parameters");
|
|
if (!ts_node_is_null(tp)) {
|
|
return tp;
|
|
}
|
|
return cs_child_of_kind(decl, "type_parameter_list");
|
|
}
|
|
|
|
/* Node text without whitespace and without verbatim '@' markers, appended to
|
|
* sb. `global::` prefixes are dropped. Text that is too long to be a type
|
|
* name, that holds a scope separator or non-whitespace control byte, or that
|
|
* is not well-formed UTF-8, is written as "?" (an unresolvable name: the
|
|
* declaring type then counts as having an open hierarchy). The whole field is
|
|
* replaced before any copy. */
|
|
static void cs_put_text_nows(cs_scan_t *s, TSNode n) {
|
|
const char *src = s->ctx->source;
|
|
uint32_t a = ts_node_start_byte(n);
|
|
uint32_t b = ts_node_end_byte(n);
|
|
if (b - a > 8 && memcmp(src + a, "global::", 8) == 0) {
|
|
a += 8;
|
|
}
|
|
bool ok = b >= a && b - a <= CS_NAME_MAX;
|
|
for (uint32_t i = a; ok && i < b; i++) {
|
|
unsigned char c = (unsigned char)src[i];
|
|
ok = c != '|' && c != ';' && c != '{' && c != '}' &&
|
|
!((c < 0x20 && !isspace(c)) || c == 0x7f);
|
|
}
|
|
if (!ok || !cs_utf8_ok(src + a, b - a)) {
|
|
sb_putc(&s->sb, '?');
|
|
return;
|
|
}
|
|
for (uint32_t i = a; i < b; i++) {
|
|
char c = src[i];
|
|
if (isspace((unsigned char)c) || c == '@') {
|
|
continue;
|
|
}
|
|
sb_putc(&s->sb, c);
|
|
}
|
|
}
|
|
|
|
static void *cs_tmp_alloc(cs_scan_t *s, size_t n) {
|
|
s->cost_bytes += n;
|
|
void *p = cbm_arena_alloc(s->tmp, n ? n : SKIP_ONE);
|
|
if (!p) {
|
|
s->failed = true;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
/* Copy of source bytes [a, b) without whitespace and '@'. NULL unless it is a
|
|
* (dotted) identifier of sane length and well-formed UTF-8: an error-recovered
|
|
* parse can hand back a "name" spanning arbitrary code, which must not become
|
|
* a declaration, and a name that is not UTF-8 cannot be written. */
|
|
static char *cs_ident_dup(cs_scan_t *s, uint32_t a, uint32_t b) {
|
|
const char *src = s->ctx->source;
|
|
if (b <= a || b - a > CS_NAME_MAX) {
|
|
return NULL;
|
|
}
|
|
char *out = (char *)cs_tmp_alloc(s, (size_t)(b - a) + SKIP_ONE);
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
size_t w = 0;
|
|
for (uint32_t i = a; i < b; i++) {
|
|
unsigned char c = (unsigned char)src[i];
|
|
if (isspace(c) || c == '@') {
|
|
continue;
|
|
}
|
|
if (!(isalnum(c) || c == '_' || c == '.' || c >= 0x80)) {
|
|
return NULL; /* not an identifier */
|
|
}
|
|
out[w++] = (char)c;
|
|
}
|
|
out[w] = '\0';
|
|
return w > 0 && cs_utf8_ok(out, w) ? out : NULL;
|
|
}
|
|
|
|
static char *cs_name_dup(cs_scan_t *s, TSNode n) {
|
|
if (ts_node_is_null(n)) {
|
|
return NULL;
|
|
}
|
|
return cs_ident_dup(s, ts_node_start_byte(n), ts_node_end_byte(n));
|
|
}
|
|
|
|
static uint32_t cs_line(TSNode n) {
|
|
return ts_node_start_point(n).row + TS_LINE_OFFSET;
|
|
}
|
|
|
|
static uint32_t cs_end_line(TSNode n) {
|
|
return ts_node_end_point(n).row + TS_LINE_OFFSET;
|
|
}
|
|
|
|
/* type_parameter_list -> "T,U" into sb (nothing when absent). */
|
|
static void cs_put_tparams(cs_scan_t *s, TSNode list) {
|
|
if (ts_node_is_null(list)) {
|
|
return;
|
|
}
|
|
bool first = true;
|
|
cs_kids_t k = cs_kids(list);
|
|
TSNode tp;
|
|
while (cs_kids_next_named(&k, &tp)) {
|
|
if (!cs_kind_is(tp, "type_parameter")) {
|
|
continue;
|
|
}
|
|
TSNode nm = cs_field(tp, "name");
|
|
if (ts_node_is_null(nm)) {
|
|
continue;
|
|
}
|
|
if (!first) {
|
|
sb_putc(&s->sb, ',');
|
|
}
|
|
cs_put_text_nows(s, nm);
|
|
first = false;
|
|
}
|
|
cs_kids_end(&k);
|
|
}
|
|
|
|
static void cs_put_sig(cs_scan_t *s, TSNode params) {
|
|
if (ts_node_is_null(params)) {
|
|
return;
|
|
}
|
|
const char *src = s->ctx->source;
|
|
bool first = true;
|
|
cs_kids_t k = cs_kids(params);
|
|
TSNode p;
|
|
while (cs_kids_next(&k, &p)) {
|
|
/* A parameter is a `parameter` node -- except the `params` one, which
|
|
* the grammar leaves inline in the list: its type is the list's own
|
|
* "type" field. */
|
|
TSNode ty = {0};
|
|
if (cs_kind_is(p, "parameter")) {
|
|
ty = cs_field(p, "type");
|
|
} else {
|
|
const char *field = ts_tree_cursor_current_field_name(&k.cur);
|
|
if (!field || strcmp(field, "type") != 0) {
|
|
continue;
|
|
}
|
|
ty = p;
|
|
}
|
|
char norm[CBM_SZ_256];
|
|
if (ts_node_is_null(ty)) {
|
|
snprintf(norm, sizeof(norm), "?");
|
|
} else {
|
|
uint32_t a = ts_node_start_byte(ty);
|
|
uint32_t b = ts_node_end_byte(ty);
|
|
cbm_doclink_cs_norm_type(src + a, (size_t)(b - a), norm, sizeof(norm));
|
|
if (!cs_utf8_ok(norm, strlen(norm))) {
|
|
snprintf(norm, sizeof(norm), "?"); /* a type nothing is known about */
|
|
}
|
|
}
|
|
if (!first) {
|
|
sb_putc(&s->sb, '|');
|
|
}
|
|
sb_puts(&s->sb, norm);
|
|
first = false;
|
|
}
|
|
cs_kids_end(&k);
|
|
}
|
|
|
|
static bool cs_has_child_kind(TSNode n, const char *kind) {
|
|
return !ts_node_is_null(cs_child_of_kind(n, kind));
|
|
}
|
|
|
|
static char cs_type_kind(const char *k) {
|
|
if (strcmp(k, "class_declaration") == 0) {
|
|
return 'c';
|
|
}
|
|
if (strcmp(k, "struct_declaration") == 0) {
|
|
return 's';
|
|
}
|
|
if (strcmp(k, "interface_declaration") == 0) {
|
|
return 'i';
|
|
}
|
|
if (strcmp(k, "enum_declaration") == 0) {
|
|
return 'e';
|
|
}
|
|
if (strcmp(k, "record_declaration") == 0) {
|
|
return 'r'; /* a record class; cs_decl_kind tells a `record struct` apart */
|
|
}
|
|
if (strcmp(k, "record_struct_declaration") == 0) {
|
|
return 't';
|
|
}
|
|
if (strcmp(k, "delegate_declaration") == 0) {
|
|
return 'd';
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* True when the anonymous token `word` is a direct child of `n`, or sits in
|
|
* one of its `modifier` children (`partial`, `static`). */
|
|
static bool cs_has_word(TSNode n, const char *word) {
|
|
bool found = false;
|
|
uint64_t steps = 0;
|
|
cs_kids_t k = cs_kids(n);
|
|
TSNode c;
|
|
while (!found && cs_kids_next(&k, &c)) {
|
|
steps++;
|
|
const char *t = ts_node_type(c);
|
|
if (!ts_node_is_named(c)) {
|
|
found = strcmp(t, word) == 0;
|
|
} else if (strcmp(t, "modifier") == 0 && ts_node_child_count(c) > 0) {
|
|
found = strcmp(ts_node_type(ts_node_child(c, 0)), word) == 0;
|
|
}
|
|
}
|
|
cs_kids_end(&k);
|
|
cs_cost_add(steps, 0);
|
|
return found;
|
|
}
|
|
|
|
/* The kind of a type declaration node: c class, s struct, i interface, e
|
|
* enum, r record class, t record struct, d delegate; 0 for any other node. */
|
|
static char cs_decl_kind(TSNode decl) {
|
|
char kind = cs_type_kind(ts_node_type(decl));
|
|
return (kind == 'r' && cs_has_word(decl, "struct")) ? 't' : kind;
|
|
}
|
|
|
|
/* ── Tokens: the block structure the tree lost ───────────────────────
|
|
*
|
|
* Error recovery closes blocks early and late, reports a block namespace as
|
|
* a file-scoped one, or gives up on a declaration and leaves its header as
|
|
* loose tokens in an error node. On the C# bench corpus 1,805 of 32,686 files
|
|
* have parse errors, and in 230 of them a namespace or type has a tree extent
|
|
* that is not its brace extent (the API reference files among them) -- which
|
|
* moves every declaration after the error into the wrong namespace or out of
|
|
* its outer type. A file whose tree has errors therefore takes its nesting
|
|
* from the text instead: the braces say where blocks begin and end, the
|
|
* declaration keywords say what the blocks are. The parser's own tokens
|
|
* cannot serve: once it loses the thread inside a string it lexes code as
|
|
* string content. So the braces are scanned here, with the lexical grammar a
|
|
* brace scanner needs -- comments, character and string literals in all
|
|
* their forms, interpolation holes, preprocessor branches. On the 30,881
|
|
* error-free corpus files this scanner yields exactly the parser's brace
|
|
* tokens. Where the braces still do not pair up, the rest of the file is not
|
|
* placed at all. */
|
|
|
|
static uint32_t cs_row_of(cs_scan_t *s, uint32_t pos) {
|
|
const char *src = s->ctx->source;
|
|
if (pos < s->row_pos) {
|
|
s->row_pos = 0;
|
|
s->row = 0;
|
|
}
|
|
for (uint32_t i = s->row_pos; i < pos; i++) {
|
|
s->row += src[i] == '\n';
|
|
}
|
|
s->row_pos = pos;
|
|
return s->row;
|
|
}
|
|
|
|
/* A new entry of the brace list; its index, or CBM_NOT_FOUND. */
|
|
static int cs_brace_push(cs_scan_t *s, uint32_t pos, char kind) {
|
|
if (s->failed) {
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
if (s->nbraces >= s->cap_braces) {
|
|
int ncap = s->cap_braces ? s->cap_braces * PAIR_LEN : CS_BRACES_INIT;
|
|
cs_brace_t *grown = (cs_brace_t *)cs_tmp_alloc(s, (size_t)ncap * sizeof(*grown));
|
|
if (!grown) {
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
if (s->nbraces > 0) {
|
|
memcpy(grown, s->braces, (size_t)s->nbraces * sizeof(*grown));
|
|
}
|
|
s->braces = grown;
|
|
s->cap_braces = ncap;
|
|
}
|
|
s->braces[s->nbraces] = (cs_brace_t){.pos = pos,
|
|
.row = cs_row_of(s, pos),
|
|
.match = CBM_NOT_FOUND,
|
|
.alias = CBM_NOT_FOUND,
|
|
.kind = kind};
|
|
return s->nbraces++;
|
|
}
|
|
|
|
/* Brace `b` is open now: it goes on top of the open braces. */
|
|
static bool cs_open_push(cs_scan_t *s, int b) {
|
|
if (s->nopen >= s->cap_open) {
|
|
int ncap = s->cap_open ? s->cap_open * PAIR_LEN : CS_HEADS_INIT;
|
|
cs_open_t *grown = (cs_open_t *)cs_tmp_alloc(s, (size_t)ncap * sizeof(*grown));
|
|
if (!grown) {
|
|
return false;
|
|
}
|
|
if (s->nopen > 0) {
|
|
memcpy(grown, s->open, (size_t)s->nopen * sizeof(*grown));
|
|
}
|
|
s->open = grown;
|
|
s->cap_open = ncap;
|
|
}
|
|
s->open[s->nopen] = (cs_open_t){.brace = b, .below = s->top};
|
|
s->top = s->nopen++;
|
|
s->sp++;
|
|
return true;
|
|
}
|
|
|
|
static void cs_mark_untrusted_at(cs_scan_t *s, uint32_t pos, uint32_t row) {
|
|
if (pos < s->untrusted) {
|
|
s->untrusted = pos;
|
|
s->untrusted_row = row;
|
|
}
|
|
}
|
|
|
|
static void cs_mark_untrusted(cs_scan_t *s, int brace) {
|
|
cs_mark_untrusted_at(s, s->braces[brace].pos, s->braces[brace].row);
|
|
}
|
|
|
|
/* The scanner reports a brace: pair it. */
|
|
static void cs_lex_brace(void *ud, uint32_t pos, bool open) {
|
|
cs_scan_t *s = (cs_scan_t *)ud;
|
|
int b = cs_brace_push(s, pos, open ? '{' : '}');
|
|
if (b < 0) {
|
|
return;
|
|
}
|
|
if (open) {
|
|
(void)cs_open_push(s, b);
|
|
} else if (s->top < 0) {
|
|
cs_mark_untrusted(s, b); /* closes nothing */
|
|
} else {
|
|
int o = s->open[s->top].brace;
|
|
s->top = s->open[s->top].below;
|
|
s->sp--;
|
|
if (s->braces[o].match < 0) {
|
|
s->braces[o].match = b; /* the first branch's close stands */
|
|
}
|
|
s->braces[b].match = o;
|
|
}
|
|
s->braces[b].depth = s->sp;
|
|
}
|
|
|
|
/* A later branch of a conditional ended. Where it leaves the same blocks
|
|
* open as the first one did, its open braces stand in for the first
|
|
* branch's (`class X : A {` / `#else` / `class X : B {` share one closing
|
|
* brace). Where the branches disagree the first one stands alone: a file
|
|
* can balance per configuration only (`#if A {` ... `#if A }`), and the
|
|
* braces left over at the end say whether this one does.
|
|
*
|
|
* Only entries opened in the current branch may acquire aliases. The first
|
|
* branch can replace a deep pre-existing stack; every empty later branch
|
|
* starts from that old stack. Walking it again would cost depth per branch. */
|
|
static void cs_branch_merge(cs_scan_t *s, const cs_pp_t *f) {
|
|
if (s->sp != f->n_end1) {
|
|
return;
|
|
}
|
|
int a = s->top;
|
|
int b = f->end1;
|
|
while (a != b && a >= f->first_open && b >= 0) {
|
|
s->cost_steps++;
|
|
s->braces[s->open[a].brace].alias = s->open[b].brace;
|
|
a = s->open[a].below;
|
|
b = s->open[b].below;
|
|
}
|
|
}
|
|
|
|
/* The scanner reports #if / #else (or #elif) / #endif. Every branch starts
|
|
* from the nesting the #if started from; after the #endif the first
|
|
* branch's result stands. */
|
|
static void cs_lex_branch(void *ud, uint32_t pos, int what) {
|
|
cs_scan_t *s = (cs_scan_t *)ud;
|
|
int mark = cs_brace_push(s, pos, '#');
|
|
if (mark < 0) {
|
|
return;
|
|
}
|
|
if (what == CS_PP_IF) {
|
|
if (s->npp >= CS_PP_MAX) {
|
|
cs_mark_untrusted(s, mark);
|
|
} else {
|
|
s->pp[s->npp++] =
|
|
(cs_pp_t){.at_if = s->top, .n_if = s->sp, .first_open = s->nopen, .mark = mark};
|
|
}
|
|
} else if (s->npp > 0) {
|
|
cs_pp_t *f = &s->pp[s->npp - SKIP_ONE];
|
|
if (f->has_end1) {
|
|
cs_branch_merge(s, f);
|
|
} else if (what == CS_PP_ELSE) {
|
|
f->end1 = s->top;
|
|
f->n_end1 = s->sp;
|
|
f->has_end1 = true;
|
|
}
|
|
if (what == CS_PP_ELSE) {
|
|
s->top = f->at_if;
|
|
s->sp = f->n_if;
|
|
f->first_open = s->nopen;
|
|
} else if (what == CS_PP_ENDIF) {
|
|
if (f->has_end1) {
|
|
s->top = f->end1;
|
|
s->sp = f->n_end1;
|
|
}
|
|
s->npp--;
|
|
}
|
|
}
|
|
s->braces[mark].depth = s->sp;
|
|
}
|
|
|
|
/* The scanner reports a declaration keyword; `partial` when the word before
|
|
* it is that modifier. */
|
|
static void cs_lex_keyword(void *ud, uint32_t kw_end, char kind, bool partial) {
|
|
cs_scan_t *s = (cs_scan_t *)ud;
|
|
if (s->failed) {
|
|
return;
|
|
}
|
|
if (s->nheads >= s->cap_heads) {
|
|
int ncap = s->cap_heads ? s->cap_heads * PAIR_LEN : CS_HEADS_INIT;
|
|
cs_head_t *grown = (cs_head_t *)cs_tmp_alloc(s, (size_t)ncap * sizeof(*grown));
|
|
if (!grown) {
|
|
return;
|
|
}
|
|
if (s->nheads > 0) {
|
|
memcpy(grown, s->heads, (size_t)s->nheads * sizeof(*grown));
|
|
}
|
|
s->heads = grown;
|
|
s->cap_heads = ncap;
|
|
}
|
|
s->heads[s->nheads++] =
|
|
(cs_head_t){.end = kw_end, .row = cs_row_of(s, kw_end), .kind = kind, .partial = partial};
|
|
}
|
|
|
|
/* --- the scanner ---------------------------------------------------- */
|
|
|
|
typedef struct {
|
|
const char *src;
|
|
uint32_t n;
|
|
void *ud;
|
|
uint64_t steps; /* positions looked at */
|
|
int nest; /* interpolation holes the scan is inside of */
|
|
uint32_t stop_pos; /* where the scan gave up (`stopped`) */
|
|
bool stopped; /* holes nested deeper than CS_LEX_MAX_NEST: the rest is not read */
|
|
bool prev_sep; /* the previous token was ':' or ',' */
|
|
bool prev_partial; /* the previous token was `partial` (or `partial record`) */
|
|
} cs_lex_t;
|
|
|
|
static bool cs_lex_word(unsigned char c) {
|
|
return isalnum(c) || c == '_' || c >= 0x80;
|
|
}
|
|
|
|
/* The declaration a keyword starts: N for `namespace`, a type kind, or 0. */
|
|
static char cs_keyword_kind(const char *text, uint32_t len) {
|
|
static const struct {
|
|
const char *word;
|
|
char kind;
|
|
} words[] = {{"namespace", 'N'}, {"class", 'c'}, {"struct", 's'},
|
|
{"interface", 'i'}, {"enum", 'e'}, {"record", 'r'}};
|
|
for (size_t i = 0; i < sizeof(words) / sizeof(words[0]); i++) {
|
|
if (strlen(words[i].word) == len && memcmp(words[i].word, text, len) == 0) {
|
|
return words[i].kind;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static uint32_t cs_lex_code(cs_lex_t *lx, uint32_t i, bool hole);
|
|
|
|
/* The code of an interpolation hole from src[i]: just past the brace that
|
|
* closes it. A hole can hold the next interpolated string, and each such
|
|
* level costs stack: past CS_LEX_MAX_NEST -- deeper than any program -- the
|
|
* scan stops for good, and what follows in the file is not placed. */
|
|
static uint32_t cs_lex_hole(cs_lex_t *lx, uint32_t i) {
|
|
if (lx->nest >= CS_LEX_MAX_NEST) {
|
|
if (!lx->stopped) {
|
|
lx->stopped = true;
|
|
lx->stop_pos = i;
|
|
}
|
|
return lx->n;
|
|
}
|
|
lx->nest++;
|
|
uint32_t end = cs_lex_code(lx, i, true);
|
|
lx->nest--;
|
|
return end;
|
|
}
|
|
|
|
/* Length of the run of `c` at src[i]. */
|
|
static uint32_t cs_lex_run(cs_lex_t *lx, uint32_t i, char c) {
|
|
uint32_t j = i;
|
|
while (j < lx->n && lx->src[j] == c) {
|
|
j++;
|
|
}
|
|
lx->steps += (uint64_t)(j - i) + SKIP_ONE;
|
|
return j - i;
|
|
}
|
|
|
|
/* 'x', '\n', 'A': past the literal, or past the lone quote when it is
|
|
* not one. */
|
|
static uint32_t cs_lex_char(const cs_lex_t *lx, uint32_t i) {
|
|
uint32_t j = i + SKIP_ONE;
|
|
if (j < lx->n && lx->src[j] == '\\') {
|
|
j += PAIR_LEN;
|
|
}
|
|
while (j < lx->n && lx->src[j] != '\'' && lx->src[j] != '\n' && j - i < CS_CHAR_LITERAL_MAX) {
|
|
j++;
|
|
}
|
|
return (j < lx->n && lx->src[j] == '\'') ? j + SKIP_ONE : i + SKIP_ONE;
|
|
}
|
|
|
|
/* "..." with backslash escapes; an unterminated one ends with its line. */
|
|
static uint32_t cs_lex_string(const cs_lex_t *lx, uint32_t i) {
|
|
uint32_t j = i + SKIP_ONE;
|
|
while (j < lx->n) {
|
|
char c = lx->src[j];
|
|
if (c == '\\') {
|
|
j += PAIR_LEN;
|
|
} else if (c == '"') {
|
|
return j + SKIP_ONE;
|
|
} else if (c == '\n') {
|
|
return j;
|
|
} else {
|
|
j++;
|
|
}
|
|
}
|
|
return lx->n;
|
|
}
|
|
|
|
/* @"..." : a quote is written twice. `i` is at the opening quote. */
|
|
static uint32_t cs_lex_verbatim(const cs_lex_t *lx, uint32_t i) {
|
|
uint32_t j = i + SKIP_ONE;
|
|
while (j < lx->n) {
|
|
if (lx->src[j] != '"') {
|
|
j++;
|
|
} else if (j + SKIP_ONE < lx->n && lx->src[j + SKIP_ONE] == '"') {
|
|
j += PAIR_LEN;
|
|
} else {
|
|
return j + SKIP_ONE;
|
|
}
|
|
}
|
|
return lx->n;
|
|
}
|
|
|
|
/* """...""" (`quotes` >= 3 of them): ends with a run of at least as many.
|
|
* With `dollars`, a run of that many `{` opens a hole of code. */
|
|
static uint32_t cs_lex_raw(cs_lex_t *lx, uint32_t i, uint32_t quotes, uint32_t dollars) {
|
|
uint32_t j = i + quotes;
|
|
while (j < lx->n) {
|
|
char c = lx->src[j];
|
|
if (c == '"') {
|
|
uint32_t run = cs_lex_run(lx, j, '"');
|
|
if (run >= quotes) {
|
|
return j + run;
|
|
}
|
|
j += run;
|
|
} else if (dollars > 0 && c == '{') {
|
|
uint32_t run = cs_lex_run(lx, j, '{');
|
|
j += run;
|
|
if (run >= dollars) {
|
|
j = cs_lex_hole(lx, j);
|
|
j += cs_lex_run(lx, j, '}'); /* the rest of the closing run */
|
|
}
|
|
} else {
|
|
j++;
|
|
}
|
|
}
|
|
return lx->n;
|
|
}
|
|
|
|
/* $"..." / $@"..." : text with {holes} of code; {{ and }} are literal braces.
|
|
* `i` is at the opening quote. */
|
|
static uint32_t cs_lex_interpolated(cs_lex_t *lx, uint32_t i, bool verbatim) {
|
|
uint32_t j = i + SKIP_ONE;
|
|
while (j < lx->n) {
|
|
char c = lx->src[j];
|
|
if (c == '"') {
|
|
if (verbatim && j + SKIP_ONE < lx->n && lx->src[j + SKIP_ONE] == '"') {
|
|
j += PAIR_LEN;
|
|
continue;
|
|
}
|
|
return j + SKIP_ONE;
|
|
}
|
|
if (c == '\\' && !verbatim) {
|
|
j += PAIR_LEN;
|
|
} else if (c == '{' || c == '}') {
|
|
if (j + SKIP_ONE < lx->n && lx->src[j + SKIP_ONE] == c) {
|
|
j += PAIR_LEN;
|
|
} else if (c == '{') {
|
|
j = cs_lex_hole(lx, j + SKIP_ONE);
|
|
} else {
|
|
j++;
|
|
}
|
|
} else if (c == '\n' && !verbatim) {
|
|
return j;
|
|
} else {
|
|
j++;
|
|
}
|
|
}
|
|
return lx->n;
|
|
}
|
|
|
|
/* A literal that starts with a quote, `@`, or `$` at src[i]; returns i itself
|
|
* when there is none there. A run of `$` is measured once: where it starts
|
|
* no literal the scan goes on behind it (or at its last `$`, when that one
|
|
* starts an ordinary interpolated string), never at its second character. */
|
|
static uint32_t cs_lex_literal(cs_lex_t *lx, uint32_t i) {
|
|
const char *src = lx->src;
|
|
uint32_t n = lx->n;
|
|
char c = src[i];
|
|
if (c == '"') {
|
|
uint32_t q = cs_lex_run(lx, i, '"');
|
|
if (q >= CS_RAW_QUOTES) {
|
|
return cs_lex_raw(lx, i, q, 0);
|
|
}
|
|
return q == PAIR_LEN ? i + PAIR_LEN : cs_lex_string(lx, i);
|
|
}
|
|
if (c == '@' && i + SKIP_ONE < n && src[i + SKIP_ONE] == '"') {
|
|
return cs_lex_verbatim(lx, i + SKIP_ONE);
|
|
}
|
|
if (c == '@' && i + PAIR_LEN < n && src[i + SKIP_ONE] == '$' && src[i + PAIR_LEN] == '"') {
|
|
return cs_lex_interpolated(lx, i + PAIR_LEN, true);
|
|
}
|
|
if (c == '$') {
|
|
uint32_t d = cs_lex_run(lx, i, '$');
|
|
uint32_t j = i + d;
|
|
if (j < n && src[j] == '"') {
|
|
uint32_t q = cs_lex_run(lx, j, '"');
|
|
if (q >= CS_RAW_QUOTES) {
|
|
return cs_lex_raw(lx, j, q, d);
|
|
}
|
|
return (d == SKIP_ONE) ? cs_lex_interpolated(lx, j, false) : j - SKIP_ONE;
|
|
}
|
|
if (j + SKIP_ONE < n && src[j] == '@' && src[j + SKIP_ONE] == '"') {
|
|
return (d == SKIP_ONE) ? cs_lex_interpolated(lx, j + SKIP_ONE, true) : j - SKIP_ONE;
|
|
}
|
|
return j;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
static bool cs_lex_is(const char *src, uint32_t w, uint32_t len, const char *word) {
|
|
return strlen(word) == len && memcmp(src + w, word, len) == 0;
|
|
}
|
|
|
|
/* A `#` directive at the start of a line: reports conditional branches and
|
|
* returns the end of the line. */
|
|
static uint32_t cs_lex_directive(cs_lex_t *lx, uint32_t i, bool hole) {
|
|
const char *src = lx->src;
|
|
uint32_t j = i + SKIP_ONE;
|
|
while (j < lx->n && (src[j] == ' ' || src[j] == '\t')) {
|
|
j++;
|
|
}
|
|
uint32_t w = j;
|
|
while (j < lx->n && isalpha((unsigned char)src[j])) {
|
|
j++;
|
|
}
|
|
uint32_t len = j - w;
|
|
if (!hole) {
|
|
if (cs_lex_is(src, w, len, "if")) {
|
|
cs_lex_branch(lx->ud, i, CS_PP_IF);
|
|
} else if (cs_lex_is(src, w, len, "else") || cs_lex_is(src, w, len, "elif")) {
|
|
cs_lex_branch(lx->ud, i, CS_PP_ELSE);
|
|
} else if (cs_lex_is(src, w, len, "endif")) {
|
|
cs_lex_branch(lx->ud, i, CS_PP_ENDIF);
|
|
}
|
|
}
|
|
while (j < lx->n && src[j] != '\n') {
|
|
j++;
|
|
}
|
|
return j;
|
|
}
|
|
|
|
/* A word at src[i] (an identifier, keyword or number, with an optional `@`):
|
|
* reports a declaration keyword and returns its end; i when there is none.
|
|
* *partial is set when the word leaves the `partial` modifier standing for
|
|
* the next keyword: `partial` itself, or `record` after it (`partial record
|
|
* struct`). */
|
|
static uint32_t cs_lex_identifier(cs_lex_t *lx, uint32_t i, bool hole, bool *partial) {
|
|
static const char modifier[] = "partial";
|
|
const char *src = lx->src;
|
|
bool verbatim = src[i] == '@';
|
|
uint32_t a = verbatim ? i + SKIP_ONE : i;
|
|
uint32_t b = a;
|
|
while (b < lx->n && cs_lex_word((unsigned char)src[b])) {
|
|
b++;
|
|
}
|
|
if (b == a) {
|
|
return i;
|
|
}
|
|
char kind = (verbatim || hole) ? 0 : cs_keyword_kind(src + a, b - a);
|
|
if (kind && !lx->prev_sep) {
|
|
cs_lex_keyword(lx->ud, b, kind, lx->prev_partial);
|
|
}
|
|
*partial = !verbatim && !hole &&
|
|
((kind == 'r' && lx->prev_partial) ||
|
|
(b - a == sizeof(modifier) - SKIP_ONE && memcmp(src + a, modifier, b - a) == 0));
|
|
return b;
|
|
}
|
|
|
|
/* Past a comment at src[i], or i when there is none. */
|
|
static uint32_t cs_lex_comment(const cs_lex_t *lx, uint32_t i) {
|
|
const char *src = lx->src;
|
|
uint32_t n = lx->n;
|
|
if (src[i] != '/' || i + SKIP_ONE >= n) {
|
|
return i;
|
|
}
|
|
if (src[i + SKIP_ONE] == '/') {
|
|
while (i < n && src[i] != '\n') {
|
|
i++;
|
|
}
|
|
return i;
|
|
}
|
|
if (src[i + SKIP_ONE] == '*') {
|
|
i += PAIR_LEN;
|
|
while (i + SKIP_ONE < n && !(src[i] == '*' && src[i + SKIP_ONE] == '/')) {
|
|
i++;
|
|
}
|
|
return i + PAIR_LEN <= n ? i + PAIR_LEN : n;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
/* Scan code from src[i]. In a `hole` (the code of an interpolation) nothing
|
|
* is reported and the scan returns just past the brace that closes it. */
|
|
static uint32_t cs_lex_code(cs_lex_t *lx, uint32_t i, bool hole) {
|
|
const char *src = lx->src;
|
|
uint32_t n = lx->n;
|
|
int depth = 0;
|
|
bool line_start = !hole;
|
|
while (i < n) {
|
|
unsigned char c = (unsigned char)src[i];
|
|
lx->steps++;
|
|
if (isspace(c)) {
|
|
line_start = line_start || c == '\n';
|
|
i++;
|
|
continue;
|
|
}
|
|
if (c == '#' && line_start) {
|
|
i = cs_lex_directive(lx, i, hole);
|
|
continue;
|
|
}
|
|
line_start = false;
|
|
uint32_t e = cs_lex_comment(lx, i);
|
|
if (e > i) {
|
|
i = e;
|
|
continue;
|
|
}
|
|
bool sep = c == ':' || c == ',';
|
|
bool partial = false;
|
|
if (c == '\'') {
|
|
e = cs_lex_char(lx, i);
|
|
} else if (c == '{' || c == '}') {
|
|
if (hole && c == '}' && depth == 0) {
|
|
return i + SKIP_ONE;
|
|
}
|
|
if (hole) {
|
|
depth += c == '{' ? SKIP_ONE : -SKIP_ONE;
|
|
} else {
|
|
cs_lex_brace(lx->ud, i, c == '{');
|
|
}
|
|
e = i + SKIP_ONE;
|
|
} else {
|
|
e = (c == '"' || c == '$' || c == '@') ? cs_lex_literal(lx, i) : i;
|
|
if (e == i && (cs_lex_word(c) || c == '@')) {
|
|
e = cs_lex_identifier(lx, i, hole, &partial);
|
|
}
|
|
if (e == i) {
|
|
e = i + SKIP_ONE;
|
|
}
|
|
}
|
|
lx->prev_sep = sep;
|
|
lx->prev_partial = partial;
|
|
i = e;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* Scan the file's braces and declaration keywords, pair the braces. The
|
|
* first brace without a partner (or conditional whose branches disagree)
|
|
* starts the untrusted part of the file. */
|
|
static void cs_scan_tokens(cs_scan_t *s) {
|
|
cs_lex_t lx = {.src = s->ctx->source, .n = s->root_end_byte, .ud = s};
|
|
(void)cs_lex_code(&lx, 0, false);
|
|
s->cost_steps += lx.steps;
|
|
if (s->failed) {
|
|
return;
|
|
}
|
|
if (lx.stopped) {
|
|
cs_mark_untrusted_at(s, lx.stop_pos, cs_row_of(s, lx.stop_pos));
|
|
}
|
|
/* the outermost brace that is still open */
|
|
int unpaired = CBM_NOT_FOUND;
|
|
for (int o = s->top; o >= 0; o = s->open[o].below) {
|
|
unpaired = s->open[o].brace;
|
|
}
|
|
if (unpaired >= 0) {
|
|
cs_mark_untrusted(s, unpaired);
|
|
}
|
|
/* a later branch's brace closes where the first branch's does */
|
|
for (int i = 0; i < s->nbraces; i++) {
|
|
cs_brace_t *b = &s->braces[i];
|
|
int a = b->alias;
|
|
for (int hops = 0; a >= 0 && b->match < 0 && hops < CS_PP_MAX; hops++) {
|
|
b->match = s->braces[a].match;
|
|
a = s->braces[a].alias;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Index of the first entry of the brace list at or after byte `pos`. */
|
|
static int cs_brace_lower(const cs_scan_t *s, uint32_t pos) {
|
|
int lo = 0;
|
|
int hi = s->nbraces;
|
|
while (lo < hi) {
|
|
int mid = lo + ((hi - lo) / PAIR_LEN);
|
|
if (s->braces[mid].pos < pos) {
|
|
lo = mid + SKIP_ONE;
|
|
} else {
|
|
hi = mid;
|
|
}
|
|
}
|
|
return lo;
|
|
}
|
|
|
|
/* The open brace AT byte `pos`, or CBM_NOT_FOUND. */
|
|
static int cs_open_brace_at(const cs_scan_t *s, uint32_t pos) {
|
|
int b = cs_brace_lower(s, pos);
|
|
return (b < s->nbraces && s->braces[b].pos == pos && s->braces[b].kind == '{') ? b
|
|
: CBM_NOT_FOUND;
|
|
}
|
|
|
|
/* Brace nesting depth at byte `pos`. */
|
|
static int cs_depth_at(const cs_scan_t *s, uint32_t pos) {
|
|
int i = cs_brace_lower(s, pos);
|
|
return i > 0 ? s->braces[i - SKIP_ONE].depth : 0;
|
|
}
|
|
|
|
/* The extent of the block opened by brace `b`. false when it has no partner. */
|
|
static bool cs_brace_extent(const cs_scan_t *s, int b, uint32_t *end, uint32_t *end_line,
|
|
int *inner_depth) {
|
|
if (b < 0 || b >= s->nbraces || s->braces[b].kind != '{' || s->braces[b].match < 0) {
|
|
return false;
|
|
}
|
|
const cs_brace_t *close = &s->braces[s->braces[b].match];
|
|
*end = close->pos + SKIP_ONE;
|
|
*end_line = close->row + TS_LINE_OFFSET;
|
|
*inner_depth = s->braces[b].depth;
|
|
return true;
|
|
}
|
|
|
|
/* ── Text ────────────────────────────────────────────────────────── */
|
|
|
|
/* Past whitespace and comments. */
|
|
static uint32_t cs_skip_space(const char *src, uint32_t i, uint32_t n) {
|
|
for (;;) {
|
|
while (i < n && isspace((unsigned char)src[i])) {
|
|
i++;
|
|
}
|
|
if (i + SKIP_ONE >= n || src[i] != '/') {
|
|
return i;
|
|
}
|
|
if (src[i + SKIP_ONE] == '/') {
|
|
while (i < n && src[i] != '\n') {
|
|
i++;
|
|
}
|
|
} else if (src[i + SKIP_ONE] == '*') {
|
|
i += PAIR_LEN;
|
|
while (i + SKIP_ONE < n && !(src[i] == '*' && src[i + SKIP_ONE] == '/')) {
|
|
i++;
|
|
}
|
|
i = i + PAIR_LEN <= n ? i + PAIR_LEN : n;
|
|
} else {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool cs_word_char(unsigned char c) {
|
|
return isalnum(c) || c == '_' || c >= 0x80;
|
|
}
|
|
|
|
/* Recovery may omit punctuation before the name node. Start at the actual
|
|
* namespace header, not at that recovered node's first byte. */
|
|
static uint32_t cs_namespace_name_start(const cs_scan_t *s, TSNode node) {
|
|
static const char keyword[] = "namespace";
|
|
const uint32_t width = (uint32_t)(sizeof(keyword) - SKIP_ONE);
|
|
uint32_t a = ts_node_start_byte(node);
|
|
uint32_t n = s->root_end_byte;
|
|
const char *src = s->ctx->source;
|
|
if (a > n || n - a < width || memcmp(src + a, keyword, width) != 0) {
|
|
return n;
|
|
}
|
|
a += width;
|
|
uint32_t start = cs_skip_space(src, a, n);
|
|
return start > a || (a < n && src[a] == '@') ? start : n;
|
|
}
|
|
|
|
/* Namespace names have nonempty identifier segments. Keep the scanner's
|
|
* existing Unicode-byte support, while allowing trivia between tokens and
|
|
* verbatim markers only at segment starts. NULL names remain unplaced items. */
|
|
static char *cs_namespace_name_dup(cs_scan_t *s, uint32_t a, uint32_t b) {
|
|
if (b <= a || b > s->root_end_byte || b - a > CS_NAME_MAX) {
|
|
return NULL;
|
|
}
|
|
const char *src = s->ctx->source;
|
|
char *out = (char *)cs_tmp_alloc(s, (size_t)(b - a) + SKIP_ONE);
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
size_t w = 0;
|
|
uint32_t i = a;
|
|
for (;;) {
|
|
i = cs_skip_space(src, i, b);
|
|
if (i < b && src[i] == '@') {
|
|
i++;
|
|
}
|
|
if (i == b || !cs_word_char((unsigned char)src[i]) || isdigit((unsigned char)src[i])) {
|
|
return NULL;
|
|
}
|
|
while (i < b && cs_word_char((unsigned char)src[i])) {
|
|
out[w++] = src[i++];
|
|
}
|
|
i = cs_skip_space(src, i, b);
|
|
if (i == b) {
|
|
out[w] = '\0';
|
|
return cs_utf8_ok(out, w) ? out : NULL; /* a name that cannot be written */
|
|
}
|
|
if (src[i] != '.') {
|
|
return NULL;
|
|
}
|
|
out[w++] = '.';
|
|
i++;
|
|
}
|
|
}
|
|
|
|
/* Keep malformed dotted candidates until their known delimiter. Dropping an
|
|
* invalid file-scoped header would place its declarations in the root. The
|
|
* next recorded declaration bounds this scan even when no delimiter exists. */
|
|
static uint32_t cs_namespace_header_end(cs_scan_t *s, uint32_t a, uint32_t stop) {
|
|
const char *src = s->ctx->source;
|
|
uint32_t i = a;
|
|
while (i < stop) {
|
|
uint32_t next = cs_skip_space(src, i, stop);
|
|
if (next != i) {
|
|
i = next;
|
|
continue;
|
|
}
|
|
unsigned char c = (unsigned char)src[i];
|
|
if (!cs_word_char(c) && c != '@' && c != '.') {
|
|
break;
|
|
}
|
|
s->cost_steps++;
|
|
i++;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
/* End of the identifier starting at src[i] (i itself when there is none);
|
|
* `dotted` accepts a qualified name. */
|
|
static uint32_t cs_ident_end(const char *src, uint32_t i, uint32_t n, bool dotted) {
|
|
uint32_t e = i;
|
|
if (e < n && src[e] == '@') {
|
|
e++;
|
|
}
|
|
uint32_t first = e;
|
|
while (e < n && (cs_word_char((unsigned char)src[e]) ||
|
|
(dotted && src[e] == '.' && e > first && e + SKIP_ONE < n &&
|
|
cs_word_char((unsigned char)src[e + SKIP_ONE])))) {
|
|
e++;
|
|
}
|
|
if (e == first || isdigit((unsigned char)src[first])) {
|
|
return i;
|
|
}
|
|
return e;
|
|
}
|
|
|
|
/* Words that follow `class` / `record` ... without being a declared name. */
|
|
static bool cs_not_a_name(const char *name) {
|
|
static const char *const words[] = {"class", "struct", "interface", "enum", "record",
|
|
"where", "in", "is", "as", "when",
|
|
"and", "or", "not", "with", "switch"};
|
|
for (size_t i = 0; i < sizeof(words) / sizeof(words[0]); i++) {
|
|
if (strcmp(name, words[i]) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* The type parameters written at src[*pos] (`<in T, U>`): their names
|
|
* ','-joined, "" when there are none. *pos moves past the list. NULL when the
|
|
* list cannot be read. Nothing at or past `stop` is read. */
|
|
static const char *cs_text_tparams(cs_scan_t *s, uint32_t *pos, uint32_t stop) {
|
|
const char *src = s->ctx->source;
|
|
uint32_t n = s->root_end_byte;
|
|
uint32_t i = cs_skip_space(src, *pos, n);
|
|
if (i >= n || src[i] != '<') {
|
|
return "";
|
|
}
|
|
if (i >= stop) {
|
|
return NULL; /* the list is the next declaration's */
|
|
}
|
|
uint32_t limit = stop - i > CS_TPARAMS_SCAN_MAX ? i + CS_TPARAMS_SCAN_MAX : stop;
|
|
char *out = (char *)cs_tmp_alloc(s, (size_t)(limit - i) + SKIP_ONE);
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
size_t w = 0;
|
|
int square = 0;
|
|
uint32_t word_s = 0;
|
|
uint32_t word_e = 0; /* the last identifier of the current parameter */
|
|
for (uint32_t k = i + SKIP_ONE; k < limit; k++) {
|
|
char c = src[k];
|
|
s->cost_steps++;
|
|
if (c == '[') {
|
|
square++;
|
|
} else if (c == ']') {
|
|
square--;
|
|
} else if (square > 0) {
|
|
continue; /* an attribute on the parameter */
|
|
} else if (c == ',' || c == '>') {
|
|
if (word_e == word_s) {
|
|
return NULL;
|
|
}
|
|
if (w > 0) {
|
|
out[w++] = ',';
|
|
}
|
|
memcpy(out + w, src + word_s, word_e - word_s);
|
|
w += word_e - word_s;
|
|
word_s = word_e = 0;
|
|
if (c == '>') {
|
|
out[w] = '\0';
|
|
*pos = k + SKIP_ONE;
|
|
return cs_utf8_ok(out, w) ? out : NULL; /* names that cannot be written */
|
|
}
|
|
} else if (cs_word_char((unsigned char)c)) {
|
|
uint32_t e = cs_ident_end(src, k, limit, false);
|
|
if (e == k) {
|
|
return NULL;
|
|
}
|
|
word_s = k;
|
|
word_e = e;
|
|
k = e - SKIP_ONE;
|
|
} else if (!isspace((unsigned char)c)) {
|
|
return NULL;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* From the end of a type's name and type parameters to the `{` that opens
|
|
* its body (returned as a brace index) or the `;` that ends a body-less
|
|
* declaration (*bodyless). CBM_NOT_FOUND with *bodyless false when neither is
|
|
* found: then this was no declaration. Nothing at or past `stop` is read. */
|
|
static int cs_text_body(cs_scan_t *s, uint32_t from, uint32_t stop, bool *bodyless) {
|
|
const char *src = s->ctx->source;
|
|
uint32_t limit = from + CS_HEADER_SCAN_MAX < stop ? from + CS_HEADER_SCAN_MAX : stop;
|
|
int round = 0;
|
|
*bodyless = false;
|
|
for (uint32_t i = from; i < limit; i++) {
|
|
char c = src[i];
|
|
s->cost_steps++;
|
|
if (c == '/' && i + SKIP_ONE < limit &&
|
|
(src[i + SKIP_ONE] == '/' || src[i + SKIP_ONE] == '*')) {
|
|
uint32_t past = cs_skip_space(src, i, limit);
|
|
if (past <= i) {
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
i = past - SKIP_ONE;
|
|
continue;
|
|
}
|
|
if (c == '(') {
|
|
round++;
|
|
} else if (c == ')') {
|
|
round--;
|
|
} else if (round > 0) {
|
|
continue;
|
|
} else if (c == ';') {
|
|
*bodyless = true;
|
|
return CBM_NOT_FOUND;
|
|
} else if (c == '{') {
|
|
return cs_open_brace_at(s, i);
|
|
} else if (c == '}' || c == '=') {
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
}
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
|
|
/* ── Collection ──────────────────────────────────────────────────── */
|
|
|
|
/* A new item; returns its index or CBM_NOT_FOUND when memory ran out. */
|
|
static int cs_item_new(cs_scan_t *s, char tag) {
|
|
if (s->failed) {
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
if (s->nitems >= s->cap_items) {
|
|
int ncap = s->cap_items ? s->cap_items * PAIR_LEN : CS_ITEMS_INIT;
|
|
cs_item_t *grown = (cs_item_t *)cs_tmp_alloc(s, (size_t)ncap * sizeof(*grown));
|
|
if (!grown) {
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
if (s->nitems > 0) {
|
|
memcpy(grown, s->items, (size_t)s->nitems * sizeof(*grown));
|
|
}
|
|
s->items = grown;
|
|
s->cap_items = ncap;
|
|
}
|
|
cs_item_t *it = &s->items[s->nitems];
|
|
memset(it, 0, sizeof(*it));
|
|
it->tag = tag;
|
|
it->owner = CS_OWNER_NONE;
|
|
it->brace = CBM_NOT_FOUND;
|
|
it->tree_idx = s->nitems;
|
|
return s->nitems++;
|
|
}
|
|
|
|
static int cs_item_of_node(cs_scan_t *s, char tag, TSNode node) {
|
|
int i = cs_item_new(s, tag);
|
|
if (i >= 0) {
|
|
s->items[i].node = node;
|
|
s->items[i].start = ts_node_start_byte(node);
|
|
s->items[i].line = cs_line(node);
|
|
}
|
|
return i;
|
|
}
|
|
|
|
/* True when the first parameter of `params` is written with `this`: an
|
|
* extension method. The text before the parameter's type says so, whatever
|
|
* shape the grammar gives the modifier. */
|
|
static bool cs_first_param_this(const cs_scan_t *s, TSNode params) {
|
|
static const char word[] = "this";
|
|
if (ts_node_is_null(params)) {
|
|
return false;
|
|
}
|
|
TSNode p = cs_child_of_kind(params, "parameter");
|
|
if (ts_node_is_null(p)) {
|
|
return false;
|
|
}
|
|
TSNode ty = cs_field(p, "type");
|
|
const char *src = s->ctx->source;
|
|
uint32_t a = ts_node_start_byte(p);
|
|
uint32_t b = ts_node_is_null(ty) ? ts_node_end_byte(p) : ts_node_start_byte(ty);
|
|
uint32_t wl = (uint32_t)(sizeof(word) - SKIP_ONE);
|
|
for (uint32_t i = a; i + wl <= b; i++) {
|
|
if (memcmp(src + i, word, wl) == 0 &&
|
|
(i == a || !cs_word_char((unsigned char)src[i - SKIP_ONE])) &&
|
|
(i + wl == b || !cs_word_char((unsigned char)src[i + wl]))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* True for a member `using static` brings in: one declared `static` or
|
|
* `const`, an enum's member -- and no extension method. */
|
|
static bool cs_member_static(const cs_scan_t *s, TSNode decl, TSNode params) {
|
|
if (cs_kind_is(decl, "enum_member_declaration")) {
|
|
return true;
|
|
}
|
|
if (!cs_has_word(decl, "static") && !cs_has_word(decl, "const")) {
|
|
return false;
|
|
}
|
|
return !cs_first_param_this(s, params);
|
|
}
|
|
|
|
static void cs_member_new(cs_scan_t *s, TSNode decl, char kind, bool explicit_impl, bool is_static,
|
|
int owner, TSNode name, TSNode tparams, TSNode params) {
|
|
if (ts_node_is_null(name)) {
|
|
return;
|
|
}
|
|
int i = cs_item_of_node(s, 'M', decl);
|
|
if (i < 0) {
|
|
return;
|
|
}
|
|
cs_item_t *it = &s->items[i];
|
|
it->kind = kind;
|
|
it->explicit_impl = explicit_impl;
|
|
it->is_static = is_static;
|
|
it->owner = owner;
|
|
it->name = name;
|
|
it->tparams = tparams;
|
|
it->params = params;
|
|
}
|
|
|
|
/* A member that has no identifier for a name: an operator (`name` is its
|
|
* token: +, ==, true, implicit ...) or an indexer (`this`). The graph has no
|
|
* node for either; the record says that the type declares one. */
|
|
static void cs_member_unnamed(cs_scan_t *s, TSNode decl, char kind, int owner, const char *name,
|
|
TSNode params) {
|
|
int i = cs_item_of_node(s, 'M', decl);
|
|
if (i < 0) {
|
|
return;
|
|
}
|
|
cs_item_t *it = &s->items[i];
|
|
it->kind = kind;
|
|
it->owner = owner;
|
|
it->text_name = name;
|
|
it->params = params;
|
|
}
|
|
|
|
/* Every variable_declarator name under a field / event field declaration. */
|
|
static void cs_collect_declarators(cs_scan_t *s, TSNode decl, char kind, int owner) {
|
|
TSNode null_node = {0};
|
|
/* Modifiers belong to this declaration and are shared by all its names. */
|
|
bool is_static = cs_member_static(s, decl, null_node);
|
|
cs_kids_t outer = cs_kids(decl);
|
|
TSNode vd;
|
|
while (cs_kids_next_named(&outer, &vd)) {
|
|
if (!cs_kind_is(vd, "variable_declaration")) {
|
|
continue;
|
|
}
|
|
cs_kids_t inner = cs_kids(vd);
|
|
TSNode d;
|
|
while (cs_kids_next_named(&inner, &d)) {
|
|
if (cs_kind_is(d, "variable_declarator")) {
|
|
cs_member_new(s, decl, kind, false, is_static, owner, cs_field(d, "name"),
|
|
null_node, null_node);
|
|
}
|
|
}
|
|
cs_kids_end(&inner);
|
|
}
|
|
cs_kids_end(&outer);
|
|
}
|
|
|
|
/* A member whose header did not parse: an error node among its own parts, or
|
|
* (for a callable) an error anywhere outside its body. Its name and signature
|
|
* cannot be trusted then -- `public safe extern int M();` comes back as a
|
|
* method named `extern`. */
|
|
static bool cs_header_broken(TSNode decl, bool callable) {
|
|
TSNode body = callable ? cs_field(decl, "body") : (TSNode){0};
|
|
bool broken = false;
|
|
cs_kids_t k = cs_kids(decl);
|
|
TSNode ch;
|
|
while (!broken && cs_kids_next(&k, &ch)) {
|
|
if (!ts_node_is_null(body) && ts_node_eq(ch, body)) {
|
|
continue;
|
|
}
|
|
broken = cs_kind_is(ch, "ERROR") || ts_node_is_missing(ch) ||
|
|
(callable && ts_node_has_error(ch));
|
|
}
|
|
cs_kids_end(&k);
|
|
return broken;
|
|
}
|
|
|
|
static void cs_collect_member(cs_scan_t *s, TSNode c, const char *k, int owner) {
|
|
TSNode null_node = {0};
|
|
bool is_operator = strcmp(k, "operator_declaration") == 0;
|
|
bool is_conversion = strcmp(k, "conversion_operator_declaration") == 0;
|
|
bool is_indexer = strcmp(k, "indexer_declaration") == 0;
|
|
bool callable = strcmp(k, "method_declaration") == 0 ||
|
|
strcmp(k, "constructor_declaration") == 0 || is_operator || is_conversion;
|
|
bool member = callable || is_indexer || strcmp(k, "property_declaration") == 0 ||
|
|
strcmp(k, "field_declaration") == 0 ||
|
|
strcmp(k, "event_field_declaration") == 0 ||
|
|
strcmp(k, "event_declaration") == 0 || strcmp(k, "enum_member_declaration") == 0;
|
|
if (member && ts_node_has_error(c) && cs_header_broken(c, callable)) {
|
|
if (owner >= 0) {
|
|
s->items[owner].broken = true; /* a member of it is hidden */
|
|
}
|
|
return;
|
|
}
|
|
if (strcmp(k, "method_declaration") == 0) {
|
|
TSNode params = cs_field(c, "parameters");
|
|
cs_member_new(s, c, 'c', cs_has_child_kind(c, "explicit_interface_specifier"),
|
|
cs_member_static(s, c, params), owner, cs_field(c, "name"), cs_type_params(c),
|
|
params);
|
|
} else if (strcmp(k, "constructor_declaration") == 0) {
|
|
TSNode params = cs_field(c, "parameters");
|
|
cs_member_new(s, c, 'c', false, cs_member_static(s, c, params), owner, cs_field(c, "name"),
|
|
null_node, params);
|
|
} else if (strcmp(k, "property_declaration") == 0) {
|
|
cs_member_new(s, c, 'p', cs_has_child_kind(c, "explicit_interface_specifier"),
|
|
cs_member_static(s, c, null_node), owner, cs_field(c, "name"), null_node,
|
|
null_node);
|
|
} else if (strcmp(k, "field_declaration") == 0) {
|
|
cs_collect_declarators(s, c, 'v', owner);
|
|
} else if (strcmp(k, "event_field_declaration") == 0) {
|
|
cs_collect_declarators(s, c, 'e', owner);
|
|
} else if (strcmp(k, "event_declaration") == 0) {
|
|
cs_member_new(s, c, 'e', cs_has_child_kind(c, "explicit_interface_specifier"),
|
|
cs_member_static(s, c, null_node), owner, cs_field(c, "name"), null_node,
|
|
null_node);
|
|
} else if (strcmp(k, "enum_member_declaration") == 0) {
|
|
cs_member_new(s, c, 'v', false, cs_member_static(s, c, null_node), owner,
|
|
cs_field(c, "name"), null_node, null_node);
|
|
} else if (is_operator) {
|
|
/* an anonymous token's type is its text */
|
|
TSNode op = cs_field(c, "operator");
|
|
if (!ts_node_is_null(op)) {
|
|
cs_member_unnamed(s, c, 'o', owner, ts_node_type(op), cs_field(c, "parameters"));
|
|
}
|
|
} else if (is_conversion) {
|
|
cs_member_unnamed(s, c, 'o', owner, cs_has_word(c, "implicit") ? "implicit" : "explicit",
|
|
cs_field(c, "parameters"));
|
|
} else if (is_indexer) {
|
|
cs_member_unnamed(s, c, 'x', owner, "this", cs_field(c, "parameters"));
|
|
}
|
|
}
|
|
|
|
/* A node whose children are being collected. */
|
|
typedef struct {
|
|
cs_kids_t kids;
|
|
int owner; /* the item of the type whose members the node holds, or CS_OWNER_* */
|
|
} cs_walk_t;
|
|
|
|
typedef struct {
|
|
cs_walk_t *frames;
|
|
int count;
|
|
int cap;
|
|
} cs_walk_stack_t;
|
|
|
|
/* Go into `node`: its children are collected next. false when memory ran out. */
|
|
static bool cs_walk_push(cs_scan_t *s, cs_walk_stack_t *w, TSNode node, int owner) {
|
|
if (ts_node_is_null(node)) {
|
|
return true;
|
|
}
|
|
if (w->count >= w->cap) {
|
|
int ncap = w->cap ? w->cap * PAIR_LEN : CS_HEADS_INIT;
|
|
cs_walk_t *grown = (cs_walk_t *)cs_tmp_alloc(s, (size_t)ncap * sizeof(*grown));
|
|
if (!grown) {
|
|
return false;
|
|
}
|
|
if (w->count > 0) {
|
|
memcpy(grown, w->frames, (size_t)w->count * sizeof(*grown));
|
|
}
|
|
w->frames = grown;
|
|
w->cap = ncap;
|
|
}
|
|
w->frames[w->count++] = (cs_walk_t){.kids = cs_kids(node), .owner = owner};
|
|
return true;
|
|
}
|
|
|
|
/* One child `c` of a collected node: record what it declares and go into it
|
|
* where declarations can sit. false when memory ran out. */
|
|
static bool cs_collect_child(cs_scan_t *s, cs_walk_stack_t *w, TSNode c, int owner) {
|
|
const char *k = ts_node_type(c);
|
|
if (strcmp(k, "ERROR") == 0) {
|
|
if (owner >= 0) {
|
|
s->items[owner].broken = true;
|
|
}
|
|
return cs_walk_push(s, w, c, CS_OWNER_LEXICAL);
|
|
}
|
|
if (strncmp(k, "preproc_", 8) == 0 || strcmp(k, "declaration_list") == 0 ||
|
|
strcmp(k, "enum_member_declaration_list") == 0) {
|
|
return cs_walk_push(s, w, c, owner);
|
|
}
|
|
if (strcmp(k, "using_directive") == 0) {
|
|
(void)cs_item_of_node(s, 'U', c);
|
|
return true;
|
|
}
|
|
bool file_scoped = strcmp(k, "file_scoped_namespace_declaration") == 0;
|
|
if (file_scoped || strcmp(k, "namespace_declaration") == 0) {
|
|
int ni = cs_item_of_node(s, file_scoped ? 'F' : 'N', c);
|
|
if (ni < 0) {
|
|
return false;
|
|
}
|
|
s->items[ni].name = cs_field(c, "name");
|
|
/* a file-scoped namespace holds nothing itself -- unless it is a
|
|
* block namespace recovery could not parse, whose declarations
|
|
* then sit in an error node under it */
|
|
TSNode body = file_scoped ? (TSNode){0} : cs_field(c, "body");
|
|
return cs_walk_push(s, w, ts_node_is_null(body) ? c : body, CS_OWNER_NONE);
|
|
}
|
|
char tk = cs_decl_kind(c);
|
|
if (tk) {
|
|
int ti = cs_item_of_node(s, 'T', c);
|
|
if (ti < 0) {
|
|
return false;
|
|
}
|
|
s->items[ti].kind = tk;
|
|
s->items[ti].partial = cs_has_word(c, "partial");
|
|
s->items[ti].name = cs_field(c, "name");
|
|
s->items[ti].tparams = cs_type_params(c);
|
|
return tk == 'd' || cs_walk_push(s, w, cs_field(c, "body"), ti);
|
|
}
|
|
if (owner != CS_OWNER_NONE) {
|
|
cs_collect_member(s, c, k, owner);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* Collect the declarations under `root` in document order. `owner` is the
|
|
* item of the type whose members a node holds (CS_OWNER_NONE outside a type).
|
|
* Preprocessor blocks and error nodes are looked into: a declaration that
|
|
* parsed is a declaration wherever recovery left it, and its placement is
|
|
* checked against the braces afterwards. What sits in an error node belongs
|
|
* to whichever block's braces hold it.
|
|
*
|
|
* The walk keeps its own stack: how deep declarations nest is the file's
|
|
* choice, and must not be this thread's stack depth. */
|
|
static void cs_collect(cs_scan_t *s, TSNode root, int owner) {
|
|
cs_walk_stack_t w = {0};
|
|
bool ok = !s->failed && cs_walk_push(s, &w, root, owner);
|
|
while (w.count > 0) {
|
|
/* a child may push a frame, which moves the array: no pointer into it
|
|
* is kept across cs_collect_child */
|
|
TSNode c;
|
|
if (!ok || s->failed || !cs_kids_next_named(&w.frames[w.count - SKIP_ONE].kids, &c)) {
|
|
cs_kids_end(&w.frames[w.count - SKIP_ONE].kids);
|
|
w.count--;
|
|
continue;
|
|
}
|
|
ok = cs_collect_child(s, &w, c, w.frames[w.count - SKIP_ONE].owner);
|
|
}
|
|
}
|
|
|
|
/* ── Declarations the tree has no node for ───────────────────────── */
|
|
|
|
static int cs_u32_cmp(const void *a, const void *b) {
|
|
uint32_t x = *(const uint32_t *)a;
|
|
uint32_t y = *(const uint32_t *)b;
|
|
return (x > y) - (x < y);
|
|
}
|
|
|
|
static int cs_item_start_cmp(const void *a, const void *b) {
|
|
const cs_item_t *x = (const cs_item_t *)a;
|
|
const cs_item_t *y = (const cs_item_t *)b;
|
|
if (x->start != y->start) {
|
|
return x->start < y->start ? -1 : 1;
|
|
}
|
|
/* declarators of one field declaration keep their order */
|
|
return (x->tree_idx > y->tree_idx) - (x->tree_idx < y->tree_idx);
|
|
}
|
|
|
|
/* Read the declaration a keyword starts from the text and add it as an item,
|
|
* unless the tree already has a node for it (`parsed`: the name positions of
|
|
* the tree's namespaces and types). A header ends where the next declaration
|
|
* keyword stands (`stop`): reading past it would read the file once per
|
|
* keyword. */
|
|
static void cs_text_item(cs_scan_t *s, const cs_head_t *h, uint32_t stop, const uint32_t *parsed,
|
|
int nparsed) {
|
|
const char *src = s->ctx->source;
|
|
uint32_t n = s->root_end_byte;
|
|
uint32_t a = cs_skip_space(src, h->end, n);
|
|
if (a == h->end) {
|
|
return; /* the keyword runs into something: not a declaration */
|
|
}
|
|
bool is_ns = h->kind == 'N';
|
|
if (bsearch(&a, parsed, (size_t)nparsed, sizeof(uint32_t), cs_u32_cmp)) {
|
|
return;
|
|
}
|
|
char *name = NULL;
|
|
int brace = CBM_NOT_FOUND;
|
|
const char *tparams = "";
|
|
char tag = 'T';
|
|
if (is_ns) {
|
|
uint32_t after = cs_namespace_header_end(s, a, stop);
|
|
name = cs_namespace_name_dup(s, a, after);
|
|
if (name && src[a] != '@' && cs_not_a_name(name)) {
|
|
name = NULL; /* bare keywords stay unplaced; verbatim identifiers are names */
|
|
}
|
|
if (after < n && src[after] == '{') {
|
|
brace = cs_open_brace_at(s, after);
|
|
tag = 'N';
|
|
} else if (after < n && src[after] == ';') {
|
|
tag = 'F';
|
|
} else {
|
|
return;
|
|
}
|
|
if (tag == 'N' && brace < 0) {
|
|
return;
|
|
}
|
|
/* An invalid name must still push an unplaced namespace frame. */
|
|
} else {
|
|
uint32_t b = cs_ident_end(src, a, n, false);
|
|
name = cs_ident_dup(s, a, b);
|
|
if (!name || cs_not_a_name(name)) {
|
|
return;
|
|
}
|
|
uint32_t pos = b;
|
|
tparams = cs_text_tparams(s, &pos, stop);
|
|
bool bodyless = false;
|
|
brace = tparams ? cs_text_body(s, pos, stop, &bodyless) : CBM_NOT_FOUND;
|
|
if (!tparams || (brace < 0 && !bodyless)) {
|
|
return;
|
|
}
|
|
}
|
|
int i = cs_item_new(s, tag);
|
|
if (i < 0) {
|
|
return;
|
|
}
|
|
cs_item_t *it = &s->items[i];
|
|
it->from_text = true;
|
|
it->tree_idx = CS_ITEM_TEXT;
|
|
it->partial = h->partial;
|
|
it->kind = is_ns ? 0 : h->kind;
|
|
it->text_name = name;
|
|
it->text_tparams = tparams;
|
|
it->brace = brace;
|
|
it->start = a; /* the name: after the modifiers, inside the same braces */
|
|
it->line = h->row + TS_LINE_OFFSET;
|
|
it->broken = true;
|
|
}
|
|
|
|
/* Add the declarations only the token stream shows and put all items in
|
|
* document order. */
|
|
static void cs_add_text_items(cs_scan_t *s) {
|
|
if (s->failed || s->nheads == 0) {
|
|
return;
|
|
}
|
|
int tree_items = s->nitems;
|
|
uint32_t *parsed =
|
|
(uint32_t *)cs_tmp_alloc(s, (size_t)(tree_items + SKIP_ONE) * sizeof(uint32_t));
|
|
if (!parsed) {
|
|
return;
|
|
}
|
|
int nparsed = 0;
|
|
for (int i = 0; i < tree_items; i++) {
|
|
const cs_item_t *it = &s->items[i];
|
|
if (it->tag == 'N' || it->tag == 'F') {
|
|
parsed[nparsed++] = cs_namespace_name_start(s, it->node);
|
|
} else if (it->tag == 'T' && !ts_node_is_null(it->name)) {
|
|
parsed[nparsed++] = ts_node_start_byte(it->name);
|
|
}
|
|
}
|
|
qsort(parsed, (size_t)nparsed, sizeof(uint32_t), cs_u32_cmp);
|
|
for (int h = 0; h < s->nheads && !s->failed; h++) {
|
|
uint32_t stop = h + SKIP_ONE < s->nheads ? s->heads[h + SKIP_ONE].end : s->root_end_byte;
|
|
cs_text_item(s, &s->heads[h], stop, parsed, nparsed);
|
|
}
|
|
if (s->nitems > tree_items) {
|
|
qsort(s->items, (size_t)s->nitems, sizeof(cs_item_t), cs_item_start_cmp);
|
|
}
|
|
}
|
|
|
|
/* ── Emission ────────────────────────────────────────────────────── */
|
|
|
|
static void cs_put_bases(cs_scan_t *s, TSNode type_decl, char kind) {
|
|
if (kind == 'e' || kind == 'd') {
|
|
return; /* an enum's base is its underlying integral type */
|
|
}
|
|
TSNode bl = cs_child_of_kind(type_decl, "base_list");
|
|
if (ts_node_is_null(bl)) {
|
|
return;
|
|
}
|
|
bool first = true;
|
|
cs_kids_t k = cs_kids(bl);
|
|
TSNode b;
|
|
while (cs_kids_next_named(&k, &b)) {
|
|
if (cs_kind_is(b, "primary_constructor_base_type")) {
|
|
TSNode ty = cs_field(b, "type");
|
|
if (ts_node_is_null(ty) && ts_node_named_child_count(b) > 0) {
|
|
ty = ts_node_named_child(b, 0);
|
|
}
|
|
b = ty;
|
|
} else if (cs_kind_is(b, "argument_list") || cs_kind_is(b, "comment")) {
|
|
continue;
|
|
}
|
|
if (ts_node_is_null(b)) {
|
|
continue;
|
|
}
|
|
if (!first) {
|
|
sb_putc(&s->sb, '|');
|
|
}
|
|
cs_put_text_nows(s, b);
|
|
first = false;
|
|
}
|
|
cs_kids_end(&k);
|
|
}
|
|
|
|
/* What an M record says of its member. */
|
|
typedef struct {
|
|
uint32_t line;
|
|
char kind;
|
|
bool explicit_impl;
|
|
bool is_static;
|
|
int type; /* the ordinal of the T record it belongs to */
|
|
const char *name;
|
|
} cs_member_rec_t;
|
|
|
|
/* A member record. A callable, an operator and an indexer carry their
|
|
* parameter types; every other member a '-'. */
|
|
static void cs_emit_member(cs_scan_t *s, const cs_member_rec_t *m, TSNode tparams, TSNode params) {
|
|
if (!m->name || m->type < 0) {
|
|
return;
|
|
}
|
|
sb_puts(&s->sb, "M\t");
|
|
sb_putu(&s->sb, m->line);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putc(&s->sb, m->kind);
|
|
if (m->is_static) {
|
|
sb_putc(&s->sb, 's');
|
|
}
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putc(&s->sb, m->explicit_impl ? '1' : '0');
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putu(&s->sb, (uint32_t)m->type);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_puts(&s->sb, m->name);
|
|
sb_putc(&s->sb, '\t');
|
|
cs_put_tparams(s, tparams);
|
|
sb_putc(&s->sb, '\t');
|
|
if (m->kind == 'c' || m->kind == 'o' || m->kind == 'x') {
|
|
cs_put_sig(s, params);
|
|
} else {
|
|
sb_putc(&s->sb, '-');
|
|
}
|
|
sb_putc(&s->sb, '\n');
|
|
}
|
|
|
|
static void cs_emit_using(cs_scan_t *s, TSNode u, int region) {
|
|
bool is_global = false;
|
|
bool is_static = false;
|
|
bool is_alias = false;
|
|
cs_kids_t k = cs_kids(u);
|
|
TSNode ch;
|
|
while (cs_kids_next(&k, &ch)) {
|
|
if (ts_node_is_named(ch)) {
|
|
continue;
|
|
}
|
|
const char *t = ts_node_type(ch);
|
|
if (strcmp(t, "global") == 0) {
|
|
is_global = true;
|
|
} else if (strcmp(t, "static") == 0) {
|
|
is_static = true;
|
|
} else if (strcmp(t, "=") == 0) {
|
|
is_alias = true;
|
|
}
|
|
}
|
|
cs_kids_end(&k);
|
|
TSNode alias = is_alias ? cs_field(u, "name") : (TSNode){0};
|
|
TSNode target = {0};
|
|
k = cs_kids(u);
|
|
while (cs_kids_next_named(&k, &ch)) {
|
|
if (is_alias && ts_node_eq(ch, alias)) {
|
|
continue;
|
|
}
|
|
if (cs_kind_is(ch, "comment")) {
|
|
continue;
|
|
}
|
|
target = ch;
|
|
}
|
|
cs_kids_end(&k);
|
|
if (ts_node_is_null(target)) {
|
|
return;
|
|
}
|
|
/* what the directive brings in, and whom it serves: `global using` (of a
|
|
* namespace, of a type's static members, of an alias alike) is in scope in
|
|
* every file of the project */
|
|
sb_puts(&s->sb, "U\t");
|
|
sb_putu(&s->sb, (uint32_t)region);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putc(&s->sb, is_alias ? 'a' : (is_static ? 's' : 'n'));
|
|
if (is_global) {
|
|
sb_putc(&s->sb, 'g');
|
|
}
|
|
sb_putc(&s->sb, '\t');
|
|
if (is_alias && !ts_node_is_null(alias)) {
|
|
cs_put_text_nows(s, alias);
|
|
} else {
|
|
sb_putc(&s->sb, '-');
|
|
}
|
|
sb_putc(&s->sb, '\t');
|
|
cs_put_text_nows(s, target);
|
|
sb_putc(&s->sb, '\n');
|
|
}
|
|
|
|
/* Lines [from, to] hold declarations that could not be placed. */
|
|
static void cs_emit_unplaced(cs_scan_t *s, uint32_t from, uint32_t to) {
|
|
sb_puts(&s->sb, "X\t");
|
|
sb_putu(&s->sb, from);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putu(&s->sb, to);
|
|
sb_putc(&s->sb, '\n');
|
|
}
|
|
|
|
/* A namespace region record: the name as its declaration writes it (`A.B`),
|
|
* under the region `parent`. Returns the new region id. */
|
|
static int cs_emit_region(cs_scan_t *s, int parent, uint32_t start, uint32_t end,
|
|
const char *name) {
|
|
int id = s->next_region++;
|
|
sb_puts(&s->sb, "R\t");
|
|
sb_putu(&s->sb, (uint32_t)id);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putu(&s->sb, (uint32_t)parent);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putu(&s->sb, start);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putu(&s->sb, end);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_puts(&s->sb, name);
|
|
sb_putc(&s->sb, '\n');
|
|
return id;
|
|
}
|
|
|
|
/* A type record, the type's ordinal being `ord`. Its outer type is named by
|
|
* that type's ordinal, never by a path: a record's size does not grow with
|
|
* the nesting. */
|
|
static void cs_emit_type(cs_scan_t *s, const cs_item_t *it, int region, uint32_t end_line,
|
|
int outer, const char *name, bool incomplete, int ord) {
|
|
sb_puts(&s->sb, "T\t");
|
|
sb_putu(&s->sb, (uint32_t)region);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putu(&s->sb, it->line);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putu(&s->sb, end_line);
|
|
sb_putc(&s->sb, '\t');
|
|
sb_putc(&s->sb, it->kind);
|
|
if (it->partial) {
|
|
sb_putc(&s->sb, 'p');
|
|
}
|
|
if (incomplete) {
|
|
sb_putc(&s->sb, '!');
|
|
}
|
|
sb_putc(&s->sb, '\t');
|
|
if (outer >= 0) {
|
|
sb_putu(&s->sb, (uint32_t)outer);
|
|
} else {
|
|
sb_putc(&s->sb, '-');
|
|
}
|
|
sb_putc(&s->sb, '\t');
|
|
sb_puts(&s->sb, name);
|
|
sb_putc(&s->sb, '\t');
|
|
if (it->from_text) {
|
|
/* its header did not parse: the bases are unknown, which the "?"
|
|
* records as a hierarchy that cannot be followed */
|
|
sb_puts(&s->sb, it->text_tparams);
|
|
sb_puts(&s->sb, it->kind == 'e' ? "\t\n" : "\t?\n");
|
|
return;
|
|
}
|
|
cs_put_tparams(s, it->tparams);
|
|
sb_putc(&s->sb, '\t');
|
|
cs_put_bases(s, it->node, it->kind);
|
|
sb_putc(&s->sb, '\n');
|
|
/* A parameter list on the type itself is its primary constructor
|
|
* (`record R(int X)`, `class C(int x)`): a constructor the graph has no
|
|
* node for. A record's parameters are properties as well. */
|
|
bool record = it->kind == 'r' || it->kind == 't';
|
|
if (!(record || it->kind == 'c' || it->kind == 's')) {
|
|
return;
|
|
}
|
|
TSNode pl = cs_child_of_kind(it->node, "parameter_list");
|
|
if (ts_node_is_null(pl)) {
|
|
return;
|
|
}
|
|
TSNode null_node = {0};
|
|
cs_member_rec_t ctor = {.line = it->line, .kind = 'c', .type = ord, .name = name};
|
|
cs_emit_member(s, &ctor, null_node, pl);
|
|
cs_kids_t params = cs_kids(pl);
|
|
TSNode p;
|
|
while (record && cs_kids_next_named(¶ms, &p)) {
|
|
if (cs_kind_is(p, "parameter")) {
|
|
cs_member_rec_t prop = {.line = cs_line(p),
|
|
.kind = 'p',
|
|
.type = ord,
|
|
.name = cs_name_dup(s, cs_field(p, "name"))};
|
|
cs_emit_member(s, &prop, null_node, null_node);
|
|
}
|
|
}
|
|
cs_kids_end(¶ms);
|
|
}
|
|
|
|
/* An open block while the items are placed. */
|
|
typedef struct {
|
|
int item; /* tree_idx of its declaration; CBM_NOT_FOUND for the file itself */
|
|
uint32_t end; /* first byte after it */
|
|
int inner_depth; /* brace depth of what it holds (files with parse errors) */
|
|
int region; /* namespace region in effect inside */
|
|
int ns_segments; /* segments of the namespace in effect inside */
|
|
int type; /* a type's block: the ordinal of its T record (CBM_NOT_FOUND: none) */
|
|
int type_depth; /* types that enclose what it holds */
|
|
bool is_type;
|
|
bool bad; /* its own place or name is unknown: so is everything inside */
|
|
} cs_frame_t;
|
|
|
|
/* The open blocks. A block is pushed only on a placed one, and a placed
|
|
* block adds a type level or at least one namespace segment, so the two
|
|
* nesting limits bound the stack: the root, the placed blocks, and one
|
|
* unplaced block on top. */
|
|
enum { CS_FRAMES = CS_MAX_TYPE_NEST + CS_MAX_NS_SEGMENTS + PAIR_LEN };
|
|
|
|
typedef struct {
|
|
cs_frame_t frames[CS_FRAMES];
|
|
int sp;
|
|
} cs_stack_t;
|
|
|
|
static void cs_frame_push(cs_scan_t *s, cs_stack_t *st, cs_frame_t f) {
|
|
if (st->sp >= CS_FRAMES) {
|
|
s->failed = true; /* cannot happen (see CS_FRAMES); no scope rather than a wrong one */
|
|
return;
|
|
}
|
|
st->frames[st->sp++] = f;
|
|
}
|
|
|
|
/* Segments of a dotted name. */
|
|
static int cs_segments(const char *name) {
|
|
int n = SKIP_ONE;
|
|
for (const char *p = name; *p; p++) {
|
|
n += *p == '.';
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* What follows a namespace's name in a file with parse errors: the brace
|
|
* that opens its block (returned), or the `;` of a file-scoped namespace
|
|
* (*file_scoped). Recovery reports a block namespace it could not parse as a
|
|
* file-scoped one whose content is an error node; the text says which it is.
|
|
* Anything else -- `namespace A` / `#else` / `namespace B` / `#endif` / `{`
|
|
* -- is a namespace this scan cannot name: CBM_NOT_FOUND, not file-scoped. */
|
|
static int cs_namespace_brace(const cs_scan_t *s, TSNode name, bool *file_scoped) {
|
|
const char *src = s->ctx->source;
|
|
uint32_t after = cs_skip_space(src, ts_node_end_byte(name), s->root_end_byte);
|
|
*file_scoped = after < s->root_end_byte && src[after] == ';';
|
|
return cs_open_brace_at(s, after);
|
|
}
|
|
|
|
static void cs_place_namespace(cs_scan_t *s, cs_stack_t *st, const cs_item_t *it, bool trusted) {
|
|
const cs_frame_t top = st->frames[st->sp - SKIP_ONE];
|
|
if (top.bad) {
|
|
return; /* inside a block that is not placed: nothing is, and nothing nests */
|
|
}
|
|
const char *name = it->from_text ? it->text_name : NULL;
|
|
if (!it->from_text && !ts_node_is_null(it->name)) {
|
|
name = cs_namespace_name_dup(s, cs_namespace_name_start(s, it->node),
|
|
ts_node_end_byte(it->name));
|
|
}
|
|
int brace = it->brace;
|
|
bool file_scoped = it->tag == 'F';
|
|
if (!it->from_text && s->lexical) {
|
|
/* the text decides what kind of namespace declaration this is */
|
|
file_scoped = false;
|
|
brace = ts_node_is_null(it->name) ? CBM_NOT_FOUND
|
|
: cs_namespace_brace(s, it->name, &file_scoped);
|
|
}
|
|
/* a file-scoped namespace is the first declaration of its file */
|
|
bool ok = trusted && !top.is_type && name && (!file_scoped || st->sp == SKIP_ONE);
|
|
uint32_t end = s->root_end_byte;
|
|
uint32_t end_line = s->root_end_line;
|
|
int inner = top.inner_depth;
|
|
if (!file_scoped) {
|
|
bool known = false;
|
|
if (brace >= 0) {
|
|
known = cs_brace_extent(s, brace, &end, &end_line, &inner);
|
|
} else if (!s->lexical && !it->from_text) {
|
|
end = ts_node_end_byte(it->node);
|
|
end_line = cs_end_line(it->node);
|
|
known = true;
|
|
}
|
|
if (!known) {
|
|
ok = false;
|
|
end = UINT32_MAX; /* where it ends is unknown: nothing after it is placed */
|
|
}
|
|
}
|
|
/* a namespace nested past the limit is not placed */
|
|
int segments = name ? top.ns_segments + cs_segments(name) : 0;
|
|
ok = ok && segments <= CS_MAX_NS_SEGMENTS;
|
|
if (!ok && it->start < s->untrusted) {
|
|
cs_emit_unplaced(s, it->line, end == UINT32_MAX ? s->root_end_line : end_line);
|
|
}
|
|
int region = ok ? cs_emit_region(s, top.region, it->line, end_line, name) : top.region;
|
|
cs_frame_push(s, st,
|
|
(cs_frame_t){.item = it->tree_idx,
|
|
.end = end,
|
|
.inner_depth = inner,
|
|
.region = region,
|
|
.ns_segments = ok ? segments : top.ns_segments,
|
|
.type = CBM_NOT_FOUND,
|
|
.type_depth = 0,
|
|
.is_type = false,
|
|
.bad = !ok});
|
|
}
|
|
|
|
static void cs_put_quarantine(cs_scan_t *s, const char *name) {
|
|
sb_puts(&s->sb, "Q\t");
|
|
sb_puts(&s->sb, name);
|
|
sb_putc(&s->sb, '\n');
|
|
}
|
|
|
|
static void cs_place_type(cs_scan_t *s, cs_stack_t *st, const cs_item_t *it, bool trusted) {
|
|
const cs_frame_t top = st->frames[st->sp - SKIP_ONE];
|
|
const char *name = it->from_text ? it->text_name : cs_name_dup(s, it->name);
|
|
if (top.bad) {
|
|
/* declared inside a block that is not placed: its name must not
|
|
* resolve to anything else; nothing nests under it */
|
|
if (name) {
|
|
cs_put_quarantine(s, name);
|
|
}
|
|
return;
|
|
}
|
|
bool block = it->brace >= 0;
|
|
uint32_t end = it->start;
|
|
uint32_t end_line = it->line;
|
|
int inner = 0;
|
|
bool paired = true;
|
|
bool whole = false; /* the tree's extent is the block's extent */
|
|
if (it->from_text) {
|
|
paired = !block || cs_brace_extent(s, it->brace, &end, &end_line, &inner);
|
|
} else {
|
|
TSNode body = it->kind == 'd' ? (TSNode){0} : cs_field(it->node, "body");
|
|
block = !ts_node_is_null(body);
|
|
end = ts_node_end_byte(it->node);
|
|
end_line = cs_end_line(it->node);
|
|
whole = true;
|
|
if (block && s->lexical) {
|
|
uint32_t node_end = end;
|
|
paired = cs_brace_extent(s, cs_open_brace_at(s, ts_node_start_byte(body)), &end,
|
|
&end_line, &inner);
|
|
whole = paired && end == node_end;
|
|
}
|
|
}
|
|
/* a type nested past the limit is not placed */
|
|
bool ok = trusted && paired && name && top.type_depth < CS_MAX_TYPE_NEST;
|
|
int ord = CBM_NOT_FOUND;
|
|
if (ok) {
|
|
ord = s->types_out++;
|
|
cs_emit_type(s, it, top.region, end_line, top.type, name, it->broken || !whole, ord);
|
|
} else if (name) {
|
|
/* declared, but where is unknown: its name must not resolve to
|
|
* anything else either */
|
|
cs_put_quarantine(s, name);
|
|
}
|
|
if (!ok && block && it->start < s->untrusted) {
|
|
cs_emit_unplaced(s, it->line, paired ? end_line : s->root_end_line);
|
|
}
|
|
if (block) {
|
|
cs_frame_push(s, st,
|
|
(cs_frame_t){.item = it->tree_idx,
|
|
.end = paired ? end : UINT32_MAX,
|
|
.inner_depth = inner,
|
|
.region = top.region,
|
|
.ns_segments = top.ns_segments,
|
|
.type = ord,
|
|
.type_depth = top.type_depth + SKIP_ONE,
|
|
.is_type = true,
|
|
.bad = !ok});
|
|
}
|
|
}
|
|
|
|
/* Place every collected declaration in the block that holds it and write
|
|
* its record. */
|
|
static void cs_emit_items(cs_scan_t *s) {
|
|
cs_stack_t *st = (cs_stack_t *)cs_tmp_alloc(s, sizeof(*st));
|
|
if (!st) {
|
|
return;
|
|
}
|
|
st->sp = 0;
|
|
st->frames[st->sp++] = (cs_frame_t){.item = CBM_NOT_FOUND,
|
|
.end = UINT32_MAX,
|
|
.inner_depth = 0,
|
|
.region = 0,
|
|
.ns_segments = 0,
|
|
.type = CBM_NOT_FOUND,
|
|
.type_depth = 0,
|
|
.is_type = false,
|
|
.bad = false};
|
|
for (int i = 0; i < s->nitems && !s->failed && !s->sb.failed; i++) {
|
|
const cs_item_t *it = &s->items[i];
|
|
/* leave the blocks that ended; with parse errors also those the
|
|
* braces say this item is not in (an #else branch re-opening the
|
|
* block its #if branch opened) */
|
|
int depth = s->lexical ? cs_depth_at(s, it->start) : 0;
|
|
while (st->sp > SKIP_ONE &&
|
|
(st->frames[st->sp - SKIP_ONE].end <= it->start ||
|
|
(s->lexical && st->frames[st->sp - SKIP_ONE].inner_depth > depth))) {
|
|
st->sp--;
|
|
}
|
|
const cs_frame_t top = st->frames[st->sp - SKIP_ONE];
|
|
bool trusted = !top.bad;
|
|
if (trusted && s->lexical) {
|
|
trusted = it->start < s->untrusted && depth == top.inner_depth;
|
|
}
|
|
switch (it->tag) {
|
|
case 'U':
|
|
if (trusted && !top.is_type) {
|
|
cs_emit_using(s, it->node, top.region);
|
|
}
|
|
break;
|
|
case 'N':
|
|
case 'F':
|
|
cs_place_namespace(s, st, it, trusted);
|
|
break;
|
|
case 'T':
|
|
cs_place_type(s, st, it, trusted);
|
|
break;
|
|
case 'M':
|
|
/* the braces decide whose member it is when the tree has errors */
|
|
if (trusted && top.is_type && (s->lexical || it->owner == top.item)) {
|
|
cs_member_rec_t rec = {.line = cs_line(it->node),
|
|
.kind = it->kind,
|
|
.explicit_impl = it->explicit_impl,
|
|
.is_static = it->is_static,
|
|
.type = top.type,
|
|
.name = it->text_name ? it->text_name
|
|
: cs_name_dup(s, it->name)};
|
|
cs_emit_member(s, &rec, it->tparams, it->params);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Field positions (0-based, tag included) that hold line numbers. */
|
|
static bool cs_line_field(char tag, int field) {
|
|
switch (tag) {
|
|
case 'R':
|
|
return field == 3 || field == 4;
|
|
case 'T':
|
|
return field == 2 || field == 3;
|
|
case 'M':
|
|
return field == 1;
|
|
case 'X':
|
|
return field == 1 || field == 2;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
char *cbm_doclink_cs_portable_scope(const char *scope) {
|
|
size_t n = strlen(scope);
|
|
char *out = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, n + SKIP_ONE);
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
size_t w = 0;
|
|
const char *p = scope;
|
|
while (*p) {
|
|
const char *nl = strchr(p, '\n');
|
|
size_t len = nl ? (size_t)(nl - p) : strlen(p);
|
|
char tag = p[0];
|
|
int field = 0;
|
|
size_t i = 0;
|
|
while (i < len) {
|
|
size_t fend = i;
|
|
while (fend < len && p[fend] != '\t') {
|
|
fend++;
|
|
}
|
|
/* a line number becomes 0; an empty field stays empty, so the
|
|
* copy is never longer than the scope it is made from */
|
|
if (cs_line_field(tag, field) && fend > i) {
|
|
out[w++] = '0';
|
|
} else {
|
|
memcpy(out + w, p + i, fend - i);
|
|
w += fend - i;
|
|
}
|
|
i = fend;
|
|
if (i < len) {
|
|
out[w++] = '\t';
|
|
i++;
|
|
field++;
|
|
}
|
|
}
|
|
if (nl) {
|
|
out[w++] = '\n';
|
|
p = nl + SKIP_ONE;
|
|
} else {
|
|
p += len;
|
|
}
|
|
}
|
|
out[w] = '\0';
|
|
return out;
|
|
}
|
|
|
|
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
|
static char cs_test_spoiled_path[CBM_SZ_512];
|
|
|
|
void cbm_doclink_cs_test_spoil_scope(const char *rel_path) {
|
|
snprintf(cs_test_spoiled_path, sizeof(cs_test_spoiled_path), "%s", rel_path ? rel_path : "");
|
|
}
|
|
#endif
|
|
|
|
const char *cbm_doclink_cs_scan_scope(CBMExtractCtx *ctx) {
|
|
if (ts_node_is_null(ctx->root)) {
|
|
return NULL;
|
|
}
|
|
cs_scan_t s = {.ctx = ctx,
|
|
.tmp = ctx->scratch ? ctx->scratch : ctx->arena,
|
|
.sb = {.a = ctx->arena},
|
|
.top = CBM_NOT_FOUND,
|
|
.untrusted = UINT32_MAX,
|
|
.next_region = SKIP_ONE};
|
|
s.root_end_byte = ctx->source_len > 0 ? (uint32_t)ctx->source_len : 0;
|
|
s.root_end_line = cs_end_line(ctx->root);
|
|
s.lexical = ts_node_has_error(ctx->root);
|
|
if (s.lexical) {
|
|
cs_scan_tokens(&s);
|
|
}
|
|
/* the root itself is an error node when nothing of the file parsed */
|
|
cs_collect(&s, ctx->root, cs_kind_is(ctx->root, "ERROR") ? CS_OWNER_LEXICAL : CS_OWNER_NONE);
|
|
cs_add_text_items(&s);
|
|
sb_puts(&s.sb, CBM_DOCLINK_CS_SCOPE_TAG "\n");
|
|
cs_emit_items(&s);
|
|
if (s.untrusted != UINT32_MAX) {
|
|
cs_emit_unplaced(&s, s.untrusted_row + TS_LINE_OFFSET, s.root_end_line);
|
|
}
|
|
cs_cost_publish(&s);
|
|
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
|
if (cbm_doclink_test_fail_alloc(CBM_DOCLINK_ALLOC_SCOPE)) {
|
|
s.failed = true;
|
|
}
|
|
if (cs_test_spoiled_path[0] && ctx->rel_path &&
|
|
strcmp(ctx->rel_path, cs_test_spoiled_path) == 0) {
|
|
sb_puts(&s.sb, "Z\tspoiled\n");
|
|
}
|
|
#endif
|
|
if (s.failed || s.sb.failed || !s.sb.buf) {
|
|
/* memory ran out: no scope would read as a file that declares
|
|
* nothing, so the layer is told */
|
|
if (ctx->result) {
|
|
ctx->result->doc_links.failed = true;
|
|
}
|
|
return NULL;
|
|
}
|
|
return s.sb.buf;
|
|
}
|
|
|
|
/* ── MSBuild project files ───────────────────────────────────────────
|
|
*
|
|
* A C# project's global usings come from its MSBuild files: the project
|
|
* file, the Directory.Build.props / .targets above it, and what those import
|
|
* (R1). The resolver must not open any of them: what it would read is a path
|
|
* the index never looked at (a symbolic link out of the repository, a named
|
|
* pipe), and it would read it again on every run. So a project file has a
|
|
* scope blob like a source file, and the resolver evaluates blobs.
|
|
*
|
|
* Project blob: the tag line "cs1", then one record per line. A field is
|
|
* empty for an attribute that is not there, else '=' and its text, XML
|
|
* entities decoded, with \\ \t \n \r escaped.
|
|
* P sdk state the first record. state '-': read;
|
|
* '!': a project file that could not be
|
|
* read; '>': one larger than a project
|
|
* file is (CSX_MAX_PROJECT_BYTES), not
|
|
* read. Nothing follows the last two
|
|
* I group-cond cond project sdk <Import>; old blobs repeat a group's
|
|
* condition in the first field
|
|
* B cond start of an <ImportGroup>, condition once
|
|
* J (empty) cond project sdk a child import; shares the B condition
|
|
* E end of the import group
|
|
* G cond <PropertyGroup>; its properties follow
|
|
* V cond name value a property; value '?' when it is not
|
|
* plain text
|
|
* H cond <ItemGroup> that has <Using> items;
|
|
* they follow
|
|
* N cond include remove static alias <Using>
|
|
* K name a property set inside a construct that
|
|
* is not evaluated (<Choose>)
|
|
* Y a <Using> that is not evaluated (in a
|
|
* <Choose>, with Update, with metadata
|
|
* elements)
|
|
* C what another construct that is not evaluated
|
|
* Document order is evaluation order. Nothing else of the file is in the
|
|
* blob (targets, other items), so an edit there leaves it as it is. */
|
|
|
|
enum { CSX_EOF = 0, CSX_OPEN, CSX_EMPTY, CSX_CLOSE, CSX_TEXT, CSX_BAD };
|
|
|
|
/* The size past which a file is no project file to this reader. */
|
|
enum { CSX_MAX_PROJECT_BYTES = 1000000 };
|
|
|
|
enum {
|
|
CSX_A_CONDITION = 0,
|
|
CSX_A_PROJECT,
|
|
CSX_A_SDK,
|
|
CSX_A_INCLUDE,
|
|
CSX_A_REMOVE,
|
|
CSX_A_UPDATE,
|
|
CSX_A_STATIC,
|
|
CSX_A_ALIAS,
|
|
CSX_A_COUNT
|
|
};
|
|
|
|
typedef struct {
|
|
uint32_t s;
|
|
uint32_t e;
|
|
bool has;
|
|
} csx_span_t;
|
|
|
|
typedef struct {
|
|
int kind;
|
|
csx_span_t name; /* element name without a namespace prefix */
|
|
csx_span_t text; /* CSX_TEXT */
|
|
bool raw; /* CSX_TEXT of a CDATA section: no entities in it */
|
|
csx_span_t attr[CSX_A_COUNT];
|
|
} csx_tok_t;
|
|
|
|
typedef struct {
|
|
const char *src;
|
|
uint32_t n;
|
|
uint32_t i;
|
|
} csx_t;
|
|
|
|
/* Index of `lit` in src[from, limit), or `limit`. */
|
|
static uint32_t csx_find(const csx_t *x, uint32_t from, uint32_t limit, const char *lit) {
|
|
size_t ll = strlen(lit);
|
|
for (uint32_t k = from; k + ll <= limit; k++) {
|
|
if (x->src[k] == lit[0] && memcmp(x->src + k, lit, ll) == 0) {
|
|
return k;
|
|
}
|
|
}
|
|
return limit;
|
|
}
|
|
|
|
static bool csx_name_char(unsigned char c) {
|
|
return isalnum(c) || c == '_' || c == ':' || c == '.' || c == '-' || c >= 0x80;
|
|
}
|
|
|
|
static csx_span_t csx_local(const csx_t *x, uint32_t s, uint32_t e) {
|
|
for (uint32_t k = e; k > s; k--) {
|
|
if (x->src[k - SKIP_ONE] == ':') {
|
|
s = k;
|
|
break;
|
|
}
|
|
}
|
|
return (csx_span_t){.s = s, .e = e, .has = true};
|
|
}
|
|
|
|
static bool csx_is(const csx_t *x, csx_span_t v, const char *word) {
|
|
return v.has && strlen(word) == v.e - v.s && memcmp(x->src + v.s, word, v.e - v.s) == 0;
|
|
}
|
|
|
|
/* The attributes the blob keeps. MSBuild reads attribute names without
|
|
* regard to case. */
|
|
static int csx_attr_index(const char *name, size_t len) {
|
|
static const char *const names[CSX_A_COUNT] = {"condition", "project", "sdk", "include",
|
|
"remove", "update", "static", "alias"};
|
|
for (int a = 0; a < CSX_A_COUNT; a++) {
|
|
if (strlen(names[a]) != len) {
|
|
continue;
|
|
}
|
|
size_t k = 0;
|
|
while (k < len && tolower((unsigned char)name[k]) == names[a][k]) {
|
|
k++;
|
|
}
|
|
if (k == len) {
|
|
return a;
|
|
}
|
|
}
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
|
|
/* Past markup that is no element at src[x->i] ('<' stands there): a comment,
|
|
* a processing instruction, a declaration. false when it does not end. A
|
|
* CDATA section is text: *cdata, and the cursor stays. */
|
|
static bool csx_skip_markup(csx_t *x, bool *skipped, bool *cdata) {
|
|
const char *s = x->src;
|
|
uint32_t rest = x->n - x->i;
|
|
*skipped = true;
|
|
*cdata = false;
|
|
if (rest >= 4 && memcmp(s + x->i, "<!--", 4) == 0) {
|
|
uint32_t e = csx_find(x, x->i + 4, x->n, "-->");
|
|
x->i = e + 3;
|
|
return e < x->n;
|
|
}
|
|
if (rest >= 9 && memcmp(s + x->i, "<![CDATA[", 9) == 0) {
|
|
*cdata = true;
|
|
return true;
|
|
}
|
|
if (rest >= PAIR_LEN && s[x->i + SKIP_ONE] == '?') {
|
|
uint32_t e = csx_find(x, x->i + PAIR_LEN, x->n, "?>");
|
|
x->i = e + PAIR_LEN;
|
|
return e < x->n;
|
|
}
|
|
if (rest >= PAIR_LEN && s[x->i + SKIP_ONE] == '!') {
|
|
/* <!DOCTYPE ...>, with an internal subset up to "]>" */
|
|
uint32_t e = csx_find(x, x->i + PAIR_LEN, x->n, ">");
|
|
uint32_t sub = csx_find(x, x->i + PAIR_LEN, e, "[");
|
|
if (sub < e) {
|
|
uint32_t close = csx_find(x, sub, x->n, "]>");
|
|
e = close < x->n ? close + SKIP_ONE : x->n;
|
|
}
|
|
x->i = e + SKIP_ONE;
|
|
return e < x->n;
|
|
}
|
|
*skipped = false;
|
|
return true;
|
|
}
|
|
|
|
/* The attributes of a start tag from src[p]; the tag's kind (CSX_OPEN,
|
|
* CSX_EMPTY) or CSX_BAD. Moves the cursor past the tag. */
|
|
static int csx_attributes(csx_t *x, uint32_t p, csx_tok_t *t) {
|
|
const char *s = x->src;
|
|
for (;;) {
|
|
while (p < x->n && isspace((unsigned char)s[p])) {
|
|
p++;
|
|
}
|
|
if (p >= x->n) {
|
|
return CSX_BAD;
|
|
}
|
|
if (s[p] == '>') {
|
|
x->i = p + SKIP_ONE;
|
|
return CSX_OPEN;
|
|
}
|
|
if (s[p] == '/' && p + SKIP_ONE < x->n && s[p + SKIP_ONE] == '>') {
|
|
x->i = p + PAIR_LEN;
|
|
return CSX_EMPTY;
|
|
}
|
|
uint32_t as = p;
|
|
while (p < x->n && csx_name_char((unsigned char)s[p])) {
|
|
p++;
|
|
}
|
|
if (p == as) {
|
|
return CSX_BAD;
|
|
}
|
|
csx_span_t an = csx_local(x, as, p);
|
|
while (p < x->n && isspace((unsigned char)s[p])) {
|
|
p++;
|
|
}
|
|
if (p >= x->n || s[p] != '=') {
|
|
return CSX_BAD;
|
|
}
|
|
p++;
|
|
while (p < x->n && isspace((unsigned char)s[p])) {
|
|
p++;
|
|
}
|
|
if (p >= x->n || (s[p] != '"' && s[p] != '\'')) {
|
|
return CSX_BAD;
|
|
}
|
|
char quote = s[p++];
|
|
uint32_t vs = p;
|
|
while (p < x->n && s[p] != quote) {
|
|
p++;
|
|
}
|
|
if (p >= x->n) {
|
|
return CSX_BAD;
|
|
}
|
|
int ai = csx_attr_index(s + an.s, an.e - an.s);
|
|
if (ai >= 0) {
|
|
t->attr[ai] = (csx_span_t){.s = vs, .e = p, .has = true};
|
|
}
|
|
p++;
|
|
}
|
|
}
|
|
|
|
/* The next token of the document. Every byte is passed once. */
|
|
static void csx_next(csx_t *x, csx_tok_t *t) {
|
|
memset(t, 0, sizeof(*t));
|
|
const char *s = x->src;
|
|
for (;;) {
|
|
if (x->i >= x->n) {
|
|
t->kind = CSX_EOF;
|
|
return;
|
|
}
|
|
if (s[x->i] != '<') {
|
|
uint32_t a = x->i;
|
|
while (x->i < x->n && s[x->i] != '<') {
|
|
x->i++;
|
|
}
|
|
t->kind = CSX_TEXT;
|
|
t->text = (csx_span_t){.s = a, .e = x->i, .has = true};
|
|
return;
|
|
}
|
|
bool skipped = false;
|
|
bool cdata = false;
|
|
if (!csx_skip_markup(x, &skipped, &cdata)) {
|
|
t->kind = CSX_BAD;
|
|
return;
|
|
}
|
|
if (cdata) {
|
|
uint32_t e = csx_find(x, x->i + 9, x->n, "]]>");
|
|
if (e >= x->n) {
|
|
t->kind = CSX_BAD;
|
|
return;
|
|
}
|
|
t->kind = CSX_TEXT;
|
|
t->raw = true;
|
|
t->text = (csx_span_t){.s = x->i + 9, .e = e, .has = true};
|
|
x->i = e + 3;
|
|
return;
|
|
}
|
|
if (!skipped) {
|
|
break;
|
|
}
|
|
}
|
|
uint32_t p = x->i + SKIP_ONE;
|
|
bool close = p < x->n && s[p] == '/';
|
|
if (close) {
|
|
p++;
|
|
}
|
|
uint32_t ns = p;
|
|
while (p < x->n && csx_name_char((unsigned char)s[p])) {
|
|
p++;
|
|
}
|
|
if (p == ns) {
|
|
t->kind = CSX_BAD;
|
|
return;
|
|
}
|
|
t->name = csx_local(x, ns, p);
|
|
if (!close) {
|
|
t->kind = csx_attributes(x, p, t);
|
|
return;
|
|
}
|
|
while (p < x->n && isspace((unsigned char)s[p])) {
|
|
p++;
|
|
}
|
|
if (p >= x->n || s[p] != '>') {
|
|
t->kind = CSX_BAD;
|
|
return;
|
|
}
|
|
x->i = p + SKIP_ONE;
|
|
t->kind = CSX_CLOSE;
|
|
}
|
|
|
|
/* One character of a field: escaped where it would break the line format. A
|
|
* control character has no place in XML text; it is written as a space. */
|
|
static void csx_put_char(cs_sb_t *sb, unsigned char c) {
|
|
if (c == '\\') {
|
|
sb_puts(sb, "\\\\");
|
|
} else if (c == '\t') {
|
|
sb_puts(sb, "\\t");
|
|
} else if (c == '\n') {
|
|
sb_puts(sb, "\\n");
|
|
} else if (c == '\r') {
|
|
sb_puts(sb, "\\r");
|
|
} else {
|
|
sb_putc(sb, c < 0x20 ? ' ' : (char)c);
|
|
}
|
|
}
|
|
|
|
/* A code point as UTF-8. A surrogate (a numeric reference may name one) is
|
|
* no character: it is written as U+FFFD. */
|
|
static void csx_put_codepoint(cs_sb_t *sb, uint32_t cp) {
|
|
if (cp >= 0xD800 && cp <= 0xDFFF) {
|
|
sb_puts(sb, CS_REPLACEMENT);
|
|
} else if (cp < 0x80) {
|
|
csx_put_char(sb, (unsigned char)cp);
|
|
} else if (cp < 0x800) {
|
|
sb_putc(sb, (char)(0xC0 | (cp >> 6)));
|
|
sb_putc(sb, (char)(0x80 | (cp & 0x3F)));
|
|
} else if (cp < 0x10000) {
|
|
sb_putc(sb, (char)(0xE0 | (cp >> 12)));
|
|
sb_putc(sb, (char)(0x80 | ((cp >> 6) & 0x3F)));
|
|
sb_putc(sb, (char)(0x80 | (cp & 0x3F)));
|
|
} else {
|
|
sb_putc(sb, (char)(0xF0 | ((cp >> 18) & 0x07)));
|
|
sb_putc(sb, (char)(0x80 | ((cp >> 12) & 0x3F)));
|
|
sb_putc(sb, (char)(0x80 | ((cp >> 6) & 0x3F)));
|
|
sb_putc(sb, (char)(0x80 | (cp & 0x3F)));
|
|
}
|
|
}
|
|
|
|
/* The entity at src[i] ('&' stands there): its code point and the index past
|
|
* it; 0 when there is none. */
|
|
static uint32_t csx_entity(const csx_t *x, uint32_t i, uint32_t end, uint32_t *cp) {
|
|
static const struct {
|
|
const char *ent;
|
|
char ch;
|
|
} ents[] = {{"<", '<'}, {">", '>'}, {"&", '&'}, {""", '"'}, {"'", '\''}};
|
|
const char *s = x->src;
|
|
for (size_t e = 0; e < sizeof(ents) / sizeof(ents[0]); e++) {
|
|
size_t el = strlen(ents[e].ent);
|
|
if (i + el <= end && memcmp(s + i, ents[e].ent, el) == 0) {
|
|
*cp = (unsigned char)ents[e].ch;
|
|
return i + (uint32_t)el;
|
|
}
|
|
}
|
|
if (i + PAIR_LEN < end && s[i + SKIP_ONE] == '#') {
|
|
bool hex = s[i + PAIR_LEN] == 'x' || s[i + PAIR_LEN] == 'X';
|
|
uint32_t k = i + PAIR_LEN + (hex ? SKIP_ONE : 0);
|
|
uint32_t v = 0;
|
|
uint32_t digits = 0;
|
|
while (k < end && digits < CBM_SZ_8 &&
|
|
(hex ? isxdigit((unsigned char)s[k]) : isdigit((unsigned char)s[k]))) {
|
|
unsigned char d = (unsigned char)s[k];
|
|
uint32_t dv = isdigit(d) ? (uint32_t)(d - '0') : (uint32_t)(tolower(d) - 'a') + 10U;
|
|
v = (v * (hex ? 16U : 10U)) + dv;
|
|
k++;
|
|
digits++;
|
|
}
|
|
if (digits > 0 && k < end && s[k] == ';' && v > 0 && v <= 0x10FFFF) {
|
|
*cp = v;
|
|
return k + SKIP_ONE;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* The bytes src[i, end) of a project file that start at i into sb: a
|
|
* well-formed UTF-8 sequence as it stands, a byte that starts none as
|
|
* U+FFFD. Returns the index past what was taken. */
|
|
static uint32_t csx_put_utf8(cs_sb_t *sb, const char *src, uint32_t i, uint32_t end) {
|
|
size_t len = cs_utf8_len((const unsigned char *)src + i, (size_t)(end - i));
|
|
if (len == 0) {
|
|
sb_puts(sb, CS_REPLACEMENT);
|
|
return i + SKIP_ONE;
|
|
}
|
|
sb_putn(sb, src + i, len);
|
|
return i + (uint32_t)len;
|
|
}
|
|
|
|
/* The text of `v`, entities decoded (unless `raw`), escaped. */
|
|
static void csx_put_text(cs_sb_t *sb, const csx_t *x, csx_span_t v, bool raw) {
|
|
for (uint32_t i = v.s; i < v.e;) {
|
|
uint32_t cp = 0;
|
|
uint32_t past = (!raw && x->src[i] == '&') ? csx_entity(x, i, v.e, &cp) : 0;
|
|
if (past) {
|
|
csx_put_codepoint(sb, cp);
|
|
i = past;
|
|
} else if ((unsigned char)x->src[i] >= 0x80) {
|
|
i = csx_put_utf8(sb, x->src, i, v.e);
|
|
} else {
|
|
csx_put_char(sb, (unsigned char)x->src[i]);
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* An element's name into sb, each byte that starts no well-formed UTF-8
|
|
* sequence as U+FFFD (the tokenizer's names hold no separator). */
|
|
static void csx_put_name(cs_sb_t *sb, const csx_t *x, csx_span_t name) {
|
|
for (uint32_t i = name.s; i < name.e;) {
|
|
i = csx_put_utf8(sb, x->src, i, name.e);
|
|
}
|
|
}
|
|
|
|
/* A field: a tab, then nothing for an absent attribute, else '=' and its text. */
|
|
static void csx_put_field(cs_sb_t *sb, const csx_t *x, csx_span_t v) {
|
|
sb_putc(sb, '\t');
|
|
if (v.has) {
|
|
sb_putc(sb, '=');
|
|
csx_put_text(sb, x, v, false);
|
|
}
|
|
}
|
|
|
|
/* What a project file's scan keeps between tokens. */
|
|
typedef struct {
|
|
csx_t x;
|
|
cs_sb_t out;
|
|
cs_sb_t value; /* a property's text so far */
|
|
int depth; /* open elements */
|
|
char group; /* the child of <Project> the cursor is in: G H i c, or 0 */
|
|
csx_span_t gcond; /* its Condition */
|
|
bool h_written; /* the ItemGroup's H record is out */
|
|
bool i_written; /* the ImportGroup's B record is out */
|
|
bool prop_open; /* a property element is open ... */
|
|
bool prop_complex; /* ... and holds elements, not just text */
|
|
bool using_open; /* a <Using> with content is open ... */
|
|
bool using_complex; /* ... and holds metadata elements */
|
|
csx_tok_t pending; /* the open property's or <Using>'s start tag */
|
|
int choose_props; /* in a <Choose>: the depth of a <PropertyGroup>'s children, or -1 */
|
|
} csx_scan_t;
|
|
|
|
static void csx_put_using(csx_scan_t *p, const csx_tok_t *t) {
|
|
if (!p->h_written) {
|
|
sb_putc(&p->out, 'H');
|
|
csx_put_field(&p->out, &p->x, p->gcond);
|
|
sb_putc(&p->out, '\n');
|
|
p->h_written = true;
|
|
}
|
|
sb_putc(&p->out, 'N');
|
|
csx_put_field(&p->out, &p->x, t->attr[CSX_A_CONDITION]);
|
|
csx_put_field(&p->out, &p->x, t->attr[CSX_A_INCLUDE]);
|
|
csx_put_field(&p->out, &p->x, t->attr[CSX_A_REMOVE]);
|
|
csx_put_field(&p->out, &p->x, t->attr[CSX_A_STATIC]);
|
|
csx_put_field(&p->out, &p->x, t->attr[CSX_A_ALIAS]);
|
|
sb_putc(&p->out, '\n');
|
|
}
|
|
|
|
static void csx_put_import(csx_scan_t *p, const csx_tok_t *t, bool grouped) {
|
|
if (grouped && !p->i_written) {
|
|
sb_putc(&p->out, 'B');
|
|
csx_put_field(&p->out, &p->x, p->gcond);
|
|
sb_putc(&p->out, '\n');
|
|
p->i_written = true;
|
|
}
|
|
sb_putc(&p->out, grouped ? 'J' : 'I');
|
|
csx_put_field(&p->out, &p->x, (csx_span_t){0});
|
|
csx_put_field(&p->out, &p->x, t->attr[CSX_A_CONDITION]);
|
|
csx_put_field(&p->out, &p->x, t->attr[CSX_A_PROJECT]);
|
|
csx_put_field(&p->out, &p->x, t->attr[CSX_A_SDK]);
|
|
sb_putc(&p->out, '\n');
|
|
}
|
|
|
|
/* The open property ends: its record. */
|
|
static void csx_put_property(csx_scan_t *p) {
|
|
const csx_tok_t *t = &p->pending;
|
|
sb_putc(&p->out, 'V');
|
|
csx_put_field(&p->out, &p->x, t->attr[CSX_A_CONDITION]);
|
|
sb_putc(&p->out, '\t');
|
|
csx_put_name(&p->out, &p->x, t->name);
|
|
sb_putc(&p->out, '\t');
|
|
if (p->prop_complex) {
|
|
sb_putc(&p->out, '?');
|
|
} else {
|
|
sb_putc(&p->out, '=');
|
|
if (p->value.len > 0) {
|
|
sb_putn(&p->out, p->value.buf, p->value.len);
|
|
}
|
|
}
|
|
sb_putc(&p->out, '\n');
|
|
}
|
|
|
|
/* An element inside a <Choose>: what it could set is named, not evaluated. */
|
|
static void csx_choose_child(csx_scan_t *p, const csx_tok_t *t) {
|
|
if (p->choose_props >= 0 && p->depth == p->choose_props) {
|
|
sb_puts(&p->out, "K\t");
|
|
csx_put_name(&p->out, &p->x, t->name);
|
|
sb_putc(&p->out, '\n');
|
|
} else if (csx_is(&p->x, t->name, "PropertyGroup") && p->choose_props < 0 &&
|
|
t->kind == CSX_OPEN) {
|
|
p->choose_props = p->depth + SKIP_ONE;
|
|
} else if (csx_is(&p->x, t->name, "Using")) {
|
|
sb_puts(&p->out, "Y\n");
|
|
} else if (csx_is(&p->x, t->name, "Import")) {
|
|
sb_puts(&p->out, "C\tImport\n");
|
|
}
|
|
}
|
|
|
|
/* A child of <Project> starts. */
|
|
static void csx_project_child(csx_scan_t *p, const csx_tok_t *t) {
|
|
p->group = 0;
|
|
p->gcond = t->attr[CSX_A_CONDITION];
|
|
if (csx_is(&p->x, t->name, "Import")) {
|
|
csx_put_import(p, t, false);
|
|
} else if (csx_is(&p->x, t->name, "PropertyGroup")) {
|
|
sb_putc(&p->out, 'G');
|
|
csx_put_field(&p->out, &p->x, p->gcond);
|
|
sb_putc(&p->out, '\n');
|
|
p->group = 'G';
|
|
} else if (csx_is(&p->x, t->name, "ItemGroup")) {
|
|
p->group = 'H';
|
|
p->h_written = false;
|
|
} else if (csx_is(&p->x, t->name, "ImportGroup")) {
|
|
p->group = 'i';
|
|
p->i_written = false;
|
|
} else if (csx_is(&p->x, t->name, "Choose")) {
|
|
sb_puts(&p->out, "C\tChoose\n");
|
|
p->group = 'c';
|
|
p->choose_props = CBM_NOT_FOUND;
|
|
}
|
|
if (t->kind == CSX_EMPTY) {
|
|
p->group = 0;
|
|
}
|
|
}
|
|
|
|
/* A grandchild of <Project> starts. */
|
|
static void csx_group_child(csx_scan_t *p, const csx_tok_t *t) {
|
|
if (p->group == 'G') {
|
|
p->pending = *t;
|
|
p->value.len = 0;
|
|
p->prop_complex = false;
|
|
if (t->kind == CSX_EMPTY) {
|
|
csx_put_property(p);
|
|
} else {
|
|
p->prop_open = true;
|
|
}
|
|
} else if (p->group == 'H' && csx_is(&p->x, t->name, "Using")) {
|
|
if (t->attr[CSX_A_UPDATE].has) {
|
|
sb_puts(&p->out, "Y\n");
|
|
} else if (t->kind == CSX_EMPTY) {
|
|
csx_put_using(p, t);
|
|
} else {
|
|
p->pending = *t;
|
|
p->using_open = true;
|
|
p->using_complex = false;
|
|
}
|
|
} else if (p->group == 'i' && csx_is(&p->x, t->name, "Import")) {
|
|
csx_put_import(p, t, true);
|
|
}
|
|
}
|
|
|
|
/* A start tag below the root. */
|
|
static void csx_element(csx_scan_t *p, const csx_tok_t *t) {
|
|
if (p->depth == SKIP_ONE) {
|
|
csx_project_child(p, t);
|
|
} else if (p->group == 'c') {
|
|
csx_choose_child(p, t);
|
|
} else if (p->depth == PAIR_LEN) {
|
|
csx_group_child(p, t);
|
|
} else {
|
|
p->prop_complex = p->prop_complex || p->prop_open;
|
|
p->using_complex = p->using_complex || p->using_open;
|
|
}
|
|
if (t->kind == CSX_OPEN) {
|
|
p->depth++;
|
|
}
|
|
}
|
|
|
|
/* An end tag: `depth` is already the depth outside the element. */
|
|
static void csx_element_end(csx_scan_t *p) {
|
|
if (p->depth == PAIR_LEN && p->prop_open) {
|
|
csx_put_property(p);
|
|
p->prop_open = false;
|
|
} else if (p->depth == PAIR_LEN && p->using_open) {
|
|
if (p->using_complex) {
|
|
sb_puts(&p->out, "Y\n");
|
|
} else {
|
|
csx_put_using(p, &p->pending);
|
|
}
|
|
p->using_open = false;
|
|
}
|
|
if (p->depth == SKIP_ONE) {
|
|
if (p->group == 'i' && p->i_written) {
|
|
sb_puts(&p->out, "E\n");
|
|
}
|
|
p->group = 0;
|
|
}
|
|
if (p->group == 'c' && p->choose_props == p->depth + SKIP_ONE) {
|
|
p->choose_props = CBM_NOT_FOUND;
|
|
}
|
|
}
|
|
|
|
static bool csx_ci_suffix(const char *s, const char *sfx) {
|
|
size_t n = s ? strlen(s) : 0;
|
|
size_t sl = strlen(sfx);
|
|
if (n < sl) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < sl; i++) {
|
|
if (tolower((unsigned char)s[n - sl + i]) != sfx[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void cbm_doclink_cs_project_parse_doc(CBMExtractCtx *ctx, const CBMDefinition *def, const char *doc,
|
|
uint32_t doc_line) {
|
|
/* a project file's comments hold no references to code */
|
|
(void)ctx;
|
|
(void)def;
|
|
(void)doc;
|
|
(void)doc_line;
|
|
}
|
|
|
|
const char *cbm_doclink_cs_project_scan_scope(CBMExtractCtx *ctx) {
|
|
/* The gate is the file's name, before a byte of it is looked at: only a
|
|
* *.csproj, *.props or *.targets file can be an MSBuild project file of a
|
|
* C# project. Every other XML file -- there are many, and large ones --
|
|
* costs nothing here. */
|
|
bool named_project = csx_ci_suffix(ctx->rel_path, ".csproj");
|
|
if (!ctx->source || !(named_project || csx_ci_suffix(ctx->rel_path, ".props") ||
|
|
csx_ci_suffix(ctx->rel_path, ".targets"))) {
|
|
return NULL;
|
|
}
|
|
/* A project file past the size a project file has is not read either,
|
|
* and its blob says so: what it holds is unknown, not absent. */
|
|
if (ctx->source_len > CSX_MAX_PROJECT_BYTES) {
|
|
const char *blob = cbm_arena_strdup(ctx->arena, CBM_DOCLINK_CS_SCOPE_TAG "\nP\t\t>\n");
|
|
if (!blob && ctx->result) {
|
|
ctx->result->doc_links.failed = true; /* memory ran out */
|
|
}
|
|
return blob;
|
|
}
|
|
/* a *.csproj marks its directory as a C# project even when it cannot be
|
|
* read; a *.props or *.targets file has no blob only when it parses up to
|
|
* a root element that is not an MSBuild <Project> */
|
|
csx_scan_t p = {
|
|
.x = {.src = ctx->source, .n = ctx->source_len > 0 ? (uint32_t)ctx->source_len : 0},
|
|
.out = {.a = ctx->arena},
|
|
.value = {.a = ctx->scratch ? ctx->scratch : ctx->arena},
|
|
.choose_props = CBM_NOT_FOUND};
|
|
static const char bom[] = "\xEF\xBB\xBF";
|
|
if (p.x.n >= 3 && memcmp(p.x.src, bom, 3) == 0) {
|
|
p.x.i = 3;
|
|
}
|
|
bool is_project = false;
|
|
bool other_root = false; /* XML whose root element is not <Project> */
|
|
bool bad = false;
|
|
csx_tok_t t;
|
|
for (;;) {
|
|
csx_next(&p.x, &t);
|
|
if (t.kind == CSX_EOF || t.kind == CSX_BAD) {
|
|
bad = t.kind == CSX_BAD || p.depth != 0;
|
|
break;
|
|
}
|
|
if (t.kind == CSX_TEXT) {
|
|
if (p.prop_open && p.depth == 3) {
|
|
csx_put_text(&p.value, &p.x, t.text, t.raw);
|
|
}
|
|
continue;
|
|
}
|
|
if (t.kind == CSX_CLOSE) {
|
|
if (p.depth == 0) {
|
|
bad = true;
|
|
break;
|
|
}
|
|
p.depth--;
|
|
csx_element_end(&p);
|
|
continue;
|
|
}
|
|
if (p.depth > 0) {
|
|
csx_element(&p, &t);
|
|
continue;
|
|
}
|
|
if (is_project) {
|
|
bad = true; /* a second root element */
|
|
break;
|
|
}
|
|
if (!csx_is(&p.x, t.name, "Project")) {
|
|
other_root = true;
|
|
break; /* XML, but no MSBuild file */
|
|
}
|
|
is_project = true;
|
|
sb_puts(&p.out, CBM_DOCLINK_CS_SCOPE_TAG "\nP");
|
|
csx_put_field(&p.out, &p.x, t.attr[CSX_A_SDK]);
|
|
sb_puts(&p.out, "\t-\n");
|
|
if (t.kind == CSX_OPEN) {
|
|
p.depth++;
|
|
}
|
|
}
|
|
cs_cost_add(p.x.i, 0);
|
|
/* What the scan could not read is unknown, not absent: a file malformed
|
|
* anywhere or ending before its root, and a *.csproj that is no MSBuild
|
|
* <Project>. With no blob, an import of such a file would count as one
|
|
* outside the repository, and as the nearest Directory.Build.* it would
|
|
* be passed over for the one above, which MSBuild does not read. */
|
|
if (bad || (!is_project && (named_project || !other_root))) {
|
|
p.out.len = 0;
|
|
sb_puts(&p.out, CBM_DOCLINK_CS_SCOPE_TAG "\nP\t\t!\n");
|
|
} else if (!is_project) {
|
|
return NULL;
|
|
}
|
|
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
|
|
if (cbm_doclink_test_fail_alloc(CBM_DOCLINK_ALLOC_PROJECT)) {
|
|
p.out.failed = true;
|
|
}
|
|
#endif
|
|
if (p.out.failed || p.value.failed || !p.out.buf) {
|
|
if (ctx->result) {
|
|
ctx->result->doc_links.failed = true; /* memory ran out, see above */
|
|
}
|
|
return NULL;
|
|
}
|
|
return p.out.buf;
|
|
}
|