-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Expand file tree
/
Copy pathSwiftLintFile+Regex.swift
More file actions
193 lines (174 loc) · 7.18 KB
/
SwiftLintFile+Regex.swift
File metadata and controls
193 lines (174 loc) · 7.18 KB
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
import Foundation
import SourceKittenFramework
public 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 SwiftLintFile {
public func regions(restrictingRuleIdentifiers: Set<RuleIdentifier>? = nil) -> [Region] {
var regions = [Region]()
var disabledRules = Set<RuleIdentifier>()
let commands: [Command]
if let restrictingRuleIdentifiers {
commands = self.commands().filter { command in
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)
case .invalid:
break
}
let start = Location(file: path, line: command.line, character: command.range?.upperBound)
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
}
public func commands(in range: NSRange? = nil) -> [Command] {
guard let range else {
return commands
.flatMap { $0.expand() }
}
let rangeStart = Location(file: self, characterOffset: range.location)
let rangeEnd = Location(file: self, characterOffset: NSMaxRange(range))
return commands
.filter { command in
let commandLocation = Location(file: path, line: command.line, character: command.range?.upperBound)
return rangeStart <= commandLocation && commandLocation <= rangeEnd
}
.flatMap { $0.expand() }
}
private 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.range?.upperBound {
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)
}
public func match(pattern: String, with syntaxKinds: [SyntaxKind], range: NSRange? = nil) -> [NSRange] {
match(pattern: pattern, range: range)
.filter { $0.1 == syntaxKinds }
.map(\.0)
}
public func match(pattern: String, range: NSRange? = nil, captureGroup: Int = 0) -> [(NSRange, [SyntaxKind])] {
let contents = stringView
let range = range ?? contents.range
let syntax = syntaxMap
return regex(pattern).matches(in: contents, options: [], range: range).compactMap { match in
let matchByteRange = contents.NSRangeToByteRange(
start: match.range.location, length: match.range.length)
return matchByteRange.map { (match.range(at: captureGroup), syntax.tokens(inByteRange: $0).kinds) }
}
}
/**
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.
*/
public func match(pattern: String,
excludingSyntaxKinds syntaxKinds: Set<SyntaxKind>,
range: NSRange? = nil,
captureGroup: Int = 0) -> [NSRange] {
match(pattern: pattern, range: range, captureGroup: captureGroup)
.filter { syntaxKinds.isDisjoint(with: $0.1) }
.map(\.0)
}
public func append(_ string: String) {
guard string.isNotEmpty else {
return
}
defer { invalidateCache() }
file.contents += string
if isVirtual {
return
}
guard let stringData = string.data(using: .utf8) else {
queuedFatalError("can't encode '\(string)' with UTF8")
}
guard let path, let fileHandle = FileHandle(forWritingAtPath: path.filepath) else {
queuedFatalError("can't write to path '\(String(describing: path))'")
}
_ = fileHandle.seekToEndOfFile()
fileHandle.write(stringData)
fileHandle.closeFile()
}
public func write<S: StringProtocol>(_ string: S) {
guard string != contents else {
return
}
defer { invalidateCache() }
file.contents = String(string)
if isVirtual {
return
}
guard let 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: path, options: .atomic)
} catch {
queuedFatalError("can't write file to \(path)")
}
}
public func ruleEnabled(violatingRanges: [NSRange], for rule: some Rule) -> [NSRange] {
let fileRegions = regions()
if fileRegions.isEmpty { return violatingRanges }
return violatingRanges.filter { range in
let region = fileRegions.first {
$0.contains(Location(file: self, characterOffset: range.location))
}
return region?.isRuleEnabled(rule) ?? true
}
}
public func ruleEnabled(violatingRange: NSRange, for rule: some Rule) -> NSRange? {
ruleEnabled(violatingRanges: [violatingRange], for: rule).first
}
public func contents(for token: SwiftLintSyntaxToken) -> String? {
stringView.substringWithByteRange(token.range)
}
}