-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
File+SwiftLint.swift
326 lines (292 loc) · 13.5 KB
/
File+SwiftLint.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
//
// File+SwiftLint.swift
// SwiftLint
//
// Created by JP Simard on 5/16/15.
// Copyright © 2015 Realm. All rights reserved.
//
import Foundation
import SourceKittenFramework
internal func regex(_ pattern: String,
options: NSRegularExpression.Options? = nil) -> NSRegularExpression {
// all patterns used for regular expressions in SwiftLint are string literals which have been
// confirmed to work, so it's ok to force-try here.
let options = options ?? [.anchorsMatchLines, .dotMatchesLineSeparators]
// swiftlint:disable:next force_try
return try! .cached(pattern: pattern, options: options)
}
extension File {
internal func regions(restrictingRuleIdentifiers: [String]? = nil) -> [Region] {
var regions = [Region]()
var disabledRules = Set<String>()
let commands: [Command]
if let restrictingRuleIdentifiers = restrictingRuleIdentifiers {
commands = self.commands().filter { command in
return command.ruleIdentifiers.contains(where: restrictingRuleIdentifiers.contains)
}
} else {
commands = self.commands()
}
let commandPairs = zip(commands, Array(commands.dropFirst().map(Optional.init)) + [nil])
for (command, nextCommand) in commandPairs {
switch command.action {
case .disable: disabledRules.formUnion(command.ruleIdentifiers)
case .enable: disabledRules.subtract(command.ruleIdentifiers)
}
let start = Location(file: path, line: command.line, character: command.character)
let end = endOf(next: nextCommand)
guard start < end else { continue }
var didSetRegion = false
for (index, region) in zip(regions.indices, regions) where region.start == start && region.end == end {
regions[index] = Region(start: start, end: end,
disabledRuleIdentifiers: disabledRules.union(region.disabledRuleIdentifiers))
didSetRegion = true
}
if !didSetRegion {
regions.append(Region(start: start, end: end, disabledRuleIdentifiers: disabledRules))
}
}
return regions
}
internal func commands(in range: NSRange? = nil) -> [Command] {
if sourcekitdFailed {
return []
}
let contents = self.contents.bridge()
let range = range ?? NSRange(location: 0, length: contents.length)
let pattern = "swiftlint:(enable|disable)(:previous|:this|:next)?\\ [^\\n]+"
return match(pattern: pattern, with: [.comment], range: range).flatMap { range in
return Command(string: contents, range: range)
}.flatMap { command in
return command.expand()
}
}
fileprivate func endOf(next command: Command?) -> Location {
guard let nextCommand = command else {
return Location(file: path, line: .max, character: .max)
}
let nextLine: Int
let nextCharacter: Int?
if let nextCommandCharacter = nextCommand.character {
nextLine = nextCommand.line
if nextCommandCharacter > 0 {
nextCharacter = nextCommandCharacter - 1
} else {
nextCharacter = nil
}
} else {
nextLine = max(nextCommand.line - 1, 0)
nextCharacter = .max
}
return Location(file: path, line: nextLine, character: nextCharacter)
}
internal func match(pattern: String, with syntaxKinds: [SyntaxKind], range: NSRange? = nil) -> [NSRange] {
return match(pattern: pattern, range: range)
.filter { $0.1 == syntaxKinds }
.map { $0.0 }
}
internal func matchesAndTokens(matching pattern: String,
range: NSRange? = nil) -> [(NSTextCheckingResult, [SyntaxToken])] {
let contents = self.contents.bridge()
let range = range ?? NSRange(location: 0, length: contents.length)
let syntax = syntaxMap
return regex(pattern).matches(in: self.contents, options: [], range: range).map { match in
let matchByteRange = contents.NSRangeToByteRange(start: match.range.location,
length: match.range.length) ?? match.range
let tokensInRange = syntax.tokens(inByteRange: matchByteRange)
return (match, tokensInRange)
}
}
internal func matchesAndSyntaxKinds(matching pattern: String,
range: NSRange? = nil) -> [(NSTextCheckingResult, [SyntaxKind])] {
return matchesAndTokens(matching: pattern, range: range).map { textCheckingResult, tokens in
(textCheckingResult, tokens.flatMap { SyntaxKind(rawValue: $0.type) })
}
}
internal func rangesAndTokens(matching pattern: String,
range: NSRange? = nil) -> [(NSRange, [SyntaxToken])] {
return matchesAndTokens(matching: pattern, range: range).map { ($0.0.range, $0.1) }
}
internal func match(pattern: String, range: NSRange? = nil) -> [(NSRange, [SyntaxKind])] {
return matchesAndSyntaxKinds(matching: pattern, range: range).map { textCheckingResult, syntaxKinds in
(textCheckingResult.range, syntaxKinds)
}
}
internal func swiftDeclarationKindsByLine() -> [[SwiftDeclarationKind]]? {
if sourcekitdFailed {
return nil
}
var results = [[SwiftDeclarationKind]](repeating: [], count: lines.count + 1)
var lineIterator = lines.makeIterator()
var structureIterator = structure.kinds().makeIterator()
var maybeLine = lineIterator.next()
var maybeStructure = structureIterator.next()
while let line = maybeLine, let structure = maybeStructure {
if NSLocationInRange(structure.byteRange.location, line.byteRange),
let swiftDeclarationKind = SwiftDeclarationKind(rawValue: structure.kind) {
results[line.index].append(swiftDeclarationKind)
}
let lineEnd = NSMaxRange(line.byteRange)
if structure.byteRange.location >= lineEnd {
maybeLine = lineIterator.next()
} else {
maybeStructure = structureIterator.next()
}
}
return results
}
internal func syntaxTokensByLine() -> [[SyntaxToken]]? {
if sourcekitdFailed {
return nil
}
var results = [[SyntaxToken]](repeating: [], count: lines.count + 1)
var tokenGenerator = syntaxMap.tokens.makeIterator()
var lineGenerator = lines.makeIterator()
var maybeLine = lineGenerator.next()
var maybeToken = tokenGenerator.next()
while let line = maybeLine, let token = maybeToken {
let tokenRange = NSRange(location: token.offset, length: token.length)
if NSLocationInRange(token.offset, line.byteRange) ||
NSLocationInRange(line.byteRange.location, tokenRange) {
results[line.index].append(token)
}
let tokenEnd = NSMaxRange(tokenRange)
let lineEnd = NSMaxRange(line.byteRange)
if tokenEnd < lineEnd {
maybeToken = tokenGenerator.next()
} else if tokenEnd > lineEnd {
maybeLine = lineGenerator.next()
} else {
maybeLine = lineGenerator.next()
maybeToken = tokenGenerator.next()
}
}
return results
}
internal func syntaxKindsByLine() -> [[SyntaxKind]]? {
guard !sourcekitdFailed, let tokens = syntaxTokensByLine() else {
return nil
}
return tokens.map { $0.flatMap { SyntaxKind(rawValue: $0.type) } }
}
//Added by S2dent
/**
This function returns only matches that are not contained in a syntax kind
specified.
- parameter pattern: regex pattern to be matched inside file.
- parameter excludingSyntaxKinds: syntax kinds the matches to be filtered
when inside them.
- returns: An array of [NSRange] objects consisting of regex matches inside
file contents.
*/
internal func match(pattern: String,
excludingSyntaxKinds syntaxKinds: Set<SyntaxKind>,
range: NSRange? = nil) -> [NSRange] {
return match(pattern: pattern, range: range)
.filter { $0.1.filter(syntaxKinds.contains).isEmpty }
.map { $0.0 }
}
internal typealias MatchMapping = (NSTextCheckingResult) -> NSRange
internal func match(pattern: String,
range: NSRange? = nil,
excludingSyntaxKinds: Set<SyntaxKind>,
excludingPattern: String,
exclusionMapping: MatchMapping = { $0.range }) -> [NSRange] {
let matches = match(pattern: pattern, excludingSyntaxKinds: excludingSyntaxKinds)
if matches.isEmpty {
return []
}
let range = range ?? NSRange(location: 0, length: contents.bridge().length)
let exclusionRanges = regex(excludingPattern).matches(in: contents, options: [],
range: range).map(exclusionMapping)
return matches.filter { !$0.intersects(exclusionRanges) }
}
internal func append(_ string: String) {
guard let stringData = string.data(using: .utf8) else {
queuedFatalError("can't encode '\(string)' with UTF8")
}
guard let path = path, let fileHandle = FileHandle(forWritingAtPath: path) else {
queuedFatalError("can't write to path '\(String(describing: self.path))'")
}
_ = fileHandle.seekToEndOfFile()
fileHandle.write(stringData)
fileHandle.closeFile()
contents += string
lines = contents.bridge().lines()
}
internal func write<S: StringProtocol>(_ string: S) {
guard string != contents else {
return
}
guard let path = path else {
queuedFatalError("file needs a path to call write(_:)")
}
guard let stringData = String(string).data(using: .utf8) else {
queuedFatalError("can't encode '\(string)' with UTF8")
}
do {
try stringData.write(to: URL(fileURLWithPath: path), options: .atomic)
} catch {
queuedFatalError("can't write file to \(path)")
}
contents = String(string)
invalidateCache()
lines = contents.bridge().lines()
}
internal func ruleEnabled(violatingRanges: [NSRange], for rule: Rule) -> [NSRange] {
let fileRegions = regions()
if fileRegions.isEmpty { return violatingRanges }
let violatingRanges = violatingRanges.filter { range in
let region = fileRegions.first {
$0.contains(Location(file: self, characterOffset: range.location))
}
return region?.isRuleEnabled(rule) ?? true
}
return violatingRanges
}
fileprivate func numberOfCommentAndWhitespaceOnlyLines(startLine: Int, endLine: Int) -> Int {
let commentKinds = SyntaxKind.commentKinds
return syntaxKindsByLines[startLine...endLine].filter { kinds in
kinds.filter { !commentKinds.contains($0) }.isEmpty
}.count
}
internal func exceedsLineCountExcludingCommentsAndWhitespace(_ start: Int, _ end: Int,
_ limit: Int) -> (Bool, Int) {
guard end - start > limit else {
return (false, end - start)
}
let count = end - start - numberOfCommentAndWhitespaceOnlyLines(startLine: start, endLine: end)
return (count > limit, count)
}
private typealias RangePatternTemplate = (NSRange, String, String)
internal func correct<R: Rule>(legacyRule: R, patterns: [String: String]) -> [Correction] {
let matches: [RangePatternTemplate]
matches = patterns.flatMap({ pattern, template -> [RangePatternTemplate] in
return match(pattern: pattern).filter { range, kinds in
return kinds.first == .identifier &&
!ruleEnabled(violatingRanges: [range], for: legacyRule).isEmpty
}.map { ($0.0, pattern, template) }
}).sorted { $0.0.location > $1.0.location } // reversed
guard !matches.isEmpty else { return [] }
let description = type(of: legacyRule).description
var corrections = [Correction]()
var contents = self.contents
for (range, pattern, template) in matches {
contents = regex(pattern).stringByReplacingMatches(in: contents, options: [],
range: range,
withTemplate: template)
let location = Location(file: self, characterOffset: range.location)
corrections.append(Correction(ruleDescription: description, location: location))
}
write(contents)
return corrections
}
internal func isACL(token: SyntaxToken) -> Bool {
guard SyntaxKind(rawValue: token.type) == .attributeBuiltin else {
return false
}
let aclString = contents.bridge().substringWithByteRange(start: token.offset,
length: token.length)
return aclString.flatMap(AccessControlLevel.init(description:)) != nil
}
}