Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 93 additions & 3 deletions Documentation/IgnoringSource.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ representation is ignored by the formatter.

## Ignore A File

In the event that an entire file cannot be formatted, add a comment that contains
`swift-format-ignore-file` at the top of the file and the formatter will leave
In the event that an entire file cannot be formatted, add a comment that contains
`swift-format-ignore-file` at the top of the file and the formatter will leave
the file completely unchanged.

```swift
Expand Down Expand Up @@ -90,7 +90,7 @@ struct Foo {
}
```
In this case, only the DoNotUseSemicolons and FullyIndirectEnum rules are disabled
throughout the file, while all other formatting rules (such as line breaking and
throughout the file, while all other formatting rules (such as line breaking and
indentation) remain active.

## Understanding Nodes
Expand All @@ -113,3 +113,93 @@ top level nodes that support suppressing formatting are:
- Any member declaration inside of a declaration (e.g. properties and
functions declared inside of a struct/class/enum). All code nested
syntactically inside of the ignored node is also ignored by the formatter.

## File-Based Ignoring with `.swift-format-ignore`

In addition to the comment-based ignoring described above, `swift-format` supports
file-based ignoring using `.swift-format-ignore` files. These files allow you to
specify patterns for files and directories that should be completely excluded from
formatting and linting operations.

### `.swift-format-ignore` File Format

`.swift-format-ignore` files use the same pattern syntax as `.gitignore` files:

```
# This is a comment
*.generated.swift
build/
**/Generated/
!important.swift
src/**/test*.swift
```

### Pattern Syntax

The `.swift-format-ignore` files supports the following pattern syntax:

- Simple patterns: `file.swift` matches any file named `file.swift` anywhere in the tree
- Wildcard patterns: `*.swift` matches all files ending with `.swift`
- Directory patterns: `build/` matches the `build` directory and all files within it
- Nested wildcards: `**/*.generated` matches files with `.generated` extension at any depth
- Negation patterns: `!important.swift` excludes `important.swift` from being ignored (even if matched by other patterns)
- Absolute patterns: `/root.swift` matches `root.swift` only at the project root
- Complex patterns: `src/**/test*.swift` matches files starting with `test` and ending with `.swift` anywhere under `src/`

### File Discovery and Precedence

`swift-format` searches for `.swift-format-ignore` files by walking up the directory
tree from each source file being processed. Multiple ignore files can exist in a
project, with the following precedence rules:

1. Closest wins: Ignore files closer to the source file take precedence
2. Directory traversal: The tool searches from the file's directory up to the project root
3. Pattern evaluation: Within each ignore file, patterns are evaluated in order
4. Negation support: Later negation patterns (`!pattern`) can override earlier ignore patterns

### Example Project Structure

```
project/
├── .swift-format-ignore # Root ignore file
├── src/
│ ├── .swift-format-ignore # Overrides root for src/ directory
│ ├── main.swift
│ └── generated/
│ └── Generated.swift
├── tests/
│ └── test.swift
└── build/
└── output.swift
```

**Root `.swift-format-ignore`:**
```
# Ignore all generated files
*.generated.swift
build/
```

**`src/.swift-format-ignore`:**
```
# Additional rules for src directory
generated/
!important.generated.swift
```

### Integration with swift-format Commands

The `.swift-format-ignore` functionality works automatically with all `swift-format` commands:

```bash
# Format all files except those matching ignore patterns
swift-format --recursive src/

# Lint all files except those matching ignore patterns
swift-format lint --recursive src/

# Both commands will automatically discover and respect .swift-format-ignore files
```

Files matched by `.swift-format-ignore` patterns are completely excluded from processing -
they will not be formatted, linted, or appear in any output.
2 changes: 2 additions & 0 deletions Sources/SwiftFormat/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ add_library(SwiftFormat
Rules/UseWhereClausesInForLoops.swift
Rules/ValidateDocumentationComments.swift
Utilities/FileIterator.swift
Utilities/GitIgnorePattern.swift
Utilities/IgnoreManager.swift
Utilities/URL+isRoot.swift)
target_link_libraries(SwiftFormat PUBLIC
SwiftMarkdown::Markdown
Expand Down
42 changes: 38 additions & 4 deletions Sources/SwiftFormat/Utilities/FileIterator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,42 @@ public struct FileIterator: Sequence, IteratorProtocol {
/// The file extension to check for when recursing through directories.
private let fileSuffix = ".swift"

/// Optional ignore manager for filtering files based on .swift-format-ignore files.
private let ignoreManager: IgnoreManager

/// Create a new file iterator over the given list of file URLs.
///
/// The given URLs may be files or directories. If they are directories, the iterator will recurse
/// into them. Symlinks are never followed on Windows platforms as Foundation doesn't support it.
/// - Parameters:
/// - urls: `Array` of files or directories to iterate.
/// - followSymlinks: `Bool` to indicate if symbolic links should be followed when iterating.
/// - workingDirectory: `URL` that indicates the current working directory. Used for testing.
public init(urls: [URL], followSymlinks: Bool, workingDirectory: URL = URL(fileURLWithPath: ".")) {
/// - followSymlinks: `Bool` to indicate if symbolic links sthe current working directory. Used for testing.
/// - ignoreManager: Optional `IgnoreManager`hould be followed when iterating.
/// - workingDirectory: `URL` that indicates to filter files based on .swift-format-ignore files.
package init(
urls: [URL],
followSymlinks: Bool,
workingDirectory: URL = URL(fileURLWithPath: "."),
ignoreManager: IgnoreManager
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ignoreManager: IgnoreManager
ignoreManager: IgnoreManager = IgnoreManager()

then we don't need the additional init(urls:followSymlinks:workingDirectory:) below. Though I still not sure if this needs to be injectable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept this here in the event we want to have dependency injection testing. I will change the access modifier to package

) {
self.workingDirectory = workingDirectory
self.urls = urls
self.urlIterator = self.urls.makeIterator()
self.followSymlinks = followSymlinks
self.ignoreManager = ignoreManager
}

public init(
urls: [URL],
followSymlinks: Bool,
workingDirectory: URL = URL(fileURLWithPath: ".")
) {
self.init(
urls: urls,
followSymlinks: followSymlinks,
workingDirectory: workingDirectory,
ignoreManager: IgnoreManager()
)
}

/// Iterate through the "paths" list, and emit the file paths in it. If we encounter a directory,
Expand All @@ -85,6 +108,9 @@ public struct FileIterator: Sequence, IteratorProtocol {
continue

case .typeDirectory:
if self.ignoreManager.shouldIgnore(file: next, isDirectory: true) {
continue
}
dirIterator = FileManager.default.enumerator(
at: next,
includingPropertiesForKeys: nil,
Expand Down Expand Up @@ -145,7 +171,15 @@ public struct FileIterator: Sequence, IteratorProtocol {
} else {
relativePath = path
}
output = URL(fileURLWithPath: relativePath, isDirectory: false, relativeTo: workingDirectory)
let fileURL = URL(fileURLWithPath: relativePath, isDirectory: false, relativeTo: workingDirectory)

// Apply ignore filtering
if self.ignoreManager.shouldIgnore(file: item, isDirectory: false) {
output = nil
continue // Skip this file and continue to next
}
Comment on lines +176 to +180
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be checked in next() again. I don't think we need this check here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in next(), we check whether directories should be ignored. Here, we are checking whether files should be ignored. Both are needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You removed the another check for files in next().
That means swift-format path/to/File.swift would perform formatting the specified file even if that file is .swift-format-ignore. Is that the desired behavior?

If so, what about the directories? E.g. swift-format --recursive path/to/Sources/MyLibrary/generated/ Should this directory be formatted even if it's ignored in .swift-format-ignore in the parent directories?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a file matches a patter found in .swift-format-ignore file, it should not be formatted unless the pattern is negated.

A patter in .swift-format-ignore can apply to files, or to directories.


output = fileURL

default:
break
Expand Down
Loading
Loading