mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
With the binding of configurations to their associated rule types "unknown configuration" errors can be made more specific mentioning also the rule's identifier in the printed message.
277 lines
8.9 KiB
Swift
277 lines
8.9 KiB
Swift
import SourceKittenFramework
|
|
|
|
struct CaptureVariableRule: ConfigurationProviderRule, AnalyzerRule, CollectingRule {
|
|
struct Variable: Hashable {
|
|
let usr: String
|
|
let offset: ByteCount
|
|
}
|
|
|
|
typealias USR = String
|
|
typealias FileInfo = Set<USR>
|
|
|
|
static let description = RuleDescription(
|
|
identifier: "capture_variable",
|
|
name: "Capture Variable",
|
|
description: "Non-constant variables should not be listed in a closure's capture list" +
|
|
" to avoid confusion about closures capturing variables at creation time",
|
|
kind: .lint,
|
|
nonTriggeringExamples: [
|
|
Example("""
|
|
class C {
|
|
let i: Int
|
|
init(_ i: Int) { self.i = i }
|
|
}
|
|
|
|
let j: Int = 0
|
|
let c = C(1)
|
|
|
|
let closure: () -> Void = { [j, c] in
|
|
print(c.i, j)
|
|
}
|
|
|
|
closure()
|
|
"""),
|
|
Example("""
|
|
let iGlobal: Int = 0
|
|
|
|
class C {
|
|
class var iClass: Int { 0 }
|
|
static let iStatic: Int = 0
|
|
let iInstance: Int = 0
|
|
|
|
func callTest() {
|
|
var iLocal: Int = 0
|
|
test { [unowned self, iGlobal, iInstance, iLocal, iClass=C.iClass, iStatic=C.iStatic] j in
|
|
print(iGlobal, iClass, iStatic, iInstance, iLocal, j)
|
|
}
|
|
}
|
|
|
|
func test(_ completionHandler: @escaping (Int) -> Void) {
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
var j: Int!
|
|
j = 0
|
|
|
|
let closure: () -> Void = { [j] in
|
|
print(j)
|
|
}
|
|
|
|
closure()
|
|
j = 1
|
|
closure()
|
|
"""),
|
|
Example("""
|
|
lazy var j: Int = { 0 }()
|
|
|
|
let closure: () -> Void = { [j] in
|
|
print(j)
|
|
}
|
|
|
|
closure()
|
|
j = 1
|
|
closure()
|
|
""")
|
|
],
|
|
triggeringExamples: [
|
|
Example("""
|
|
var j: Int = 0
|
|
|
|
let closure: () -> Void = { [j] in
|
|
print(j)
|
|
}
|
|
|
|
closure()
|
|
j = 1
|
|
closure()
|
|
"""),
|
|
Example("""
|
|
class C {
|
|
let i: Int
|
|
init(_ i: Int) { self.i = i }
|
|
}
|
|
|
|
var c = C(0)
|
|
let closure: () -> Void = { [c] in
|
|
print(c.i)
|
|
}
|
|
|
|
closure()
|
|
c = C(1)
|
|
closure()
|
|
"""),
|
|
Example("""
|
|
var iGlobal: Int = 0
|
|
|
|
class C {
|
|
func callTest() {
|
|
test { [iGlobal] j in
|
|
print(iGlobal, j)
|
|
}
|
|
}
|
|
|
|
func test(_ completionHandler: @escaping (Int) -> Void) {
|
|
}
|
|
}
|
|
"""),
|
|
Example("""
|
|
class C {
|
|
static var iStatic: Int = 0
|
|
|
|
static func callTest() {
|
|
test { [↓iStatic] j in
|
|
print(iStatic, j)
|
|
}
|
|
}
|
|
|
|
static func test(_ completionHandler: @escaping (Int) -> Void) {
|
|
completionHandler(2)
|
|
C.iStatic = 1
|
|
completionHandler(3)
|
|
}
|
|
}
|
|
|
|
C.callTest()
|
|
"""),
|
|
Example("""
|
|
class C {
|
|
var iInstance: Int = 0
|
|
|
|
func callTest() {
|
|
test { [iInstance] j in
|
|
print(iInstance, j)
|
|
}
|
|
}
|
|
|
|
func test(_ completionHandler: @escaping (Int) -> Void) {
|
|
}
|
|
}
|
|
""")
|
|
],
|
|
requiresFileOnDisk: true
|
|
)
|
|
|
|
var configuration = SeverityConfiguration<Self>(.warning)
|
|
|
|
func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> CaptureVariableRule.FileInfo {
|
|
file.declaredVariables(compilerArguments: compilerArguments)
|
|
}
|
|
|
|
func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: CaptureVariableRule.FileInfo],
|
|
compilerArguments: [String]) -> [StyleViolation] {
|
|
file.captureListVariables(compilerArguments: compilerArguments)
|
|
.filter { capturedVariable in collectedInfo.values.contains { $0.contains(capturedVariable.usr) } }
|
|
.map {
|
|
StyleViolation(ruleDescription: Self.description,
|
|
severity: configuration.severity,
|
|
location: Location(file: file, byteOffset: $0.offset))
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension SwiftLintFile {
|
|
static var checkedDeclarationKinds: [SwiftDeclarationKind] {
|
|
[.varClass, .varGlobal, .varInstance, .varStatic]
|
|
}
|
|
|
|
func captureListVariableOffsets() -> Set<ByteCount> {
|
|
Self.captureListVariableOffsets(parentEntity: structureDictionary)
|
|
}
|
|
|
|
static func captureListVariableOffsets(parentEntity: SourceKittenDictionary) -> Set<ByteCount> {
|
|
parentEntity.substructure
|
|
.reversed()
|
|
.reduce(into: (foundOffsets: Set<ByteCount>(), afterClosure: nil as ByteCount?)) { acc, entity in
|
|
guard let offset = entity.offset else { return }
|
|
|
|
if entity.expressionKind == .closure {
|
|
acc.afterClosure = offset
|
|
} else if let closureOffset = acc.afterClosure,
|
|
closureOffset < offset,
|
|
let length = entity.length,
|
|
let nameLength = entity.nameLength,
|
|
entity.declarationKind == .varLocal {
|
|
acc.foundOffsets.insert(offset + length - nameLength)
|
|
} else {
|
|
acc.afterClosure = nil
|
|
}
|
|
|
|
acc.foundOffsets.formUnion(captureListVariableOffsets(parentEntity: entity))
|
|
}
|
|
.foundOffsets
|
|
}
|
|
|
|
func captureListVariables(compilerArguments: [String]) -> Set<CaptureVariableRule.Variable> {
|
|
let offsets = self.captureListVariableOffsets()
|
|
guard !offsets.isEmpty, let indexEntities = index(compilerArguments: compilerArguments) else { return Set() }
|
|
|
|
return Set(indexEntities.traverseEntitiesDepthFirst {
|
|
guard
|
|
let kind = $0.kind,
|
|
kind.hasPrefix("source.lang.swift.ref.var."),
|
|
let usr = $0.usr,
|
|
let line = $0.line,
|
|
let column = $0.column,
|
|
let offset = stringView.byteOffset(forLine: line, bytePosition: column)
|
|
else { return nil }
|
|
return offsets.contains(offset) ? CaptureVariableRule.Variable(usr: usr, offset: offset) : nil
|
|
})
|
|
}
|
|
|
|
func declaredVariableOffsets() -> Set<ByteCount> {
|
|
Self.declaredVariableOffsets(parentStructure: structureDictionary)
|
|
}
|
|
|
|
static func declaredVariableOffsets(parentStructure: SourceKittenDictionary) -> Set<ByteCount> {
|
|
Set(
|
|
parentStructure.traverseDepthFirst {
|
|
let hasSetter = $0.setterAccessibility != nil
|
|
let isAutoUnwrap = $0.typeName?.hasSuffix("!") ?? false
|
|
guard
|
|
hasSetter,
|
|
!isAutoUnwrap,
|
|
let declarationKind = $0.declarationKind,
|
|
checkedDeclarationKinds.contains(declarationKind),
|
|
!$0.enclosedSwiftAttributes.contains(.lazy),
|
|
let nameOffset = $0.nameOffset
|
|
else { return [] }
|
|
return [nameOffset]
|
|
}
|
|
)
|
|
}
|
|
|
|
func declaredVariables(compilerArguments: [String]) -> Set<CaptureVariableRule.USR> {
|
|
let offsets = self.declaredVariableOffsets()
|
|
guard !offsets.isEmpty, let indexEntities = index(compilerArguments: compilerArguments) else { return Set() }
|
|
|
|
return Set(indexEntities.traverseEntitiesDepthFirst {
|
|
guard
|
|
let declarationKind = $0.declarationKind,
|
|
Self.checkedDeclarationKinds.contains(declarationKind),
|
|
let line = $0.line,
|
|
let column = $0.column,
|
|
let offset = stringView.byteOffset(forLine: line, bytePosition: column),
|
|
offsets.contains(offset)
|
|
else { return nil }
|
|
return $0.usr
|
|
})
|
|
}
|
|
|
|
func index(compilerArguments: [String]) -> SourceKittenDictionary? {
|
|
guard
|
|
let path = self.path,
|
|
let response = try? Request.index(file: path, arguments: compilerArguments).sendIfNotDisabled()
|
|
else {
|
|
Issue.indexingError(path: path, ruleID: CaptureVariableRule.description.identifier).print()
|
|
return nil
|
|
}
|
|
|
|
return SourceKittenDictionary(response)
|
|
}
|
|
}
|
|
|
|
private extension SourceKittenDictionary {
|
|
var usr: String? { value["key.usr"] as? String }
|
|
}
|