mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
ac701c088c
The current algorithm is like "collect all included files and subtract all excluded files".
Collecting all included and all excluded files relies on the file system. This can become slow
when the patterns used to exclude files resolve to a large number of files.
The new approach only collects all lintable files and checks them against the exclude patterns.
This can be done by in-memory string-regex-match and does therefore not require file system accesses.
The new implementation also no longer traverses directories that already match an exclude pattern.
(cherry picked from commit 152355e36f)
160 lines
5.4 KiB
Swift
160 lines
5.4 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
public extension String {
|
|
func hasTrailingWhitespace() -> Bool {
|
|
if isEmpty {
|
|
return false
|
|
}
|
|
|
|
if let unicodescalar = unicodeScalars.last {
|
|
return CharacterSet.whitespaces.contains(unicodescalar)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func isUppercase() -> Bool {
|
|
self == uppercased()
|
|
}
|
|
|
|
func isLowercase() -> Bool {
|
|
self == lowercased()
|
|
}
|
|
|
|
private subscript (range: Range<Int>) -> String {
|
|
let nsrange = NSRange(location: range.lowerBound,
|
|
length: range.upperBound - range.lowerBound)
|
|
if let indexRange = nsrangeToIndexRange(nsrange) {
|
|
return String(self[indexRange])
|
|
}
|
|
queuedFatalError("invalid range")
|
|
}
|
|
|
|
func substring(from: Int, length: Int? = nil) -> String {
|
|
if let length {
|
|
return self[from..<from + length]
|
|
}
|
|
return String(self[index(startIndex, offsetBy: from, limitedBy: endIndex)!...])
|
|
}
|
|
|
|
func lastIndex(of search: String) -> Int? {
|
|
if let range = range(of: search, options: [.literal, .backwards]) {
|
|
return distance(from: startIndex, to: range.lowerBound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func nsrangeToIndexRange(_ nsrange: NSRange) -> Range<Index>? {
|
|
guard nsrange.location != NSNotFound else {
|
|
return nil
|
|
}
|
|
let from16 = utf16.index(utf16.startIndex, offsetBy: nsrange.location,
|
|
limitedBy: utf16.endIndex) ?? utf16.endIndex
|
|
let to16 = utf16.index(from16, offsetBy: nsrange.length,
|
|
limitedBy: utf16.endIndex) ?? utf16.endIndex
|
|
|
|
guard let fromIndex = Index(from16, within: self),
|
|
let toIndex = Index(to16, within: self) else {
|
|
return nil
|
|
}
|
|
|
|
return fromIndex..<toIndex
|
|
}
|
|
|
|
var fullNSRange: NSRange {
|
|
NSRange(location: 0, length: utf16.count)
|
|
}
|
|
|
|
/// Returns a new string, converting the path to a canonical absolute path.
|
|
///
|
|
/// > Important: This method might use an incorrect working directory internally. This can cause test failures
|
|
/// in Bazel builds but does not seem to cause trouble in production.
|
|
///
|
|
/// - returns: A new `String`.
|
|
func absolutePathStandardized() -> String {
|
|
URL(fileURLWithPath: bridge().standardizingPath.absolutePathRepresentation()).filepath
|
|
}
|
|
|
|
var isFile: Bool {
|
|
if isEmpty {
|
|
return false
|
|
}
|
|
var isDirectoryObjC: ObjCBool = false
|
|
if FileManager.default.fileExists(atPath: self, isDirectory: &isDirectoryObjC) {
|
|
return !isDirectoryObjC.boolValue
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Count the number of occurrences of the given character in `self`
|
|
/// - Parameter character: Character to count
|
|
/// - Returns: Number of times `character` occurs in `self`
|
|
func countOccurrences(of character: Character) -> Int {
|
|
reduce(0) { $1 == character ? $0 + 1 : $0 }
|
|
}
|
|
|
|
/// If self is a path, this method can be used to get a path expression relative to a root directory
|
|
func path(relativeTo rootDirectory: String) -> String {
|
|
let normalizedRootDir = rootDirectory.bridge().standardizingPath
|
|
let normalizedSelf = bridge().standardizingPath
|
|
if normalizedRootDir.isEmpty {
|
|
return normalizedSelf
|
|
}
|
|
var rootDirComps = normalizedRootDir.components(separatedBy: "/")
|
|
let rootDirCompsCount = rootDirComps.count
|
|
|
|
while true {
|
|
let sharedRootDir = rootDirComps.joined(separator: "/")
|
|
if normalizedSelf == sharedRootDir || normalizedSelf.hasPrefix(sharedRootDir + "/") {
|
|
let path = (0 ..< rootDirCompsCount - rootDirComps.count).map { _ in "/.." }.flatMap(\.self)
|
|
+ String(normalizedSelf.dropFirst(sharedRootDir.count))
|
|
return String(path.dropFirst()) // Remove leading '/'
|
|
}
|
|
rootDirComps = rootDirComps.dropLast()
|
|
}
|
|
}
|
|
|
|
func deletingPrefix(_ prefix: String) -> String {
|
|
guard hasPrefix(prefix) else { return self }
|
|
return String(dropFirst(prefix.count))
|
|
}
|
|
|
|
func indent(by spaces: Int, skipFirst: Bool = false, skipEmptyLines: Bool = true) -> String {
|
|
let lines = components(separatedBy: "\n")
|
|
if skipFirst, let firstLine = lines.first {
|
|
return firstLine + "\n" + lines.dropFirst().indent(by: spaces, skipEmptyLines: skipEmptyLines)
|
|
}
|
|
return lines.indent(by: spaces, skipEmptyLines: skipEmptyLines)
|
|
}
|
|
|
|
func linesPrefixed(with prefix: Self) -> Self {
|
|
split(separator: "\n").joined(separator: "\n\(prefix)")
|
|
}
|
|
|
|
func characterPosition(of utf8Offset: Int) -> Int? {
|
|
guard utf8Offset != 0 else {
|
|
return 0
|
|
}
|
|
guard utf8Offset > 0, utf8Offset < lengthOfBytes(using: .utf8) else {
|
|
return nil
|
|
}
|
|
for (offset, index) in indices.enumerated() where self[...index].lengthOfBytes(using: .utf8) == utf8Offset {
|
|
return offset + 1
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private extension Sequence where Element == String {
|
|
func indent(by spaces: Int, skipEmptyLines: Bool = true) -> String {
|
|
map { line in
|
|
if skipEmptyLines, line.isEmpty {
|
|
return line
|
|
}
|
|
return String(repeating: " ", count: spaces) + line
|
|
}
|
|
.joined(separator: "\n")
|
|
}
|
|
}
|