Skip to content

fix(core): teach the Dockerfile parser about heredocs#17

Open
PunGrumpy wants to merge 3 commits into
advisor/003-fix-rule-specificity-bugsfrom
advisor/005-dockerfile-heredoc-support
Open

fix(core): teach the Dockerfile parser about heredocs#17
PunGrumpy wants to merge 3 commits into
advisor/003-fix-rule-specificity-bugsfrom
advisor/005-dockerfile-heredoc-support

Conversation

@PunGrumpy

Copy link
Copy Markdown
Owner

What

Teaches the Dockerfile parser about heredoc (<<EOF) syntax and anchors the instruction regex to the real Dockerfile keyword set. Implements plan 005. Depends on 002 (CLI integration tests) and 003 (rule-specificity fixes).

Why

The parser had no heredoc handling and a case-insensitive [A-Z]+ instruction match, so every heredoc body line was parsed as an instruction:

RUN <<EOF
apt-get update
apt-get install -y curl
EOF

...parsed to a RUN whose args were literally <<EOF (the real command lost) plus phantom APT-GET instructions. Two consequences, both fixed here and verified live against the built CLI:

  • Mass false negatives — content-based rules (clean-package-cache, combine-apt-update-install, use-pipefail, avoid-dev-dependencies, order-layers) inspect RUN args and saw only <<EOF. Now the body is folded into the instruction's args, so a heredoc RUN looks the same to a rule as a backslash-continuation RUN. Confirmed: clean-package-cache + combine-apt-update-install now fire on a heredoc apt-get install.
  • False positives — a USER root line inside an unrelated heredoc body flipped no-root-user. Confirmed: phantom USER root in a heredoc body no longer produces a diagnostic.

How

  • Regex anchor (commit 1): [A-Za-z]+ extracts a candidate word, then a module-level DOCKERFILE_KEYWORDS Set gates it — a lowercase from node:22 is still valid, but apt-get update is no longer an "instruction".
  • Heredoc state machine (commit 2): a FIFO delimiter queue supports multiple openers per line (COPY <<F1 <<F2 /dest/), closed in open order; a heredoc opener implies continuation without a trailing \; the #-comment skip is guarded so shell comments inside a body aren't dropped; EOF flushes an unterminated heredoc instead of crashing.

The parser was refactored into small pure helpers (matchInstructionKeyword, findHeredocDelimiters, processHeredocLine, processInstructionLine) to satisfy the repo's eslint(complexity) gate — control flow and semantics are unchanged (all pre-existing parser and rule tests pass unchanged).

Deliberately not done (design decision): no heredoc field on DockerfileInstruction — folding the body into args is what makes every content rule work without touching any of them. Known remaining gaps documented in the plan: # escape= directive, # syntax= frontmatter, ARG before first FROM.

Behavior change (why the changeset)

.changeset/dockerfile-heredoc-parsing.md@docker-doctor/cli patch. Scan output changes on real Dockerfiles that use heredocs (previously-hidden violations now surface; phantom-instruction diagnostics disappear).

Tests

  • packages/core/test/parser.test.ts — +7 tests (keyword anchoring, lowercase, heredoc body-into-args, body-not-instructions, quoted/<<- delimiters, raw preservation, CRLF). Purely additive.
  • New fixture packages/docker-doctor/test/fixtures/heredoc/Dockerfile + a CLI test asserting ≥1 diagnostic — end-to-end proof the parser fix reaches the rule layer.
  • core: 42 pass / 0 fail · cli: 13 pass / 0 fail · typecheck 0 · ultracite check 0.

Stacking

Base is advisor/003-fix-rule-specificity-bugs (PR #15), which stacks on advisor/002-... (PR #14). This is a sibling of PR #16 (004) — both branch off 003, and 005 does not touch the image-ref parser 004 rewrote, so there's no conflict. GitHub auto-retargets this PR to main once #14 and #15 merge. Merge order: #14#15 → (#16 and this, in either order).

🤖 Generated with Claude Code

The permissive case-insensitive [A-Z]+ regex treated any "word arg" line
as an instruction, causing false parses. Anchor to the real Dockerfile
keyword set (module-level, built once) while still accepting lowercase
instructions.
BuildKit heredocs (<<EOF ... EOF) had no parser support: every body line
was mis-parsed as its own instruction, producing phantom instructions
(false positives, e.g. USER inside a RUN heredoc flipping no-root-user)
and losing the real RUN command (false negatives for content-based rules
like clean-package-cache and combine-apt-update-install).

Add heredoc state tracking to the Dockerfile parser: detect one or more
openers per line (<<EOF, <<-EOF, <<'EOF', <<"EOF"), fold body lines into
the owning instruction's args (not matched as instructions, comments
included verbatim), and close in FIFO delimiter order. An opener implies
continuation even without a trailing backslash; an unterminated heredoc
still emits at EOF rather than being dropped.

Add an end-to-end heredoc fixture and CLI test proving the fix reaches
content-based rules (clean-package-cache now fires on the heredoc RUN
body).
packages/core is bundled into @docker-doctor/cli, and heredoc bodies now
feed content-based rules, so this is a published behavior change.
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docker-doctor Ready Ready Preview, Comment Jul 23, 2026 2:16am

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant