Skip to content

Commit fbe9eae

Browse files
authored
Merge branch 'main' into fix/1692-csharp-multi-attribute-decorates
2 parents bb21edb + aa44c28 commit fbe9eae

28 files changed

Lines changed: 1857 additions & 154 deletions

‎Makefile.cbm‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ endif
9898
KOTLIN_DEDUP_TEST_DEFINE = -DCBM_KOTLIN_DEDUP_TEST_API=1
9999
CALL_REFERENCE_LOOKUP_TEST_DEFINE = -DCBM_CALL_REFERENCE_LOOKUP_TEST_API=1
100100
INCREMENTAL_TEST_DEFINE = -DCBM_INCREMENTAL_TEST_API=1
101+
COVERAGE_MARKER_TEST_DEFINE = -DCBM_COVERAGE_MARKER_TEST_API=1
101102
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(SANITIZED_DEFINE) \
102103
$(KOTLIN_DEDUP_TEST_DEFINE) $(CALL_REFERENCE_LOOKUP_TEST_DEFINE) \
103-
$(INCREMENTAL_TEST_DEFINE) -g -O1 $(SANITIZE)
104+
$(INCREMENTAL_TEST_DEFINE) $(COVERAGE_MARKER_TEST_DEFINE) -g -O1 $(SANITIZE)
104105
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE) $(CXX_STDLIB_FLAGS)
105106

106107
# TSan (can't combine with ASan)
@@ -118,6 +119,7 @@ TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
118119
# macro of ours.
119120
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
120121
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
122+
$(COVERAGE_MARKER_TEST_DEFINE) \
121123
-DCBM_SANITIZED_BUILD=1 -g -O1 $(TSAN_SANITIZE)
122124
CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -DCBM_SANITIZED_BUILD=1 -g -O1 \
123125
$(TSAN_SANITIZE)

‎internal/cbm/cbm.c‎

Lines changed: 413 additions & 31 deletions
Large diffs are not rendered by default.

‎internal/cbm/cbm.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,21 @@ typedef struct CBMFileResult {
522522
* completeness guarantee. Callers should treat a flagged file as "prefer
523523
* grep here", never treat an unflagged file as provably complete. */
524524
bool parse_incomplete;
525+
/* True when the ranges cover so much of the file that they are no longer
526+
* useful advice — one range over 80% of the line count. The file WAS
527+
* indexed, but pointing a reader at almost every line tells them nothing,
528+
* so the report says "read the source" instead of listing the range.
529+
*
530+
* Its main customers are non-C languages. The refinement that narrows a
531+
* whole-file range using the preprocessed parse only runs for C, C++ and
532+
* CUDA, so a Python, Java or Ruby file whose root node is ERROR still
533+
* reports 1-N.
534+
*
535+
* Note the naming: this field and the phase string it produces are both
536+
* `parse_unusable`. The older `parse_incomplete` field emits the phase
537+
* `parse_partial` instead. That mismatch is historical, not deliberate —
538+
* do not copy it. */
539+
bool parse_unusable;
525540
const char *error_ranges;
526541
int error_region_count;
527542
bool is_test_file;

‎internal/cbm/extract_imports.c‎

Lines changed: 80 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,12 +1383,13 @@ static void parse_spec_imports(CBMExtractCtx *ctx) {
13831383
ts_tree_cursor_delete(&cursor);
13841384
}
13851385

1386-
// --- Embedded-language imports ---
1387-
// Generic walker for host grammars (Svelte, Vue, HTML, Astro, ...) whose AST
1388-
// keeps embedded sub-language source as raw_text (or similar) without parsing
1389-
// it. The host's CBMLangSpec.embedded_imports declares which content nodes
1390-
// hold which sub-language; we re-parse each match with the embedded grammar
1391-
// and run the standard ES import walker over the inner AST.
1386+
// --- Embedded-language structure and imports ---
1387+
// Generic walker for host grammars (Svelte, Vue, HTML, Astro, CFML, ...) whose
1388+
// AST keeps embedded sub-language source as raw_text (or similar) without
1389+
// parsing it. The host's CBMLangSpec.embedded_imports declares which content
1390+
// nodes hold which sub-language; we re-parse each match with the embedded
1391+
// grammar and run the definition, import and call extractors over the inner
1392+
// AST, so a component's script contributes symbols like a standalone file.
13921393
//
13931394
// No grammar symbols are referenced here — the embedded TSLanguage is
13941395
// resolved through cbm_ts_language(spec->embedded_language), the same hook
@@ -1458,66 +1459,94 @@ static bool ascii_trimmed_equals(const char *value, const char *expected) {
14581459
return true;
14591460
}
14601461

1461-
/* Vue is the only host that opts into structural embedded extraction. Restrict
1462-
* its inline programs to the language forms whose parsers we can select
1463-
* exactly. A src attribute always denotes an external program and therefore
1464-
* suppresses any inline extraction, even for malformed mixed markup. */
1465-
static bool vue_embedded_language(CBMExtractCtx *ctx, TSNode script, CBMLanguage *language) {
1466-
enum { VUE_ATTR_STACK_CAP = 128 };
1467-
TSNode stack[VUE_ATTR_STACK_CAP];
1462+
/* The text of a <script> attribute's value, or "" when it carries none
1463+
* (`<script type>`), so every attribute reads through one path. */
1464+
static const char *script_attribute_value(CBMExtractCtx *ctx, TSNode attribute) {
1465+
TSNode value_node = attribute;
1466+
if (!find_first_descendant_of(attribute, "attribute_value", &value_node)) {
1467+
return "";
1468+
}
1469+
return cbm_node_text(ctx->arena, value_node, ctx->source);
1470+
}
1471+
1472+
/* Apply one <script> attribute to the parse decision. Returns false when the
1473+
* attribute rules the block out of inline extraction:
1474+
* src= always names an external program, so nothing inline is extracted,
1475+
* even for malformed mixed markup;
1476+
* lang= selects the JavaScript or TypeScript grammar; any other language
1477+
* has no parser here;
1478+
* type= plain HTML routinely carries non-program blocks (application/json,
1479+
* importmap, text/x-template, application/ld+json) that would parse
1480+
* into garbage definitions and calls. Only the JavaScript forms in
1481+
* current use pass — absent or empty, module, text/javascript,
1482+
* application/javascript — and the rest bail. */
1483+
static bool apply_script_attribute(CBMExtractCtx *ctx, TSNode attribute, CBMLanguage *language) {
1484+
if (ts_node_named_child_count(attribute) == 0) {
1485+
return false;
1486+
}
1487+
const char *name = cbm_node_text(ctx->arena, ts_node_named_child(attribute, 0), ctx->source);
1488+
if (ascii_trimmed_equals(name, "src")) {
1489+
return false;
1490+
}
1491+
if (ascii_trimmed_equals(name, "lang")) {
1492+
const char *value = script_attribute_value(ctx, attribute);
1493+
if (ascii_trimmed_equals(value, "js") || ascii_trimmed_equals(value, "javascript")) {
1494+
*language = CBM_LANG_JAVASCRIPT;
1495+
return true;
1496+
}
1497+
if (ascii_trimmed_equals(value, "ts") || ascii_trimmed_equals(value, "typescript")) {
1498+
*language = CBM_LANG_TYPESCRIPT;
1499+
return true;
1500+
}
1501+
return false;
1502+
}
1503+
if (ascii_trimmed_equals(name, "type")) {
1504+
const char *value = script_attribute_value(ctx, attribute);
1505+
return ascii_trimmed_equals(value, "") || ascii_trimmed_equals(value, "module") ||
1506+
ascii_trimmed_equals(value, "text/javascript") ||
1507+
ascii_trimmed_equals(value, "application/javascript");
1508+
}
1509+
return true;
1510+
}
1511+
1512+
/* Markup hosts (Vue, Svelte, HTML, Astro) hang the attributes that decide
1513+
* whether and how an inline program is parsed off the <script> start tag.
1514+
* *language enters holding the spec row's default and leaves holding the
1515+
* grammar to parse with; an attribute-free block (Astro's frontmatter fence)
1516+
* keeps the row's language rather than falling through to JavaScript. */
1517+
static bool script_embedded_language(CBMExtractCtx *ctx, TSNode script, CBMLanguage *language) {
1518+
enum { SCRIPT_ATTR_STACK_CAP = 128 };
1519+
TSNode stack[SCRIPT_ATTR_STACK_CAP];
14681520
int top = 0;
1469-
bool has_lang = false;
1470-
bool lang_supported = true;
1471-
CBMLanguage selected = CBM_LANG_JAVASCRIPT;
14721521
stack[top++] = script;
14731522
while (top > 0) {
14741523
TSNode node = stack[--top];
14751524
if (strcmp(ts_node_type(node), "attribute") == 0) {
1476-
uint32_t named_count = ts_node_named_child_count(node);
1477-
if (named_count == 0) {
1478-
return false;
1479-
}
1480-
TSNode name_node = ts_node_named_child(node, 0);
1481-
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
1482-
if (ascii_trimmed_equals(name, "src")) {
1525+
if (!apply_script_attribute(ctx, node, language)) {
14831526
return false;
14841527
}
1485-
if (ascii_trimmed_equals(name, "lang")) {
1486-
TSNode value_node = node;
1487-
if (!find_first_descendant_of(node, "attribute_value", &value_node)) {
1488-
return false;
1489-
}
1490-
char *value = cbm_node_text(ctx->arena, value_node, ctx->source);
1491-
has_lang = true;
1492-
if (ascii_trimmed_equals(value, "js") ||
1493-
ascii_trimmed_equals(value, "javascript")) {
1494-
selected = CBM_LANG_JAVASCRIPT;
1495-
} else if (ascii_trimmed_equals(value, "ts") ||
1496-
ascii_trimmed_equals(value, "typescript")) {
1497-
selected = CBM_LANG_TYPESCRIPT;
1498-
} else {
1499-
lang_supported = false;
1500-
}
1501-
}
15021528
continue;
15031529
}
15041530
uint32_t count = ts_node_named_child_count(node);
1505-
if ((int)count > VUE_ATTR_STACK_CAP - top) {
1531+
if ((int)count > SCRIPT_ATTR_STACK_CAP - top) {
15061532
return false;
15071533
}
15081534
for (int i = (int)count - 1; i >= 0; i--) {
15091535
stack[top++] = ts_node_named_child(node, (uint32_t)i);
15101536
}
15111537
}
1512-
if (has_lang && !lang_supported) {
1513-
return false;
1514-
}
1515-
*language = selected;
15161538
return true;
15171539
}
15181540

1541+
/* CFML's <cfscript> carries no attributes and its embedded language is fixed
1542+
* by the spec row; only the markup hosts consult the <script> tag. */
1543+
static bool markup_script_host(CBMLanguage language) {
1544+
return language == CBM_LANG_VUE || language == CBM_LANG_SVELTE || language == CBM_LANG_HTML ||
1545+
language == CBM_LANG_ASTRO;
1546+
}
1547+
15191548
static void parse_one_embedded_block(CBMExtractCtx *ctx, const CBMEmbeddedBlock *block,
1520-
CBMLanguage embedded_language, bool extract_structure) {
1549+
CBMLanguage embedded_language) {
15211550
const TSLanguage *language = cbm_ts_language(embedded_language);
15221551
if (!language || ctx->source_len < 0) {
15231552
return;
@@ -1564,13 +1593,9 @@ static void parse_one_embedded_block(CBMExtractCtx *ctx, const CBMEmbeddedBlock
15641593
.macro_table = ctx->macro_table,
15651594
.return_type_table = ctx->return_type_table,
15661595
};
1567-
if (extract_structure) {
1568-
cbm_extract_definitions_without_module(&sub_ctx);
1569-
}
1596+
cbm_extract_definitions_without_module(&sub_ctx);
15701597
walk_es_imports(&sub_ctx, sub_ctx.root);
1571-
if (extract_structure) {
1572-
cbm_extract_unified(&sub_ctx);
1573-
}
1598+
cbm_extract_unified(&sub_ctx);
15741599

15751600
ts_tree_delete(tree);
15761601
ts_parser_delete(parser);
@@ -1588,22 +1613,11 @@ static void parse_embedded_imports(CBMExtractCtx *ctx) {
15881613
embedded_collect_content_nodes(ctx->root, e, hits, &hit_count, MAX_EMBEDDED_BLOCKS);
15891614
for (int i = 0; i < hit_count; i++) {
15901615
CBMLanguage embedded_language = e->embedded_language;
1591-
/* Structure (defs + calls), not just imports, for hosts whose
1592-
* embedded language carries real code. Vue since #1852; CFML's
1593-
* <cfscript> since the #1412 distillation. */
1594-
bool extract_structure =
1595-
ctx->language == CBM_LANG_VUE || ctx->language == CBM_LANG_CFML;
1596-
/* The attribute resolver is Vue's: it inspects <script lang=/src=>
1597-
* and OVERRIDES the spec's embedded language (JS default, TS on
1598-
* lang="ts", bail on src=). CFML's cf_script_tag carries no such
1599-
* attributes and its embedded language is fixed by the spec row
1600-
* (CFSCRIPT) — running the resolver would silently rewrite it to
1601-
* JavaScript. Resolve only for Vue. */
1602-
if (ctx->language == CBM_LANG_VUE &&
1603-
!vue_embedded_language(ctx, hits[i].script, &embedded_language)) {
1616+
if (markup_script_host(ctx->language) &&
1617+
!script_embedded_language(ctx, hits[i].script, &embedded_language)) {
16041618
continue;
16051619
}
1606-
parse_one_embedded_block(ctx, &hits[i], embedded_language, extract_structure);
1620+
parse_one_embedded_block(ctx, &hits[i], embedded_language);
16071621
}
16081622
}
16091623
}

‎internal/cbm/lang_specs.c‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -890,8 +890,9 @@ static const char *graphql_field_types[] = {"field_definition", "input_value_def
890890
// ==================== Embedded sub-languages ====================
891891
// Host grammars (Svelte/Vue/HTML/Astro) treat <script> bodies as raw_text and
892892
// do not recurse into them. Declaring the host's script-content node here lets
893-
// the generic embedded-imports walker re-parse that slice with the JS grammar
894-
// so the existing ES import extractor sees real import_statement nodes.
893+
// the generic embedded walker re-parse that slice with the declared grammar so
894+
// its definitions, imports and calls extract in host-file coordinates. The
895+
// declared language is the default; a <script lang=> attribute overrides it.
895896
// Terminator: an entry whose script_node_type is NULL.
896897
static const CBMEmbeddedLangSpec cfml_embedded_imports[] = {
897898
/* Tag-dialect CFML keeps <cfscript> bodies as opaque cf_script_content;
@@ -915,10 +916,12 @@ static const CBMEmbeddedLangSpec html_embedded_imports[] = {
915916
};
916917
static const CBMEmbeddedLangSpec astro_embedded_imports[] = {
917918
/* Astro component scripts live in the `---` frontmatter fence, which the
918-
* grammar keeps as an unparsed frontmatter_js_block. Re-parse that slice
919-
* with the JS grammar so `import X from './X.astro'` becomes a real edge. */
920-
{"frontmatter", "frontmatter_js_block", CBM_LANG_JAVASCRIPT},
921-
{"script_element", "raw_text", CBM_LANG_JAVASCRIPT},
919+
* grammar keeps as an unparsed frontmatter_js_block. Astro type-checks the
920+
* fence and its <script> bodies as TypeScript without any lang= attribute
921+
* saying so (the fence cannot carry one), so TypeScript is the deliberate
922+
* default for both; it parses untyped JavaScript unchanged. */
923+
{"frontmatter", "frontmatter_js_block", CBM_LANG_TYPESCRIPT},
924+
{"script_element", "raw_text", CBM_LANG_TYPESCRIPT},
922925
{NULL, NULL, 0},
923926
};
924927

‎src/cli/config_yaml_edit.c‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3601,8 +3601,49 @@ int cbm_yaml_upsert_mapping_entry(const char *file_path, const char *section_key
36013601
return result == 0 && release_result == 0 ? 0 : YAML_ERROR;
36023602
}
36033603

3604+
/* Removal is idempotent: a config that does not exist has nothing to remove.
3605+
* The JSON removers already answer OK here (cbm_json_like_read_document's
3606+
* "missing" result); the YAML removers instead took the lock first, and the
3607+
* lock directory is created beside the target — so with the parent directory
3608+
* absent (`~/.hermes/` never created because Hermes was detected by its
3609+
* binary on PATH, not by its home) every uninstall reported
3610+
* "does not exist or cannot be inspected" and refused to remove the
3611+
* executable. Only a true ENOENT short-circuits: a dangling symlink still
3612+
* reaches the reader and its byte-identical rejection. */
3613+
static bool yaml_remove_target_absent(const char *path) {
3614+
if (!path) {
3615+
return false;
3616+
}
3617+
#ifdef _WIN32
3618+
/* FILE_FLAG_OPEN_REPARSE_POINT, as in toml_read_file: opens the link
3619+
* itself rather than following it, so a dangling symlink does NOT count as
3620+
* absent and still reaches the reader — matching the POSIX branch, where
3621+
* lstat does not follow either. */
3622+
wchar_t *wide = cbm_utf8_to_wide(path);
3623+
if (!wide) {
3624+
return false;
3625+
}
3626+
HANDLE handle = CreateFileW(
3627+
wide, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
3628+
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
3629+
DWORD error = handle == INVALID_HANDLE_VALUE ? GetLastError() : 0;
3630+
free(wide);
3631+
if (handle != INVALID_HANDLE_VALUE) {
3632+
CloseHandle(handle);
3633+
return false;
3634+
}
3635+
return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
3636+
#else
3637+
struct stat path_state;
3638+
return lstat(path, &path_state) != 0 && errno == ENOENT;
3639+
#endif
3640+
}
3641+
36043642
int cbm_yaml_remove_mapping_entry(const char *file_path, const char *section_key,
36053643
const char *entry_key) {
3644+
if (yaml_remove_target_absent(file_path)) {
3645+
return 0;
3646+
}
36063647
yaml_config_lock_t lock;
36073648
if (yaml_lock_acquire(file_path, &lock) != 0) {
36083649
return YAML_ERROR;
@@ -3626,6 +3667,9 @@ int cbm_yaml_upsert_owned_mapping_entry(const char *file_path, const char *secti
36263667

36273668
int cbm_yaml_remove_owned_mapping_entry(const char *file_path, const char *section_key,
36283669
const char *entry_key, const char *canonical_entry_block) {
3670+
if (yaml_remove_target_absent(file_path)) {
3671+
return CBM_YAML_IDENTITY_EDIT_OK;
3672+
}
36293673
yaml_config_lock_t lock;
36303674
if (yaml_lock_acquire(file_path, &lock) != 0) {
36313675
return CBM_YAML_IDENTITY_EDIT_ERROR;
@@ -3653,6 +3697,9 @@ int cbm_yaml_upsert_mapping_sequence_item(const char *file_path, const char *con
36533697
int cbm_yaml_remove_mapping_sequence_item(const char *file_path, const char *const *sequence_path,
36543698
size_t sequence_path_len, const char *identity_key,
36553699
const char *identity_scalar, const char *canonical_item) {
3700+
if (yaml_remove_target_absent(file_path)) {
3701+
return CBM_YAML_IDENTITY_EDIT_OK;
3702+
}
36563703
yaml_config_lock_t lock;
36573704
if (yaml_lock_acquire(file_path, &lock) != 0) {
36583705
return CBM_YAML_IDENTITY_EDIT_ERROR;
@@ -3675,6 +3722,9 @@ int cbm_yaml_upsert_string_list_item(const char *file_path, const char *key, con
36753722
}
36763723

36773724
int cbm_yaml_remove_string_list_item(const char *file_path, const char *key, const char *item) {
3725+
if (yaml_remove_target_absent(file_path)) {
3726+
return 0;
3727+
}
36783728
yaml_config_lock_t lock;
36793729
if (yaml_lock_acquire(file_path, &lock) != 0) {
36803730
return YAML_ERROR;

0 commit comments

Comments
 (0)