mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
53d8a3aba9
* Add UnusedPrivateDeclarationRule * Temporarily disable UnusedPrivateDeclarationRule tests on Xcode 10 So we can merge UnusedPrivateDeclarationRule without having to wait for CircleCI to update its Xcode 10 version.
141 lines
5.8 KiB
Swift
141 lines
5.8 KiB
Swift
import Foundation
|
|
import SourceKittenFramework
|
|
|
|
public struct UnusedPrivateDeclarationRule: ConfigurationProviderRule, AnalyzerRule {
|
|
public var configuration = SeverityConfiguration(.warning)
|
|
|
|
public init() {}
|
|
|
|
public static let description = RuleDescription(
|
|
identifier: "unused_private_declaration",
|
|
name: "Unused Private Declaration",
|
|
description: "Private declarations should be referenced in that file.",
|
|
kind: .lint,
|
|
nonTriggeringExamples: [
|
|
"""
|
|
private let kConstant = 0
|
|
_ = kConstant
|
|
"""
|
|
],
|
|
triggeringExamples: [
|
|
"""
|
|
private let ↓kConstant = 0
|
|
"""
|
|
],
|
|
requiresFileOnDisk: true
|
|
)
|
|
|
|
public func validate(file: File, compilerArguments: [String]) -> [StyleViolation] {
|
|
return violationOffsets(in: file, compilerArguments: compilerArguments).map {
|
|
StyleViolation(ruleDescription: type(of: self).description,
|
|
severity: configuration.severity,
|
|
location: Location(file: file, byteOffset: $0))
|
|
}
|
|
}
|
|
|
|
private func violationOffsets(in file: File, compilerArguments: [String]) -> [Int] {
|
|
guard !compilerArguments.isEmpty else {
|
|
queuedPrintError("""
|
|
Attempted to lint file at path '\(file.path ?? "...")' with the \
|
|
\(type(of: self).description.identifier) rule without any compiler arguments.
|
|
""")
|
|
return []
|
|
}
|
|
|
|
let allCursorInfo = file.allCursorInfo(compilerArguments: compilerArguments)
|
|
let privateDeclarationUSRs = File.declaredUSRs(allCursorInfo: allCursorInfo, acls: [.private, .fileprivate])
|
|
let referencedUSRs = File.referencedUSRs(allCursorInfo: allCursorInfo)
|
|
let unusedPrivateDeclarations = privateDeclarationUSRs.filter { !referencedUSRs.contains($0.usr) }
|
|
return unusedPrivateDeclarations.map { $0.nameOffset }
|
|
}
|
|
}
|
|
|
|
// MARK: - File Extensions
|
|
|
|
private extension File {
|
|
func allCursorInfo(compilerArguments: [String]) -> [[String: SourceKitRepresentable]] {
|
|
guard let path = path, let editorOpen = try? Request.editorOpen(file: self).send() else {
|
|
return []
|
|
}
|
|
|
|
return syntaxMap.tokens.compactMap { token in
|
|
let offset = Int64(token.offset)
|
|
var cursorInfo = try? Request.cursorInfo(file: path, offset: offset,
|
|
arguments: compilerArguments).send()
|
|
if let acl = File.aclAtOffset(offset, substructureElement: editorOpen) {
|
|
cursorInfo?["key.accessibility"] = acl
|
|
}
|
|
cursorInfo?["swiftlint.offset"] = offset
|
|
return cursorInfo
|
|
}
|
|
}
|
|
|
|
static func declaredUSRs(allCursorInfo: [[String: SourceKitRepresentable]],
|
|
acls: [AccessControlLevel]) -> [(usr: String, nameOffset: Int)] {
|
|
return allCursorInfo.compactMap { declaredUSRAndOffset(cursorInfo: $0, acls: acls) }
|
|
}
|
|
|
|
static func referencedUSRs(allCursorInfo: [[String: SourceKitRepresentable]]) -> [String] {
|
|
return allCursorInfo.compactMap(referencedUSR)
|
|
}
|
|
|
|
private static func declaredUSRAndOffset(cursorInfo: [String: SourceKitRepresentable],
|
|
acls: [AccessControlLevel]) -> (usr: String, nameOffset: Int)? {
|
|
if let offset = cursorInfo["swiftlint.offset"] as? Int64,
|
|
let usr = cursorInfo["key.usr"] as? String,
|
|
let kind = (cursorInfo["key.kind"] as? String).flatMap(SwiftDeclarationKind.init(rawValue:)),
|
|
!declarationKindsToSkip.contains(kind),
|
|
let acl = (cursorInfo["key.accessibility"] as? String).flatMap(AccessControlLevel.init(rawValue:)),
|
|
acls.contains(acl) {
|
|
// Skip declarations marked as @IBOutlet, @IBAction or @objc
|
|
// since those might not be referenced in code, but only dynamically (e.g. Interface Builder)
|
|
if let annotatedDecl = cursorInfo["key.annotated_decl"] as? String,
|
|
["@IBOutlet", "@IBAction", "@objc"].contains(where: annotatedDecl.contains) {
|
|
return nil
|
|
}
|
|
// Skip declarations that override another. This works for both subclass overrides &
|
|
// protocol extension overrides.
|
|
if cursorInfo["key.overrides"] != nil {
|
|
return nil
|
|
}
|
|
return (usr, Int(offset))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private static func referencedUSR(cursorInfo: [String: SourceKitRepresentable]) -> String? {
|
|
if let usr = cursorInfo["key.usr"] as? String,
|
|
let kind = cursorInfo["key.kind"] as? String,
|
|
kind.contains("source.lang.swift.ref") {
|
|
return usr
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private static func aclAtOffset(_ offset: Int64, substructureElement: [String: SourceKitRepresentable]) -> String? {
|
|
if let nameOffset = substructureElement["key.nameoffset"] as? Int64,
|
|
nameOffset == offset,
|
|
let acl = substructureElement["key.accessibility"] as? String {
|
|
return acl
|
|
}
|
|
if let substructure = substructureElement[SwiftDocKey.substructure.rawValue] as? [SourceKitRepresentable] {
|
|
let nestedSubstructure = substructure.compactMap({ $0 as? [String: SourceKitRepresentable] })
|
|
for child in nestedSubstructure {
|
|
if let acl = File.aclAtOffset(offset, substructureElement: child) {
|
|
return acl
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Skip initializers, enum cases and subscripts since we can't reliably detect if they're used.
|
|
private let declarationKindsToSkip: Set<SwiftDeclarationKind> = [
|
|
.functionConstructor,
|
|
.enumelement,
|
|
.functionSubscript
|
|
]
|