forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathNoGroupingExtensionRule.swift
61 lines (51 loc) · 2.6 KB
/
NoGroupingExtensionRule.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import SourceKittenFramework
public struct NoGroupingExtensionRule: OptInRule, ConfigurationProviderRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "no_grouping_extension",
name: "No Grouping Extension",
description: "Extensions shouldn't be used to group code within the same source file.",
kind: .idiomatic,
nonTriggeringExamples: [
Example("protocol Food {}\nextension Food {}\n"),
Example("class Apples {}\nextension Oranges {}\n"),
Example("class Box<T> {}\nextension Box where T: Vegetable {}\n")
],
triggeringExamples: [
Example("enum Fruit {}\n↓extension Fruit {}\n"),
Example("↓extension Tea: Error {}\nstruct Tea {}\n"),
Example("class Ham { class Spam {}}\n↓extension Ham.Spam {}\n"),
Example("extension External { struct Gotcha {}}\n↓extension External.Gotcha {}\n")
]
)
public func validate(file: SwiftLintFile) -> [StyleViolation] {
let collector = NamespaceCollector(dictionary: file.structureDictionary)
let elements = collector.findAllElements(of: [.class, .enum, .struct, .extension])
let susceptibleNames = Set(elements.compactMap { $0.kind != .extension ? $0.name : nil })
return elements.compactMap { element in
guard element.kind == .extension, susceptibleNames.contains(element.name) else {
return nil
}
guard !hasWhereClause(dictionary: element.dictionary, file: file) else {
return nil
}
return StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: element.offset))
}
}
private func hasWhereClause(dictionary: SourceKittenDictionary, file: SwiftLintFile) -> Bool {
guard let nameOffset = dictionary.nameOffset,
let nameLength = dictionary.nameLength,
let bodyOffset = dictionary.bodyOffset,
case let contents = file.stringView,
case let rangeStart = nameOffset + nameLength,
case let rangeLength = bodyOffset - rangeStart,
let range = contents.byteRangeToNSRange(ByteRange(location: rangeStart, length: rangeLength))
else {
return false
}
return file.match(pattern: "\\bwhere\\b", with: [.keyword], range: range).isNotEmpty
}
}