mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
194 lines
7.2 KiB
Swift
194 lines
7.2 KiB
Swift
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
|
|
}
|
|
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) else {
|
|
queuedFatalError("can't write to path '\(String(describing: path))'")
|
|
}
|
|
_ = fileHandle.seekToEndOfFile()
|
|
fileHandle.write(stringData)
|
|
fileHandle.closeFile()
|
|
invalidateCache()
|
|
}
|
|
|
|
public func write<S: StringProtocol>(_ string: S) {
|
|
guard string != contents else {
|
|
return
|
|
}
|
|
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: URL(fileURLWithPath: path, isDirectory: false), options: .atomic)
|
|
} catch {
|
|
queuedFatalError("can't write file to \(path)")
|
|
}
|
|
invalidateCache()
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|