Skip to content
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

Ability To Turn Off Formatting For Subdirectory #873

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
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
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
),
]

Expand Down
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,19 +200,24 @@ 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. If the
file is not found, then it looks in the parent directory, and so on.
For any source file being checked or formatted, `swift-format` looks for
configuration files in the same directory, and parent directories.

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 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.

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 <file>` 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
Expand Down
14 changes: 0 additions & 14 deletions Sources/SwiftFormat/API/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -472,17 +472,3 @@ public struct NoAssignmentInExpressionsConfiguration: Codable, Equatable {

public init() {}
}

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
}
}
93 changes: 93 additions & 0 deletions Sources/SwiftFormat/Core/IgnoreFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//===----------------------------------------------------------------------===//
//
// 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

/// 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
/// 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.
///
/// 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 {
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
}
}
34 changes: 33 additions & 1 deletion Sources/SwiftFormat/Utilities/FileIterator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public struct FileIterator: Sequence, IteratorProtocol {
/// The given URLs may be files or directories. If they are directories, the iterator will recurse
/// into them.
public init(urls: [URL], followSymlinks: Bool) {
self.urls = urls
self.urls = urls.filter(inputShouldBeProcessed(at:))
self.urlIterator = self.urls.makeIterator()
self.followSymlinks = followSymlinks
}
Expand Down Expand Up @@ -83,6 +83,21 @@ public struct FileIterator: Sequence, IteratorProtocol {
fallthrough

case .typeDirectory:
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
}

dirIterator = FileManager.default.enumerator(
at: next,
includingPropertiesForKeys: nil,
Expand Down Expand Up @@ -169,3 +184,20 @@ 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.
/// 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
}

let ignoreFile = try? IgnoreFile(for: url)
return ignoreFile?.shouldProcess(url) ?? true
}
27 changes: 27 additions & 0 deletions Sources/SwiftFormat/Utilities/URL+IsRoot.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 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

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
}
}
7 changes: 7 additions & 0 deletions Sources/swift-format/Frontend/Frontend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions Tests/SwiftFormatTests/Core/IgnoreFileTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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!))
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))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is an invalid pattern
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*