From eb29cb847cc4d37b728879c34452c5c0e965be3e Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 10:43:45 +0000 Subject: [PATCH 01/26] Initial stab at option to disable all formatting. --- Sources/SwiftFormat/API/Configuration.swift | 29 +++++++++++++++++++- Sources/swift-format/Frontend/Frontend.swift | 5 ++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 70ac916aa..42b5f176c 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -24,8 +24,19 @@ internal let highestSupportedConfigurationVersion = 1 /// Holds the complete set of configured values and defaults. public struct Configuration: Codable, Equatable { + /// Name of the configuration file to look for. + /// The presence of this file in a directory will cause the formatter + /// to use the configuration specified in that file. + private static let configurationFileName = ".swift-format" + + /// Name of the suppression file to look for. + /// The presence of this file in a directory will cause the formatter + /// to skip formatting files in that directory and its subdirectories. + private static let suppressionFileName = ".no-swift-format" + private enum CodingKeys: CodingKey { case version + case allDisabled case maximumBlankLines case lineLength case spacesBeforeEndOfLineComments @@ -60,6 +71,9 @@ public struct Configuration: Codable, Equatable { /// MARK: Common configuration + /// Is all formatting disbled? + public var allDisabled: Bool + /// The dictionary containing the rule names that we wish to run on. A rule is not used if it is /// marked as `false`, or if it is missing from the dictionary. public var rules: [String: Bool] @@ -270,6 +284,11 @@ public struct Configuration: Codable, Equatable { /// Creates a new `Configuration` by loading it from a configuration file. public init(contentsOf url: URL) throws { + if url.lastPathComponent == Self.suppressionFileName { + self = Configuration(allDisabled: true) + return + } + let data = try Data(contentsOf: url) try self.init(data: data) } @@ -303,6 +322,9 @@ public struct Configuration: Codable, Equatable { // default-initialized instance. let defaults = Configuration() + self.allDisabled = + try container.decideIfPresent(Bool.self, forKey: .allDisabled) + ?? false self.maximumBlankLines = try container.decodeIfPresent(Int.self, forKey: .maximumBlankLines) ?? defaults.maximumBlankLines @@ -396,6 +418,7 @@ public struct Configuration: Codable, Equatable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(version, forKey: .version) + try container.encode(allDisabled, forKey: .allDisabled) try container.encode(maximumBlankLines, forKey: .maximumBlankLines) try container.encode(lineLength, forKey: .lineLength) try container.encode(spacesBeforeEndOfLineComments, forKey: .spacesBeforeEndOfLineComments) @@ -440,10 +463,14 @@ public struct Configuration: Codable, Equatable { } repeat { candidateDirectory.deleteLastPathComponent() - let candidateFile = candidateDirectory.appendingPathComponent(".swift-format") + let candidateFile = candidateDirectory.appendingPathComponent(Self.configurationFileName) if FileManager.default.isReadableFile(atPath: candidateFile.path) { return candidateFile } + let suppressingFile = candidateDirectory.appendingPathComponent(Self.suppressionFileName) + if FileManager.default.isReadableFile(atPath: suppressingFile.path) { + return suppressingFile + } } while !candidateDirectory.isRoot return nil diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index a3ea18a4f..fa6eb5063 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -182,6 +182,11 @@ class Frontend { return nil } + guard !configuration.allDisabled else { + // Formatting is suppressed for this file. + return nil + } + return FileToProcess( fileHandle: sourceFile, url: url, From 356b55f596946c26e49906528c80893b19a11100 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 10:55:58 +0000 Subject: [PATCH 02/26] Updated config documentation in README. --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index db2e090a9..4a9b26a7f 100644 --- a/README.md +++ b/README.md @@ -202,8 +202,14 @@ configuration, by redirecting it to a file and editing it. For any source file being checked or formatted, `swift-format` looks for a JSON-formatted file named `.swift-format` in the same directory. If one is -found, then that file is loaded to determine the tool's configuration. If the -file is not found, then it looks in the parent directory, and so on. +found, then that file is loaded to determine the tool's configuration. + +If the file is not found, then it looks in the same directory for a file +called `.no-swift-format`. The presence of this file, if found, will +disable all formatting (the contents of the file are ignored). + +If neither file is found, the search for files continues in the parent +directory, and so on. If no configuration file is found, a default configuration is used. The settings in the default configuration can be viewed by running From 021d6b3cf8215e464f635c1a32c652a458ae7ae3 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 11:00:42 +0000 Subject: [PATCH 03/26] Fixed typo, initialiser. --- Sources/SwiftFormat/API/Configuration.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 42b5f176c..527c8a603 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -72,7 +72,7 @@ public struct Configuration: Codable, Equatable { /// MARK: Common configuration /// Is all formatting disbled? - public var allDisabled: Bool + public var allDisabled = false /// The dictionary containing the rule names that we wish to run on. A rule is not used if it is /// marked as `false`, or if it is missing from the dictionary. @@ -285,7 +285,9 @@ public struct Configuration: Codable, Equatable { /// Creates a new `Configuration` by loading it from a configuration file. public init(contentsOf url: URL) throws { if url.lastPathComponent == Self.suppressionFileName { - self = Configuration(allDisabled: true) + var config = Configuration() + config.allDisabled = true + self = config return } @@ -323,7 +325,7 @@ public struct Configuration: Codable, Equatable { let defaults = Configuration() self.allDisabled = - try container.decideIfPresent(Bool.self, forKey: .allDisabled) + try container.decodeIfPresent(Bool.self, forKey: .allDisabled) ?? false self.maximumBlankLines = try container.decodeIfPresent(Int.self, forKey: .maximumBlankLines) From 3e4c5d20a5a16d5e6aea0006c51a9000463e82e6 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 11:06:49 +0000 Subject: [PATCH 04/26] Documented allDisabled key --- Documentation/Configuration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index 14072d84f..2267319f8 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -11,6 +11,9 @@ top-level keys and values: * `version` _(number)_: The version of the configuration file. For now, this should always be `1`. +* `allDisabled` _(boolean)_: If this is true, all other configuration + options are ignored, and formatting is disabled. + * `lineLength` _(number)_: The maximum allowed length of a line, in characters. From 8bade837bf2585c901f9e80e1e691e679713eb0b Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 11:37:28 +0000 Subject: [PATCH 05/26] Emit unmodified source if allDisabled is true. --- Sources/SwiftFormat/API/SwiftFormatter.swift | 6 ++++++ Sources/swift-format/Frontend/Frontend.swift | 5 ----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftFormat/API/SwiftFormatter.swift b/Sources/SwiftFormat/API/SwiftFormatter.swift index ddd0bbe2c..bfabc9477 100644 --- a/Sources/SwiftFormat/API/SwiftFormatter.swift +++ b/Sources/SwiftFormat/API/SwiftFormatter.swift @@ -111,6 +111,12 @@ public final class SwiftFormatter { // also does not touch an empty file even if the setting to add trailing newlines is enabled.) guard !source.isEmpty else { return } + // If allDisabled is set, just emit the source as-is. + guard !configuration.allDisabled else { + outputStream.write(source) + return + } + let sourceFile = try parseAndEmitDiagnostics( source: source, operatorTable: .standardOperators, diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index fa6eb5063..a3ea18a4f 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -182,11 +182,6 @@ class Frontend { return nil } - guard !configuration.allDisabled else { - // Formatting is suppressed for this file. - return nil - } - return FileToProcess( fileHandle: sourceFile, url: url, From aac6fe8deb71ca6d225adb2f51ff08a020332aa3 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 11:38:53 +0000 Subject: [PATCH 06/26] Don't lint if allDisabled is set. --- Sources/SwiftFormat/API/SwiftLinter.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/SwiftFormat/API/SwiftLinter.swift b/Sources/SwiftFormat/API/SwiftLinter.swift index cfc4639f6..871d0f61d 100644 --- a/Sources/SwiftFormat/API/SwiftLinter.swift +++ b/Sources/SwiftFormat/API/SwiftLinter.swift @@ -99,6 +99,11 @@ public final class SwiftLinter { // also does not touch an empty file even if the setting to add trailing newlines is enabled.) guard !source.isEmpty else { return } + // If allDisabled is set, do nothing. + guard !configuration.allDisabled else { + return + } + let sourceFile = try parseAndEmitDiagnostics( source: source, operatorTable: .standardOperators, From a7d4c246eef7fca74358a5f6cc27b5e99fc291fb Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 11:43:17 +0000 Subject: [PATCH 07/26] Change flag to skipAll. --- Documentation/Configuration.md | 2 +- Sources/SwiftFormat/API/Configuration.swift | 12 ++++++------ Sources/SwiftFormat/API/SwiftFormatter.swift | 4 ++-- Sources/SwiftFormat/API/SwiftLinter.swift | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index 2267319f8..dc2eb86d0 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -11,7 +11,7 @@ top-level keys and values: * `version` _(number)_: The version of the configuration file. For now, this should always be `1`. -* `allDisabled` _(boolean)_: If this is true, all other configuration +* `skipAll` _(boolean)_: If this is true, all other configuration options are ignored, and formatting is disabled. * `lineLength` _(number)_: The maximum allowed length of a line, in diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 527c8a603..3f2b4d818 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -36,7 +36,7 @@ public struct Configuration: Codable, Equatable { private enum CodingKeys: CodingKey { case version - case allDisabled + case skipAll case maximumBlankLines case lineLength case spacesBeforeEndOfLineComments @@ -72,7 +72,7 @@ public struct Configuration: Codable, Equatable { /// MARK: Common configuration /// Is all formatting disbled? - public var allDisabled = false + public var skipAll = false /// The dictionary containing the rule names that we wish to run on. A rule is not used if it is /// marked as `false`, or if it is missing from the dictionary. @@ -286,7 +286,7 @@ public struct Configuration: Codable, Equatable { public init(contentsOf url: URL) throws { if url.lastPathComponent == Self.suppressionFileName { var config = Configuration() - config.allDisabled = true + config.skipAll = true self = config return } @@ -324,8 +324,8 @@ public struct Configuration: Codable, Equatable { // default-initialized instance. let defaults = Configuration() - self.allDisabled = - try container.decodeIfPresent(Bool.self, forKey: .allDisabled) + self.skipAll = + try container.decodeIfPresent(Bool.self, forKey: .skipAll) ?? false self.maximumBlankLines = try container.decodeIfPresent(Int.self, forKey: .maximumBlankLines) @@ -420,7 +420,7 @@ public struct Configuration: Codable, Equatable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(version, forKey: .version) - try container.encode(allDisabled, forKey: .allDisabled) + try container.encode(skipAll, forKey: .skipAll) try container.encode(maximumBlankLines, forKey: .maximumBlankLines) try container.encode(lineLength, forKey: .lineLength) try container.encode(spacesBeforeEndOfLineComments, forKey: .spacesBeforeEndOfLineComments) diff --git a/Sources/SwiftFormat/API/SwiftFormatter.swift b/Sources/SwiftFormat/API/SwiftFormatter.swift index bfabc9477..299d71159 100644 --- a/Sources/SwiftFormat/API/SwiftFormatter.swift +++ b/Sources/SwiftFormat/API/SwiftFormatter.swift @@ -111,8 +111,8 @@ public final class SwiftFormatter { // also does not touch an empty file even if the setting to add trailing newlines is enabled.) guard !source.isEmpty else { return } - // If allDisabled is set, just emit the source as-is. - guard !configuration.allDisabled else { + // If skipAll is set, just emit the source as-is. + guard !configuration.skipAll else { outputStream.write(source) return } diff --git a/Sources/SwiftFormat/API/SwiftLinter.swift b/Sources/SwiftFormat/API/SwiftLinter.swift index 871d0f61d..9cbce0c6c 100644 --- a/Sources/SwiftFormat/API/SwiftLinter.swift +++ b/Sources/SwiftFormat/API/SwiftLinter.swift @@ -99,8 +99,8 @@ public final class SwiftLinter { // also does not touch an empty file even if the setting to add trailing newlines is enabled.) guard !source.isEmpty else { return } - // If allDisabled is set, do nothing. - guard !configuration.allDisabled else { + // If skipAll is set, do nothing. + guard !configuration.skipAll else { return } From 3046d92640588fa317f4262542c214629304f486 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 11:48:38 +0000 Subject: [PATCH 08/26] Tweaked docs --- Documentation/Configuration.md | 2 +- README.md | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index dc2eb86d0..69d909f27 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -12,7 +12,7 @@ top-level keys and values: should always be `1`. * `skipAll` _(boolean)_: If this is true, all other configuration - options are ignored, and formatting is disabled. + options are ignored, and formatting/linting is disabled. * `lineLength` _(number)_: The maximum allowed length of a line, in characters. diff --git a/README.md b/README.md index 4a9b26a7f..405befa82 100644 --- a/README.md +++ b/README.md @@ -205,16 +205,17 @@ JSON-formatted file named `.swift-format` in the same directory. If one is found, then that file is loaded to determine the tool's configuration. If the file is not found, then it looks in the same directory for a file -called `.no-swift-format`. The presence of this file, if found, will -disable all formatting (the contents of the file are ignored). +called `.no-swift-format`. The presence of this file will +disable all formatting and linting. The contents of `.no-swift-format` +are ignored - it can be an empty file. -If neither file is found, the search for files continues in the parent -directory, and so on. +If neither configuration file is found, the search for files continues +in the parent directory, and so on. -If no configuration file is found, a default configuration is used. The -settings in the default configuration can be viewed by running -`swift-format dump-configuration`, which will dump it to standard -output. +If no configuration file is found at any level, a default configuration +is used. The settings in the default configuration can be viewed by +running `swift-format dump-configuration`, which will dump it to +standard output. If the `--configuration ` option is passed to `swift-format`, then that configuration will be used unconditionally and the file system will not be From bb4382eee3e6a8858b0094e6b1128f43673e48b2 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 12:00:12 +0000 Subject: [PATCH 09/26] Check for suppression file first. --- README.md | 10 +++++----- Sources/SwiftFormat/API/Configuration.swift | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 405befa82..107a6e81c 100644 --- a/README.md +++ b/README.md @@ -201,13 +201,13 @@ configuration, by redirecting it to a file and editing it. ### Configuring the Command Line Tool For any source file being checked or formatted, `swift-format` looks for a -JSON-formatted file named `.swift-format` in the same directory. If one is -found, then that file is loaded to determine the tool's configuration. +JSON-formatted file named `.no-swift-format` in the same directory. +The presence of this file will disable all formatting and linting. +The contents of `.no-swift-format` are ignored - it can be an empty file. If the file is not found, then it looks in the same directory for a file -called `.no-swift-format`. The presence of this file will -disable all formatting and linting. The contents of `.no-swift-format` -are ignored - it can be an empty file. +called `.swift-format`. If one is found, then that file is loaded to +determine the tool's configuration. If neither configuration file is found, the search for files continues in the parent directory, and so on. diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 3f2b4d818..2bdecfa84 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -465,14 +465,14 @@ public struct Configuration: Codable, Equatable { } repeat { candidateDirectory.deleteLastPathComponent() - let candidateFile = candidateDirectory.appendingPathComponent(Self.configurationFileName) - if FileManager.default.isReadableFile(atPath: candidateFile.path) { - return candidateFile - } let suppressingFile = candidateDirectory.appendingPathComponent(Self.suppressionFileName) if FileManager.default.isReadableFile(atPath: suppressingFile.path) { return suppressingFile } + let candidateFile = candidateDirectory.appendingPathComponent(Self.configurationFileName) + if FileManager.default.isReadableFile(atPath: candidateFile.path) { + return candidateFile + } } while !candidateDirectory.isRoot return nil From 17dc1bd1a86e0fa974f9e3247ebf269872682978 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 14:16:40 +0000 Subject: [PATCH 10/26] Use .swift-format-ignore as the file name. --- README.md | 4 ++-- Sources/SwiftFormat/API/Configuration.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 107a6e81c..cdaf1ea8f 100644 --- a/README.md +++ b/README.md @@ -201,9 +201,9 @@ configuration, by redirecting it to a file and editing it. ### Configuring the Command Line Tool For any source file being checked or formatted, `swift-format` looks for a -JSON-formatted file named `.no-swift-format` in the same directory. +JSON-formatted file named `.swift-format-ignore` in the same directory. The presence of this file will disable all formatting and linting. -The contents of `.no-swift-format` are ignored - it can be an empty file. +The contents of `.swift-format-ignore` are ignored - it can be an empty file. If the file is not found, then it looks in the same directory for a file called `.swift-format`. If one is found, then that file is loaded to diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 2bdecfa84..427e6151f 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -32,7 +32,7 @@ public struct Configuration: Codable, Equatable { /// Name of the suppression file to look for. /// The presence of this file in a directory will cause the formatter /// to skip formatting files in that directory and its subdirectories. - private static let suppressionFileName = ".no-swift-format" + private static let suppressionFileName = ".swift-format-ignore" private enum CodingKeys: CodingKey { case version From cf27a3cabf019ed4c1ddbb7806690496fb3da890 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 14:32:28 +0000 Subject: [PATCH 11/26] Move suppression logic up to FileIterator --- Documentation/Configuration.md | 3 -- Sources/SwiftFormat/API/Configuration.swift | 30 +------------------ Sources/SwiftFormat/API/SwiftFormatter.swift | 6 ---- Sources/SwiftFormat/API/SwiftLinter.swift | 5 ---- .../SwiftFormat/Utilities/FileIterator.swift | 10 +++++++ 5 files changed, 11 insertions(+), 43 deletions(-) diff --git a/Documentation/Configuration.md b/Documentation/Configuration.md index 69d909f27..14072d84f 100644 --- a/Documentation/Configuration.md +++ b/Documentation/Configuration.md @@ -11,9 +11,6 @@ top-level keys and values: * `version` _(number)_: The version of the configuration file. For now, this should always be `1`. -* `skipAll` _(boolean)_: If this is true, all other configuration - options are ignored, and formatting/linting is disabled. - * `lineLength` _(number)_: The maximum allowed length of a line, in characters. diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 427e6151f..130025fa6 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -24,19 +24,9 @@ internal let highestSupportedConfigurationVersion = 1 /// Holds the complete set of configured values and defaults. public struct Configuration: Codable, Equatable { - /// Name of the configuration file to look for. - /// The presence of this file in a directory will cause the formatter - /// to use the configuration specified in that file. - private static let configurationFileName = ".swift-format" - - /// Name of the suppression file to look for. - /// The presence of this file in a directory will cause the formatter - /// to skip formatting files in that directory and its subdirectories. - private static let suppressionFileName = ".swift-format-ignore" private enum CodingKeys: CodingKey { case version - case skipAll case maximumBlankLines case lineLength case spacesBeforeEndOfLineComments @@ -71,9 +61,6 @@ public struct Configuration: Codable, Equatable { /// MARK: Common configuration - /// Is all formatting disbled? - public var skipAll = false - /// The dictionary containing the rule names that we wish to run on. A rule is not used if it is /// marked as `false`, or if it is missing from the dictionary. public var rules: [String: Bool] @@ -284,13 +271,6 @@ public struct Configuration: Codable, Equatable { /// Creates a new `Configuration` by loading it from a configuration file. public init(contentsOf url: URL) throws { - if url.lastPathComponent == Self.suppressionFileName { - var config = Configuration() - config.skipAll = true - self = config - return - } - let data = try Data(contentsOf: url) try self.init(data: data) } @@ -324,9 +304,6 @@ public struct Configuration: Codable, Equatable { // default-initialized instance. let defaults = Configuration() - self.skipAll = - try container.decodeIfPresent(Bool.self, forKey: .skipAll) - ?? false self.maximumBlankLines = try container.decodeIfPresent(Int.self, forKey: .maximumBlankLines) ?? defaults.maximumBlankLines @@ -420,7 +397,6 @@ public struct Configuration: Codable, Equatable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(version, forKey: .version) - try container.encode(skipAll, forKey: .skipAll) try container.encode(maximumBlankLines, forKey: .maximumBlankLines) try container.encode(lineLength, forKey: .lineLength) try container.encode(spacesBeforeEndOfLineComments, forKey: .spacesBeforeEndOfLineComments) @@ -465,11 +441,7 @@ public struct Configuration: Codable, Equatable { } repeat { candidateDirectory.deleteLastPathComponent() - let suppressingFile = candidateDirectory.appendingPathComponent(Self.suppressionFileName) - if FileManager.default.isReadableFile(atPath: suppressingFile.path) { - return suppressingFile - } - let candidateFile = candidateDirectory.appendingPathComponent(Self.configurationFileName) + let candidateFile = candidateDirectory.appendingPathComponent(".swift-format") if FileManager.default.isReadableFile(atPath: candidateFile.path) { return candidateFile } diff --git a/Sources/SwiftFormat/API/SwiftFormatter.swift b/Sources/SwiftFormat/API/SwiftFormatter.swift index 299d71159..ddd0bbe2c 100644 --- a/Sources/SwiftFormat/API/SwiftFormatter.swift +++ b/Sources/SwiftFormat/API/SwiftFormatter.swift @@ -111,12 +111,6 @@ public final class SwiftFormatter { // also does not touch an empty file even if the setting to add trailing newlines is enabled.) guard !source.isEmpty else { return } - // If skipAll is set, just emit the source as-is. - guard !configuration.skipAll else { - outputStream.write(source) - return - } - let sourceFile = try parseAndEmitDiagnostics( source: source, operatorTable: .standardOperators, diff --git a/Sources/SwiftFormat/API/SwiftLinter.swift b/Sources/SwiftFormat/API/SwiftLinter.swift index 9cbce0c6c..3062f5cc5 100644 --- a/Sources/SwiftFormat/API/SwiftLinter.swift +++ b/Sources/SwiftFormat/API/SwiftLinter.swift @@ -98,11 +98,6 @@ public final class SwiftLinter { // newline from being diagnosed for an empty file. (This is consistent with clang-format, which // also does not touch an empty file even if the setting to add trailing newlines is enabled.) guard !source.isEmpty else { return } - - // If skipAll is set, do nothing. - guard !configuration.skipAll else { - return - } let sourceFile = try parseAndEmitDiagnostics( source: source, diff --git a/Sources/SwiftFormat/Utilities/FileIterator.swift b/Sources/SwiftFormat/Utilities/FileIterator.swift index b0a8d2f06..a69d974ed 100644 --- a/Sources/SwiftFormat/Utilities/FileIterator.swift +++ b/Sources/SwiftFormat/Utilities/FileIterator.swift @@ -21,6 +21,11 @@ import WinSDK @_spi(Internal) public struct FileIterator: Sequence, IteratorProtocol { + /// Name of the suppression file to look for. + /// The presence of this file in a directory will cause the formatter + /// to skip formatting files in that directory and its subdirectories. + private static let suppressionFileName = ".swift-format-ignore" + /// List of file and directory URLs to iterate over. private let urls: [URL] @@ -92,6 +97,11 @@ public struct FileIterator: Sequence, IteratorProtocol { fallthrough case .typeDirectory: + let suppressionFile = next.appendingPathComponent(Self.suppressionFileName) + if FileManager.default.fileExists(atPath: suppressionFile.path) { + continue + } + dirIterator = FileManager.default.enumerator( at: next, includingPropertiesForKeys: nil, From 7b22d511af1f9a6b5955220f9e2f57e2c8946352 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 14:35:15 +0000 Subject: [PATCH 12/26] Removed spurious newlines --- Sources/SwiftFormat/API/Configuration.swift | 1 - Sources/SwiftFormat/API/SwiftLinter.swift | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/SwiftFormat/API/Configuration.swift b/Sources/SwiftFormat/API/Configuration.swift index 130025fa6..70ac916aa 100644 --- a/Sources/SwiftFormat/API/Configuration.swift +++ b/Sources/SwiftFormat/API/Configuration.swift @@ -24,7 +24,6 @@ internal let highestSupportedConfigurationVersion = 1 /// Holds the complete set of configured values and defaults. public struct Configuration: Codable, Equatable { - private enum CodingKeys: CodingKey { case version case maximumBlankLines diff --git a/Sources/SwiftFormat/API/SwiftLinter.swift b/Sources/SwiftFormat/API/SwiftLinter.swift index 3062f5cc5..58d49ac3b 100644 --- a/Sources/SwiftFormat/API/SwiftLinter.swift +++ b/Sources/SwiftFormat/API/SwiftLinter.swift @@ -97,8 +97,7 @@ public final class SwiftLinter { // If the file or input string is completely empty, do nothing. This prevents even a trailing // newline from being diagnosed for an empty file. (This is consistent with clang-format, which // also does not touch an empty file even if the setting to add trailing newlines is enabled.) - guard !source.isEmpty else { return } - + guard !source.isEmpty else { return } let sourceFile = try parseAndEmitDiagnostics( source: source, operatorTable: .standardOperators, From fb8b06f1ba81715cb76e275f1ed2d5b512d740ef Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 15:05:22 +0000 Subject: [PATCH 13/26] Filter urls to remove ignored ones. --- .../SwiftFormat/Utilities/FileIterator.swift | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftFormat/Utilities/FileIterator.swift b/Sources/SwiftFormat/Utilities/FileIterator.swift index a69d974ed..b5c47175f 100644 --- a/Sources/SwiftFormat/Utilities/FileIterator.swift +++ b/Sources/SwiftFormat/Utilities/FileIterator.swift @@ -21,10 +21,10 @@ import WinSDK @_spi(Internal) public struct FileIterator: Sequence, IteratorProtocol { - /// Name of the suppression file to look for. + /// Name of the ignore file to look for. /// The presence of this file in a directory will cause the formatter /// to skip formatting files in that directory and its subdirectories. - private static let suppressionFileName = ".swift-format-ignore" + fileprivate static let ignoreFileName = ".swift-format-ignore" /// List of file and directory URLs to iterate over. private let urls: [URL] @@ -62,7 +62,7 @@ public struct FileIterator: Sequence, IteratorProtocol { /// - workingDirectory: `URL` that indicates the current working directory. Used for testing. public init(urls: [URL], followSymlinks: Bool, workingDirectory: URL = URL(fileURLWithPath: ".")) { self.workingDirectory = workingDirectory - self.urls = urls + self.urls = urls.filter(inputShouldBeProcessed(at:)) self.urlIterator = self.urls.makeIterator() self.followSymlinks = followSymlinks } @@ -97,8 +97,8 @@ public struct FileIterator: Sequence, IteratorProtocol { fallthrough case .typeDirectory: - let suppressionFile = next.appendingPathComponent(Self.suppressionFileName) - if FileManager.default.fileExists(atPath: suppressionFile.path) { + let ignoreFile = next.appendingPathComponent(Self.ignoreFileName) + if FileManager.default.fileExists(atPath: ignoreFile.path) { continue } @@ -189,3 +189,37 @@ private func fileType(at url: URL) -> FileAttributeType? { // Linux. return try? FileManager.default.attributesOfItem(atPath: url.path)[.type] as? FileAttributeType } + +/// Returns true if the file should be processed. +/// Directories are always processed. +/// Other files are processed if there is not a +/// ignore file in the containing directory or any of its parents. +private func inputShouldBeProcessed(at url: URL) -> Bool { + guard fileType(at: url) != .typeDirectory else { + return true + } + + var containingDirectory = url.absoluteURL.standardized.deletingLastPathComponent() + repeat { + containingDirectory.deleteLastPathComponent() + let candidateFile = containingDirectory.appendingPathComponent(FileIterator.ignoreFileName) + if FileManager.default.isReadableFile(atPath: candidateFile.path) { + return false + } + } while !containingDirectory.isRoot + return true +} + +fileprivate extension URL { + var isRoot: Bool { + #if os(Windows) + // FIXME: We should call into Windows' native check to check if this path is a root once https://github.com/swiftlang/swift-foundation/issues/976 is fixed. + // https://github.com/swiftlang/swift-format/issues/844 + return self.pathComponents.count <= 1 + #else + // On Linux, we may end up with an string for the path due to https://github.com/swiftlang/swift-foundation/issues/980 + // TODO: Remove the check for "" once https://github.com/swiftlang/swift-foundation/issues/980 is fixed. + return self.path == "/" || self.path == "" + #endif + } +} \ No newline at end of file From 5d85e22c62057d77af027acfdc8a55135d7affc2 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 15:12:20 +0000 Subject: [PATCH 14/26] Tweaked readme. --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cdaf1ea8f..0463fa241 100644 --- a/README.md +++ b/README.md @@ -200,17 +200,14 @@ configuration, by redirecting it to a file and editing it. ### Configuring the Command Line Tool -For any source file being checked or formatted, `swift-format` looks for a -JSON-formatted file named `.swift-format-ignore` in the same directory. -The presence of this file will disable all formatting and linting. -The contents of `.swift-format-ignore` are ignored - it can be an empty file. +For any source file being checked or formatted, `swift-format` looks for +configuration files in the same directory, and parent directories. -If the file is not found, then it looks in the same directory for a file -called `.swift-format`. If one is found, then that file is loaded to -determine the tool's configuration. +If it finds a file named `.swift-format-ignore`, it will disable all +formatting and linting. -If neither configuration file is found, the search for files continues -in the parent directory, and so on. +If it finds a JSON-formatted file called `.swift-format`, then that +file is loaded to determine the tool's configuration. If no configuration file is found at any level, a default configuration is used. The settings in the default configuration can be viewed by @@ -225,6 +222,9 @@ See [Documentation/Configuration.md](Documentation/Configuration.md) for a description of the configuration file format and the settings that are available. +Note that the contents of any `.swift-format-ignore` are ignored; the +file can be empty. + ### Miscellaneous Running `swift-format -v` or `swift-format --version` will print version From 9ab91a5f4daf4b2321ae53c314a986c591d10e2d Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 15:13:50 +0000 Subject: [PATCH 15/26] Removed obsolete addition. --- Sources/SwiftFormat/API/SwiftLinter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftFormat/API/SwiftLinter.swift b/Sources/SwiftFormat/API/SwiftLinter.swift index 58d49ac3b..cfc4639f6 100644 --- a/Sources/SwiftFormat/API/SwiftLinter.swift +++ b/Sources/SwiftFormat/API/SwiftLinter.swift @@ -97,7 +97,8 @@ public final class SwiftLinter { // If the file or input string is completely empty, do nothing. This prevents even a trailing // newline from being diagnosed for an empty file. (This is consistent with clang-format, which // also does not touch an empty file even if the setting to add trailing newlines is enabled.) - guard !source.isEmpty else { return } + guard !source.isEmpty else { return } + let sourceFile = try parseAndEmitDiagnostics( source: source, operatorTable: .standardOperators, From 7b35513213e45433357557c04fbe7e4582a572ac Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 15:20:22 +0000 Subject: [PATCH 16/26] Oops - skipping initial directory... --- Sources/SwiftFormat/Utilities/FileIterator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftFormat/Utilities/FileIterator.swift b/Sources/SwiftFormat/Utilities/FileIterator.swift index b5c47175f..124c433dc 100644 --- a/Sources/SwiftFormat/Utilities/FileIterator.swift +++ b/Sources/SwiftFormat/Utilities/FileIterator.swift @@ -199,7 +199,7 @@ private func inputShouldBeProcessed(at url: URL) -> Bool { return true } - var containingDirectory = url.absoluteURL.standardized.deletingLastPathComponent() + var containingDirectory = url.absoluteURL.standardized repeat { containingDirectory.deleteLastPathComponent() let candidateFile = containingDirectory.appendingPathComponent(FileIterator.ignoreFileName) From b2975de715b402c92630f3cfb94286a46395930f Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Thu, 7 Nov 2024 16:50:47 +0000 Subject: [PATCH 17/26] Documentation tweaks. --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0463fa241..1d24b4321 100644 --- a/README.md +++ b/README.md @@ -203,8 +203,9 @@ configuration, by redirecting it to a file and editing it. For any source file being checked or formatted, `swift-format` looks for configuration files in the same directory, and parent directories. -If it finds a file named `.swift-format-ignore`, it will disable all -formatting and linting. +If it finds a file named `.swift-format-ignore`, its contents will determine +which files in that directory will be ignored by `swift-format`. Currently +the only supported option is `*`, which ignores all files. If it finds a JSON-formatted file called `.swift-format`, then that file is loaded to determine the tool's configuration. @@ -216,15 +217,12 @@ standard output. If the `--configuration ` option is passed to `swift-format`, then that configuration will be used unconditionally and the file system will not be -searched. +searched for `.swift-format` files. See [Documentation/Configuration.md](Documentation/Configuration.md) for a description of the configuration file format and the settings that are available. -Note that the contents of any `.swift-format-ignore` are ignored; the -file can be empty. - ### Miscellaneous Running `swift-format -v` or `swift-format --version` will print version From ed1d3c47b2d82fef9c197dbab625329b96fc00d9 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Wed, 27 Nov 2024 10:37:12 +0000 Subject: [PATCH 18/26] Add minimal IgnoreFile abstraction. --- Sources/SwiftFormat/Core/IgnoreFile.swift | 82 +++++++++++++++++++ .../SwiftFormat/Utilities/FileIterator.swift | 38 +++++---- Sources/swift-format/Frontend/Frontend.swift | 7 ++ 3 files changed, 109 insertions(+), 18 deletions(-) create mode 100644 Sources/SwiftFormat/Core/IgnoreFile.swift diff --git a/Sources/SwiftFormat/Core/IgnoreFile.swift b/Sources/SwiftFormat/Core/IgnoreFile.swift new file mode 100644 index 000000000..21a1aa3ec --- /dev/null +++ b/Sources/SwiftFormat/Core/IgnoreFile.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +public class IgnoreFile { + /// Name of the ignore file to look for. + /// The presence of this file in a directory will cause the formatter + /// to skip formatting files in that directory and its subdirectories. + fileprivate static let fileName = ".swift-format-ignore" + + /// Errors that can be thrown by the IgnoreFile initializer. + public enum Error: Swift.Error { + /// Error thrown when an ignore file has invalid content. + case invalidContent(URL) + } + + /// Create an instance from the contents of the file at the given URL. + /// Throws an error if the file content can't be read, or is not valid. + public init(contentsOf url: URL) throws { + let content = try String(contentsOf: url, encoding: .utf8) + guard content.trimmingCharacters(in: .whitespacesAndNewlines) == "*" else { + throw Error.invalidContent(url) + } + } + + /// Create an instance for the given directory, if a valid + /// ignore file with the standard name is found in that directory. + /// Returns nil if no ignore file is found. + /// Throws an error if an invalid ignore file is found. + /// + /// Note that this initializer does not search parent directories for ignore files. + public convenience init?(forDirectory directory: URL) throws { + let url = directory.appendingPathComponent(IgnoreFile.fileName) + guard FileManager.default.isReadableFile(atPath: url.path) else { + return nil + } + + try self.init(contentsOf: url) + } + + /// Create an instance to use for the given URL. + /// We search for an ignore file starting from the given URL's container, + /// and moving up the directory tree, until we reach the root directory. + /// Returns nil if no ignore file is found. + /// Throws an error if an invalid ignore file is found somewhere + /// in the directory tree. + public convenience init?(for url: URL) throws { + var containingDirectory = url.absoluteURL.standardized + repeat { + containingDirectory.deleteLastPathComponent() + let url = containingDirectory.appendingPathComponent(IgnoreFile.fileName) + if FileManager.default.isReadableFile(atPath: url.path) { + try self.init(contentsOf: url) + return + } + } while !containingDirectory.isRoot + return nil + } + + /// Should the given URL be processed? + /// Currently the only valid ignore file content is "*", + /// which means that all files should be ignored. + func shouldProcess(_ url: URL) -> Bool { + return false + } + + /// Returns true if the name of the given URL matches + /// the standard ignore file name. + public static func isStandardIgnoreFile(_ url: URL) -> Bool { + return url.lastPathComponent == fileName + } +} \ No newline at end of file diff --git a/Sources/SwiftFormat/Utilities/FileIterator.swift b/Sources/SwiftFormat/Utilities/FileIterator.swift index 124c433dc..fb36b9ddc 100644 --- a/Sources/SwiftFormat/Utilities/FileIterator.swift +++ b/Sources/SwiftFormat/Utilities/FileIterator.swift @@ -21,11 +21,6 @@ import WinSDK @_spi(Internal) public struct FileIterator: Sequence, IteratorProtocol { - /// Name of the ignore file to look for. - /// The presence of this file in a directory will cause the formatter - /// to skip formatting files in that directory and its subdirectories. - fileprivate static let ignoreFileName = ".swift-format-ignore" - /// List of file and directory URLs to iterate over. private let urls: [URL] @@ -97,8 +92,18 @@ public struct FileIterator: Sequence, IteratorProtocol { fallthrough case .typeDirectory: - let ignoreFile = next.appendingPathComponent(Self.ignoreFileName) - if FileManager.default.fileExists(atPath: ignoreFile.path) { + do { + if let ignoreFile = try IgnoreFile(forDirectory: next), !ignoreFile.shouldProcess(next) { + // skip this directory and its subdirectories if it should be ignored + continue + } + } catch IgnoreFile.Error.invalidContent(let url) { + // we hit an invalid ignore file + // we skip the directory, but return the path of the ignore file + // so that we can report an error + output = url + } catch { + // we hit another unexpected error; just skip the directory continue } @@ -192,22 +197,19 @@ private func fileType(at url: URL) -> FileAttributeType? { /// Returns true if the file should be processed. /// Directories are always processed. -/// Other files are processed if there is not a -/// ignore file in the containing directory or any of its parents. +/// For other files, we look for an ignore file in the containing +/// directory or any of its parents. +/// If there is no ignore file, we process the file. +/// If an ignore file is found, we consult it to see if the file should be processed. +/// An invalid ignore file is treated here as if it does not exist, but +/// will be reported as an error when we try to process the directory. private func inputShouldBeProcessed(at url: URL) -> Bool { guard fileType(at: url) != .typeDirectory else { return true } - var containingDirectory = url.absoluteURL.standardized - repeat { - containingDirectory.deleteLastPathComponent() - let candidateFile = containingDirectory.appendingPathComponent(FileIterator.ignoreFileName) - if FileManager.default.isReadableFile(atPath: candidateFile.path) { - return false - } - } while !containingDirectory.isRoot - return true + let ignoreFile = try? IgnoreFile(for: url) + return ignoreFile?.shouldProcess(url) ?? true } fileprivate extension URL { diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index a3ea18a4f..ffd2a7b67 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -165,6 +165,13 @@ class Frontend { /// Read and prepare the file at the given path for processing, optionally synchronizing /// diagnostic output. private func openAndPrepareFile(at url: URL) -> FileToProcess? { + guard !IgnoreFile.isStandardIgnoreFile(url) else { + diagnosticsEngine.emitError( + "Invalid ignore file \(url.relativePath): currently the only supported content for ignore files is a single asterisk `*`, which matches all files." + ) + return nil + } + guard let sourceFile = try? FileHandle(forReadingFrom: url) else { diagnosticsEngine.emitError( "Unable to open \(url.relativePath): file is not readable or does not exist" From 3bc036e0717e26d8bfe64edbe648805208b433c5 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Wed, 27 Nov 2024 10:40:20 +0000 Subject: [PATCH 19/26] Comment for IgnoreFile class --- Sources/SwiftFormat/Core/IgnoreFile.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/SwiftFormat/Core/IgnoreFile.swift b/Sources/SwiftFormat/Core/IgnoreFile.swift index 21a1aa3ec..5ad1b0309 100644 --- a/Sources/SwiftFormat/Core/IgnoreFile.swift +++ b/Sources/SwiftFormat/Core/IgnoreFile.swift @@ -12,6 +12,12 @@ import Foundation +/// A file that describes which files and directories should be ignored by the formatter. +/// In the future, this file may contain complex rules for ignoring files, based +/// on pattern matching file paths. +/// +/// Currently, the only valid content for an ignore file is a single asterisk "*", +/// optionally surrounded by whitespace. public class IgnoreFile { /// Name of the ignore file to look for. /// The presence of this file in a directory will cause the formatter From cff2b22a3a5ce1880589609e854e91c3665744f8 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Wed, 27 Nov 2024 11:13:52 +0000 Subject: [PATCH 20/26] Added IgnoreFile unit tests --- Package.swift | 3 +- .../Core/IgnoreFileTests.swift | 30 +++++++++++++++++++ .../Ignore Files/empty/.swift-format-ignore | 0 .../Ignore Files/invalid/.swift-format-ignore | 1 + .../Ignore Files/valid/.swift-format-ignore | 1 + 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 Tests/SwiftFormatTests/Core/IgnoreFileTests.swift create mode 100644 Tests/SwiftFormatTests/Resources/Ignore Files/empty/.swift-format-ignore create mode 100644 Tests/SwiftFormatTests/Resources/Ignore Files/invalid/.swift-format-ignore create mode 100644 Tests/SwiftFormatTests/Resources/Ignore Files/valid/.swift-format-ignore diff --git a/Package.swift b/Package.swift index 228b13f5f..c78bdc7cc 100644 --- a/Package.swift +++ b/Package.swift @@ -114,7 +114,8 @@ var targets: [Target] = [ "SwiftFormat", "_SwiftFormatTestSupport", .product(name: "Markdown", package: "swift-markdown"), - ] + swiftSyntaxDependencies(["SwiftOperators", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"]) + ] + swiftSyntaxDependencies(["SwiftOperators", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"]), + resources: [.copy("Resources")] ), ] diff --git a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift new file mode 100644 index 000000000..d8a2c2d6b --- /dev/null +++ b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift @@ -0,0 +1,30 @@ +import SwiftFormat +import XCTest + +final class IgnoreFileTests: XCTestCase { + + func testMissingIgnoreFile() throws { + let url = Bundle.module.url(forResource: "missing", withExtension: "", subdirectory: "Ignore Files") + XCTAssertNotNil(url) + XCTAssertNil(try IgnoreFile(forDirectory: url!)) + } + + func testValidIgnoreFile() throws { + let url = Bundle.module.url(forResource: "valid", withExtension: "", subdirectory: "Ignore Files") + XCTAssertNotNil(url) + XCTAssertNotNil(try IgnoreFile(forDirectory: url!)) + } + + func testInvalidIgnoreFile() throws { + let url = Bundle.module.url(forResource: "invalid", withExtension: "", subdirectory: "Ignore Files") + XCTAssertNotNil(url) + XCTAssertThrowsError(try IgnoreFile(forDirectory: url!)) + } + + func testEmptyIgnoreFile() throws { + let url = Bundle.module.url(forResource: "empty", withExtension: "", subdirectory: "Ignore Files") + XCTAssertNotNil(url) + XCTAssertThrowsError(try IgnoreFile(forDirectory: url!)) + } + +} diff --git a/Tests/SwiftFormatTests/Resources/Ignore Files/empty/.swift-format-ignore b/Tests/SwiftFormatTests/Resources/Ignore Files/empty/.swift-format-ignore new file mode 100644 index 000000000..e69de29bb diff --git a/Tests/SwiftFormatTests/Resources/Ignore Files/invalid/.swift-format-ignore b/Tests/SwiftFormatTests/Resources/Ignore Files/invalid/.swift-format-ignore new file mode 100644 index 000000000..3264a2e3c --- /dev/null +++ b/Tests/SwiftFormatTests/Resources/Ignore Files/invalid/.swift-format-ignore @@ -0,0 +1 @@ +this is an invalid pattern \ No newline at end of file diff --git a/Tests/SwiftFormatTests/Resources/Ignore Files/valid/.swift-format-ignore b/Tests/SwiftFormatTests/Resources/Ignore Files/valid/.swift-format-ignore new file mode 100644 index 000000000..f59ec20aa --- /dev/null +++ b/Tests/SwiftFormatTests/Resources/Ignore Files/valid/.swift-format-ignore @@ -0,0 +1 @@ +* \ No newline at end of file From e9861ca5f5b30838cd135eda37ef37b2e6d00829 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Wed, 27 Nov 2024 11:27:20 +0000 Subject: [PATCH 21/26] Added test for nested directory. --- Sources/SwiftFormat/Core/IgnoreFile.swift | 5 +++++ Tests/SwiftFormatTests/Core/IgnoreFileTests.swift | 11 +++++++++++ .../Ignore Files/nested/.swift-format-ignore | 1 + 3 files changed, 17 insertions(+) create mode 100644 Tests/SwiftFormatTests/Resources/Ignore Files/nested/.swift-format-ignore diff --git a/Sources/SwiftFormat/Core/IgnoreFile.swift b/Sources/SwiftFormat/Core/IgnoreFile.swift index 5ad1b0309..269fe0535 100644 --- a/Sources/SwiftFormat/Core/IgnoreFile.swift +++ b/Sources/SwiftFormat/Core/IgnoreFile.swift @@ -60,6 +60,11 @@ public class IgnoreFile { /// Returns nil if no ignore file is found. /// Throws an error if an invalid ignore file is found somewhere /// in the directory tree. + /// + /// Note that we start the search from the given URL's **container**, + /// not the URL itself; the URL passed in is expected to be for a file. + /// If you pass a directory URL, the search will not include the contents + /// of that directory. public convenience init?(for url: URL) throws { var containingDirectory = url.absoluteURL.standardized repeat { diff --git a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift index d8a2c2d6b..436dfec2e 100644 --- a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift +++ b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift @@ -7,24 +7,35 @@ final class IgnoreFileTests: XCTestCase { let url = Bundle.module.url(forResource: "missing", withExtension: "", subdirectory: "Ignore Files") XCTAssertNotNil(url) XCTAssertNil(try IgnoreFile(forDirectory: url!)) + XCTAssertNil(try IgnoreFile(for: url!.appending(path:"file.swift"))) } func testValidIgnoreFile() throws { let url = Bundle.module.url(forResource: "valid", withExtension: "", subdirectory: "Ignore Files") XCTAssertNotNil(url) XCTAssertNotNil(try IgnoreFile(forDirectory: url!)) + XCTAssertNotNil(try IgnoreFile(for: url!.appending(path:"file.swift"))) } func testInvalidIgnoreFile() throws { let url = Bundle.module.url(forResource: "invalid", withExtension: "", subdirectory: "Ignore Files") XCTAssertNotNil(url) XCTAssertThrowsError(try IgnoreFile(forDirectory: url!)) + XCTAssertThrowsError(try IgnoreFile(for: url!.appending(path:"file.swift"))) } func testEmptyIgnoreFile() throws { let url = Bundle.module.url(forResource: "empty", withExtension: "", subdirectory: "Ignore Files") XCTAssertNotNil(url) XCTAssertThrowsError(try IgnoreFile(forDirectory: url!)) + XCTAssertThrowsError(try IgnoreFile(for: url!.appending(path:"file.swift"))) + } + + func testNestedIgnoreFile() throws { + let url = Bundle.module.url(forResource: "nested", withExtension: "", subdirectory: "Ignore Files") + XCTAssertNotNil(url) + let subdirectory = url!.appendingPathComponent("subdirectory").appending(path: "file.swift") + XCTAssertNotNil(try IgnoreFile(for: subdirectory)) } } diff --git a/Tests/SwiftFormatTests/Resources/Ignore Files/nested/.swift-format-ignore b/Tests/SwiftFormatTests/Resources/Ignore Files/nested/.swift-format-ignore new file mode 100644 index 000000000..f59ec20aa --- /dev/null +++ b/Tests/SwiftFormatTests/Resources/Ignore Files/nested/.swift-format-ignore @@ -0,0 +1 @@ +* \ No newline at end of file From ba343ca1a2baca6a9464f4f389ea0984a3c7f44d Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Wed, 11 Dec 2024 11:00:47 +0000 Subject: [PATCH 22/26] Added string constructor. Don't skip dir. --- Sources/SwiftFormat/Core/IgnoreFile.swift | 70 ++++++++++++------- .../SwiftFormat/Utilities/FileIterator.swift | 13 ++-- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/Sources/SwiftFormat/Core/IgnoreFile.swift b/Sources/SwiftFormat/Core/IgnoreFile.swift index 269fe0535..316828ea5 100644 --- a/Sources/SwiftFormat/Core/IgnoreFile.swift +++ b/Sources/SwiftFormat/Core/IgnoreFile.swift @@ -15,7 +15,7 @@ import Foundation /// A file that describes which files and directories should be ignored by the formatter. /// In the future, this file may contain complex rules for ignoring files, based /// on pattern matching file paths. -/// +/// /// Currently, the only valid content for an ignore file is a single asterisk "*", /// optionally surrounded by whitespace. public class IgnoreFile { @@ -26,18 +26,30 @@ public class IgnoreFile { /// Errors that can be thrown by the IgnoreFile initializer. public enum Error: Swift.Error { - /// Error thrown when an ignore file has invalid content. - case invalidContent(URL) + /// Error thrown when initialising with invalid content. + case invalidContent + + /// Error thrown when we fail to initialise with the given URL. + case invalidFile(URL, Swift.Error) + } + + /// Create an instance from a string. + /// Returns nil if the content is not valid. + public init(_ content: String) throws { + guard content.trimmingCharacters(in: .whitespacesAndNewlines) == "*" else { + throw Error.invalidContent + } } /// Create an instance from the contents of the file at the given URL. /// Throws an error if the file content can't be read, or is not valid. - public init(contentsOf url: URL) throws { - let content = try String(contentsOf: url, encoding: .utf8) - guard content.trimmingCharacters(in: .whitespacesAndNewlines) == "*" else { - throw Error.invalidContent(url) - } - } + public convenience init(contentsOf url: URL) throws { + do { + try self.init(try String(contentsOf: url, encoding: .utf8)) + } catch { + throw Error.invalidFile(url, error) + } + } /// Create an instance for the given directory, if a valid /// ignore file with the standard name is found in that directory. @@ -47,18 +59,24 @@ public class IgnoreFile { /// Note that this initializer does not search parent directories for ignore files. public convenience init?(forDirectory directory: URL) throws { let url = directory.appendingPathComponent(IgnoreFile.fileName) - guard FileManager.default.isReadableFile(atPath: url.path) else { - return nil - } - try self.init(contentsOf: url) + do { + try self.init(contentsOf: url) + } catch { + if case let Error.invalidFile(_, underlying) = error, (underlying as NSError).domain == NSCocoaErrorDomain, + (underlying as NSError).code == NSFileReadNoSuchFileError + { + return nil + } + throw error + } } /// Create an instance to use for the given URL. /// We search for an ignore file starting from the given URL's container, /// and moving up the directory tree, until we reach the root directory. /// Returns nil if no ignore file is found. - /// Throws an error if an invalid ignore file is found somewhere + /// Throws an error if an invalid ignore file is found somewhere /// in the directory tree. /// /// Note that we start the search from the given URL's **container**, @@ -66,16 +84,16 @@ public class IgnoreFile { /// If you pass a directory URL, the search will not include the contents /// of that directory. public convenience init?(for url: URL) throws { - var containingDirectory = url.absoluteURL.standardized - repeat { - containingDirectory.deleteLastPathComponent() - let url = containingDirectory.appendingPathComponent(IgnoreFile.fileName) - if FileManager.default.isReadableFile(atPath: url.path) { - try self.init(contentsOf: url) - return - } - } while !containingDirectory.isRoot - return nil + var containingDirectory = url.absoluteURL.standardized + repeat { + containingDirectory.deleteLastPathComponent() + let url = containingDirectory.appendingPathComponent(IgnoreFile.fileName) + if FileManager.default.isReadableFile(atPath: url.path) { + try self.init(contentsOf: url) + return + } + } while !containingDirectory.isRoot + return nil } /// Should the given URL be processed? @@ -85,9 +103,9 @@ public class IgnoreFile { return false } - /// Returns true if the name of the given URL matches + /// Returns true if the name of the given URL matches /// the standard ignore file name. public static func isStandardIgnoreFile(_ url: URL) -> Bool { return url.lastPathComponent == fileName } -} \ No newline at end of file +} diff --git a/Sources/SwiftFormat/Utilities/FileIterator.swift b/Sources/SwiftFormat/Utilities/FileIterator.swift index fb36b9ddc..4a573dd3c 100644 --- a/Sources/SwiftFormat/Utilities/FileIterator.swift +++ b/Sources/SwiftFormat/Utilities/FileIterator.swift @@ -97,14 +97,13 @@ public struct FileIterator: Sequence, IteratorProtocol { // skip this directory and its subdirectories if it should be ignored continue } - } catch IgnoreFile.Error.invalidContent(let url) { + } catch IgnoreFile.Error.invalidFile(let url, _) { // we hit an invalid ignore file - // we skip the directory, but return the path of the ignore file - // so that we can report an error + // 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; just skip the directory - continue + // we hit another unexpected error; process the directory as normal } dirIterator = FileManager.default.enumerator( @@ -197,7 +196,7 @@ private func fileType(at url: URL) -> FileAttributeType? { /// Returns true if the file should be processed. /// Directories are always processed. -/// For other files, we look for an ignore file in the containing +/// For other files, we look for an ignore file in the containing /// directory or any of its parents. /// If there is no ignore file, we process the file. /// If an ignore file is found, we consult it to see if the file should be processed. @@ -207,7 +206,7 @@ private func inputShouldBeProcessed(at url: URL) -> Bool { guard fileType(at: url) != .typeDirectory else { return true } - + let ignoreFile = try? IgnoreFile(for: url) return ignoreFile?.shouldProcess(url) ?? true } From c9a14e344c9a266d0e8bd0230d6a0d8c4d6e50fe Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Wed, 11 Dec 2024 12:17:41 +0000 Subject: [PATCH 23/26] Removed test resources --- Package.swift | 3 +- Sources/SwiftFormat/Core/IgnoreFile.swift | 16 +-- .../SwiftFormat/Utilities/FileIterator.swift | 1 - Sources/swift-format/Frontend/Frontend.swift | 2 +- .../Core/IgnoreFileTests.swift | 127 +++++++++++++++--- .../Ignore Files/empty/.swift-format-ignore | 0 .../Ignore Files/invalid/.swift-format-ignore | 1 - .../Ignore Files/valid/.swift-format-ignore | 1 - 8 files changed, 115 insertions(+), 36 deletions(-) delete mode 100644 Tests/SwiftFormatTests/Resources/Ignore Files/empty/.swift-format-ignore delete mode 100644 Tests/SwiftFormatTests/Resources/Ignore Files/invalid/.swift-format-ignore delete mode 100644 Tests/SwiftFormatTests/Resources/Ignore Files/valid/.swift-format-ignore diff --git a/Package.swift b/Package.swift index c78bdc7cc..228b13f5f 100644 --- a/Package.swift +++ b/Package.swift @@ -114,8 +114,7 @@ var targets: [Target] = [ "SwiftFormat", "_SwiftFormatTestSupport", .product(name: "Markdown", package: "swift-markdown"), - ] + swiftSyntaxDependencies(["SwiftOperators", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"]), - resources: [.copy("Resources")] + ] + swiftSyntaxDependencies(["SwiftOperators", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"]) ), ] diff --git a/Sources/SwiftFormat/Core/IgnoreFile.swift b/Sources/SwiftFormat/Core/IgnoreFile.swift index 316828ea5..9bd009c02 100644 --- a/Sources/SwiftFormat/Core/IgnoreFile.swift +++ b/Sources/SwiftFormat/Core/IgnoreFile.swift @@ -22,7 +22,7 @@ public class IgnoreFile { /// Name of the ignore file to look for. /// The presence of this file in a directory will cause the formatter /// to skip formatting files in that directory and its subdirectories. - fileprivate static let fileName = ".swift-format-ignore" + public static let standardFileName = ".swift-format-ignore" /// Errors that can be thrown by the IgnoreFile initializer. public enum Error: Swift.Error { @@ -58,7 +58,7 @@ public class IgnoreFile { /// /// Note that this initializer does not search parent directories for ignore files. public convenience init?(forDirectory directory: URL) throws { - let url = directory.appendingPathComponent(IgnoreFile.fileName) + let url = directory.appendingPathComponent(IgnoreFile.standardFileName) do { try self.init(contentsOf: url) @@ -84,10 +84,14 @@ public class IgnoreFile { /// If you pass a directory URL, the search will not include the contents /// of that directory. public convenience init?(for url: URL) throws { + guard !url.isRoot else { + return nil + } + var containingDirectory = url.absoluteURL.standardized repeat { containingDirectory.deleteLastPathComponent() - let url = containingDirectory.appendingPathComponent(IgnoreFile.fileName) + let url = containingDirectory.appendingPathComponent(IgnoreFile.standardFileName) if FileManager.default.isReadableFile(atPath: url.path) { try self.init(contentsOf: url) return @@ -102,10 +106,4 @@ public class IgnoreFile { func shouldProcess(_ url: URL) -> Bool { return false } - - /// Returns true if the name of the given URL matches - /// the standard ignore file name. - public static func isStandardIgnoreFile(_ url: URL) -> Bool { - return url.lastPathComponent == fileName - } } diff --git a/Sources/SwiftFormat/Utilities/FileIterator.swift b/Sources/SwiftFormat/Utilities/FileIterator.swift index 4a573dd3c..cf773f4e2 100644 --- a/Sources/SwiftFormat/Utilities/FileIterator.swift +++ b/Sources/SwiftFormat/Utilities/FileIterator.swift @@ -174,7 +174,6 @@ public struct FileIterator: Sequence, IteratorProtocol { relativePath = path } output = URL(fileURLWithPath: relativePath, isDirectory: false, relativeTo: workingDirectory) - default: break } diff --git a/Sources/swift-format/Frontend/Frontend.swift b/Sources/swift-format/Frontend/Frontend.swift index ffd2a7b67..4d3c4002a 100644 --- a/Sources/swift-format/Frontend/Frontend.swift +++ b/Sources/swift-format/Frontend/Frontend.swift @@ -165,7 +165,7 @@ class Frontend { /// Read and prepare the file at the given path for processing, optionally synchronizing /// diagnostic output. private func openAndPrepareFile(at url: URL) -> FileToProcess? { - guard !IgnoreFile.isStandardIgnoreFile(url) else { + guard url.lastPathComponent != IgnoreFile.standardFileName else { diagnosticsEngine.emitError( "Invalid ignore file \(url.relativePath): currently the only supported content for ignore files is a single asterisk `*`, which matches all files." ) diff --git a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift index 436dfec2e..78b48dc6e 100644 --- a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift +++ b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift @@ -1,41 +1,126 @@ -import SwiftFormat +@_spi(Internal) import SwiftFormat import XCTest final class IgnoreFileTests: XCTestCase { + var testTreeURL: URL? + + /// Description of a file or directory tree to create for testing. + enum TestTree { + case file(String, String) + case directory(String, [TestTree]) + } + + override func tearDown() { + // Clean up any test tree after each test. + if let testTreeURL { + // try? FileManager.default.removeItem(at: testTreeURL) + } + } + + /// Make a temporary directory tree for testing. + /// Returns the URL of the root directory. + /// The tree will be cleaned up after the test. + /// If a tree is already set up, it will be cleaned up first. + 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 + } + + /// Write a file or directory tree to the given root URL. + func writeTree(_ tree: TestTree, to root: URL) throws { + switch tree { + case let .file(name, contents): + print("Writing file \(name) to \(root)") + try contents.write(to: root.appendingPathComponent(name), atomically: true, encoding: .utf8) + case let .directory(name, children): + let directory = root.appendingPathComponent(name) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + for child in children { + try writeTree(child, to: directory) + } + } + } func testMissingIgnoreFile() throws { - let url = Bundle.module.url(forResource: "missing", withExtension: "", subdirectory: "Ignore Files") - XCTAssertNotNil(url) - XCTAssertNil(try IgnoreFile(forDirectory: url!)) - XCTAssertNil(try IgnoreFile(for: url!.appending(path:"file.swift"))) + let url = URL(filePath: "/") + XCTAssertNil(try IgnoreFile(forDirectory: url)) + XCTAssertNil(try IgnoreFile(for: url.appending(path: "file.swift"))) } func testValidIgnoreFile() throws { - let url = Bundle.module.url(forResource: "valid", withExtension: "", subdirectory: "Ignore Files") - XCTAssertNotNil(url) - XCTAssertNotNil(try IgnoreFile(forDirectory: url!)) - XCTAssertNotNil(try IgnoreFile(for: url!.appending(path:"file.swift"))) + let url = try makeTempTree(.file(IgnoreFile.standardFileName, "*")) + XCTAssertNotNil(try IgnoreFile(forDirectory: url)) + XCTAssertNotNil(try IgnoreFile(for: url.appending(path: "file.swift"))) } func testInvalidIgnoreFile() throws { - let url = Bundle.module.url(forResource: "invalid", withExtension: "", subdirectory: "Ignore Files") - XCTAssertNotNil(url) - XCTAssertThrowsError(try IgnoreFile(forDirectory: url!)) - XCTAssertThrowsError(try IgnoreFile(for: url!.appending(path:"file.swift"))) + let url = try makeTempTree(.file(IgnoreFile.standardFileName, "this is an invalid pattern")) + XCTAssertThrowsError(try IgnoreFile(forDirectory: url)) + XCTAssertThrowsError(try IgnoreFile(for: url.appending(path: "file.swift"))) } func testEmptyIgnoreFile() throws { - let url = Bundle.module.url(forResource: "empty", withExtension: "", subdirectory: "Ignore Files") - XCTAssertNotNil(url) - XCTAssertThrowsError(try IgnoreFile(forDirectory: url!)) - XCTAssertThrowsError(try IgnoreFile(for: url!.appending(path:"file.swift"))) + XCTAssertThrowsError(try IgnoreFile("")) } func testNestedIgnoreFile() throws { - let url = Bundle.module.url(forResource: "nested", withExtension: "", subdirectory: "Ignore Files") - XCTAssertNotNil(url) - let subdirectory = url!.appendingPathComponent("subdirectory").appending(path: "file.swift") - XCTAssertNotNil(try IgnoreFile(for: subdirectory)) + let url = try makeTempTree(.file(IgnoreFile.standardFileName, "*")) + let fileInSubdirectory = url.appendingPathComponent("subdirectory").appending(path: "file.swift") + XCTAssertNotNil(try IgnoreFile(for: fileInSubdirectory)) + } + + func testIterateWithIgnoreFile() throws { + let url = try makeTempTree(.file(IgnoreFile.standardFileName, "*")) + let iterator = FileIterator(urls: [url], followSymlinks: false) + let files = Array(iterator) + XCTAssertEqual(files.count, 0) + } + + func testIterateWithInvalidIgnoreFile() throws { + let url = try makeTempTree(.file(IgnoreFile.standardFileName, "this file is invalid")) + let iterator = FileIterator(urls: [url], followSymlinks: false) + let files = Array(iterator) + XCTAssertEqual(files.count, 1) + XCTAssertTrue(files.first?.lastPathComponent == IgnoreFile.standardFileName) + } + + func testIterateWithNestedIgnoreFile() throws { + let url = try makeTempTree( + .directory( + "Source", + [ + .directory( + "Ignored", + [ + .file(IgnoreFile.standardFileName, "*"), + .file("file.swift", "contents"), + ] + ), + .directory( + "Not Ignored", + [ + .file("file.swift", "contents") + ] + ), + ] + ) + ) + + XCTAssertNil(try IgnoreFile(forDirectory: url)) + XCTAssertNil(try IgnoreFile(for: url.appending(path: "Source/file.swift"))) + XCTAssertNotNil(try IgnoreFile(for: url.appending(path: "Source/Ignored/file.swift"))) + let iterator = FileIterator(urls: [url], followSymlinks: false) + let files = Array(iterator) + print(files) + XCTAssertEqual(files.count, 1) + XCTAssertEqual(files.first?.lastPathComponent, "file.swift") } } diff --git a/Tests/SwiftFormatTests/Resources/Ignore Files/empty/.swift-format-ignore b/Tests/SwiftFormatTests/Resources/Ignore Files/empty/.swift-format-ignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/Tests/SwiftFormatTests/Resources/Ignore Files/invalid/.swift-format-ignore b/Tests/SwiftFormatTests/Resources/Ignore Files/invalid/.swift-format-ignore deleted file mode 100644 index 3264a2e3c..000000000 --- a/Tests/SwiftFormatTests/Resources/Ignore Files/invalid/.swift-format-ignore +++ /dev/null @@ -1 +0,0 @@ -this is an invalid pattern \ No newline at end of file diff --git a/Tests/SwiftFormatTests/Resources/Ignore Files/valid/.swift-format-ignore b/Tests/SwiftFormatTests/Resources/Ignore Files/valid/.swift-format-ignore deleted file mode 100644 index f59ec20aa..000000000 --- a/Tests/SwiftFormatTests/Resources/Ignore Files/valid/.swift-format-ignore +++ /dev/null @@ -1 +0,0 @@ -* \ No newline at end of file From 707061fb1c9765283af2b54a5306a5ae454123e1 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Wed, 11 Dec 2024 12:34:58 +0000 Subject: [PATCH 24/26] fixed nested test with explanatory comment --- Tests/SwiftFormatTests/Core/IgnoreFileTests.swift | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift index 78b48dc6e..f3ec07eac 100644 --- a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift +++ b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift @@ -118,9 +118,17 @@ final class IgnoreFileTests: XCTestCase { XCTAssertNotNil(try IgnoreFile(for: url.appending(path: "Source/Ignored/file.swift"))) let iterator = FileIterator(urls: [url], followSymlinks: false) let files = Array(iterator) - print(files) - XCTAssertEqual(files.count, 1) - XCTAssertEqual(files.first?.lastPathComponent, "file.swift") + + // note that the ignore file has no effect on the FileIterator here, + // because it is not in the root directory + // this is expected behavior, and a limitation of the current implementation + // of FileIterator, as it flattens subdirectories into a single list rather + // than recursing into them; however, the ignore file itself will still be + // picked up and obeyed when each source file is processed. + XCTAssertEqual(files.count, 2) + for file in files { + XCTAssertEqual(file.lastPathComponent, "file.swift") + } } } From 85840d61e01cbae0ce294231de58e3aa5dd416bd Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Wed, 11 Dec 2024 12:50:52 +0000 Subject: [PATCH 25/26] Fixed merge conflict --- Sources/SwiftFormat/Utilities/FileIterator.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/SwiftFormat/Utilities/FileIterator.swift b/Sources/SwiftFormat/Utilities/FileIterator.swift index cf773f4e2..4a573dd3c 100644 --- a/Sources/SwiftFormat/Utilities/FileIterator.swift +++ b/Sources/SwiftFormat/Utilities/FileIterator.swift @@ -174,6 +174,7 @@ public struct FileIterator: Sequence, IteratorProtocol { relativePath = path } output = URL(fileURLWithPath: relativePath, isDirectory: false, relativeTo: workingDirectory) + default: break } From 5b5db57a4228fa277b9e7500e8e25edc7daed470 Mon Sep 17 00:00:00 2001 From: Sam Deane Date: Wed, 11 Dec 2024 13:25:26 +0000 Subject: [PATCH 26/26] Actually, this test should fail (currently) --- Tests/SwiftFormatTests/Core/IgnoreFileTests.swift | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift index f3ec07eac..2bff90f36 100644 --- a/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift +++ b/Tests/SwiftFormatTests/Core/IgnoreFileTests.swift @@ -119,16 +119,8 @@ final class IgnoreFileTests: XCTestCase { let iterator = FileIterator(urls: [url], followSymlinks: false) let files = Array(iterator) - // note that the ignore file has no effect on the FileIterator here, - // because it is not in the root directory - // this is expected behavior, and a limitation of the current implementation - // of FileIterator, as it flattens subdirectories into a single list rather - // than recursing into them; however, the ignore file itself will still be - // picked up and obeyed when each source file is processed. - XCTAssertEqual(files.count, 2) - for file in files { - XCTAssertEqual(file.lastPathComponent, "file.swift") - } + XCTAssertEqual(files.count, 1) + XCTAssertEqual(files.first?.lastPathComponent, "file.swift") } }