Skip to content

Commit c00d83b

Browse files
author
hazyhaar
committed
fix(tools): pass terminal stdin marker to ruff and clarify path exclusions
1 parent e93871d commit c00d83b

2 files changed

Lines changed: 187 additions & 17 deletions

File tree

‎internal/tools/format_on_write.go‎

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,13 @@ func (result formatOnWriteResult) notice(relativePath string) string {
7171
//
7272
// STDIN ADAPTERS READ THE WRITTEN BYTES ON STDIN AND PRINT THE FORMATTED RESULT
7373
// TO STDOUT. That route lets filenameFlag carry the logical destination path,
74-
// so any rule written against the file's own path (.clang-format-ignore, an
75-
// .editorconfig section, rustfmt's ignore) still resolves against the file the
76-
// caller named instead of a random staging name. Physical adapters have no
77-
// such option: they are handed a private staging copy inside an owner-only
78-
// sibling directory (including auxiliary files) and keep the destination
79-
// directory as their working directory.
74+
// so a rule written against the file's own path is aimed at the name the
75+
// caller used instead of a random staging name. Aiming the rule at that name
76+
// does not make a path exclusion apply to a file that does not exist yet; see
77+
// formatterCommands. Physical adapters have no such option: they are handed a
78+
// private staging copy inside an owner-only sibling directory (including
79+
// auxiliary files) and keep the destination directory as their working
80+
// directory.
8081
type formatterAdapter struct {
8182
argv []string
8283
// stdin selects the stdin/stdout route. When false the private staging path
@@ -86,14 +87,28 @@ type formatterAdapter struct {
8687
// stdin adapter. It is emitted as "--flag=<target>" and empty when the
8788
// formatter resolves its configuration from the working directory alone.
8889
filenameFlag string
90+
// stdinArg is appended after filenameFlag, and must not also be stored in
91+
// argv: a "-" kept in argv would be emitted before the filename flag.
92+
// For Ruff, "-" serves as an explicit, defensive marker ensuring input is read
93+
// from standard input when invoked as `ruff format --stdin-filename=<path> -`.
94+
// Empty for formatters that read stdin when given no path.
95+
stdinArg string
8996
}
9097

9198
// formatterCommands maps a file extension to its formatter. A missing binary
9299
// silently skips formatting.
100+
//
101+
// PATH EXCLUSIONS DO NOT APPLY ON THE FIRST WRITE OF A NEW FILE. The stdin
102+
// adapters pass the logical destination, but a formatter that consults its
103+
// ignore file only when that path already exists (clang-format and
104+
// .clang-format-ignore) formats a not-yet-created file even when the path
105+
// matches an exclusion. An overwrite still finds the previous file at that
106+
// path, so the same rule can apply. TestFormatOnWritePathIgnoreDoesNotApplyToNewFiles
107+
// pins this split; it is the contract, not an oversight.
93108
var formatterCommands = map[string]formatterAdapter{
94109
".go": {argv: []string{"gofmt"}, stdin: true},
95110
".rs": {argv: []string{"rustfmt"}, stdin: true},
96-
".py": {argv: []string{"ruff", "format", "--quiet"}, stdin: true, filenameFlag: "--stdin-filename"},
111+
".py": {argv: []string{"ruff", "format", "--quiet"}, stdin: true, filenameFlag: "--stdin-filename", stdinArg: "-"},
97112
".ts": {argv: []string{"prettier", "--log-level", "silent"}, stdin: true, filenameFlag: "--stdin-filepath"},
98113
".tsx": {argv: []string{"prettier", "--log-level", "silent"}, stdin: true, filenameFlag: "--stdin-filepath"},
99114
".js": {argv: []string{"prettier", "--log-level", "silent"}, stdin: true, filenameFlag: "--stdin-filepath"},
@@ -142,11 +157,11 @@ func maybeFormatWrittenFile(ctx context.Context, absolutePath string, writtenCon
142157
// maybeFormatWrittenFileScoped runs the configured formatter over a transient
143158
// copy of the written bytes (stdin, or a private staging file) and returns the
144159
// bytes to publish. The destination is never opened or rewritten here. If
145-
// absolutePath does not resolve inside one of the scope's write roots, the
146-
// formatting is refused and the written bytes pass through unchanged: a
147-
// formatter must not be able to read or write outside the same roots the tool
148-
// itself is confined to. The formatter's working directory is pinned inside the
149-
// matched root for the same reason.
160+
// absolutePath does not resolve inside one of the scope's write roots,
161+
// formatting is refused and the written bytes pass through unchanged. That
162+
// refusal is the pre-launch path check in openFormattedFileRoot. The
163+
// formatter's working directory is a path string derived from that check, not
164+
// a directory handle the subprocess is bound to.
150165
func maybeFormatWrittenFileScoped(ctx context.Context, workspaceRoot string, scope PathScope, absolutePath string, writtenContent string) formatOnWriteResult {
151166
unformatted := formatOnWriteResult{Content: writtenContent}
152167
if !formatOnWriteEnabled() {
@@ -172,10 +187,19 @@ func maybeFormatWrittenFileScoped(ctx context.Context, workspaceRoot string, sco
172187
return formatWithStaging(ctx, adapter, binaryPath, absolutePath, writtenContent, workDir)
173188
}
174189

175-
// openFormattedFileRoot resolves absolutePath against the scope's write roots
176-
// and opens the first root that contains it. It returns the descriptor-bound
177-
// root and the path relative to it, or an error when the path lies outside
178-
// every allowed root.
190+
// openFormattedFileRoot checks, before a formatter subprocess is started, that
191+
// absolutePath resolves inside one of the scope's write roots. It resolves
192+
// each configured root, applies a lexical prefix check to the candidate path
193+
// (previously validated by resolveScopedTargetPath), and opens the first root
194+
// containing it. It returns that root and the path relative to it, or an error
195+
// when the path lies outside every allowed root.
196+
//
197+
// The caller does not use the root as a handle for formatter I/O. It joins
198+
// root.Name() with the relative directory and passes that string as
199+
// exec.Cmd.Dir. The check is therefore a pre-launch lexical path check, not
200+
// descriptor-level confinement of the subprocess: a symlink or Windows junction
201+
// swapped onto a component of that directory between the check and process start
202+
// is not excluded, and there is no portable way to hand Cmd.Dir a directory handle.
179203
func openFormattedFileRoot(workspaceRoot string, scope PathScope, absolutePath string) (*os.Root, string, error) {
180204
roots, err := scopedRoots(workspaceRoot, scope)
181205
if err != nil {
@@ -273,6 +297,9 @@ func formatWithStdin(ctx context.Context, adapter formatterAdapter, binaryPath,
273297
if adapter.filenameFlag != "" {
274298
arguments = append(arguments, adapter.filenameFlag+"="+absolutePath)
275299
}
300+
if adapter.stdinArg != "" {
301+
arguments = append(arguments, adapter.stdinArg)
302+
}
276303
formatter := exec.CommandContext(formatCtx, binaryPath, arguments...)
277304
formatter.Dir = dir
278305
formatter.Stdin = strings.NewReader(writtenContent)

‎internal/tools/format_on_write_test.go‎

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,9 @@ func TestFormatOnWritePrettierUsesDestinationFilename(t *testing.T) {
229229
//
230230
// A stdin adapter receives the logical destination through its filename flag,
231231
// so a .clang-format-ignore pattern, an .editorconfig section, or rustfmt's
232-
// ignore still matches "vendor/lib.hintfmt" rather than a random staging name.
232+
// ignore is aimed at "vendor/lib.hintfmt" rather than a random staging name.
233+
// Whether the formatter consults that rule for a path that does not exist yet
234+
// is a separate contract; see TestFormatOnWritePathIgnoreDoesNotApplyToNewFiles.
233235
func TestFormatOnWritePassesLogicalDestinationToStdinFormatter(t *testing.T) {
234236
if runtime.GOOS == "windows" {
235237
t.Skip("fake formatter shim is a POSIX script")
@@ -265,6 +267,85 @@ func TestFormatOnWritePassesLogicalDestinationToStdinFormatter(t *testing.T) {
265267
}
266268
}
267269

270+
// PATH EXCLUSIONS DO NOT APPLY TO A FILE THAT DOES NOT EXIST YET.
271+
//
272+
// clang-format consults .clang-format-ignore only when --assume-filename names
273+
// a path that is already on disk. Formatting runs before publication, so the
274+
// first write of a new file is formatted even when that path would match an
275+
// exclusion. An overwrite still finds the previous file: the formatter prints
276+
// nothing, and the empty-output guard keeps the written bytes. Depositing the
277+
// bytes at the destination before the formatter runs would make the subsequent
278+
// exclusive create fail, so this split is the contract.
279+
func TestFormatOnWritePathIgnoreDoesNotApplyToNewFiles(t *testing.T) {
280+
if runtime.GOOS == "windows" {
281+
t.Skip("fake formatter shim is a POSIX script")
282+
}
283+
t.Setenv("ZERO_FORMAT_ON_WRITE", "1")
284+
installExistenceGatedFormatter(t, ".ignfmt", "ignfmt", "--assume-filename")
285+
286+
dir := t.TempDir()
287+
target := filepath.Join(dir, "vendor", "lib.ignfmt")
288+
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
289+
t.Fatal(err)
290+
}
291+
292+
const written = "unformatted\n"
293+
created := maybeFormatWrittenFile(context.Background(), target, written)
294+
if created.Content != "REFORMATTED\n" {
295+
t.Fatalf("a new file must be formatted even when its path would be ignored, got %q", created.Content)
296+
}
297+
if _, err := os.Stat(target); !os.IsNotExist(err) {
298+
t.Fatalf("formatting a new file must not create it ahead of publication: %v", err)
299+
}
300+
301+
if err := os.WriteFile(target, []byte("previous\n"), 0o644); err != nil {
302+
t.Fatal(err)
303+
}
304+
overwritten := maybeFormatWrittenFile(context.Background(), target, written)
305+
if overwritten.Content != written {
306+
t.Fatalf("an existing ignored path must keep the written bytes, got %q", overwritten.Content)
307+
}
308+
onDisk, err := os.ReadFile(target)
309+
if err != nil || string(onDisk) != "previous\n" {
310+
t.Fatalf("formatter must not rewrite the destination, got %q, %v", onDisk, err)
311+
}
312+
}
313+
314+
// installExistenceGatedFormatter registers a stdin formatter that honours a
315+
// path exclusion only when the assumed path already exists, matching
316+
// clang-format's .clang-format-ignore behaviour (where existing ignored files
317+
// have their input returned unchanged). A missing path is reformatted.
318+
func installExistenceGatedFormatter(t *testing.T, extension, name, filenameFlag string) {
319+
t.Helper()
320+
directory := t.TempDir()
321+
script := "#!/bin/sh\n" +
322+
"path=\n" +
323+
"for arg in \"$@\"; do\n" +
324+
" case \"$arg\" in\n" +
325+
" " + filenameFlag + "=*) path=${arg#" + filenameFlag + "=} ;;\n" +
326+
" esac\n" +
327+
"done\n" +
328+
"if [ -n \"$path\" ] && [ -e \"$path\" ]; then\n" +
329+
" cat\n" +
330+
" exit 0\n" +
331+
"fi\n" +
332+
"cat >/dev/null\n" +
333+
"printf 'REFORMATTED\\n'\n"
334+
if err := os.WriteFile(filepath.Join(directory, name), []byte(script), 0o755); err != nil {
335+
t.Fatal(err)
336+
}
337+
t.Setenv("PATH", directory+string(os.PathListSeparator)+os.Getenv("PATH"))
338+
previous, existed := formatterCommands[extension]
339+
formatterCommands[extension] = formatterAdapter{argv: []string{name}, stdin: true, filenameFlag: filenameFlag}
340+
t.Cleanup(func() {
341+
if existed {
342+
formatterCommands[extension] = previous
343+
return
344+
}
345+
delete(formatterCommands, extension)
346+
})
347+
}
348+
268349
// THE PRODUCTION TABLE MUST CARRY THE HINTS, NOT ONLY THE TEST SEAM.
269350
func TestFormatterFilenameHints(t *testing.T) {
270351
want := map[string]string{
@@ -676,6 +757,68 @@ func installFakePhysicalFormatter(t *testing.T, extension, name, record string)
676757
})
677758
}
678759

760+
// RUFF MUST READ STDIN WITH AN EXPLICIT TERMINAL MARKER.
761+
//
762+
// The stdin route passes an explicit "-" marker after --stdin-filename,
763+
// ensuring Ruff strictly reads input from stdin as documented.
764+
func TestFormatOnWriteRuffPassesTerminalStdinMarker(t *testing.T) {
765+
t.Setenv("ZERO_FORMAT_ON_WRITE", "1")
766+
adapter := formatterCommands[".py"]
767+
if !adapter.stdin || adapter.filenameFlag != "--stdin-filename" || adapter.stdinArg != "-" {
768+
t.Fatalf("ruff adapter = %+v, want stdin with --stdin-filename and stdinArg %q", adapter, "-")
769+
}
770+
771+
directory := t.TempDir()
772+
binaryName := "ruff" + formatterScriptExtension()
773+
script := "#!/bin/sh\ncat\n"
774+
if runtime.GOOS == "windows" {
775+
script = "@echo off\r\nexit /b 0\r\n"
776+
}
777+
if err := os.WriteFile(filepath.Join(directory, binaryName), []byte(script), 0o755); err != nil {
778+
t.Fatal(err)
779+
}
780+
t.Setenv("PATH", directory+string(os.PathListSeparator)+os.Getenv("PATH"))
781+
782+
var seen *exec.Cmd
783+
previous := formatterCommandObserver
784+
formatterCommandObserver = func(command *exec.Cmd) { seen = command }
785+
t.Cleanup(func() { formatterCommandObserver = previous })
786+
787+
dir := t.TempDir()
788+
target := filepath.Join(dir, "pkg", "new_file.py")
789+
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
790+
t.Fatal(err)
791+
}
792+
_ = maybeFormatWrittenFile(context.Background(), target, "x=1\n")
793+
if seen == nil {
794+
t.Fatal("ruff was not executed")
795+
}
796+
args := seen.Args
797+
if len(args) == 0 || args[len(args)-1] != "-" {
798+
t.Fatalf("ruff execution args %q must end with the stdin marker \"-\"", args)
799+
}
800+
dashes := 0
801+
flagAt := -1
802+
wantFlag := "--stdin-filename=" + target
803+
for i, arg := range args {
804+
if arg == "-" {
805+
dashes++
806+
}
807+
if arg == wantFlag {
808+
flagAt = i
809+
}
810+
if arg == target {
811+
t.Fatalf("ruff execution args %q pass the path as a file operand; the stdin route must not", args)
812+
}
813+
}
814+
if dashes != 1 {
815+
t.Fatalf("ruff execution args %q must contain exactly one stdin marker", args)
816+
}
817+
if flagAt < 0 || flagAt >= len(args)-1 {
818+
t.Fatalf("ruff execution args %q must carry %q before the terminal \"-\"", args, wantFlag)
819+
}
820+
}
821+
679822
func TestFormatOnWriteRuffUsesLogicalDestinationForWriteAndEdit(t *testing.T) {
680823
if _, err := exec.LookPath("ruff"); err != nil {
681824
t.Skip("ruff not installed")

0 commit comments

Comments
 (0)