Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift
T
JP SimardandGitHub 6b094dd711 [SwiftSyntax] Update to latest 509.0.0 tag (#4920)
* [bazel] Remove custom SwiftSyntax BUILD file

Something similar to this has been merged upstream instead now. This
also renames the repo name to SwiftSyntax in preparation for it being in
the BCR

* [SwiftSyntax] Update to latest 509.0.0 tag

https://github.com/apple/swift-syntax/releases/tag/509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-04-25-b
2023-05-03 11:34:26 -04:00

238 lines
8.9 KiB
Swift

import Foundation
import SourceKittenFramework
import SwiftSyntax
struct OperatorUsageWhitespaceRule: OptInRule, CorrectableRule, ConfigurationProviderRule, SourceKitFreeRule {
var configuration = OperatorUsageWhitespaceConfiguration()
static let description = RuleDescription(
identifier: "operator_usage_whitespace",
name: "Operator Usage Whitespace",
description: "Operators should be surrounded by a single whitespace when they are being used",
kind: .style,
nonTriggeringExamples: OperatorUsageWhitespaceRuleExamples.nonTriggeringExamples,
triggeringExamples: OperatorUsageWhitespaceRuleExamples.triggeringExamples,
corrections: OperatorUsageWhitespaceRuleExamples.corrections
)
func validate(file: SwiftLintFile) -> [StyleViolation] {
return violationRanges(file: file).map { range, _ in
StyleViolation(ruleDescription: Self.description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, byteOffset: range.location))
}
}
private func violationRanges(file: SwiftLintFile) -> [(ByteRange, String)] {
OperatorUsageWhitespaceVisitor(
allowedNoSpaceOperators: configuration.allowedNoSpaceOperators
)
.walk(file: file, handler: \.violationRanges)
.filter { byteRange, _ in
!configuration.skipAlignedConstants || !isAlignedConstant(in: byteRange, file: file)
}.sorted { lhs, rhs in
lhs.0.location < rhs.0.location
}
}
func correct(file: SwiftLintFile) -> [Correction] {
let violatingRanges = violationRanges(file: file)
.compactMap { byteRange, correction -> (NSRange, String)? in
guard let range = file.stringView.byteRangeToNSRange(byteRange) else {
return nil
}
return (range, correction)
}
.filter { range, _ in
return file.ruleEnabled(violatingRanges: [range], for: self).isNotEmpty
}
var correctedContents = file.contents
var adjustedLocations = [Int]()
for (violatingRange, correction) in violatingRanges.reversed() {
if let indexRange = correctedContents.nsrangeToIndexRange(violatingRange) {
correctedContents = correctedContents
.replacingCharacters(in: indexRange, with: correction)
adjustedLocations.insert(violatingRange.location, at: 0)
}
}
file.write(correctedContents)
return adjustedLocations.map {
Correction(ruleDescription: Self.description,
location: Location(file: file, characterOffset: $0))
}
}
private func isAlignedConstant(in byteRange: ByteRange, file: SwiftLintFile) -> Bool {
// Make sure we have match with assignment operator and with spaces before it
guard let matchedString = file.stringView.substringWithByteRange(byteRange) else {
return false
}
let equalityOperatorRegex = regex("\\s+=\\s")
guard let match = equalityOperatorRegex.firstMatch(
in: matchedString,
options: [],
range: matchedString.fullNSRange),
match.range == matchedString.fullNSRange
else {
return false
}
guard let (lineNumber, _) = file.stringView.lineAndCharacter(forByteOffset: byteRange.upperBound),
case let lineIndex = lineNumber - 1, lineIndex >= 0 else {
return false
}
// Find lines above and below with the same location of =
let currentLine = file.stringView.lines[lineIndex].content
let index = currentLine.firstIndex(of: "=")
guard let offset = index.map({ currentLine.distance(from: currentLine.startIndex, to: $0) }) else {
return false
}
// Look around for assignment operator in lines around
let lineIndexesAround = (1...configuration.linesLookAround)
.flatMap { [lineIndex + $0, lineIndex - $0] }
func isValidIndex(_ idx: Int) -> Bool {
return idx != lineIndex && idx >= 0 && idx < file.stringView.lines.count
}
for lineIndex in lineIndexesAround where isValidIndex(lineIndex) {
let line = file.stringView.lines[lineIndex].content
guard !line.isEmpty else { continue }
let index = line.index(line.startIndex,
offsetBy: offset,
limitedBy: line.index(line.endIndex, offsetBy: -1))
if index.map({ line[$0] }) == "=" {
return true
}
}
return false
}
}
private class OperatorUsageWhitespaceVisitor: SyntaxVisitor {
private let allowedNoSpaceOperators: Set<String>
private(set) var violationRanges: [(ByteRange, String)] = []
init(allowedNoSpaceOperators: [String]) {
self.allowedNoSpaceOperators = Set(allowedNoSpaceOperators)
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: BinaryOperatorExprSyntax) {
if let violation = violation(operatorToken: node.operatorToken) {
violationRanges.append(violation)
}
}
override func visitPost(_ node: InitializerClauseSyntax) {
if let violation = violation(operatorToken: node.equal) {
violationRanges.append(violation)
}
}
override func visitPost(_ node: TypeInitializerClauseSyntax) {
if let violation = violation(operatorToken: node.equal) {
violationRanges.append(violation)
}
}
override func visitPost(_ node: AssignmentExprSyntax) {
if let violation = violation(operatorToken: node.assignToken) {
violationRanges.append(violation)
}
}
override func visitPost(_ node: TernaryExprSyntax) {
if let violation = violation(operatorToken: node.colonMark) {
violationRanges.append(violation)
}
if let violation = violation(operatorToken: node.questionMark) {
violationRanges.append(violation)
}
}
override func visitPost(_ node: UnresolvedTernaryExprSyntax) {
if let violation = violation(operatorToken: node.colonMark) {
violationRanges.append(violation)
}
if let violation = violation(operatorToken: node.questionMark) {
violationRanges.append(violation)
}
}
private func violation(operatorToken: TokenSyntax) -> (ByteRange, String)? {
guard let previousToken = operatorToken.previousToken(viewMode: .sourceAccurate),
let nextToken = operatorToken.nextToken(viewMode: .sourceAccurate) else {
return nil
}
let noSpacingBefore = previousToken.trailingTrivia.isEmpty && operatorToken.leadingTrivia.isEmpty
let noSpacingAfter = operatorToken.trailingTrivia.isEmpty && nextToken.leadingTrivia.isEmpty
let noSpacing = noSpacingBefore || noSpacingAfter
let operatorText = operatorToken.text
if noSpacing && allowedNoSpaceOperators.contains(operatorText) {
return nil
}
let tooMuchSpacingBefore = previousToken.trailingTrivia.containsTooMuchWhitespacing &&
!operatorToken.leadingTrivia.containsNewlines()
let tooMuchSpacingAfter = operatorToken.trailingTrivia.containsTooMuchWhitespacing &&
!operatorToken.trailingTrivia.containsNewlines()
let tooMuchSpacing = (tooMuchSpacingBefore || tooMuchSpacingAfter) &&
!operatorToken.leadingTrivia.containsComments &&
!operatorToken.trailingTrivia.containsComments &&
!nextToken.leadingTrivia.containsComments
guard noSpacing || tooMuchSpacing else {
return nil
}
let location = ByteCount(previousToken.endPositionBeforeTrailingTrivia)
let endPosition = ByteCount(nextToken.positionAfterSkippingLeadingTrivia)
let range = ByteRange(
location: location,
length: endPosition - location
)
let correction = allowedNoSpaceOperators.contains(operatorText) ? operatorText : " \(operatorText) "
return (range, correction)
}
}
private extension Trivia {
var containsTooMuchWhitespacing: Bool {
return contains { element in
guard case let .spaces(spaces) = element, spaces > 1 else {
return false
}
return true
}
}
var containsComments: Bool {
return contains { element in
switch element {
case .blockComment, .docLineComment, .docBlockComment, .lineComment:
return true
case .backslashes, .carriageReturnLineFeeds, .carriageReturns, .formfeeds, .newlines, .pounds,
.shebang, .spaces, .tabs, .unexpectedText, .verticalTabs:
return false
}
}
}
}