Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 23 additions & 25 deletions internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -1434,6 +1434,26 @@ static void cbm_mark_pp_error_rows(TSNode n, uint8_t *rows, uint32_t row_count,
}
}

/* The ONE line counter for a source buffer (#1967). Every caller that asks
* "how many lines does this file have" uses it, so the coverage report never
* holds two answers for the same buffer.
*
* Convention: a '\n' TERMINATES a line; it does not open a new one. So the
* count is the number of '\n' that have at least one byte after them, plus
* one. "a\nb" and "a\nb\n" both have 2 lines (the trailing newline adds no
* phantom empty line, matching what an editor shows); "a\nb" without a final
* newline still counts its last line. An empty buffer counts as 1 line, because
* tree-sitter still reports row 0 for it and 1-based line maps index line 1. */
uint32_t cbm_source_line_count(const char *src, int src_len) {
uint32_t n = 1;
for (int i = 0; i + 1 < src_len; i++) {
if (src[i] == '\n') {
n++;
}
}
return n;
}

/* Recovery subtraction (#963): tree-sitter error recovery plus the
* ERROR-descending def walker often still extract constructs INSIDE a failed
* region (verified: a function in an #ifdef-split ERROR region and even a
Expand All @@ -1451,12 +1471,7 @@ static void cbm_mark_pp_error_rows(TSNode n, uint8_t *rows, uint32_t row_count,
* Now the uncovered gaps are reported instead, and a gap holding only blank,
* comment or preprocessor lines is not a miss at all. */
static uint32_t *cbm_line_offsets(const char *src, int src_len, uint32_t *out_lines) {
uint32_t lines = 1;
for (int i = 0; i < src_len; i++) {
if (src[i] == '\n') {
lines++;
}
}
uint32_t lines = cbm_source_line_count(src, src_len);
uint32_t *offs =
(uint32_t *)cbm_alloc(CBM_MEM_CLASS_EXTRACT, (size_t)(lines + 1) * sizeof(uint32_t));
if (!offs) {
Expand Down Expand Up @@ -2061,18 +2076,6 @@ static void cbm_refine_regions_with_pp_lines(cbm_error_regions_t *regs, const ui
* this repo covers 25.5% of its file, and the next widest 3.9%. */
#define CBM_UNUSABLE_PCT 80

/* Number of 1-based lines in `src`. A file that does not end with a newline
* still has a last line, so the count is separators plus one. */
static uint32_t cbm_count_lines(const char *src, int src_len) {
uint32_t n = 1;
for (int i = 0; i < src_len; i++) {
if (src[i] == '\n' && i + 1 < src_len) {
n++;
}
}
return n;
}

/* Serialize collected regions as "start-end,start-end,...", with a trailing
* ",+<N>" when the cap threw N ranges away.
*
Expand Down Expand Up @@ -2868,12 +2871,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
* total loss (root is ERROR), because then it vouches for
* nothing and there is no refinement to make. */
if (strcmp(ts_node_type(pp_root), "ERROR") != 0) {
uint32_t orig_lines = 1;
for (int ci = 0; ci < source_len; ci++) {
if (source[ci] == '\n') {
orig_lines++;
}
}
uint32_t orig_lines = cbm_source_line_count(source, source_len);
uint8_t *map = (uint8_t *)cbm_arena_alloc(a, (size_t)orig_lines + 2);
int exp_lines = preprocessed->expanded_line_count;
uint8_t *bad_rows =
Expand Down Expand Up @@ -3069,7 +3067,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
* so the report can say "read the source" instead. See
* parse_unusable in cbm.h for which files land here and why. */
if (regs.count == 1 && regs.dropped == 0) {
uint32_t total = cbm_count_lines(source, source_len);
uint32_t total = cbm_source_line_count(source, source_len);
uint32_t span = regs.ends[0] - regs.starts[0] + 1;
if (total > 0 && span * 100 >= total * CBM_UNUSABLE_PCT) {
result->parse_unusable = true;
Expand Down
5 changes: 5 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,11 @@ uint64_t cbm_usage_field_lookup_test_work(void);
uint64_t cbm_usage_slow_parent_fallback_test_count(void);
#endif

// Number of 1-based lines in a source buffer. The single line-count convention
// for coverage reporting (#1967): a trailing '\n' ends the last line and opens
// no new one; an empty buffer counts as 1 line. See the definition in cbm.c.
uint32_t cbm_source_line_count(const char *src, int src_len);

// Toggle C/C++ preprocessor Macro-node extraction (#375). The pipeline enables
// it only for full/advanced index modes (it dominates extraction on macro-dense
// codebases). Default ON. Set before extraction; read-only during.
Expand Down
18 changes: 18 additions & 0 deletions tests/test_parse_coverage.c
Original file line number Diff line number Diff line change
Expand Up @@ -1615,7 +1615,25 @@ TEST(cs_malformed_conditional_remains_partial_issue1748) {
PASS();
}

/* #1967: every line counter over one source buffer answers the same
* question the same way. A trailing newline ends the last line; it does not
* open a new one. */
TEST(source_line_count_one_convention) {
ASSERT_EQ(cbm_source_line_count("", 0), 1u);
ASSERT_EQ(cbm_source_line_count("a", 1), 1u);
ASSERT_EQ(cbm_source_line_count("a\n", 2), 1u);
ASSERT_EQ(cbm_source_line_count("\n", 1), 1u);
ASSERT_EQ(cbm_source_line_count("a\nb", 3), 2u);
ASSERT_EQ(cbm_source_line_count("a\nb\n", 4), 2u);
ASSERT_EQ(cbm_source_line_count("a\n\n", 3), 2u);
ASSERT_EQ(cbm_source_line_count("a\r\nb\r\n", 6), 2u);
/* src_len bounds the count, not a NUL terminator. */
ASSERT_EQ(cbm_source_line_count("a\nb\nc", 2), 1u);
PASS();
}

SUITE(parse_coverage) {
RUN_TEST(source_line_count_one_convention);
RUN_TEST(c_ifdef_split_brace_sets_parse_incomplete);
RUN_TEST(c_ifdef_split_brace_neighbors_still_extracted);
RUN_TEST(c_error_range_points_at_failed_region);
Expand Down
Loading