putup forfeited a command's whole dependency scan unless every leading
invocation was a compile it recognized, so `echo building f.c && gcc -c
f.c`, `FOO=1 gcc -c f.c` and a leading `;` each cost the rule all of its
header dependencies -- the rule built, and every header it read went
unrecorded until someone touched a source file.
Recognition now follows one rule: what precedes a compile is either
reproduced in the scan or proven inert, and never silently dropped.
Leading NAME=VALUE words are absorbed by the scanner's compiler index and
kept verbatim in front of the compiler, so the scan preprocesses under the
environment the compile did -- keeping them is what makes it sound, since
a dropped CPATH would resolve the header search against a path the compile
never read. An invocation that runs nothing, or runs only echo/true/:,
changes neither directory nor environment nor any file the compile reads,
so the walk steps over it. Everything else still ends the scannable
region.
Both fences live in one predicate because words are split on whitespace:
`FOO=1;` is an assignment the shell ends and `x>f.h` a redirection it
never stood alone as, and neither is a word putup ever classified. The
word-shape test and the transparency test are shared in dep_words.cpp, so
the gcc and clang-cl scanners cannot drift on either.
The prefix walk ends at the last compile in the run, which keeps a
non-empty prefix and a non-empty scan set the same answer -- the unscanned
report consumes the production predicate, so a matcher that claimed a
command of pure echoes would report it covered and scan nothing.
The in-tree gcc BSP example is unchanged at 103 unscanned objects: those
are shell-loop rules, which stay opaque.
Closes #352
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjcxNmyFfoTJYSZKKgjrCp
putup forfeited a command's whole dependency scan unless every leading invocation was a compile it recognized — a harmless prefix (
echo building %f &&,FOO=1, a leading;) cost the rule all of its header dependencies.Recognition now follows one rule: what precedes a compile is either reproduced in the scan or proven inert, and never silently dropped.
NAME=VALUEwords are absorbed by the scanner's compiler index and kept verbatim in the scan text, so the scan preprocesses under the environment the compile did — for every variable,CPATHincluded, with no per-name judgment.echo/true/:with all-plain words, is scan-transparent: the prefix walk steps over it. The allowlist is deliberately tiny and grows only on evidence; the unscanned-object reporter continues to measure the lag.cd,export, standalone assignments, redirections, shell-special words — still ends the scannable region.Both new fences live in one shared predicate (
hides_shell_syntax): words are split on whitespace, soFOO=1;is an assignment the shell ends andx>f.ha redirection it never stood alone as. The word-shape and transparency tests are shared indep_words.cpp, so the gcc and clang-cl scanners cannot drift on either. The prefix walk truncates at the last compile, keeping "non-empty prefix ⇔ at least one scan" true by construction.Spec:
dep-scan.ears.mdgainsREQ-SCAN-KEEPS-ASSIGNMENT-PREFIXandREQ-SCAN-TRANSPARENT-INVOCATION, and the region-ending requirement is reworded to "neither a recognized compile nor scan-transparent".DESIGN.mdrecords the reproduce-or-prove-inert law.Verification: four unit RED runs and one E2E RED run quoted before implementation; full suite green (170231 assertions, 835 cases); spec-check, format, tidy, iwyu clean. The gcc BSP example's unscanned count is unchanged at 103 — measured, not assumed: those are shell-loop rules, which stay correctly opaque; the widening's payoff is pinned by the new tests and the E2E fixture.
Closes #352
🤖 Generated with Claude Code
https://claude.ai/code/session_01AjcxNmyFfoTJYSZKKgjrCp