test(lint-once): follow a short file flag with its value attached
All checks were successful
check / check (push) Successful in 14s

`docker build -fDockerfile.lint .` is a plain `-f` naming a literal file —
the flag parser reads the attached value for any shorthand — but the
resolver's `[=\s]+` required a separator, so the invocation fell through to
the default `Dockerfile` edge. Mutating the `check` recipe to that spelling
left the suite 52/52 green while `-f Dockerfile.lint` was caught.

The header already promised this form was followed: it claimed the file
named by `-f` is resolved wherever the flag sits, and disclosed only a
bundled short cluster (`-qf <file>`) as unfollowed. Overclaiming is the
defect, so the resolver is taught the form rather than the claim narrowed.

The attached form is allowed only for the short flag, keeping `--force-rm`
out of it; `--file` still requires `=` or whitespace. The `-qf` cluster
limitation is untouched and still pinned.
This commit is contained in:
user
2026-09-04 11:37:08 +00:00
parent 197296edba
commit 2fcb3e6ced

View File

@@ -39,11 +39,12 @@
// one pass over literals, so a value that itself names another variable, a // one pass over literals, so a value that itself names another variable, a
// value built by a make function (`$(shell ...)`, `$(addprefix ...)`), and the // value built by a make function (`$(shell ...)`, `$(addprefix ...)`), and the
// body of a `define`/`endef` block are not expanded and not followed. A // body of a `define`/`endef` block are not expanded and not followed. A
// `docker build` resolves to the file named by `-f`, `-f=`, `--file` or // `docker build` resolves to the file named by `-f`, `-f=`, `-f<file>` with
// `--file=`, wherever in the invocation that flag sits, and to `Dockerfile` // the value attached to the flag, `--file` or `--file=`, wherever in the
// when it names none; a Dockerfile chosen some other way — a bundled short // invocation that flag sits, and to `Dockerfile` when it names none; a
// flag cluster (`-qf <file>`), a value this walk cannot expand to a literal — // Dockerfile chosen some other way — a bundled short flag cluster
// resolves to the default rather than to the real file. Within those edges, a // (`-qf <file>`), a value this walk cannot expand to a literal — resolves to
// the default rather than to the real file. Within those edges, a
// prettier call is caught wherever it is added. // prettier call is caught wherever it is added.
// //
// Two entrypoints are walked, because they cover different graphs: `make check` // Two entrypoints are walked, because they cover different graphs: `make check`
@@ -437,16 +438,19 @@ const edgesOf = (line: string): string[] => {
// it the count would miss the one invocation that is supposed to survive. // it the count would miss the one invocation that is supposed to survive.
// //
// Every spelling of the file flag is read, not just `-f <file>`. Docker // Every spelling of the file flag is read, not just `-f <file>`. Docker
// accepts `--file <file>`, `--file=<file>` and `-f=<file>` for the same // accepts `--file <file>`, `--file=<file>`, `-f=<file>` and the value
// thing, and matching `-f ` alone resolved all of them to the default // attached to the short flag as one token (`-f<file>`) for the same thing,
// and matching `-f ` alone resolved all of them to the default
// `Dockerfile` edge — so `docker build --file=Dockerfile.lint .` in a // `Dockerfile` edge — so `docker build --file=Dockerfile.lint .` in a
// recipe was followed into the wrong file, counted nothing, and reported // recipe was followed into the wrong file, counted nothing, and reported
// green. The flag is searched for anywhere in the invocation, as `-f` // green. The flag is searched for anywhere in the invocation, as `-f`
// already was, because a real build line wraps it in `--build-arg` and // already was, because a real build line wraps it in `--build-arg` and
// other flags. A leading `\s` is required so that a longer flag ending in // other flags. A leading `\s` is required so that a longer flag ending in
// the same letters cannot supply the match. // the same letters cannot supply the match, and the attached form is
// allowed only for the short flag, so that `--force-rm` — a long flag that
// merely starts with the same letters — still does not read as one.
if (/\bdocker\s+build\b/.test(line)) { if (/\bdocker\s+build\b/.test(line)) {
const file = /\s(?:-f|--file)[=\s]+(\S+)/.exec(line); const file = /\s(?:--file[=\s]+|-f=?\s*)(\S+)/.exec(line);
edges.push(`docker:${file === null ? "Dockerfile" : file[1]}`); edges.push(`docker:${file === null ? "Dockerfile" : file[1]}`);
} }
@@ -1021,6 +1025,21 @@ describe("the resolver reads what the shell would run", () => {
]); ]);
}); });
// The value may be attached to the short flag with no separator at all:
// `-fDockerfile.lint` is a single token, and the flag parser reads it as
// `-f` naming that file, exactly as `-f Dockerfile.lint` does. Requiring a
// separator sent it to the default `Dockerfile` edge instead — the same
// false green as the long spellings above, and one the header promised was
// followed: a second prettier pass added as `docker build
// -fDockerfile.lint .` was resolved into the wrong file, counted nothing,
// and left the build green. `-fFILE` is an ordinary thing for a human to
// write, so it is followed rather than merely documented as unfollowed.
it("follows a short file flag with its value attached", () => {
expect(edgesOf("docker build -fDockerfile.lint .")).toEqual([
"docker:Dockerfile.lint",
]);
});
// The flag is found anywhere in the invocation rather than at a fixed // The flag is found anywhere in the invocation rather than at a fixed
// position, matching how `-f` was already read, since a real build line // position, matching how `-f` was already read, since a real build line
// carries `--build-arg` and friends around it. // carries `--build-arg` and friends around it.