Let `number_separator` rule trigger on misplaced separators (#4685)
This commit is contained in:
parent
60d0dd8a05
commit
d3ebfc5567
|
@ -42,6 +42,10 @@
|
|||
[David Steinacher](https://github.com/stonko1994)
|
||||
[#4626](https://github.com/realm/SwiftLint/issues/4626)
|
||||
|
||||
* Let `number_separator` rule trigger on misplaced separators, e.g. `10_00`.
|
||||
[SimplyDanny](https://github.com/SimplyDanny)
|
||||
[#4637](https://github.com/realm/SwiftLint/issues/4637)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* Report violations in all `<scope>_length` rules when the error threshold is
|
||||
|
|
|
@ -13,13 +13,24 @@ struct NumberSeparatorRule: OptInRule, SwiftSyntaxCorrectableRule, Configuration
|
|||
static let description = RuleDescription(
|
||||
identifier: "number_separator",
|
||||
name: "Number Separator",
|
||||
description: "Underscores should be used as thousand separator in large decimal numbers",
|
||||
description: """
|
||||
Underscores should be used as thousand separator in large numbers with a configurable number of digits. In \
|
||||
other words, there should be an underscore after every 3 digits in the integral as well as the fractional \
|
||||
part of a number.
|
||||
""",
|
||||
kind: .style,
|
||||
nonTriggeringExamples: NumberSeparatorRuleExamples.nonTriggeringExamples,
|
||||
triggeringExamples: NumberSeparatorRuleExamples.triggeringExamples,
|
||||
corrections: NumberSeparatorRuleExamples.corrections
|
||||
)
|
||||
|
||||
static let missingSeparatorsReason = """
|
||||
Underscores should be used as thousand separators
|
||||
"""
|
||||
static let misplacedSeparatorsReason = """
|
||||
Underscore(s) used as thousand separator(s) should be added after every 3 digits only
|
||||
"""
|
||||
|
||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||
Visitor(configuration: configuration)
|
||||
}
|
||||
|
@ -44,13 +55,13 @@ private extension NumberSeparatorRule {
|
|||
|
||||
override func visitPost(_ node: FloatLiteralExprSyntax) {
|
||||
if let violation = violation(token: node.floatingDigits) {
|
||||
violations.append(violation.position)
|
||||
violations.append(ReasonedRuleViolation(position: violation.position, reason: violation.reason))
|
||||
}
|
||||
}
|
||||
|
||||
override func visitPost(_ node: IntegerLiteralExprSyntax) {
|
||||
if let violation = violation(token: node.digits) {
|
||||
violations.append(violation.position)
|
||||
violations.append(ReasonedRuleViolation(position: violation.position, reason: violation.reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,8 +112,34 @@ private protocol NumberSeparatorValidator {
|
|||
var configuration: NumberSeparatorConfiguration { get }
|
||||
}
|
||||
|
||||
extension NumberSeparatorValidator {
|
||||
func violation(token: TokenSyntax) -> (position: AbsolutePosition, correction: String)? {
|
||||
private enum NumberSeparatorViolation {
|
||||
case missingSeparator(position: AbsolutePosition, correction: String)
|
||||
case misplacedSeparator(position: AbsolutePosition, correction: String)
|
||||
|
||||
var reason: String {
|
||||
switch self {
|
||||
case .missingSeparator: return NumberSeparatorRule.missingSeparatorsReason
|
||||
case .misplacedSeparator: return NumberSeparatorRule.misplacedSeparatorsReason
|
||||
}
|
||||
}
|
||||
|
||||
var position: AbsolutePosition {
|
||||
switch self {
|
||||
case let .missingSeparator(position, _): return position
|
||||
case let .misplacedSeparator(position, _): return position
|
||||
}
|
||||
}
|
||||
|
||||
var correction: String {
|
||||
switch self {
|
||||
case let .missingSeparator(_, correction): return correction
|
||||
case let .misplacedSeparator(_, correction): return correction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension NumberSeparatorValidator {
|
||||
func violation(token: TokenSyntax) -> NumberSeparatorViolation? {
|
||||
let content = token.withoutTrivia().text
|
||||
guard isDecimal(number: content),
|
||||
!isInValidRanges(number: content)
|
||||
|
@ -121,9 +158,7 @@ extension NumberSeparatorValidator {
|
|||
var validFraction = true
|
||||
var expectedFraction: String?
|
||||
if components.count == 2, let fractionSubstring = components.last {
|
||||
let result = isValid(number: fractionSubstring, isFraction: true)
|
||||
validFraction = result.0
|
||||
expectedFraction = result.1
|
||||
(validFraction, expectedFraction) = isValid(number: fractionSubstring, isFraction: true)
|
||||
}
|
||||
|
||||
guard let integerSubstring = components.first,
|
||||
|
@ -143,7 +178,10 @@ extension NumberSeparatorValidator {
|
|||
corrected += exponentialSymbol + exponential
|
||||
}
|
||||
|
||||
return (token.positionAfterSkippingLeadingTrivia, corrected)
|
||||
if content.contains("_") {
|
||||
return .misplacedSeparator(position: token.positionAfterSkippingLeadingTrivia, correction: corrected)
|
||||
}
|
||||
return .missingSeparator(position: token.positionAfterSkippingLeadingTrivia, correction: corrected)
|
||||
}
|
||||
|
||||
private func isDecimal(number: String) -> Bool {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@testable import SwiftLintFramework
|
||||
import SwiftParser
|
||||
import XCTest
|
||||
|
||||
class NumberSeparatorRuleTests: XCTestCase {
|
||||
|
@ -97,4 +98,47 @@ class NumberSeparatorRuleTests: XCTestCase {
|
|||
]
|
||||
)
|
||||
}
|
||||
|
||||
func testSpecificViolationReasons() {
|
||||
XCTAssertEqual(
|
||||
violations(in: "1_000"),
|
||||
[]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
violations(in: "1000"),
|
||||
[NumberSeparatorRule.missingSeparatorsReason]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
violations(in: "1.000000", config: ["minimum_fraction_length": 5]),
|
||||
[NumberSeparatorRule.missingSeparatorsReason]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
violations(in: "10_00"),
|
||||
[NumberSeparatorRule.misplacedSeparatorsReason]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
violations(in: "1_000_0"),
|
||||
[NumberSeparatorRule.misplacedSeparatorsReason]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
violations(in: "1000.0_00"),
|
||||
[NumberSeparatorRule.misplacedSeparatorsReason]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
violations(in: "10_00", config: ["minimum_length": 5]),
|
||||
[NumberSeparatorRule.misplacedSeparatorsReason]
|
||||
)
|
||||
XCTAssertEqual(
|
||||
violations(in: "1000.0_00", config: ["minimum_fraction_length": 5]),
|
||||
[NumberSeparatorRule.misplacedSeparatorsReason]
|
||||
)
|
||||
}
|
||||
|
||||
private func violations(in code: String, config: Any = []) -> [String] {
|
||||
var rule = NumberSeparatorRule()
|
||||
try? rule.configuration.apply(configuration: config)
|
||||
let visitor = rule.makeVisitor(file: SwiftLintFile(contents: ""))
|
||||
visitor.walk(Parser.parse(source: "let a = " + code))
|
||||
return visitor.violations.compactMap(\.reason)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue