// // NumberSeparatorRule.swift // SwiftLint // // Created by Marcelo Fabri on 12/05/16. // Copyright © 2016 Realm. All rights reserved. // import Foundation import SourceKittenFramework public struct NumberSeparatorRule: OptInRule, CorrectableRule, ConfigurationProviderRule { public var configuration = NumberSeparatorConfiguration(minimumLength: 0, minimumFractionLength: nil) public init() {} public static let description = RuleDescription( identifier: "number_separator", name: "Number Separator", description: "Underscores should be used as thousand separator in large decimal numbers.", nonTriggeringExamples: NumberSeparatorRuleExamples.nonTriggeringExamples, triggeringExamples: NumberSeparatorRuleExamples.swift3TriggeringExamples, corrections: NumberSeparatorRuleExamples.swift3Corrections ) public func validate(file: File) -> [StyleViolation] { return violatingRanges(in: file).map { range, _ in return StyleViolation(ruleDescription: type(of: self).description, severity: configuration.severityConfiguration.severity, location: Location(file: file, characterOffset: range.location)) } } private func violatingRanges(in file: File) -> [(NSRange, String)] { let numberTokens = file.syntaxMap.tokens.filter { SyntaxKind(rawValue: $0.type) == .number } return numberTokens.flatMap { token in guard let content = contentFrom(file: file, token: token), isDecimal(number: content) else { return nil } let signs = CharacterSet(charactersIn: "+-") let exponential = CharacterSet(charactersIn: "eE") guard let nonSign = content.components(separatedBy: signs).last, case let exponentialComponents = nonSign.components(separatedBy: exponential), let nonExponential = exponentialComponents.first else { return nil } let components = nonExponential.components(separatedBy: ".") 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 } guard let integerSubstring = components.first, case let (valid, expected) = isValid(number: integerSubstring, isFraction: false), !valid || !validFraction, let range = file.contents.bridge().byteRangeToNSRange(start: token.offset, length: token.length) else { return nil } var corrected = "" let hasSign = content.countOfLeadingCharacters(in: signs) == 1 if hasSign { corrected += String(content.characters.prefix(1)) } corrected += expected if let fraction = expectedFraction { corrected += "." + fraction } if exponentialComponents.count == 2, let exponential = exponentialComponents.last { let exponentialSymbol = content.contains("e") ? "e" : "E" corrected += exponentialSymbol + exponential } return (range, corrected) } } public func correct(file: File) -> [Correction] { let violatingRanges = self.violatingRanges(in: file).filter { range, _ in return !file.ruleEnabled(violatingRanges: [range], for: self).isEmpty } 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: type(of: self).description, location: Location(file: file, characterOffset: $0)) } } private func isDecimal(number: String) -> Bool { let lowercased = number.lowercased() let prefixes = ["0x", "0o", "0b"].flatMap { [$0, "-" + $0, "+" + $0] } return prefixes.filter { lowercased.hasPrefix($0) }.isEmpty } private func isValid(number: String, isFraction: Bool) -> (Bool, String) { var correctComponents = [String]() let clean = number.replacingOccurrences(of: "_", with: "") let minimumLength: Int if isFraction { minimumLength = configuration.minimumFractionLength ?? configuration.minimumLength } else { minimumLength = configuration.minimumLength } let shouldAddSeparators = clean.characters.count >= minimumLength for (idx, char) in reversedIfNeeded(Array(clean.characters), reversed: !isFraction).enumerated() { if idx % 3 == 0 && idx > 0 && shouldAddSeparators { correctComponents.append("_") } correctComponents.append(String(char)) } let expected = reversedIfNeeded(correctComponents, reversed: !isFraction).joined() return (expected == number, expected) } private func reversedIfNeeded(_ array: [T], reversed: Bool) -> [T] { if reversed { return array.reversed() } return array } private func contentFrom(file: File, token: SyntaxToken) -> String? { return file.contents.bridge().substringWithByteRange(start: token.offset, length: token.length) } }