diff --git a/Documentation/IgnoringSource.md b/Documentation/IgnoringSource.md index 40637426a..42bf4e0f3 100644 --- a/Documentation/IgnoringSource.md +++ b/Documentation/IgnoringSource.md @@ -77,6 +77,22 @@ var a = foo+bar+baz These ignore comments also apply to all children of the node, identical to the behavior of the formatting ignore directive described above. +You can also disable specific source transforming rules for an entire file +by using the file-level ignore directive with a list of rule names. For example: + +```swift +// swift-format-ignore-file: DoNotUseSemicolons, FullyIndirectEnum +import Zoo +import Arrays + +struct Foo { + func foo() { bar();baz(); } +} +``` +In this case, only the DoNotUseSemicolons and FullyIndirectEnum rules are disabled +throughout the file, while all other formatting rules (such as line breaking and +indentation) remain active. + ## Understanding Nodes `swift-format` parses Swift into an abstract syntax tree, where each element of diff --git a/Sources/SwiftFormat/Core/RuleMask.swift b/Sources/SwiftFormat/Core/RuleMask.swift index 9edf2449c..de660faaf 100644 --- a/Sources/SwiftFormat/Core/RuleMask.swift +++ b/Sources/SwiftFormat/Core/RuleMask.swift @@ -35,6 +35,16 @@ import SwiftSyntax /// 2. | let a = 123 /// Ignores `RuleName` and `OtherRuleName` for line 2. /// +/// 1. | // swift-format-ignore-file: RuleName +/// 2. | let a = 123 +/// 3. | class Foo { } +/// Ignores `RuleName` for the entire file (lines 2-3). +/// +/// 1. | // swift-format-ignore-file: RuleName, OtherRuleName +/// 2. | let a = 123 +/// 3. | class Foo { } +/// Ignores `RuleName` and `OtherRuleName` for the entire file (lines 2-3). +/// /// The rules themselves reference RuleMask to see if it is disabled for the line it is currently /// examining. @_spi(Testing) @@ -85,6 +95,34 @@ extension SourceRange { } } +/// Represents the kind of ignore directive encountered in the source. +enum IgnoreDirective: CustomStringConvertible { + /// A node-level directive that disables rules for the following node and its children. + case node + /// A file-level directive that disables rules for the entire file. + case file + + var description: String { + switch self { + case .node: + return "swift-format-ignore" + case .file: + return "swift-format-ignore-file" + } + } + + /// Regex pattern to match an ignore comment. This pattern supports 0 or more comma delimited rule + /// names. The rule name(s), when present, are in capture group #3. + private var pattern: String { + return #"^\s*\/\/\s*"# + description + #"((:\s+(([A-z0-9]+[,\s]*)+))?$|\s+$)"# + } + + /// Rule ignore regex object. + fileprivate var regex: NSRegularExpression { + return try! NSRegularExpression(pattern: pattern, options: []) + } +} + /// A syntax visitor that finds `SourceRange`s of nodes that have rule status modifying comment /// directives. The changes requested in each comment is parsed and collected into a map to support /// status lookup per rule name. @@ -106,20 +144,6 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor { /// Computes source locations and ranges for syntax nodes in a source file. private let sourceLocationConverter: SourceLocationConverter - /// Regex pattern to match an ignore comment. This pattern supports 0 or more comma delimited rule - /// names. The rule name(s), when present, are in capture group #3. - private let ignorePattern = - #"^\s*\/\/\s*swift-format-ignore((:\s+(([A-z0-9]+[,\s]*)+))?$|\s+$)"# - - /// Rule ignore regex object. - private let ignoreRegex: NSRegularExpression - - /// Regex pattern to match an ignore comment that applies to an entire file. - private let ignoreFilePattern = #"^\s*\/\/\s*swift-format-ignore-file$"# - - /// Rule ignore regex object. - private let ignoreFileRegex: NSRegularExpression - /// Stores the source ranges in which all rules are ignored. var allRulesIgnoredRanges: [SourceRange] = [] @@ -127,9 +151,6 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor { var ruleMap: [String: [SourceRange]] = [:] init(sourceLocationConverter: SourceLocationConverter) { - ignoreRegex = try! NSRegularExpression(pattern: ignorePattern, options: []) - ignoreFileRegex = try! NSRegularExpression(pattern: ignoreFilePattern, options: []) - self.sourceLocationConverter = sourceLocationConverter super.init(viewMode: .sourceAccurate) } @@ -140,40 +161,28 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor { guard let firstToken = node.firstToken(viewMode: .sourceAccurate) else { return .visitChildren } - let comments = loneLineComments(in: firstToken.leadingTrivia, isFirstToken: true) - var foundIgnoreFileComment = false - for comment in comments { - let range = NSRange(comment.startIndex.. SyntaxVisitorContinueKind { guard let firstToken = node.firstToken(viewMode: .sourceAccurate) else { return .visitChildren } - return appendRuleStatusDirectives(from: firstToken, of: Syntax(node)) + let sourceRange = node.sourceRange(converter: sourceLocationConverter) + return appendRuleStatus(from: firstToken, of: sourceRange, for: .node) } override func visit(_ node: MemberBlockItemSyntax) -> SyntaxVisitorContinueKind { guard let firstToken = node.firstToken(viewMode: .sourceAccurate) else { return .visitChildren } - return appendRuleStatusDirectives(from: firstToken, of: Syntax(node)) + let sourceRange = node.sourceRange(converter: sourceLocationConverter) + return appendRuleStatus(from: firstToken, of: sourceRange, for: .node) } // MARK: - Helper Methods @@ -183,17 +192,19 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor { /// /// - Parameters: /// - token: A token that may have comments that modify the status of rules. - /// - node: The node to which the token belongs. - private func appendRuleStatusDirectives( + /// - sourceRange: The range covering the node to which `token` belongs. If an ignore directive + /// is found among the comments, this entire range is used to ignore the specified rules. + /// - directive: The type of ignore directive to look for. + private func appendRuleStatus( from token: TokenSyntax, - of node: Syntax + of sourceRange: SourceRange, + for directive: IgnoreDirective ) -> SyntaxVisitorContinueKind { let isFirstInFile = token.previousToken(viewMode: .sourceAccurate) == nil - let matches = loneLineComments(in: token.leadingTrivia, isFirstToken: isFirstInFile) - .compactMap(ruleStatusDirectiveMatch) - let sourceRange = node.sourceRange(converter: sourceLocationConverter) - for match in matches { - switch match { + let comments = loneLineComments(in: token.leadingTrivia, isFirstToken: isFirstInFile) + for comment in comments { + guard let matchResult = ruleStatusDirectiveMatch(in: comment, using: directive.regex) else { continue } + switch matchResult { case .all: allRulesIgnoredRanges.append(sourceRange) @@ -210,9 +221,12 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor { /// Checks if a comment containing the given text matches a rule status directive. When it does /// match, its contents (e.g. list of rule names) are returned. - private func ruleStatusDirectiveMatch(in text: String) -> RuleStatusDirectiveMatch? { + private func ruleStatusDirectiveMatch( + in text: String, + using regex: NSRegularExpression + ) -> RuleStatusDirectiveMatch? { let textRange = NSRange(text.startIndex..