Skip to content

Commit

Permalink
Add minimal IgnoreFile abstraction.
Browse files Browse the repository at this point in the history
  • Loading branch information
samdeane committed Nov 27, 2024
1 parent c532d89 commit c078ad9
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 18 deletions.
82 changes: 82 additions & 0 deletions Sources/SwiftFormat/Core/IgnoreFile.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
38 changes: 20 additions & 18 deletions Sources/SwiftFormat/Utilities/FileIterator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ import Foundation
@_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]

Expand Down Expand Up @@ -88,8 +83,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
}

Expand Down Expand Up @@ -182,20 +187,17 @@ 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
}
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

0 comments on commit c078ad9

Please sign in to comment.