Skip to content

Commit

Permalink
Update UnusedDeclarationRule to use SourceKittenDictionary
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulTaykalo committed Nov 7, 2019
1 parent 18a3896 commit 9aa0aea
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 67 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

#### Breaking

* None.
* Use SourceKittenDictionary wrapper over dictionaries
returned from SourceKitten
[PaulTaykalo](https://github.com/PaulTaykalo)
[#2922](https://github.com/realm/SwiftLint/issues/2922)

#### Experimental

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public struct DiscardedNotificationCenterObserverRule: ASTRule, ConfigurationPro

if let lastMatch = file.match(pattern: "\\breturn\\s+", with: [.keyword], range: range).last,
lastMatch.location == range.length - lastMatch.length,
let lastFunction = functions(forByteOffset: offset, in: file.structureDictionary).last,
let lastFunction = file.structureDictionary.functions(forByteOffset: offset).last,
!lastFunction.enclosedSwiftAttributes.contains(.discardableResult) {
return []
}
Expand All @@ -82,23 +82,24 @@ public struct DiscardedNotificationCenterObserverRule: ASTRule, ConfigurationPro
}
}

private func functions(forByteOffset byteOffset: Int, in dictionary: SourceKittenDictionary)
-> [SourceKittenDictionary] {
var results = [SourceKittenDictionary]()

func parse(_ dictionary: SourceKittenDictionary) {
guard let offset = dictionary.offset,
let byteRange = dictionary.length.map({ NSRange(location: offset, length: $0) }),
NSLocationInRange(byteOffset, byteRange) else {
return
}

if let kind = dictionary.kind.flatMap(SwiftDeclarationKind.init),
SwiftDeclarationKind.functionKinds.contains(kind) {
results.append(dictionary)
private extension SourceKittenDictionary {
func functions(forByteOffset byteOffset: Int) -> [SourceKittenDictionary] {
var results = [SourceKittenDictionary]()

func parse(_ dictionary: SourceKittenDictionary) {
guard let offset = dictionary.offset,
let byteRange = dictionary.length.map({ NSRange(location: offset, length: $0) }),
NSLocationInRange(byteOffset, byteRange) else {
return
}

if let kind = dictionary.kind.flatMap(SwiftDeclarationKind.init),
SwiftDeclarationKind.functionKinds.contains(kind) {
results.append(dictionary)
}
dictionary.substructure.forEach(parse)
}
dictionary.substructure.forEach(parse)
parse(self)
return results
}
parse(dictionary)
return results
}
105 changes: 58 additions & 47 deletions Source/SwiftLintFramework/Rules/Lint/UnusedDeclarationRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,108 +126,110 @@ public struct UnusedDeclarationRule: AutomaticTestableRule, ConfigurationProvide
// MARK: - File Extensions

private extension File {
func allCursorInfo(compilerArguments: [String]) -> [[String: SourceKitRepresentable]] {
func allCursorInfo(compilerArguments: [String]) -> [SourceKittenDictionary] {
guard let path = path, let editorOpen = try? Request.editorOpen(file: self).sendIfNotDisabled() else {
return []
}

return syntaxMap.tokens.compactMap { token in
guard let kind = SyntaxKind(rawValue: token.type), !syntaxKindsToSkip.contains(kind) else {
return nil
}

let offset = Int64(token.offset)
let request = Request.cursorInfo(file: path, offset: offset, arguments: compilerArguments)
guard var cursorInfo = try? request.sendIfNotDisabled() else {
return nil
}

if let acl = File.aclAtOffset(offset, substructureElement: editorOpen) {
cursorInfo["key.accessibility"] = acl

return syntaxMap.tokens
.compactMap { token in
guard let kind = SyntaxKind(rawValue: token.type), !syntaxKindsToSkip.contains(kind) else {
return nil
}

let offset = Int64(token.offset)
let request = Request.cursorInfo(file: path, offset: offset, arguments: compilerArguments)
guard var cursorInfo = try? request.sendIfNotDisabled() else {
return nil
}

if let acl = File.aclAtOffset(offset, substructureElement: editorOpen) {
cursorInfo["key.accessibility"] = acl
}
cursorInfo["swiftlint.offset"] = offset
return cursorInfo
}
cursorInfo["swiftlint.offset"] = offset
return cursorInfo
}
.map(SourceKittenDictionary.init)
}

static func declaredUSRs(allCursorInfo: [[String: SourceKitRepresentable]], includePublicAndOpen: Bool)
static func declaredUSRs(allCursorInfo: [SourceKittenDictionary], includePublicAndOpen: Bool)
-> [(usr: String, nameOffset: Int)] {
return allCursorInfo.compactMap { cursorInfo in
return declaredUSRAndOffset(cursorInfo: cursorInfo, includePublicAndOpen: includePublicAndOpen)
}
}

static func referencedUSRs(allCursorInfo: [[String: SourceKitRepresentable]]) -> [String] {
static func referencedUSRs(allCursorInfo: [SourceKittenDictionary]) -> [String] {
return allCursorInfo.compactMap(referencedUSR)
}

static func testCaseUSRs(allCursorInfo: [[String: SourceKitRepresentable]]) -> Set<String> {
static func testCaseUSRs(allCursorInfo: [SourceKittenDictionary]) -> Set<String> {
return Set(allCursorInfo.compactMap(testCaseUSR))
}

private static func declaredUSRAndOffset(cursorInfo: [String: SourceKitRepresentable], includePublicAndOpen: Bool)
private static func declaredUSRAndOffset(cursorInfo: SourceKittenDictionary, includePublicAndOpen: Bool)
-> (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:)),
if let offset = cursorInfo.swiftlintOffset,
let usr = cursorInfo.usr,
let kind = cursorInfo.kind.flatMap(SwiftDeclarationKind.init(rawValue:)),
!declarationKindsToSkip.contains(kind),
let acl = (cursorInfo["key.accessibility"] as? String).flatMap(AccessControlLevel.init(rawValue:)),
let acl = cursorInfo.accessibility.flatMap(AccessControlLevel.init(rawValue:)),
includePublicAndOpen || [.internal, .private, .fileprivate].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,
if let annotatedDecl = cursorInfo.annotatedDeclaration,
["@IBOutlet", "@IBAction", "@objc", "@IBInspectable"].contains(where: annotatedDecl.contains) {
return nil
}

// Classes marked as @UIApplicationMain are used by the operating system as the entry point into the app.
if let annotatedDecl = cursorInfo["key.annotated_decl"] as? String,
if let annotatedDecl = cursorInfo.annotatedDeclaration,
annotatedDecl.contains("@UIApplicationMain") {
return nil
}

// Skip declarations that override another. This works for both subclass overrides &
// protocol extension overrides.
if cursorInfo["key.overrides"] != nil {
if cursorInfo.value["key.overrides"] != nil {
return nil
}

// Sometimes default protocol implementations don't have `key.overrides` set but they do have
// `key.related_decls`.
if cursorInfo["key.related_decls"] != nil {
if cursorInfo.value["key.related_decls"] != nil {
return nil
}

// Skip CodingKeys as they are used
if kind == .enum,
cursorInfo.name == "CodingKeys",
let annotatedDecl = cursorInfo["key.annotated_decl"] as? String,
let annotatedDecl = cursorInfo.annotatedDeclaration,
annotatedDecl.contains("usr=\"s:s9CodingKeyP\">CodingKey<") {
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,
private static func referencedUSR(cursorInfo: SourceKittenDictionary) -> String? {
if let usr = cursorInfo.usr,
let kind = cursorInfo.kind,
kind.contains("source.lang.swift.ref") {
return usr
}

return nil
}

private static func testCaseUSR(cursorInfo: [String: SourceKitRepresentable]) -> String? {
if let kind = (cursorInfo["key.kind"] as? String).flatMap(SwiftDeclarationKind.init(rawValue:)),
private static func testCaseUSR(cursorInfo: SourceKittenDictionary) -> String? {
if let kind = (cursorInfo.kind).flatMap(SwiftDeclarationKind.init(rawValue:)),
kind == .class,
let annotatedDecl = cursorInfo["key.annotated_decl"] as? String,
let annotatedDecl = cursorInfo.annotatedDeclaration,
annotatedDecl.contains("<Type usr=\"c:objc(cs)XCTestCase\">XCTestCase</Type>"),
let usr = cursorInfo["key.usr"] as? String {
let usr = cursorInfo.usr {
return usr
}

Expand All @@ -252,10 +254,19 @@ private extension File {
}
}

private extension Dictionary where Value == SourceKitRepresentable, Key == String {
var name: String? {
return self["key.name"] as? String
private extension SourceKittenDictionary{
var swiftlintOffset: Int64? {
return value["swiftlint.offset"] as? Int64
}

var usr: String? {
return value["key.usr"] as? String
}

var annotatedDeclaration: String? {
return value["key.annotated_decl"] as? String
}

}

// Skip initializers, deinit, enum cases and subscripts since we can't reliably detect if they're used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ public struct MultilineFunctionChainsRule: ASTRule, OptInRule, ConfigurationProv
}
}

fileprivate extension SourceKittenDictionary {
private extension SourceKittenDictionary {
var subcalls: [SourceKittenDictionary] {
return substructure.compactMap { dictionary -> SourceKittenDictionary? in
guard dictionary.kind.flatMap(SwiftExpressionKind.init(rawValue:)) == .call else {
Expand Down

0 comments on commit 9aa0aea

Please sign in to comment.