diff --git a/CHANGELOG.md b/CHANGELOG.md index e63be5e364f..2792e93de52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Source/SwiftLintFramework/Rules/Lint/DiscardedNotificationCenterObserverRule.swift b/Source/SwiftLintFramework/Rules/Lint/DiscardedNotificationCenterObserverRule.swift index 92ed966219b..88f5b342a0e 100644 --- a/Source/SwiftLintFramework/Rules/Lint/DiscardedNotificationCenterObserverRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/DiscardedNotificationCenterObserverRule.swift @@ -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 [] } @@ -82,23 +82,24 @@ public struct DiscardedNotificationCenterObserverRule: ASTRule, ConfigurationPro } } -private func functions(forByteOffset byteOffset: Int, in dictionary: SourceKittenDictionary) - -> [SourceKittenDictionary] { - var results = [SourceKittenDictionary]() +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 - } + 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) + 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 } diff --git a/Source/SwiftLintFramework/Rules/Lint/UnusedDeclarationRule.swift b/Source/SwiftLintFramework/Rules/Lint/UnusedDeclarationRule.swift index c70e67e5a91..ab4e982dc5b 100644 --- a/Source/SwiftLintFramework/Rules/Lint/UnusedDeclarationRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/UnusedDeclarationRule.swift @@ -126,82 +126,86 @@ public struct UnusedDeclarationRule: AutomaticTestableRule, ConfigurationProvide // MARK: - File Extensions private extension File { - func allCursorInfo(compilerArguments: [String]) -> [[String: SourceKitRepresentable]] { - guard let path = path, let editorOpen = try? Request.editorOpen(file: self).sendIfNotDisabled() else { + func allCursorInfo(compilerArguments: [String]) -> [SourceKittenDictionary] { + guard let path = path, + let editorOpen = (try? Request.editorOpen(file: self).sendIfNotDisabled()) + .map(SourceKittenDictionary.init) else { return [] } - return syntaxMap.tokens.compactMap { token in - guard let kind = SyntaxKind(rawValue: token.type), !syntaxKindsToSkip.contains(kind) else { - return nil - } + 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 - } + 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 + 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 { + static func testCaseUSRs(allCursorInfo: [SourceKittenDictionary]) -> Set { 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 } @@ -212,9 +216,9 @@ private extension File { 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 } @@ -222,39 +226,44 @@ private extension File { 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("XCTestCase"), - let usr = cursorInfo["key.usr"] as? String { + let usr = cursorInfo.usr { return usr } return nil } - private static func aclAtOffset(_ offset: Int64, substructureElement: [String: SourceKitRepresentable]) -> String? { - if let nameOffset = substructureElement["key.nameoffset"] as? Int64, + private static func aclAtOffset(_ offset: Int64, substructureElement: SourceKittenDictionary) -> String? { + if let nameOffset = substructureElement.nameOffset, nameOffset == offset, - let acl = substructureElement["key.accessibility"] as? String { + let acl = substructureElement.accessibility { 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 - } + for child in substructureElement.substructure { + if let acl = File.aclAtOffset(offset, substructureElement: child) { + return acl } } return nil } } -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 } } diff --git a/Source/SwiftLintFramework/Rules/Lint/UnusedImportRule.swift b/Source/SwiftLintFramework/Rules/Lint/UnusedImportRule.swift index ff3b31a2969..2f254552329 100644 --- a/Source/SwiftLintFramework/Rules/Lint/UnusedImportRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/UnusedImportRule.swift @@ -187,13 +187,13 @@ private extension File { } let cursorInfoRequest = Request.cursorInfo(file: path!, offset: Int64(token.offset), arguments: compilerArguments) - guard let cursorInfo = try? cursorInfoRequest.sendIfNotDisabled() else { + guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()).map(SourceKittenDictionary.init) else { queuedPrintError("Could not get cursor info") continue } if nextIsModuleImport { - if let importedModule = cursorInfo["key.modulename"] as? String, - cursorInfo["key.kind"] as? String == "source.lang.swift.ref.module" { + if let importedModule = cursorInfo.moduleName, + cursorInfo.kind == "source.lang.swift.ref.module" { imports.insert(importedModule) nextIsModuleImport = false continue @@ -260,7 +260,8 @@ private extension File { guard !processedTokenOffsets.contains(Int(offset)) else { continue } let cursorInfoRequest = Request.cursorInfo(file: path!, offset: offset, arguments: arguments) - guard let cursorInfo = try? cursorInfoRequest.sendIfNotDisabled() else { + guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()) + .map(SourceKittenDictionary.init) else { queuedPrintError("Could not get cursor info") continue } @@ -306,8 +307,8 @@ private extension File { ].contains { kind.hasPrefix($0) } } - func appendUsedImports(cursorInfo: [String: SourceKitRepresentable], usrFragments: inout Set) { - if let usr = cursorInfo["key.modulename"] as? String { + func appendUsedImports(cursorInfo: SourceKittenDictionary, usrFragments: inout Set) { + if let usr = cursorInfo.moduleName { usrFragments.formUnion(usr.split(separator: ".").map(String.init)) } } @@ -329,6 +330,13 @@ private extension File { } } +private extension SourceKittenDictionary { + /// Module name in @import expressions + var moduleName: String? { + return value["key.modulename"] as? String + } +} + private extension Dictionary where Value == SourceKitRepresentable, Key == String { var entities: [[String: SourceKitRepresentable]] { let entities = self["key.entities"] as? [SourceKitRepresentable] ?? [] diff --git a/Source/SwiftLintFramework/Rules/Style/MultilineFunctionChainsRule.swift b/Source/SwiftLintFramework/Rules/Style/MultilineFunctionChainsRule.swift index d93e982e5d6..8d58e357459 100644 --- a/Source/SwiftLintFramework/Rules/Style/MultilineFunctionChainsRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/MultilineFunctionChainsRule.swift @@ -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 {