Skip to content

Commit 7365d3e

Browse files
committed
Add minimal IgnoreFile abstraction.
1 parent df4a432 commit 7365d3e

File tree

3 files changed

+109
-18
lines changed

3 files changed

+109
-18
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
public class IgnoreFile {
16+
/// Name of the ignore file to look for.
17+
/// The presence of this file in a directory will cause the formatter
18+
/// to skip formatting files in that directory and its subdirectories.
19+
fileprivate static let fileName = ".swift-format-ignore"
20+
21+
/// Errors that can be thrown by the IgnoreFile initializer.
22+
public enum Error: Swift.Error {
23+
/// Error thrown when an ignore file has invalid content.
24+
case invalidContent(URL)
25+
}
26+
27+
/// Create an instance from the contents of the file at the given URL.
28+
/// Throws an error if the file content can't be read, or is not valid.
29+
public init(contentsOf url: URL) throws {
30+
let content = try String(contentsOf: url, encoding: .utf8)
31+
guard content.trimmingCharacters(in: .whitespacesAndNewlines) == "*" else {
32+
throw Error.invalidContent(url)
33+
}
34+
}
35+
36+
/// Create an instance for the given directory, if a valid
37+
/// ignore file with the standard name is found in that directory.
38+
/// Returns nil if no ignore file is found.
39+
/// Throws an error if an invalid ignore file is found.
40+
///
41+
/// Note that this initializer does not search parent directories for ignore files.
42+
public convenience init?(forDirectory directory: URL) throws {
43+
let url = directory.appendingPathComponent(IgnoreFile.fileName)
44+
guard FileManager.default.isReadableFile(atPath: url.path) else {
45+
return nil
46+
}
47+
48+
try self.init(contentsOf: url)
49+
}
50+
51+
/// Create an instance to use for the given URL.
52+
/// We search for an ignore file starting from the given URL's container,
53+
/// and moving up the directory tree, until we reach the root directory.
54+
/// Returns nil if no ignore file is found.
55+
/// Throws an error if an invalid ignore file is found somewhere
56+
/// in the directory tree.
57+
public convenience init?(for url: URL) throws {
58+
var containingDirectory = url.absoluteURL.standardized
59+
repeat {
60+
containingDirectory.deleteLastPathComponent()
61+
let url = containingDirectory.appendingPathComponent(IgnoreFile.fileName)
62+
if FileManager.default.isReadableFile(atPath: url.path) {
63+
try self.init(contentsOf: url)
64+
return
65+
}
66+
} while !containingDirectory.isRoot
67+
return nil
68+
}
69+
70+
/// Should the given URL be processed?
71+
/// Currently the only valid ignore file content is "*",
72+
/// which means that all files should be ignored.
73+
func shouldProcess(_ url: URL) -> Bool {
74+
return false
75+
}
76+
77+
/// Returns true if the name of the given URL matches
78+
/// the standard ignore file name.
79+
public static func isStandardIgnoreFile(_ url: URL) -> Bool {
80+
return url.lastPathComponent == fileName
81+
}
82+
}

Sources/SwiftFormat/Utilities/FileIterator.swift

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@ import WinSDK
2121
@_spi(Internal)
2222
public struct FileIterator: Sequence, IteratorProtocol {
2323

24-
/// Name of the ignore file to look for.
25-
/// The presence of this file in a directory will cause the formatter
26-
/// to skip formatting files in that directory and its subdirectories.
27-
fileprivate static let ignoreFileName = ".swift-format-ignore"
28-
2924
/// List of file and directory URLs to iterate over.
3025
private let urls: [URL]
3126

@@ -97,8 +92,18 @@ public struct FileIterator: Sequence, IteratorProtocol {
9792
fallthrough
9893

9994
case .typeDirectory:
100-
let ignoreFile = next.appendingPathComponent(Self.ignoreFileName)
101-
if FileManager.default.fileExists(atPath: ignoreFile.path) {
95+
do {
96+
if let ignoreFile = try IgnoreFile(forDirectory: next), !ignoreFile.shouldProcess(next) {
97+
// skip this directory and its subdirectories if it should be ignored
98+
continue
99+
}
100+
} catch IgnoreFile.Error.invalidContent(let url) {
101+
// we hit an invalid ignore file
102+
// we skip the directory, but return the path of the ignore file
103+
// so that we can report an error
104+
output = url
105+
} catch {
106+
// we hit another unexpected error; just skip the directory
102107
continue
103108
}
104109

@@ -192,20 +197,17 @@ private func fileType(at url: URL) -> FileAttributeType? {
192197

193198
/// Returns true if the file should be processed.
194199
/// Directories are always processed.
195-
/// Other files are processed if there is not a
196-
/// ignore file in the containing directory or any of its parents.
200+
/// For other files, we look for an ignore file in the containing
201+
/// directory or any of its parents.
202+
/// If there is no ignore file, we process the file.
203+
/// If an ignore file is found, we consult it to see if the file should be processed.
204+
/// An invalid ignore file is treated here as if it does not exist, but
205+
/// will be reported as an error when we try to process the directory.
197206
private func inputShouldBeProcessed(at url: URL) -> Bool {
198207
guard fileType(at: url) != .typeDirectory else {
199208
return true
200209
}
201210

202-
var containingDirectory = url.absoluteURL.standardized
203-
repeat {
204-
containingDirectory.deleteLastPathComponent()
205-
let candidateFile = containingDirectory.appendingPathComponent(FileIterator.ignoreFileName)
206-
if FileManager.default.isReadableFile(atPath: candidateFile.path) {
207-
return false
208-
}
209-
} while !containingDirectory.isRoot
210-
return true
211+
let ignoreFile = try? IgnoreFile(for: url)
212+
return ignoreFile?.shouldProcess(url) ?? true
211213
}

Sources/swift-format/Frontend/Frontend.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ class Frontend {
165165
/// Read and prepare the file at the given path for processing, optionally synchronizing
166166
/// diagnostic output.
167167
private func openAndPrepareFile(at url: URL) -> FileToProcess? {
168+
guard !IgnoreFile.isStandardIgnoreFile(url) else {
169+
diagnosticsEngine.emitError(
170+
"Invalid ignore file \(url.relativePath): currently the only supported content for ignore files is a single asterisk `*`, which matches all files."
171+
)
172+
return nil
173+
}
174+
168175
guard let sourceFile = try? FileHandle(forReadingFrom: url) else {
169176
diagnosticsEngine.emitError(
170177
"Unable to open \(url.relativePath): file is not readable or does not exist"

0 commit comments

Comments
 (0)