mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
@@ -85,6 +85,10 @@
|
||||
[Marcelo Fabri](https://github.com/marcelofabri)
|
||||
[#1065](https://github.com/realm/SwiftLint/issues/1065)
|
||||
|
||||
* Add `generic_type_name` rule that validates generic constraint type names.
|
||||
[Marcelo Fabri](https://github.com/marcelofabri)
|
||||
[#51](https://github.com/realm/SwiftLint/issues/51)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* Ignore close parentheses on `vertical_parameter_alignment` rule.
|
||||
|
||||
@@ -95,6 +95,7 @@ public let masterRuleList = RuleList(rules:
|
||||
ForceUnwrappingRule.self,
|
||||
FunctionBodyLengthRule.self,
|
||||
FunctionParameterCountRule.self,
|
||||
GenericTypeNameRule.self,
|
||||
ImplicitGetterRule.self,
|
||||
LargeTupleRule.self,
|
||||
LeadingWhitespaceRule.self,
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//
|
||||
// GenericTypeNameRule.swift
|
||||
// SwiftLint
|
||||
//
|
||||
// Created by Marcelo Fabri on 12/25/16.
|
||||
// Copyright © 2016 Realm. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SourceKittenFramework
|
||||
|
||||
public struct GenericTypeNameRule: ASTRule, ConfigurationProviderRule {
|
||||
public var configuration = NameConfiguration(minLengthWarning: 1,
|
||||
minLengthError: 0,
|
||||
maxLengthWarning: 20,
|
||||
maxLengthError: 1000)
|
||||
|
||||
public init() {}
|
||||
|
||||
public static let description = RuleDescription(
|
||||
identifier: "generic_type_name",
|
||||
name: "Generic Type Name",
|
||||
description: "Generic type name should only contain alphanumeric characters, start with an " +
|
||||
"uppercase character and span between 1 and 20 characters in length.",
|
||||
nonTriggeringExamples: [
|
||||
"func foo<T>() {}\n",
|
||||
"func foo<T>() -> T {}\n",
|
||||
"func foo<T, U>(param: U) -> T {}\n",
|
||||
"func foo<T: Hashable, U: Rule>(param: U) -> T {}\n",
|
||||
"struct Foo<T> {}\n",
|
||||
"class Foo<T> {}\n",
|
||||
"func run(_ options: NoOptions<CommandantError<()>>) {}\n",
|
||||
"func foo(_ options: Set<type>) {}\n",
|
||||
"func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool\n",
|
||||
"func configureWith(data: Either<MessageThread, (project: Project, backing: Backing)>)\n"
|
||||
],
|
||||
triggeringExamples: [
|
||||
"func foo<↓T_Foo>() {}\n",
|
||||
"func foo<T, ↓U_Foo>(param: U_Foo) -> T {}\n",
|
||||
"func foo<↓\(String(repeating: "T", count: 21))>() {}\n",
|
||||
"func foo<↓type>() {}\n"
|
||||
] + ["class", "struct"].flatMap { type in
|
||||
[
|
||||
"\(type) Foo<↓T_Foo> {}\n",
|
||||
"\(type) Foo<T, ↓U_Foo> {}\n",
|
||||
"\(type) Foo<↓T_Foo, ↓U_Foo> {}\n",
|
||||
"\(type) Foo<↓\(String(repeating: "T", count: 21))> {}\n",
|
||||
"\(type) Foo<↓type> {}\n"
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
private let pattern = regex("<(\\s*\\w.*?)>")
|
||||
|
||||
public func validateFile(_ file: File,
|
||||
kind: SwiftDeclarationKind,
|
||||
dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {
|
||||
let types = genericTypesForType(file, kind: kind, dictionary: dictionary) +
|
||||
genericTypesForFunction(file, kind: kind, dictionary: dictionary)
|
||||
|
||||
return types.flatMap { validateName(name: $0.0, file: file, offset: $0.1) }
|
||||
}
|
||||
|
||||
private func genericTypesForType(_ file: File,
|
||||
kind: SwiftDeclarationKind,
|
||||
dictionary: [String: SourceKitRepresentable]) -> [(String, Int)] {
|
||||
guard kind == .class || kind == .struct,
|
||||
let nameOffset = (dictionary["key.nameoffset"] as? Int64).flatMap({ Int($0) }),
|
||||
let nameLength = (dictionary["key.namelength"] as? Int64).flatMap({ Int($0) }),
|
||||
let bodyOffset = (dictionary["key.bodyoffset"] as? Int64).flatMap({ Int($0) }),
|
||||
case let contents = file.contents.bridge(),
|
||||
case let start = nameOffset + nameLength,
|
||||
case let length = bodyOffset - start,
|
||||
let range = contents.byteRangeToNSRange(start: start, length: length),
|
||||
let match = pattern.firstMatch(in: file.contents, options: [], range: range)?.rangeAt(1) else {
|
||||
return []
|
||||
}
|
||||
|
||||
let genericConstraint = contents.substring(with: match)
|
||||
return extractTypesFromGenericConstraint(genericConstraint, offset: match.location, file: file)
|
||||
}
|
||||
|
||||
private func genericTypesForFunction(_ file: File,
|
||||
kind: SwiftDeclarationKind,
|
||||
dictionary: [String: SourceKitRepresentable]) -> [(String, Int)] {
|
||||
guard SwiftDeclarationKind.functionKinds().contains(kind),
|
||||
let offset = (dictionary["key.nameoffset"] as? Int64).flatMap({ Int($0) }),
|
||||
let length = (dictionary["key.namelength"] as? Int64).flatMap({ Int($0) }),
|
||||
case let contents = file.contents.bridge(),
|
||||
let range = contents.byteRangeToNSRange(start: offset, length: length),
|
||||
let match = pattern.firstMatch(in: file.contents, options: [], range: range)?.rangeAt(1),
|
||||
match.location < minParameterOffset(parameters: dictionary.enclosedVarParameters, file: file) else {
|
||||
return []
|
||||
}
|
||||
|
||||
let genericConstraint = contents.substring(with: match)
|
||||
return extractTypesFromGenericConstraint(genericConstraint, offset: match.location, file: file)
|
||||
}
|
||||
|
||||
private func minParameterOffset(parameters: [[String: SourceKitRepresentable]], file: File) -> Int {
|
||||
let offsets = parameters.flatMap {
|
||||
($0["key.offset"] as? Int64).flatMap({ Int($0) })
|
||||
}.flatMap {
|
||||
file.contents.bridge().byteRangeToNSRange(start: $0, length: 0)?.location
|
||||
}
|
||||
|
||||
return offsets.min() ?? Int.max
|
||||
}
|
||||
|
||||
private func extractTypesFromGenericConstraint(_ constraint: String, offset: Int, file: File) -> [(String, Int)] {
|
||||
guard let beforeWhere = constraint.components(separatedBy: "where").first else {
|
||||
return []
|
||||
}
|
||||
|
||||
let namesAndRanges: [(String, NSRange)] = beforeWhere.split(separator: ",").flatMap { string, range in
|
||||
return string.split(separator: ":").first.map {
|
||||
let (trimmed, trimmedRange) = $0.0.trimmingWhitespaces()
|
||||
return (trimmed, NSRange(location: range.location + trimmedRange.location,
|
||||
length: trimmedRange.length))
|
||||
}
|
||||
}
|
||||
|
||||
let contents = file.contents.bridge()
|
||||
return namesAndRanges.flatMap { (name, range) -> (String, Int)? in
|
||||
guard let byteRange = contents.NSRangeToByteRange(start: range.location + offset,
|
||||
length: range.length),
|
||||
case let kinds = file.syntaxMap.tokensIn(byteRange).flatMap({ SyntaxKind(rawValue: $0.type) }),
|
||||
kinds == [.identifier] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (name, byteRange.location)
|
||||
}
|
||||
}
|
||||
|
||||
private func validateName(name: String, file: File, offset: Int) -> [StyleViolation] {
|
||||
guard !configuration.excluded.contains(name) else {
|
||||
return []
|
||||
}
|
||||
|
||||
let nameCharacterSet = CharacterSet(charactersIn: name)
|
||||
if !CharacterSet.alphanumerics.isSuperset(of: nameCharacterSet) {
|
||||
return [
|
||||
StyleViolation(ruleDescription: type(of: self).description,
|
||||
severity: .error,
|
||||
location: Location(file: file, byteOffset: offset),
|
||||
reason: "Generic type name should only contain alphanumeric characters: '\(name)'")
|
||||
]
|
||||
} else if !name.substring(to: name.index(after: name.startIndex)).isUppercase() {
|
||||
return [
|
||||
StyleViolation(ruleDescription: type(of: self).description,
|
||||
severity: .error,
|
||||
location: Location(file: file, byteOffset: offset),
|
||||
reason: "Generic type name should start with an uppercase character: '\(name)'")
|
||||
]
|
||||
} else if let severity = severity(forLength: name.characters.count) {
|
||||
return [
|
||||
StyleViolation(ruleDescription: type(of: self).description,
|
||||
severity: severity,
|
||||
location: Location(file: file, byteOffset: offset),
|
||||
reason: "Generic type name should be between \(configuration.minLengthThreshold) and " +
|
||||
"\(configuration.maxLengthThreshold) characters long: '\(name)'")
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
fileprivate func split(separator: Character) -> [(String, NSRange)] {
|
||||
var offsets = [0]
|
||||
var ends = [Int]()
|
||||
var currentOffset = 0
|
||||
let components = characters.split { character in
|
||||
currentOffset += 1
|
||||
if character == separator {
|
||||
offsets.append(currentOffset)
|
||||
ends.append(currentOffset - 1)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}.map(String.init)
|
||||
|
||||
ends.append(characters.count)
|
||||
|
||||
let ranges = offsets.enumerated().map { index, offset -> NSRange in
|
||||
let next = ends[index]
|
||||
return NSRange(location: offset, length: next - offset)
|
||||
}
|
||||
|
||||
return Array(zip(components, ranges))
|
||||
}
|
||||
|
||||
fileprivate func trimmingWhitespaces() -> (String, NSRange) {
|
||||
let range = NSRange(location: 0, length: bridge().length)
|
||||
guard let match = regex("^\\s*(\\S*)\\s*$").firstMatch(in: self, options: [], range: range),
|
||||
NSEqualRanges(range, match.range) else {
|
||||
return (self, range)
|
||||
}
|
||||
|
||||
let trimmedRange = match.rangeAt(1)
|
||||
return (bridge().substring(with: trimmedRange), trimmedRange)
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,8 @@
|
||||
D4B022981E102EE8007E5297 /* ObjectLiteralRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B022971E102EE8007E5297 /* ObjectLiteralRule.swift */; };
|
||||
D4B022B01E109816007E5297 /* CharacterSet+LinuxHack.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B022AF1E109816007E5297 /* CharacterSet+LinuxHack.swift */; };
|
||||
D4B022B21E10B613007E5297 /* RedundantVoidReturnRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B022B11E10B613007E5297 /* RedundantVoidReturnRule.swift */; };
|
||||
D4B022981E102EE8007E5297 /* ObjectLiteralRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B022971E102EE8007E5297 /* ObjectLiteralRule.swift */; };
|
||||
D4B022A41E105636007E5297 /* GenericTypeNameRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B022A31E105636007E5297 /* GenericTypeNameRule.swift */; };
|
||||
D4C4A34C1DEA4FF000E0E04C /* AttributesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A34A1DEA4FD700E0E04C /* AttributesConfiguration.swift */; };
|
||||
D4C4A34E1DEA877200E0E04C /* FileHeaderRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */; };
|
||||
D4C4A3521DEFBBB700E0E04C /* FileHeaderConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */; };
|
||||
@@ -374,6 +376,8 @@
|
||||
D4B022971E102EE8007E5297 /* ObjectLiteralRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectLiteralRule.swift; sourceTree = "<group>"; };
|
||||
D4B022AF1E109816007E5297 /* CharacterSet+LinuxHack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CharacterSet+LinuxHack.swift"; sourceTree = "<group>"; };
|
||||
D4B022B11E10B613007E5297 /* RedundantVoidReturnRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedundantVoidReturnRule.swift; sourceTree = "<group>"; };
|
||||
D4B022971E102EE8007E5297 /* ObjectLiteralRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectLiteralRule.swift; sourceTree = "<group>"; };
|
||||
D4B022A31E105636007E5297 /* GenericTypeNameRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericTypeNameRule.swift; sourceTree = "<group>"; };
|
||||
D4C4A34A1DEA4FD700E0E04C /* AttributesConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributesConfiguration.swift; sourceTree = "<group>"; };
|
||||
D4C4A34D1DEA877200E0E04C /* FileHeaderRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHeaderRule.swift; sourceTree = "<group>"; };
|
||||
D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileHeaderConfiguration.swift; sourceTree = "<group>"; };
|
||||
@@ -774,6 +778,7 @@
|
||||
B58AEED51C492C7B00E901FD /* ForceUnwrappingRule.swift */,
|
||||
E88DEA8F1B099A3100A66CB0 /* FunctionBodyLengthRule.swift */,
|
||||
2E5761A91C573B83003271AF /* FunctionParameterCountRule.swift */,
|
||||
D4B022A31E105636007E5297 /* GenericTypeNameRule.swift */,
|
||||
D43DB1071DC573DA00281215 /* ImplicitGetterRule.swift */,
|
||||
D4DA1DF91E18D6200037413D /* LargeTupleRule.swift */,
|
||||
E88DEA7D1B098F2A00A66CB0 /* LeadingWhitespaceRule.swift */,
|
||||
@@ -1133,6 +1138,7 @@
|
||||
E88198601BEA98F000333A11 /* VariableNameRule.swift in Sources */,
|
||||
E88DEA791B098D4400A66CB0 /* RuleParameter.swift in Sources */,
|
||||
D4DA1DFA1E18D6200037413D /* LargeTupleRule.swift in Sources */,
|
||||
D4B022A41E105636007E5297 /* GenericTypeNameRule.swift in Sources */,
|
||||
E86396CB1BADB519002C9E88 /* CSVReporter.swift in Sources */,
|
||||
37B3FA8B1DFD45A700AD30D2 /* Dictionary+SwiftLint.swift in Sources */,
|
||||
D4B022B01E109816007E5297 /* CharacterSet+LinuxHack.swift in Sources */,
|
||||
|
||||
@@ -102,6 +102,10 @@ class RulesTests: XCTestCase {
|
||||
verifyRule(FunctionParameterCountRule.description)
|
||||
}
|
||||
|
||||
func testGenericTypeName() {
|
||||
verifyRule(GenericTypeNameRule.description)
|
||||
}
|
||||
|
||||
func testImplicitGetter() {
|
||||
verifyRule(ImplicitGetterRule.description)
|
||||
}
|
||||
@@ -363,6 +367,7 @@ extension RulesTests {
|
||||
// ("testForceUnwrapping", testForceUnwrapping),
|
||||
("testFunctionBodyLength", testFunctionBodyLength),
|
||||
("testFunctionParameterCount", testFunctionParameterCount),
|
||||
("testGenericTypeName", testGenericTypeName),
|
||||
("testImplicitGetter", testImplicitGetter),
|
||||
("testLargeTuple", testLargeTuple),
|
||||
("testLeadingWhitespace", testLeadingWhitespace),
|
||||
|
||||
Reference in New Issue
Block a user