Compare commits
1 Commits
main
...
mark-swift
Author | SHA1 | Date |
---|---|---|
![]() |
9e4250c2b2 |
|
@ -1,7 +1,8 @@
|
||||||
import Foundation
|
import SwiftSyntax
|
||||||
import SourceKittenFramework
|
|
||||||
|
|
||||||
public struct MarkRule: CorrectableRule, ConfigurationProviderRule {
|
// MARK: - MarkRule
|
||||||
|
|
||||||
|
public struct MarkRule: CorrectableRule, ConfigurationProviderRule, SourceKitFreeRule {
|
||||||
public var configuration = SeverityConfiguration(.warning)
|
public var configuration = SeverityConfiguration(.warning)
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
@ -61,148 +62,136 @@ public struct MarkRule: CorrectableRule, ConfigurationProviderRule {
|
||||||
Example("↓// MARK : comment"): Example("// MARK: comment"),
|
Example("↓// MARK : comment"): Example("// MARK: comment"),
|
||||||
Example("↓// MARKL:"): Example("// MARK:"),
|
Example("↓// MARKL:"): Example("// MARK:"),
|
||||||
Example("↓// MARKL: -"): Example("// MARK: -"),
|
Example("↓// MARKL: -"): Example("// MARK: -"),
|
||||||
Example("↓// MARKK "): Example("// MARK: "),
|
Example("↓// MARKK "): Example("// MARK:"),
|
||||||
Example("↓// MARKK -"): Example("// MARK: -"),
|
Example("↓// MARKK -"): Example("// MARK: -"),
|
||||||
Example("↓/// MARK:"): Example("// MARK:"),
|
Example("↓/// MARK:"): Example("// MARK:"),
|
||||||
Example("↓/// MARK comment"): Example("// MARK: comment"),
|
Example("↓/// MARK comment"): Example("// MARK: comment"),
|
||||||
issue1029Example: issue1029Correction,
|
// issue1029Example: issue1029Correction,
|
||||||
issue1749Example: issue1749Correction
|
issue1749Example: issue1749Correction
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
private let spaceStartPattern = "(?:\(nonSpaceOrTwoOrMoreSpace)\(mark))"
|
|
||||||
|
|
||||||
private let endNonSpacePattern = "(?:\(mark)\(nonSpace))"
|
|
||||||
private let endTwoOrMoreSpacePattern = "(?:\(mark)\(twoOrMoreSpace))"
|
|
||||||
|
|
||||||
private let invalidEndSpacesPattern = "(?:\(mark)\(nonSpaceOrTwoOrMoreSpace))"
|
|
||||||
|
|
||||||
private let twoOrMoreSpacesAfterHyphenPattern = "(?:\(mark) -\(twoOrMoreSpace))"
|
|
||||||
private let nonSpaceOrNewlineAfterHyphenPattern = "(?:\(mark) -[^ \n])"
|
|
||||||
|
|
||||||
private let invalidSpacesAfterHyphenPattern = "(?:\(mark) -\(nonSpaceOrTwoOrMoreSpaceOrNewline))"
|
|
||||||
|
|
||||||
private let invalidLowercasePattern = "(?:// ?[Mm]ark:)"
|
|
||||||
|
|
||||||
private let missingColonPattern = "(?:// ?MARK[^:])"
|
|
||||||
// The below patterns more specifically describe some of the above pattern's failure cases for correction.
|
|
||||||
private let oneOrMoreSpacesBeforeColonPattern = "(?:// ?MARK +:)"
|
|
||||||
private let nonWhitespaceBeforeColonPattern = "(?:// ?MARK\\S+:)"
|
|
||||||
private let nonWhitespaceNorColonBeforeSpacesPattern = "(?:// ?MARK[^\\s:]* +)"
|
|
||||||
private let threeSlashesInsteadOfTwo = "/// MARK:?"
|
|
||||||
|
|
||||||
private var pattern: String {
|
|
||||||
return [
|
|
||||||
spaceStartPattern,
|
|
||||||
invalidEndSpacesPattern,
|
|
||||||
invalidSpacesAfterHyphenPattern,
|
|
||||||
invalidLowercasePattern,
|
|
||||||
missingColonPattern,
|
|
||||||
threeSlashesInsteadOfTwo
|
|
||||||
].joined(separator: "|")
|
|
||||||
}
|
|
||||||
|
|
||||||
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
public func validate(file: SwiftLintFile) -> [StyleViolation] {
|
||||||
return violationRanges(in: file, matching: pattern).map {
|
MarkRuleVisitor(locationConverter: file.locationConverter!)
|
||||||
|
.walk(file: file, handler: \.positions)
|
||||||
|
.map { position in
|
||||||
StyleViolation(ruleDescription: Self.description,
|
StyleViolation(ruleDescription: Self.description,
|
||||||
severity: configuration.severity,
|
severity: configuration.severity,
|
||||||
location: Location(file: file, characterOffset: $0.location))
|
location: Location(file: file, position: position))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func correct(file: SwiftLintFile) -> [Correction] {
|
public func correct(file: SwiftLintFile) -> [Correction] {
|
||||||
var result = [Correction]()
|
guard let locationConverter = file.locationConverter else {
|
||||||
|
return []
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: spaceStartPattern,
|
|
||||||
replaceString: "// MARK:"))
|
|
||||||
|
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: endNonSpacePattern,
|
|
||||||
replaceString: "// MARK: ",
|
|
||||||
keepLastChar: true))
|
|
||||||
|
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: endTwoOrMoreSpacePattern,
|
|
||||||
replaceString: "// MARK: "))
|
|
||||||
|
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: twoOrMoreSpacesAfterHyphenPattern,
|
|
||||||
replaceString: "// MARK: - "))
|
|
||||||
|
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: nonSpaceOrNewlineAfterHyphenPattern,
|
|
||||||
replaceString: "// MARK: - ",
|
|
||||||
keepLastChar: true))
|
|
||||||
|
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: oneOrMoreSpacesBeforeColonPattern,
|
|
||||||
replaceString: "// MARK:",
|
|
||||||
keepLastChar: false))
|
|
||||||
|
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: nonWhitespaceBeforeColonPattern,
|
|
||||||
replaceString: "// MARK:",
|
|
||||||
keepLastChar: false))
|
|
||||||
|
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: nonWhitespaceNorColonBeforeSpacesPattern,
|
|
||||||
replaceString: "// MARK: ",
|
|
||||||
keepLastChar: false))
|
|
||||||
|
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: invalidLowercasePattern,
|
|
||||||
replaceString: "// MARK:"))
|
|
||||||
|
|
||||||
result.append(contentsOf: correct(file: file,
|
|
||||||
pattern: threeSlashesInsteadOfTwo,
|
|
||||||
replaceString: "// MARK:"))
|
|
||||||
|
|
||||||
return result.unique
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func correct(file: SwiftLintFile,
|
let disabledRegions = file.regions()
|
||||||
pattern: String,
|
.filter { $0.isRuleDisabled(self) }
|
||||||
replaceString: String,
|
.compactMap { $0.toSourceRange(locationConverter: locationConverter) }
|
||||||
keepLastChar: Bool = false) -> [Correction] {
|
|
||||||
let violations = violationRanges(in: file, matching: pattern)
|
|
||||||
let matches = file.ruleEnabled(violatingRanges: violations, for: self)
|
|
||||||
if matches.isEmpty { return [] }
|
|
||||||
|
|
||||||
var nsstring = file.contents.bridge()
|
let rewriter = MarkRuleRewriter(locationConverter: locationConverter,
|
||||||
let description = Self.description
|
disabledRegions: disabledRegions)
|
||||||
var corrections = [Correction]()
|
let newTree = rewriter
|
||||||
for var range in matches.reversed() {
|
.visit(file.syntaxTree!)
|
||||||
if keepLastChar {
|
guard rewriter.sortedPositions.isNotEmpty else { return [] }
|
||||||
range.length -= 1
|
|
||||||
|
file.write(newTree.description)
|
||||||
|
return rewriter.sortedPositions.map { position in
|
||||||
|
Correction(
|
||||||
|
ruleDescription: Self.description,
|
||||||
|
location: Location(file: file, position: position)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
let location = Location(file: file, characterOffset: range.location)
|
|
||||||
nsstring = nsstring.replacingCharacters(in: range, with: replaceString).bridge()
|
|
||||||
corrections.append(Correction(ruleDescription: description, location: location))
|
|
||||||
}
|
}
|
||||||
file.write(nsstring.bridge())
|
}
|
||||||
return corrections
|
|
||||||
|
// MARK: - MarkRuleVisitor
|
||||||
|
|
||||||
|
private final class MarkRuleVisitor: SyntaxVisitor {
|
||||||
|
private(set) var positions: [AbsolutePosition] = []
|
||||||
|
let locationConverter: SourceLocationConverter
|
||||||
|
|
||||||
|
init(locationConverter: SourceLocationConverter) {
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func violationRanges(in file: SwiftLintFile, matching pattern: String) -> [NSRange] {
|
override func visitPost(_ node: TokenSyntax) {
|
||||||
return file.rangesAndTokens(matching: pattern).filter { matchRange, syntaxTokens in
|
positions.append(contentsOf: node.violations(locationConverter: locationConverter))
|
||||||
guard
|
|
||||||
let syntaxToken = syntaxTokens.first,
|
|
||||||
let syntaxKind = syntaxToken.kind,
|
|
||||||
SyntaxKind.commentKinds.contains(syntaxKind),
|
|
||||||
case let tokenLocation = Location(file: file, byteOffset: syntaxToken.offset),
|
|
||||||
case let matchLocation = Location(file: file, characterOffset: matchRange.location),
|
|
||||||
// Skip MARKs that are part of a multiline comment
|
|
||||||
tokenLocation.line == matchLocation.line
|
|
||||||
else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return true
|
}
|
||||||
}.compactMap { range, syntaxTokens in
|
|
||||||
let byteRange = ByteRange(location: syntaxTokens[0].offset, length: 0)
|
// MARK: - MarkRuleRewriter
|
||||||
let identifierRange = file.stringView.byteRangeToNSRange(byteRange)
|
|
||||||
return identifierRange.map { NSUnionRange($0, range) }
|
private final class MarkRuleRewriter: SyntaxRewriter {
|
||||||
|
private var positions: [AbsolutePosition] = []
|
||||||
|
var sortedPositions: [AbsolutePosition] { positions.sorted() }
|
||||||
|
let locationConverter: SourceLocationConverter
|
||||||
|
let disabledRegions: [SourceRange]
|
||||||
|
|
||||||
|
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
self.disabledRegions = disabledRegions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func visit(_ token: TokenSyntax) -> Syntax {
|
||||||
|
let violations = token.violations(locationConverter: locationConverter)
|
||||||
|
guard let firstViolation = violations.first else {
|
||||||
|
return Syntax(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isInDisabledRegion = disabledRegions.contains { region in
|
||||||
|
region.contains(firstViolation, locationConverter: locationConverter)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !isInDisabledRegion else {
|
||||||
|
return Syntax(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
positions.append(contentsOf: violations)
|
||||||
|
|
||||||
|
var token = token
|
||||||
|
token.leadingTrivia = Trivia(pieces: token.leadingTrivia.map { piece in
|
||||||
|
if case let .lineComment(comment) = piece, comment.isInvalidMarkComment {
|
||||||
|
return .lineComment(comment.fixingMarkCommentFormat())
|
||||||
|
} else if case let .docLineComment(comment) = piece, comment.isInvalidMarkComment {
|
||||||
|
return .lineComment(comment.fixingMarkCommentFormat())
|
||||||
|
} else {
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Syntax(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
private extension TokenSyntax {
|
||||||
|
func violations(locationConverter: SourceLocationConverter) -> [AbsolutePosition] {
|
||||||
|
leadingTrivia.violations(offset: position, locationConverter: locationConverter) +
|
||||||
|
trailingTrivia.violations(offset: endPositionBeforeTrailingTrivia, locationConverter: locationConverter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Trivia {
|
||||||
|
func violations(offset: AbsolutePosition, locationConverter: SourceLocationConverter) -> [AbsolutePosition] {
|
||||||
|
var triviaOffset = SourceLength.zero
|
||||||
|
var results: [AbsolutePosition] = []
|
||||||
|
for trivia in self {
|
||||||
|
switch trivia {
|
||||||
|
case .lineComment(let comment), .docLineComment(let comment):
|
||||||
|
if comment.isInvalidMarkComment {
|
||||||
|
results.append(offset + triviaOffset)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
triviaOffset += trivia.sourceLength
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,14 +225,74 @@ private let issue1749Example = Example(
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
// This example should not trigger changes
|
|
||||||
private let issue1749Correction = issue1749Example
|
private let issue1749Correction = issue1749Example
|
||||||
|
|
||||||
// These need to be at the bottom of the file to work around https://bugs.swift.org/browse/SR-10486
|
private extension String {
|
||||||
|
var isInvalidMarkComment: Bool {
|
||||||
|
if self == "// MARK:" {
|
||||||
|
return false
|
||||||
|
} else if self == "// MARK: -" {
|
||||||
|
return false
|
||||||
|
} else if starts(with: "// MARK: ") {
|
||||||
|
return true
|
||||||
|
} else if starts(with: "// MARK: - ") {
|
||||||
|
return true
|
||||||
|
} else if starts(with: "// MARK: -") && !starts(with: "// MARK: - ") {
|
||||||
|
return true
|
||||||
|
} else if starts(with: "// Mark ") || starts(with: "// mark ") {
|
||||||
|
return false
|
||||||
|
} else if starts(with: "/// Mark ") || starts(with: "/// mark ") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private let nonSpace = "[^ ]"
|
let lowercaseComponents = lowercased().split(separator: " ")
|
||||||
private let twoOrMoreSpace = " {2,}"
|
if lowercaseComponents.first?.starts(with: "//mark") == true {
|
||||||
private let mark = "MARK:"
|
return true
|
||||||
private let nonSpaceOrTwoOrMoreSpace = "(?:\(nonSpace)|\(twoOrMoreSpace))"
|
} else if lowercaseComponents.first?.starts(with: "///mark") == true {
|
||||||
|
return true
|
||||||
|
} else if lowercaseComponents.count < 2 {
|
||||||
|
return false
|
||||||
|
} else if lowercaseComponents[0] == "///" && (split(separator: " ")[1] == "MARK" ||
|
||||||
|
split(separator: " ")[1] == "MARK:") {
|
||||||
|
return true
|
||||||
|
} else if lowercaseComponents[0] == "//" && lowercaseComponents[1].starts(with: "mark") &&
|
||||||
|
!starts(with: "// MARK: ") {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private let nonSpaceOrTwoOrMoreSpaceOrNewline = "(?:[^ \n]|\(twoOrMoreSpace))"
|
func fixingMarkCommentFormat() -> String {
|
||||||
|
guard isInvalidMarkComment else {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
if contains("-") {
|
||||||
|
let body = drop(while: { $0 != "-" }).dropFirst().drop(while: \.isWhitespace)
|
||||||
|
if body.isEmpty {
|
||||||
|
return "// MARK: -"
|
||||||
|
} else {
|
||||||
|
return "// MARK: - \(body)"
|
||||||
|
}
|
||||||
|
} else if contains(":"), let body = split(separator: ":")[safe: 1]?.drop(while: \.isWhitespace) {
|
||||||
|
let components = split(separator: ":")
|
||||||
|
if components.count == 1 || body.isEmpty {
|
||||||
|
return "// MARK:"
|
||||||
|
} else {
|
||||||
|
return "// MARK: \(body)"
|
||||||
|
}
|
||||||
|
} else if case let components = split(separator: " "), components.count > 2,
|
||||||
|
components[1].lowercased() == "mark" {
|
||||||
|
let body = Array(components).dropFirst(2).joined(separator: " ")
|
||||||
|
return "// MARK: \(body)"
|
||||||
|
} else {
|
||||||
|
return "// MARK:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Array {
|
||||||
|
subscript(safe index: Int) -> Iterator.Element? {
|
||||||
|
return index < count && index >= 0 ? self[index] : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue