From fde1695de2849862fff267fd5d6a3f144b26b07c Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 27 Aug 2026 12:39:30 +0200 Subject: [PATCH] fix: ignore trailing Dockerfile whitespace at EOF Signed-off-by: Martin Vogel --- internal/cbm/cbm.c | 36 +++++++++++++++++++------ tests/test_parse_coverage.c | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index 6c46d7c91..85c0ba97b 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -810,17 +810,37 @@ static void cbm_error_regions_push(cbm_error_regions_t *acc, TSNode n) { * Deliberately narrow — ZERO-WIDTH AT EOF ONLY. A MISSING or ERROR node with * WIDTH still counts even at EOF (a Makefile whose last recipe line is * unterminated really does lose the recipe), and anything before EOF is - * untouched. */ -static bool cbm_is_eof_terminator_miss(TSNode n, int source_len) { + * untouched. + * + * #1746: the Dockerfile grammar places that zero-width missing newline before + * trailing horizontal whitespace rather than at raw EOF. Preserve the broad + * exact-EOF rule above; only extend it past spaces/tabs when the missing token + * is specifically a newline. */ +static bool cbm_is_eof_terminator_miss(TSNode n, const char *source, int source_len) { if (!ts_node_is_missing(n) || source_len < 0) { return false; } uint32_t start = ts_node_start_byte(n); uint32_t end = ts_node_end_byte(n); - return start == end && end == (uint32_t)source_len; + if (start != end || end > (uint32_t)source_len) { + return false; + } + if (end == (uint32_t)source_len) { + return true; + } + if (!source || strcmp(ts_node_type(n), "\n") != 0) { + return false; + } + for (uint32_t i = end; i < (uint32_t)source_len; i++) { + if (source[i] != ' ' && source[i] != '\t') { + return false; + } + } + return true; } -static void cbm_collect_error_regions(TSNode n, cbm_error_regions_t *acc, int source_len) { +static void cbm_collect_error_regions(TSNode n, cbm_error_regions_t *acc, const char *source, + int source_len) { if (acc->count >= CBM_MAX_ERROR_REGIONS) { return; } @@ -828,12 +848,12 @@ static void cbm_collect_error_regions(TSNode n, cbm_error_regions_t *acc, int so for (uint32_t i = 0; i < k && acc->count < CBM_MAX_ERROR_REGIONS; i++) { TSNode c = ts_node_child(n, i); if (ts_node_is_missing(c) || strcmp(ts_node_type(c), "ERROR") == 0) { - if (cbm_is_eof_terminator_miss(c, source_len)) { + if (cbm_is_eof_terminator_miss(c, source, source_len)) { continue; /* absent final newline only — nothing was dropped */ } cbm_error_regions_push(acc, c); /* top-most region; do not descend */ } else if (ts_node_has_error(c)) { - cbm_collect_error_regions(c, acc, source_len); + cbm_collect_error_regions(c, acc, source, source_len); } } } @@ -1456,7 +1476,7 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua * already extract. */ if (ts_node_has_error(root)) { cbm_error_regions_t raw_regs = {{0}, {0}, 0}; - cbm_collect_error_regions(root, &raw_regs, source_len); + cbm_collect_error_regions(root, &raw_regs, source, source_len); if (raw_regs.count > 0) { int defs_before = result->defs.count; cbm_extract_definitions(&pp_ctx); @@ -1614,7 +1634,7 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua if (strcmp(ts_node_type(root), "ERROR") == 0) { cbm_error_regions_push(®s, root); /* whole file unparseable */ } else { - cbm_collect_error_regions(root, ®s, source_len); + cbm_collect_error_regions(root, ®s, source, source_len); } cbm_subtract_recovered_regions(®s, &result->defs); /* #1071: don't flag a benign function-like-macro call (defined in-file) diff --git a/tests/test_parse_coverage.c b/tests/test_parse_coverage.c index 1c0720c94..423406612 100644 --- a/tests/test_parse_coverage.c +++ b/tests/test_parse_coverage.c @@ -295,6 +295,55 @@ TEST(dockerfile_with_final_newline_still_clean_issue1610) { PASS(); } +/* #1746: on Windows, a Dockerfile whose final instruction is followed by one + * ASCII space and then EOF was reported as parse_partial. */ +TEST(dockerfile_trailing_space_at_eof_not_flagged_issue1746) { + const char *src = "FROM scratch\nENTRYPOINT [\"a\"] "; + CBMFileResult *r = do_extract(src, CBM_LANG_DOCKERFILE, "Dockerfile"); + ASSERT_NOT_NULL(r); + bool flagged = r->parse_incomplete; + if (flagged) { + fprintf(stderr, " exact issue #1746 fixture flagged: ranges=%s\n", + r->error_ranges ? r->error_ranges : "(none)"); + } + cbm_free_result(r); + if (flagged) { + FAIL("trailing horizontal whitespace at Dockerfile EOF must not be parse_partial"); + } + PASS(); +} + +/* The same bytes WITH the LF must stay clean. This is a separate test so the + * control executes even while the exact affected fixture is RED. */ +TEST(dockerfile_trailing_space_with_final_newline_clean_issue1746) { + const char *src = "FROM scratch\nENTRYPOINT [\"a\"] \n"; + CBMFileResult *r = do_extract(src, CBM_LANG_DOCKERFILE, "Dockerfile"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->parse_incomplete); + cbm_free_result(r); + PASS(); +} + +/* Removing the trailing space while retaining EOF must also stay clean. */ +TEST(dockerfile_without_trailing_space_at_eof_clean_issue1746) { + const char *src = "FROM scratch\nENTRYPOINT [\"a\"]"; + CBMFileResult *r = do_extract(src, CBM_LANG_DOCKERFILE, "Dockerfile"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->parse_incomplete); + cbm_free_result(r); + PASS(); +} + +/* The reporter also observed the same failure when the first line uses CRLF. */ +TEST(dockerfile_crlf_trailing_space_at_eof_not_flagged_issue1746) { + const char *src = "FROM scratch\r\nENTRYPOINT [\"a\"] "; + CBMFileResult *r = do_extract(src, CBM_LANG_DOCKERFILE, "Dockerfile"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->parse_incomplete); + cbm_free_result(r); + PASS(); +} + /* Language-general, not a Dockerfile patch: these four were each proven to flag * on a stripped trailing newline. */ TEST(missing_final_newline_not_flagged_across_grammars_issue1610) { @@ -381,6 +430,10 @@ SUITE(parse_coverage) { RUN_TEST(c_trailing_recovered_defs_keep_flag); RUN_TEST(dockerfile_missing_final_newline_not_flagged_issue1610); RUN_TEST(dockerfile_with_final_newline_still_clean_issue1610); + RUN_TEST(dockerfile_trailing_space_at_eof_not_flagged_issue1746); + RUN_TEST(dockerfile_trailing_space_with_final_newline_clean_issue1746); + RUN_TEST(dockerfile_without_trailing_space_at_eof_clean_issue1746); + RUN_TEST(dockerfile_crlf_trailing_space_at_eof_not_flagged_issue1746); RUN_TEST(missing_final_newline_not_flagged_across_grammars_issue1610); RUN_TEST(real_error_before_eof_still_flagged_without_final_newline_issue1610); RUN_TEST(width_bearing_error_at_eof_still_flagged_issue1610);