-
Notifications
You must be signed in to change notification settings - Fork 246
Ability To Turn Off Formatting For Subdirectory #873
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for looking into this! I think it's a useful feature to have. Some high-level comments:
-
I think the name of the file should be
.swift-format-ignore
, which aligns nicely with the name used in comments that the formatter supports, and also similar tools like.gitignore
and.prettierignore
. -
Lowering this down into the
SwiftFormatter
andSwiftLinter
APIs feels like the wrong approach, because it means the tool is still doing all the work of iterating over and parsing all the files that are going to be ignored anyway.
For the second point, raising this check into the frontend seems more appropriate. Maybe FileIterator
should be responsible for looking for the .swift-format-ignore
file in the directory or its parents, and if one is found, it doesn't recurse into the directory further? That may end up being simpler to implement (you don't have to touch the configuration at all).
It means that somebody using the SwiftFormatter
/SwiftLinter
APIs directly wouldn't get that logic, but I think that's fine; those APIs are meant to be used by clients who want to format something directly and supplying a configuration in memory, and they're not interested in the search logic used by the frontend.
Yes, makes sense 👍 |
I think I was fixated on the idea that the ignoring mode needed to be configurable from within a I'll try your plan instead. |
Ok, I got rid of the setting and moved the check into FileIterator's directory handling. It works well for input directories, and for subdirectories reached via recursion, but doesn't work for input files (e.g: in the case where individual file paths are specified on the command line directly). For input files, we need to perform a check for |
That makes the most sense to me. |
9851d7a
to
bf42ce7
Compare
Ok, 'tis a little closer now. One question is what it should do if The README still says that the filesystem won't be searched; which is true for The path of least resistance is to clarify the README, but that depends whether that's the behaviour we want. Alternatively I'll need to add a flag to FileIterator to indicate whether or not it should check for the ignore file. Which is perfectly doable, of course. |
The implication of that statement is that the filesystem won't be searched for the configuration (i.e., If we want a way to ignore the ignore file, we could add something like a |
Yeah, I was thinking the same. I'll reword the docs. |
repeat { | ||
containingDirectory.deleteLastPathComponent() | ||
let candidateFile = containingDirectory.appendingPathComponent(FileIterator.ignoreFileName) | ||
if FileManager.default.isReadableFile(atPath: candidateFile.path) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More than just checking for the presence of the file, I think we go the extra step of validating that it only contains *
so that users aren't surprised later when we add more functionality and it subtly changes the behavior based on those contents. If it contains anything else, we should throw an error saying it's the only supported form (and turn that into a nice human-readable error message when we exit).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to upgrade to gitignore like sytanx in the future, should we make the default value **/*
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK, *
is sufficient because according to the documentation:
- "An asterisk '
*
' matches anything except a slash." - "If there is a separator at the beginning or middle (or both) of the pattern, [...]. Otherwise the pattern may also match at any level below the
.gitignore
level." - "If there is a separator at the end of the pattern then the pattern will only match directories, otherwise the pattern can match both files and directories."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, got distracted, but am looking at this now.
One complication is that FileIterator
doesn't throw, and instead relies on the caller to generate diagnostics for non-existent paths.
In order to report invalid ignore files inside FileIterator
we'd either need to be able to:
- throw an error from it
- be able to emit diagnostics directly from it
- return the path of the invalid ignore file (or a special pseudo-path), as if they were to be processed, then spot that path in the front end and emit an "invalid ignore file" diagnostic
I've gone for the third option, but it feels like a bit of a hack.
Actually I've realised that the current design won't work as written, because when FileIterator encounters a directory, it collapses all subdirectories below it, and therefore will not check for the presence of nested .swift-format-ignore files. FileIterator will need to be rewritten to check for an ignore file when entering a directory, and to pass that instance down to any subdirectories recursively. |
I'm happy to do this, but it raises a couple of design questions:
|
Slightly annoyingly, my original naive implementation was simple and worked fine, since it wasn't the front end doing the work. I don't think it would be unreasonable to go with that simpler approach as the first iteration, and then refine it subsequently -- perfect being the enemy of good and all that. That said, I'm happy to look at reworking FileIterator instead. |
In other news, the current main branch (ed01028, without my changes) will not build with my version of Swift:
(and a bunch of other errors) |
fwiw git bisect tells me that the problematic commit is c3b0c9f |
Doing this in the frontend is still the right approach; the API layer shouldn't care about
I think the answer has to be the actual (physical) parent. Otherwise you could end up with different ignore states for the same file depending on how you got to it, which wouldn't be desirable. This does make the implementation slightly more complicated, but I think it would be workable to have
This sounds like a nice optimization! We flatten the iterator into an array in the frontend to do parallel processing so we don't want to have the full The same caveat about which parent to use applies here as above. For symlinks, we'd want the configuration we choose to be the one from the physical parent, not the logical parent, because we don't want files to be formatted differently based on how you got there.
Is your swift-syntax checkout out of date? The change you referenced later was made to support an API change merged a few days ago in swift-syntax. |
Hmm, I don't have SWIFTCI_USE_LOCAL_DEPS set, so it should just be using what the package specifies. Unfortunately that is just the main branch, which I think is the problem. Some combination of If it can't be pinned to a version, wouldn't it be better to pin it to an actual commit? |
The |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay since my last review and thanks a lot for continuing to push this forward. I like the overall design and like the new tests that create the test files in a temporary directory. I left a few comments inline.
return nil | ||
} | ||
|
||
var containingDirectory = url.absoluteURL.standardized |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to standardize the URL here? Say we have a directory structure like
/
LibA
LibA.swift
LibB
Externals
.swift-format-ignore = "*"
LibA -> /LibA
Then, when running swift-format on /LibB/Externals/LibA/LibA.swift
, I would expect formatting to be skipped because of /LibB/Externals/.swift-format-ignore
. But with the current logic, we would standardize /LibB/Externals/LibA/LibA.swift
to /LibA/LibA.swift
in here and thus not find the .swift-format-ignore
file.
if FileManager.default.isReadableFile(atPath: url.path) { | ||
try self.init(contentsOf: url) | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be able to skip this file system access and just check if self.init(contentsOf:)
returns a non-nil value. Just trying to avoid race conditions where the file exists here but is gone by the time that self.init(contentsOf:)
tries to read it.
if FileManager.default.isReadableFile(atPath: url.path) { | |
try self.init(contentsOf: url) | |
return | |
} | |
if let ignoreFile = try self.init(contentsOf: url) { | |
self = ignoreFile | |
return | |
} |
} catch IgnoreFile.Error.invalidFile(let url, _) { | ||
// we hit an invalid ignore file | ||
// we return the path of the ignore file so that we can report an error | ||
// and process the directory as normal | ||
output = url | ||
} catch { | ||
// we hit another unexpected error; process the directory as normal | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to set output = url
in the IgnoreFile.Error.invalidFile
case but not in the generic error case?
return ignoreFile?.shouldProcess(url) ?? true | ||
} | ||
|
||
fileprivate extension URL { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use isRoot
from URL+isRoot.swift
instead of defining a new implementation of it here?
@@ -0,0 +1,126 @@ | |||
@_spi(Internal) import SwiftFormat |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a copyright header to this test file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this file still needed?
override func tearDown() { | ||
// Clean up any test tree after each test. | ||
if let testTreeURL { | ||
// try? FileManager.default.removeItem(at: testTreeURL) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No longer needed?
func makeTempTree(_ tree: TestTree) throws -> URL { | ||
if let testTreeURL { | ||
try? FileManager.default.removeItem(at: testTreeURL) | ||
} | ||
let tempDir = FileManager.default.temporaryDirectory | ||
let tempURL = tempDir.appendingPathComponent(UUID().uuidString) | ||
try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true) | ||
try writeTree(tree, to: tempURL) | ||
testTreeURL = tempURL | ||
return tempURL | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about making this a withTempTree
function that deletes the temporary directory after it executes the tests? Otherwise these trees will be left as artifacts. Ie something like
func makeTempTree(_ tree: TestTree) throws -> URL { | |
if let testTreeURL { | |
try? FileManager.default.removeItem(at: testTreeURL) | |
} | |
let tempDir = FileManager.default.temporaryDirectory | |
let tempURL = tempDir.appendingPathComponent(UUID().uuidString) | |
try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true) | |
try writeTree(tree, to: tempURL) | |
testTreeURL = tempURL | |
return tempURL | |
} | |
private func withTempTree(_ tree: TestTree, _ body: (URL) throws -> Void) throws { | |
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("swift-format-test-scratch").appendingPathComponent(UUID().uuidString) | |
defer { | |
try? FileManager.default.removeItem(at: tempURL) | |
} | |
try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true) // I would move this to `writeTree` | |
try writeTree(tree, to: tempURL) | |
body(tempURL) | |
} |
func writeTree(_ tree: TestTree, to root: URL) throws { | ||
switch tree { | ||
case let .file(name, contents): | ||
print("Writing file \(name) to \(root)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leftover debug output?
} | ||
|
||
/// Write a file or directory tree to the given root URL. | ||
func writeTree(_ tree: TestTree, to root: URL) throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make this private
func writeTree(_ tree: TestTree, to root: URL) throws { | |
private func writeTree(_ tree: TestTree, to root: URL) throws { |
Draft implementation of #870.
This implementation adds a check for a
.swift-format-ignore
file, in the same places that a.swift-format
file could be.If present, this file potentially contains gitignore-style rules describing which files to ignore.
Currently the only rule supported is
*
, to ignore everything in the containing directory and any subdirectories. Later, full pattern-matching rules could be implemented, if desired. For now, we simply check that the content of the file is*
, to ease any future migration problems.The presence of
.swift-format-ignore
is checked first at any given directory level. Even if a.swift-format
file also exists at the same level, the ignore file takes precedence.Motivation
The use case which motivated this change is a scenario where my workspace has a
.swift-format
file, and I've turned on format-on-save, but I also have submodules containing third-party code.If I make a small edit to a file in a submodule, it is formatted using the global workspace settings. This generates a large diff unrelated to my edit, as the submodule is hand-formatted using different rules.
I want to be able to disable formatting for specific submodules on my local machine, whilst retaining formatting for the rest of the workspace.