@@ -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.
8081type 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.
93108var 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 .
150165func 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.
179203func 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 )
0 commit comments