mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
191 lines
7.6 KiB
Swift
191 lines
7.6 KiB
Swift
//
|
|
// LargeTupleRule.swift
|
|
// SwiftLint
|
|
//
|
|
// Created by Marcelo Fabri on 01/01/17.
|
|
// Copyright © 2017 Realm. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
private enum LargeTupleRuleError: Error {
|
|
case unbalencedParentheses
|
|
}
|
|
|
|
public struct LargeTupleRule: ASTRule, ConfigurationProviderRule {
|
|
|
|
public var configuration = SeverityLevelsConfiguration(warning: 2, error: 3)
|
|
|
|
public init() {}
|
|
|
|
public static let description = RuleDescription(
|
|
identifier: "large_tuple",
|
|
name: "Large Tuple",
|
|
description: "Tuples shouldn't have too many members. Create a custom type instead.",
|
|
nonTriggeringExamples: [
|
|
"let foo: (Int, Int)\n",
|
|
"let foo: (start: Int, end: Int)\n",
|
|
"let foo: (Int, (Int, String))\n",
|
|
"func foo() -> (Int, Int)\n",
|
|
"func foo() -> (Int, Int) {}\n",
|
|
"func foo(bar: String) -> (Int, Int)\n",
|
|
"func foo(bar: String) -> (Int, Int) {}\n",
|
|
"func foo() throws -> (Int, Int)\n",
|
|
"func foo() throws -> (Int, Int) {}\n",
|
|
"let foo: (Int, Int, Int) -> Void\n"
|
|
],
|
|
triggeringExamples: [
|
|
"↓let foo: (Int, Int, Int)\n",
|
|
"↓let foo: (start: Int, end: Int, value: String)\n",
|
|
"↓let foo: (Int, (Int, Int, Int))\n",
|
|
"func foo(↓bar: (Int, Int, Int))\n",
|
|
"func foo() -> ↓(Int, Int, Int)\n",
|
|
"func foo() -> ↓(Int, Int, Int) {}\n",
|
|
"func foo(bar: String) -> ↓(Int, Int, Int)\n",
|
|
"func foo(bar: String) -> ↓(Int, Int, Int) {}\n",
|
|
"func foo() throws -> ↓(Int, Int, Int)\n",
|
|
"func foo() throws -> ↓(Int, Int, Int) {}\n",
|
|
"func foo() throws -> ↓(Int, ↓(String, String, String), Int) {}\n"
|
|
]
|
|
)
|
|
|
|
public func validateFile(_ file: File, kind: SwiftDeclarationKind,
|
|
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
|
|
let offsets = violationOffsetsForTypes(file, dictionary: dictionary, kind: kind) +
|
|
violationOffsetsForFunctions(file, dictionary: dictionary, kind: kind)
|
|
|
|
return offsets.flatMap { location, size in
|
|
for parameter in configuration.params where size > parameter.value {
|
|
let reason = "Tuples should have at most \(parameter.value) members."
|
|
return StyleViolation(ruleDescription: type(of: self).description,
|
|
severity: parameter.severity,
|
|
location: Location(file: file, byteOffset: location),
|
|
reason: reason)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func violationOffsetsForTypes(_ file: File,
|
|
dictionary: [String: SourceKitRepresentable],
|
|
kind: SwiftDeclarationKind) -> [(offset: Int, size: Int)] {
|
|
let kinds = SwiftDeclarationKind.variableKinds().filter { $0 != .varLocal }
|
|
guard kinds.contains(kind),
|
|
let type = dictionary["key.typename"] as? String,
|
|
let offset = (dictionary["key.offset"] as? Int64).flatMap({ Int($0) }),
|
|
let ranges = try? parenthesesRanges(type) else {
|
|
return []
|
|
}
|
|
|
|
var text = type.bridge()
|
|
var maxSize: Int?
|
|
for range in ranges {
|
|
let substring = text.substring(with: range)
|
|
let size = substring.components(separatedBy: ",").count
|
|
maxSize = max(size, maxSize ?? .min)
|
|
|
|
let replacement = String(repeating: " ", count: substring.bridge().length)
|
|
text = text.replacingCharacters(in: range, with: replacement).bridge()
|
|
}
|
|
|
|
return maxSize.flatMap { [(offset: offset, size: $0)] } ?? []
|
|
}
|
|
|
|
private func violationOffsetsForFunctions(_ file: File,
|
|
dictionary: [String: SourceKitRepresentable],
|
|
kind: SwiftDeclarationKind) -> [(offset: Int, size: Int)] {
|
|
let contents = file.contents.bridge()
|
|
guard SwiftDeclarationKind.functionKinds().contains(kind),
|
|
let returnRange = returnRangeForFunction(dictionary),
|
|
let returnSubstring = contents.substringWithByteRange(start: returnRange.location,
|
|
length: returnRange.length),
|
|
let ranges = try? parenthesesRanges(returnSubstring) else {
|
|
return []
|
|
}
|
|
|
|
var text = returnSubstring.bridge()
|
|
var offsets = [(offset: Int, size: Int)]()
|
|
|
|
for range in ranges {
|
|
let substring = text.substring(with: range)
|
|
if let byteRange = text.NSRangeToByteRange(start: range.location, length: range.length) {
|
|
let size = substring.components(separatedBy: ",").count
|
|
let offset = byteRange.location + returnRange.location
|
|
offsets.append((offset: offset, size: size))
|
|
}
|
|
|
|
let replacement = String(repeating: " ", count: substring.bridge().length)
|
|
text = text.replacingCharacters(in: range, with: replacement).bridge()
|
|
}
|
|
|
|
return offsets.sorted(by: { $0.offset < $1.offset })
|
|
}
|
|
|
|
private func returnRangeForFunction(_ dictionary: [String: SourceKitRepresentable]) -> NSRange? {
|
|
guard let nameOffset = (dictionary["key.nameoffset"] as? Int64).flatMap({ Int($0) }),
|
|
let nameLength = (dictionary["key.namelength"] as? Int64).flatMap({ Int($0) }),
|
|
let length = (dictionary["key.length"] as? Int64).flatMap({ Int($0) }),
|
|
let offset = (dictionary["key.offset"] as? Int64).flatMap({ Int($0) }) else {
|
|
return nil
|
|
}
|
|
|
|
let start = nameOffset + nameLength
|
|
let end = (dictionary["key.bodyoffset"] as? Int64).flatMap({ Int($0) }) ?? length + offset
|
|
|
|
guard end - start > 0 else {
|
|
return nil
|
|
}
|
|
|
|
return NSRange(location: start, length: end - start)
|
|
}
|
|
|
|
private func parenthesesRanges(_ text: String) throws -> [NSRange] {
|
|
var stack = [Int]()
|
|
var balanced = true
|
|
var ranges = [NSRange]()
|
|
|
|
let nsText = text.bridge()
|
|
let parentheses = CharacterSet(charactersIn: "()")
|
|
var index = 0
|
|
let length = nsText.length
|
|
|
|
while balanced {
|
|
let searchRange = NSRange(location: index, length: length - index)
|
|
let range = nsText.rangeOfCharacter(from: parentheses, options: [], range: searchRange)
|
|
if range.location == NSNotFound {
|
|
break
|
|
}
|
|
|
|
index = NSMaxRange(range)
|
|
let symbol = nsText.substring(with: range)
|
|
|
|
if symbol == "(" {
|
|
stack.append(range.location)
|
|
} else if let startIdx = stack.popLast() {
|
|
ranges.append(NSRange(location: startIdx, length: range.location - startIdx + 1))
|
|
} else {
|
|
balanced = false
|
|
}
|
|
}
|
|
|
|
guard balanced && stack.isEmpty else {
|
|
throw LargeTupleRuleError.unbalencedParentheses
|
|
}
|
|
|
|
let arrowRegex = regex("\\s*->")
|
|
return ranges.filter { range in
|
|
let start = NSMaxRange(range)
|
|
let restOfStringRange = NSRange(location: start, length: length - start)
|
|
if let match = arrowRegex.firstMatch(in: text, options: [], range: restOfStringRange)?.range,
|
|
match.location == start {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
}
|