forked from realm/SwiftLint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
LineLengthRule.swift
149 lines (130 loc) · 6.51 KB
/
LineLengthRule.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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//
// LineLengthRule.swift
// SwiftLint
//
// Created by JP Simard on 5/16/15.
// Copyright © 2015 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
public struct LineLengthRule: ConfigurationProviderRule {
public var configuration = LineLengthConfiguration(warning: 120, error: 200)
public init() {}
private let commentKinds = Set(SyntaxKind.commentKinds())
private let nonCommentKinds = Set(SyntaxKind.allKinds()).subtracting(SyntaxKind.commentKinds())
private let functionKinds = Set(SwiftDeclarationKind.functionKinds())
public static let description = RuleDescription(
identifier: "line_length",
name: "Line Length",
description: "Lines should not span too many characters.",
kind: .metrics,
nonTriggeringExamples: [
String(repeating: "/", count: 120) + "\n",
String(repeating: "#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)", count: 120) + "\n",
String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 120) + "\n"
],
triggeringExamples: [
String(repeating: "/", count: 121) + "\n",
String(repeating: "#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)", count: 121) + "\n",
String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 121) + "\n"
]
)
public func validate(file: File) -> [StyleViolation] {
let minValue = configuration.params.map({ $0.value }).min() ?? .max
let swiftDeclarationKindsByLine = file.swiftDeclarationKindsByLine() ?? []
let syntaxKindsByLine = file.syntaxKindsByLine() ?? []
return file.lines.flatMap { line in
// `line.content.characters.count` <= `line.range.length` is true.
// So, `check line.range.length` is larger than minimum parameter value.
// for avoiding using heavy `line.content.characters.count`.
if line.range.length < minValue {
return nil
}
if configuration.ignoresFunctionDeclarations &&
lineHasKinds(line: line,
kinds: functionKinds,
kindsByLine: swiftDeclarationKindsByLine) {
return nil
}
if configuration.ignoresComments &&
lineHasKinds(line: line,
kinds: commentKinds,
kindsByLine: syntaxKindsByLine) &&
!lineHasKinds(line: line,
kinds: nonCommentKinds,
kindsByLine: syntaxKindsByLine) {
return nil
}
var strippedString = line.content
if configuration.ignoresURLs {
strippedString = strippedString.strippingURLs
}
strippedString = stripLiterals(fromSourceString: strippedString,
withDelimiter: "#colorLiteral")
strippedString = stripLiterals(fromSourceString: strippedString,
withDelimiter: "#imageLiteral")
let length = strippedString.characters.count
for param in configuration.params where length > param.value {
let reason = "Line should be \(configuration.length.warning) characters or less: " +
"currently \(length) characters"
return StyleViolation(ruleDescription: type(of: self).description,
severity: param.severity,
location: Location(file: file.path, line: line.index),
reason: reason)
}
return nil
}
}
/// Takes a string and replaces any literals specified by the `delimiter` parameter with `#`
///
/// - parameter sourceString: Original string, possibly containing literals
/// - parameter delimiter: Delimiter of the literal
/// (characters before the parentheses, e.g. `#colorLiteral`)
///
/// - returns: sourceString with the given literals replaced by `#`
private func stripLiterals(fromSourceString sourceString: String,
withDelimiter delimiter: String) -> String {
var modifiedString = sourceString
// While copy of content contains literal, replace with a single character
while modifiedString.contains("\(delimiter)(") {
if let rangeStart = modifiedString.range(of: "\(delimiter)("),
let rangeEnd = modifiedString.range(of: ")",
options: .literal,
range:
rangeStart.lowerBound..<modifiedString.endIndex) {
modifiedString.replaceSubrange(rangeStart.lowerBound..<rangeEnd.upperBound,
with: "#")
} else { // Should never be the case, but break to avoid accidental infinity loop
break
}
}
return modifiedString
}
private func lineHasKinds<Kind>(line: Line, kinds: Set<Kind>, kindsByLine: [[Kind]]) -> Bool {
let index = line.index
if index >= kindsByLine.count {
return false
}
return !kinds.intersection(kindsByLine[index]).isEmpty
}
}
private extension String {
var strippingURLs: String {
let range = NSRange(location: 0, length: bridge().length)
// Workaround for Linux until NSDataDetector is available
#if os(Linux)
// Regex pattern from http://daringfireball.net/2010/07/improved_regex_for_matching_urls
let pattern = "(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)" +
"(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*" +
"\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))"
let urlRegex = regex(pattern)
return urlRegex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "")
#else
let types = NSTextCheckingResult.CheckingType.link.rawValue
guard let urlDetector = try? NSDataDetector(types: types) else {
return self
}
return urlDetector.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "")
#endif
}
}