Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ if buildOnlyTests {
let package = Package(
name: "swift-format",
platforms: [
.macOS("12.0"),
.iOS("13.0"),
.macOS("13.0"),
.iOS("16.0"),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also raised the iOS deployment target to 16 to use Swift Regex.
One thing I'm curious about—does swift-format need to support iOS?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone added it a while back. I guess theoretically you could write an editor for iPad and want to format Swift code in it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see! That could be a possible use case.

],
products: products,
dependencies: dependencies,
Expand Down
35 changes: 19 additions & 16 deletions Sources/SwiftFormat/Core/RuleMask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ extension SourceRange {

/// Represents the kind of ignore directive encountered in the source.
enum IgnoreDirective: CustomStringConvertible {
typealias RegexExpression = Regex<(Substring, ruleNames: Substring?)>

/// 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.
Expand All @@ -111,10 +113,14 @@ enum IgnoreDirective: CustomStringConvertible {
}
}

/// 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.
fileprivate var pattern: String {
return #"^\s*\/\/\s*"# + description + #"((:\s+(([A-z0-9]+[,\s]*)+))?$|\s+$)"#
/// Regex pattern to match an ignore directive comment.
/// - Captures rule names when `:` is present.
///
/// Note: We are using a string-based regex instead of a regex literal (`#/regex/#`)
/// because Windows did not have full support for regex literals until Swift 5.10.
fileprivate func makeRegex() -> RegexExpression {
let pattern = #"^\s*\/\/\s*"# + description + #"(?:\s*:\s*(?<ruleNames>.+))?$"#
return try! Regex(pattern)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment that we can’t use regex literals here because Windows didn’t have support for regex literals until Swift 5.10 (at least I think it was 5.10)? Just so we don’t go back and wonder why we aren’t using regex literals here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I've added it.

}
}

Expand All @@ -140,10 +146,10 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
private let sourceLocationConverter: SourceLocationConverter

/// Cached regex object for ignoring rules at the node.
private let ignoreRegex: NSRegularExpression
private let ignoreRegex: IgnoreDirective.RegexExpression

/// Cached regex object for ignoring rules at the file.
private let ignoreFileRegex: NSRegularExpression
private let ignoreFileRegex: IgnoreDirective.RegexExpression

/// Stores the source ranges in which all rules are ignored.
var allRulesIgnoredRanges: [SourceRange] = []
Expand All @@ -152,8 +158,8 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
var ruleMap: [String: [SourceRange]] = [:]

init(sourceLocationConverter: SourceLocationConverter) {
ignoreRegex = try! NSRegularExpression(pattern: IgnoreDirective.node.pattern, options: [])
ignoreFileRegex = try! NSRegularExpression(pattern: IgnoreDirective.file.pattern, options: [])
ignoreRegex = IgnoreDirective.node.makeRegex()
ignoreFileRegex = IgnoreDirective.file.makeRegex()

self.sourceLocationConverter = sourceLocationConverter
super.init(viewMode: .sourceAccurate)
Expand Down Expand Up @@ -202,7 +208,7 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
private func appendRuleStatus(
from token: TokenSyntax,
of sourceRange: SourceRange,
using regex: NSRegularExpression
using regex: IgnoreDirective.RegexExpression
) -> SyntaxVisitorContinueKind {
let isFirstInFile = token.previousToken(viewMode: .sourceAccurate) == nil
let comments = loneLineComments(in: token.leadingTrivia, isFirstToken: isFirstInFile)
Expand All @@ -227,18 +233,15 @@ fileprivate class RuleStatusCollectionVisitor: SyntaxVisitor {
/// match, its contents (e.g. list of rule names) are returned.
private func ruleStatusDirectiveMatch(
in text: String,
using regex: NSRegularExpression
using regex: IgnoreDirective.RegexExpression
) -> RuleStatusDirectiveMatch? {
let textRange = NSRange(text.startIndex..<text.endIndex, in: text)
guard let match = regex.firstMatch(in: text, options: [], range: textRange) else {
guard let match = text.firstMatch(of: regex) else {
return nil
}
guard match.numberOfRanges == 5 else { return .all }
let matchRange = match.range(at: 3)
guard matchRange.location != NSNotFound, let ruleNamesRange = Range(matchRange, in: text) else {
guard let matchedRuleNames = match.output.ruleNames else {
return .all
}
let rules = text[ruleNamesRange].split(separator: ",")
let rules = matchedRuleNames.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { $0.count > 0 }
return .subset(ruleNames: rules)
Expand Down
71 changes: 31 additions & 40 deletions Sources/_SwiftFormatTestSupport/DiagnosingTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,54 +213,45 @@ open class DiagnosingTestCase: XCTestCase {
file: StaticString = #file,
line: UInt = #line
) {
// Use `CollectionDifference` on supported platforms to get `diff`-like line-based output. On
// older platforms, fall back to simple string comparison.
if #available(macOS 10.15, *) {
let actualLines = actual.components(separatedBy: .newlines)
let expectedLines = expected.components(separatedBy: .newlines)
let actualLines = actual.components(separatedBy: .newlines)
let expectedLines = expected.components(separatedBy: .newlines)

let difference = actualLines.difference(from: expectedLines)
if difference.isEmpty { return }
let difference = actualLines.difference(from: expectedLines)
if difference.isEmpty { return }

var result = ""
var result = ""

var insertions = [Int: String]()
var removals = [Int: String]()
var insertions = [Int: String]()
var removals = [Int: String]()

for change in difference {
switch change {
case .insert(let offset, let element, _):
insertions[offset] = element
case .remove(let offset, let element, _):
removals[offset] = element
}
for change in difference {
switch change {
case .insert(let offset, let element, _):
insertions[offset] = element
case .remove(let offset, let element, _):
removals[offset] = element
}
}

var expectedLine = 0
var actualLine = 0
var expectedLine = 0
var actualLine = 0

while expectedLine < expectedLines.count || actualLine < actualLines.count {
if let removal = removals[expectedLine] {
result += "-\(removal)\n"
expectedLine += 1
} else if let insertion = insertions[actualLine] {
result += "+\(insertion)\n"
actualLine += 1
} else {
result += " \(expectedLines[expectedLine])\n"
expectedLine += 1
actualLine += 1
}
while expectedLine < expectedLines.count || actualLine < actualLines.count {
if let removal = removals[expectedLine] {
result += "-\(removal)\n"
expectedLine += 1
} else if let insertion = insertions[actualLine] {
result += "+\(insertion)\n"
actualLine += 1
} else {
result += " \(expectedLines[expectedLine])\n"
expectedLine += 1
actualLine += 1
}

let failureMessage = "Actual output (+) differed from expected output (-):\n\(result)"
let fullMessage = message.isEmpty ? failureMessage : "\(message) - \(failureMessage)"
XCTFail(fullMessage, file: file, line: line)
} else {
// Fall back to simple string comparison on platforms that don't support CollectionDifference.
let failureMessage = "Actual output differed from expected output:"
let fullMessage = message.isEmpty ? failureMessage : "\(message) - \(failureMessage)"
XCTAssertEqual(actual, expected, fullMessage, file: file, line: line)
}

let failureMessage = "Actual output (+) differed from expected output (-):\n\(result)"
let fullMessage = message.isEmpty ? failureMessage : "\(message) - \(failureMessage)"
XCTFail(fullMessage, file: file, line: line)
}
}
19 changes: 19 additions & 0 deletions Tests/SwiftFormatTests/Core/RuleMaskTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,25 @@ final class RuleMaskTests: XCTestCase {
XCTAssertEqual(mask.ruleState("rule2", at: location(ofLine: 8)), .default)
}

func testIgnoreComplexRuleNames() {
let text =
"""
// swift-format-ignore: ru_le, rule!, ru&le, rule?, rule[], rule(), rule;
let a = 123
"""

let mask = createMask(sourceText: text)

XCTAssertEqual(mask.ruleState("ru_le", at: location(ofLine: 2)), .disabled)
XCTAssertEqual(mask.ruleState("rule!", at: location(ofLine: 2)), .disabled)
XCTAssertEqual(mask.ruleState("ru&le", at: location(ofLine: 2)), .disabled)
XCTAssertEqual(mask.ruleState("rule?", at: location(ofLine: 2)), .disabled)
XCTAssertEqual(mask.ruleState("rule[]", at: location(ofLine: 2)), .disabled)
XCTAssertEqual(mask.ruleState("rule()", at: location(ofLine: 2)), .disabled)
XCTAssertEqual(mask.ruleState("rule;", at: location(ofLine: 2)), .disabled)
XCTAssertEqual(mask.ruleState("default", at: location(ofLine: 2)), .default)
}

func testDuplicateNested() {
let text =
"""
Expand Down