From d0d9ad575f6cb8ae48bc75cf7ad30bb4c714c8c7 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Wed, 2 Sep 2020 15:31:45 -0400 Subject: [PATCH 1/8] Add Linux binary to release artifacts There are a lot of libraries dynamically linked, so it's unlikely to work in most places other than the same Docker image used to build the binary (latest official Swift Docker image). It might still be useful if you can guarantee that you'll use this from that image. ldd .build/release/swiftlint linux-vdso.so.1 (0x00007fff38db5000) libswiftCore.so => /usr/lib/swift/linux/libswiftCore.so (0x00007f11503cd000) libFoundation.so => /usr/lib/swift/linux/libFoundation.so (0x00007f114fba6000) libswiftGlibc.so => /usr/lib/swift/linux/libswiftGlibc.so (0x00007f1151264000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f114f987000) libutil.so.1 => /lib/x86_64-linux-gnu/libutil.so.1 (0x00007f114f784000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f114f580000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f114f1e2000) libswiftDispatch.so => /usr/lib/swift/linux/libswiftDispatch.so (0x00007f115122b000) libdispatch.so => /usr/lib/swift/linux/libdispatch.so (0x00007f114ef82000) libBlocksRuntime.so => /usr/lib/swift/linux/libBlocksRuntime.so (0x00007f114ed7f000) libFoundationXML.so => /usr/lib/swift/linux/libFoundationXML.so (0x00007f11511d2000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f114e98e000) libicui18nswift.so.65 => /usr/lib/swift/linux/libicui18nswift.so.65 (0x00007f114e463000) libicuucswift.so.65 => /usr/lib/swift/linux/libicuucswift.so.65 (0x00007f114e062000) libicudataswift.so.65 => /usr/lib/swift/linux/libicudataswift.so.65 (0x00007f114c3b3000) libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f114c02a000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f114be12000) /lib64/ld-linux-x86-64.so.2 (0x00007f1151053000) librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f114bc0a000) libxml2.so.2 => /usr/lib/x86_64-linux-gnu/libxml2.so.2 (0x00007f114b849000) libicuuc.so.60 => /usr/lib/x86_64-linux-gnu/libicuuc.so.60 (0x00007f114b491000) libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f114b274000) liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007f114b04e000) libicudata.so.60 => /usr/lib/x86_64-linux-gnu/libicudata.so.60 (0x00007f11494a5000) --- .gitignore | 1 + Makefile | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0c6b89c4380..212f6becc5a 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ SwiftLint.pkg SwiftLintFramework.framework.zip benchmark_* portable_swiftlint.zip +swiftlint_linux.zip osscheck/ docs/ rule_docs/ diff --git a/Makefile b/Makefile index 0cec98405c0..2e278ef2122 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,13 @@ portable_zip: installables cp -f "$(LICENSE_PATH)" "$(TEMPORARY_FOLDER)" (cd "$(TEMPORARY_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./portable_swiftlint.zip" +zip_linux: + $(eval TMP_FOLDER := $(shell mktemp -d)) + docker run -v `pwd`:`pwd` -w `pwd` --rm swift:latest swift build -c release + mv .build/release/swiftlint "$(TMP_FOLDER)" + cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)" + (cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip" + package: installables pkgbuild \ --identifier "io.realm.swiftlint" \ @@ -119,7 +126,7 @@ archive: carthage build --no-skip-current --platform mac carthage archive SwiftLintFramework -release: package archive portable_zip +release: package archive portable_zip zip_linux docker_test: docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm norionomura/swift:5.2.4 swift test --parallel From 8945038087faae645d085c6c98caacd3b9ac6bd7 Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Thu, 3 Sep 2020 11:26:57 +0200 Subject: [PATCH 2/8] Use correct term for associated values See https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html for reference --- .../SwiftLintFramework/Rules/Style/EmptyEnumArgumentsRule.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/SwiftLintFramework/Rules/Style/EmptyEnumArgumentsRule.swift b/Source/SwiftLintFramework/Rules/Style/EmptyEnumArgumentsRule.swift index 0975a4d65ef..120cb8cb448 100644 --- a/Source/SwiftLintFramework/Rules/Style/EmptyEnumArgumentsRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/EmptyEnumArgumentsRule.swift @@ -32,7 +32,7 @@ public struct EmptyEnumArgumentsRule: SubstitutionCorrectableASTRule, Configurat public static let description = RuleDescription( identifier: "empty_enum_arguments", name: "Empty Enum Arguments", - description: "Arguments can be omitted when matching enums with associated types if they are not used.", + description: "Arguments can be omitted when matching enums with associated values if they are not used.", kind: .style, nonTriggeringExamples: [ wrapInSwitch("case .bar"), From 96c89189826a7889916b6f6e8cc67a36caf76269 Mon Sep 17 00:00:00 2001 From: Lukas Schmidt Date: Thu, 3 Sep 2020 12:23:34 +0200 Subject: [PATCH 3/8] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d53ec97b393..8f8247bc008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ #### Enhancements -* None. +* Improves description for `empty_enum_arguments` + [Lukas Schmidt](https://github.com/lightsprint09) #### Bug Fixes From 51544bb4cc0e808708d0e8780ae2e7ab023022b1 Mon Sep 17 00:00:00 2001 From: Ryan Demo Date: Thu, 10 Sep 2020 18:08:41 -0700 Subject: [PATCH 4/8] Add `excluded_match_kinds` custom rule config parameter (#3336) This allows custom rules to define an `excluded_match_kinds` array instead of listing out all but one or a few of the `SyntaxKind`s in `match_kinds`. Rules that include both `match_kinds` and `excluded_match_kinds` will be invalid, since there's not an obvious way to resolve the two without an arbitrary priority between them. --- CHANGELOG.md | 3 ++ .../Models/ConfigurationError.swift | 2 + .../RegexConfiguration.swift | 27 +++++++++-- .../Rules/Style/CustomRules.swift | 3 +- Tests/LinuxMain.swift | 4 +- .../CustomRulesTests.swift | 47 ++++++++++++++++++- 6 files changed, 76 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f8247bc008..004787d97d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ * Improves description for `empty_enum_arguments` [Lukas Schmidt](https://github.com/lightsprint09) +* Add support for `excluded_match_kinds` custom rule config parameter. + [Ryan Demo](https://github.com/ryandemo) + #### Bug Fixes * None. diff --git a/Source/SwiftLintFramework/Models/ConfigurationError.swift b/Source/SwiftLintFramework/Models/ConfigurationError.swift index 01ade19ba02..2069b9e7460 100644 --- a/Source/SwiftLintFramework/Models/ConfigurationError.swift +++ b/Source/SwiftLintFramework/Models/ConfigurationError.swift @@ -2,4 +2,6 @@ public enum ConfigurationError: Error { /// The configuration didn't match internal expectations. case unknownConfiguration + /// The configuration had both `match_kind` and `excluded_match_kind` parameters. + case ambiguousMatchKindParameters } diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/RegexConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/RegexConfiguration.swift index 9b980fc40c3..6773267552f 100644 --- a/Source/SwiftLintFramework/Rules/RuleConfigurations/RegexConfiguration.swift +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/RegexConfiguration.swift @@ -8,7 +8,7 @@ public struct RegexConfiguration: RuleConfiguration, Hashable, CacheDescriptionP public var regex: NSRegularExpression! public var included: NSRegularExpression? public var excluded: NSRegularExpression? - public var matchKinds = SyntaxKind.allKinds + public var excludedMatchKinds = Set() public var severityConfiguration = SeverityConfiguration(.warning) public var captureGroup: Int = 0 @@ -28,7 +28,8 @@ public struct RegexConfiguration: RuleConfiguration, Hashable, CacheDescriptionP regex.pattern, included?.pattern ?? "", excluded?.pattern ?? "", - matchKinds.map({ $0.rawValue }).sorted(by: <).joined(separator: ","), + SyntaxKind.allKinds.subtracting(excludedMatchKinds) + .map({ $0.rawValue }).sorted(by: <).joined(separator: ","), severityConfiguration.consoleDescription ] if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject), @@ -69,9 +70,6 @@ public struct RegexConfiguration: RuleConfiguration, Hashable, CacheDescriptionP if let message = configurationDict["message"] as? String { self.message = message } - if let matchKinds = [String].array(of: configurationDict["match_kinds"]) { - self.matchKinds = Set(try matchKinds.map({ try SyntaxKind(shortName: $0) })) - } if let severityString = configurationDict["severity"] as? String { try severityConfiguration.apply(configuration: severityString) } @@ -81,9 +79,28 @@ public struct RegexConfiguration: RuleConfiguration, Hashable, CacheDescriptionP } self.captureGroup = captureGroup } + + self.excludedMatchKinds = try self.excludedMatchKinds(from: configurationDict) } public func hash(into hasher: inout Hasher) { hasher.combine(identifier) } + + private func excludedMatchKinds(from configurationDict: [String: Any]) throws -> Set { + let matchKinds = [String].array(of: configurationDict["match_kinds"]) + let excludedMatchKinds = [String].array(of: configurationDict["excluded_match_kinds"]) + + switch (matchKinds, excludedMatchKinds) { + case (.some(let matchKinds), nil): + let includedKinds = Set(try matchKinds.map({ try SyntaxKind(shortName: $0) })) + return SyntaxKind.allKinds.subtracting(includedKinds) + case (nil, .some(let excludedMatchKinds)): + return Set(try excludedMatchKinds.map({ try SyntaxKind(shortName: $0) })) + case (nil, nil): + return .init() + case (.some, .some): + throw ConfigurationError.ambiguousMatchKindParameters + } + } } diff --git a/Source/SwiftLintFramework/Rules/Style/CustomRules.swift b/Source/SwiftLintFramework/Rules/Style/CustomRules.swift index 19e09e489f0..9fba6b6d24b 100644 --- a/Source/SwiftLintFramework/Rules/Style/CustomRules.swift +++ b/Source/SwiftLintFramework/Rules/Style/CustomRules.swift @@ -1,5 +1,4 @@ import Foundation -import SourceKittenFramework private extension Region { func isRuleDisabled(customRuleIdentifier: String) -> Bool { @@ -89,7 +88,7 @@ public struct CustomRules: Rule, ConfigurationProviderRule, CacheDescriptionProv return configurations.flatMap { configuration -> [StyleViolation] in let pattern = configuration.regex.pattern let captureGroup = configuration.captureGroup - let excludingKinds = SyntaxKind.allKinds.subtracting(configuration.matchKinds) + let excludingKinds = configuration.excludedMatchKinds return file.match(pattern: pattern, excludingSyntaxKinds: excludingKinds, captureGroup: captureGroup).map({ StyleViolation(ruleDescription: configuration.description, severity: configuration.severity, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 6e5c2f17256..e7a2851ec30 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -255,8 +255,10 @@ extension ConvenienceTypeRuleTests { extension CustomRulesTests { static var allTests: [(String, (CustomRulesTests) -> () throws -> Void)] = [ - ("testCustomRuleConfigurationSetsCorrectly", testCustomRuleConfigurationSetsCorrectly), + ("testCustomRuleConfigurationSetsCorrectlyWithMatchKinds", testCustomRuleConfigurationSetsCorrectlyWithMatchKinds), + ("testCustomRuleConfigurationSetsCorrectlyWithExcludedMatchKinds", testCustomRuleConfigurationSetsCorrectlyWithExcludedMatchKinds), ("testCustomRuleConfigurationThrows", testCustomRuleConfigurationThrows), + ("testCustomRuleConfigurationMatchKindAmbiguity", testCustomRuleConfigurationMatchKindAmbiguity), ("testCustomRuleConfigurationIgnoreInvalidRules", testCustomRuleConfigurationIgnoreInvalidRules), ("testCustomRules", testCustomRules), ("testLocalDisableCustomRule", testLocalDisableCustomRule), diff --git a/Tests/SwiftLintFrameworkTests/CustomRulesTests.swift b/Tests/SwiftLintFrameworkTests/CustomRulesTests.swift index 6283538ecb8..494343e83c2 100644 --- a/Tests/SwiftLintFrameworkTests/CustomRulesTests.swift +++ b/Tests/SwiftLintFrameworkTests/CustomRulesTests.swift @@ -3,7 +3,7 @@ import SourceKittenFramework import XCTest class CustomRulesTests: XCTestCase { - func testCustomRuleConfigurationSetsCorrectly() { + func testCustomRuleConfigurationSetsCorrectlyWithMatchKinds() { let configDict = [ "my_custom_rule": [ "name": "MyCustomRule", @@ -18,7 +18,34 @@ class CustomRulesTests: XCTestCase { comp.message = "Message" comp.regex = regex("regex") comp.severityConfiguration = SeverityConfiguration(.error) - comp.matchKinds = Set([SyntaxKind.comment]) + comp.excludedMatchKinds = SyntaxKind.allKinds.subtracting([.comment]) + var compRules = CustomRulesConfiguration() + compRules.customRuleConfigurations = [comp] + do { + var configuration = CustomRulesConfiguration() + try configuration.apply(configuration: configDict) + XCTAssertEqual(configuration, compRules) + } catch { + XCTFail("Did not configure correctly") + } + } + + func testCustomRuleConfigurationSetsCorrectlyWithExcludedMatchKinds() { + let configDict = [ + "my_custom_rule": [ + "name": "MyCustomRule", + "message": "Message", + "regex": "regex", + "excluded_match_kinds": "comment", + "severity": "error" + ] + ] + var comp = RegexConfiguration(identifier: "my_custom_rule") + comp.name = "MyCustomRule" + comp.message = "Message" + comp.regex = regex("regex") + comp.severityConfiguration = SeverityConfiguration(.error) + comp.excludedMatchKinds = Set([.comment]) var compRules = CustomRulesConfiguration() compRules.customRuleConfigurations = [comp] do { @@ -38,6 +65,22 @@ class CustomRulesTests: XCTestCase { } } + func testCustomRuleConfigurationMatchKindAmbiguity() { + let configDict = [ + "name": "MyCustomRule", + "message": "Message", + "regex": "regex", + "match_kinds": "comment", + "excluded_match_kinds": "argument", + "severity": "error" + ] + + var configuration = RegexConfiguration(identifier: "my_custom_rule") + checkError(ConfigurationError.ambiguousMatchKindParameters) { + try configuration.apply(configuration: configDict) + } + } + func testCustomRuleConfigurationIgnoreInvalidRules() throws { let configDict = [ "my_custom_rule": ["name": "MyCustomRule", From 70094638763b0e2a1a366a2c5ef14ebff3e25696 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Thu, 10 Sep 2020 21:15:07 -0400 Subject: [PATCH 5/8] Format changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 004787d97d3..857529663ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ #### Enhancements -* Improves description for `empty_enum_arguments` +* Improve description for `empty_enum_arguments`. [Lukas Schmidt](https://github.com/lightsprint09) * Add support for `excluded_match_kinds` custom rule config parameter. From 9ab72060b6d57d62a601250c15879ada088a2d99 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Thu, 10 Sep 2020 22:25:10 -0400 Subject: [PATCH 6/8] release 0.40.2 --- CHANGELOG.md | 2 +- Source/SwiftLintFramework/Models/Version.swift | 2 +- Source/SwiftLintFramework/Supporting Files/Info.plist | 2 +- Source/swiftlint/Supporting Files/Info.plist | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857529663ec..504c3f3d9cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Master +## 0.40.2: Demo Unit #### Breaking diff --git a/Source/SwiftLintFramework/Models/Version.swift b/Source/SwiftLintFramework/Models/Version.swift index 48b38ab9cad..1d562febe2e 100644 --- a/Source/SwiftLintFramework/Models/Version.swift +++ b/Source/SwiftLintFramework/Models/Version.swift @@ -4,5 +4,5 @@ public struct Version { public let value: String /// The current SwiftLint version. - public static let current = Version(value: "0.40.1") + public static let current = Version(value: "0.40.2") } diff --git a/Source/SwiftLintFramework/Supporting Files/Info.plist b/Source/SwiftLintFramework/Supporting Files/Info.plist index ffc39d02097..5079cb6c3ce 100644 --- a/Source/SwiftLintFramework/Supporting Files/Info.plist +++ b/Source/SwiftLintFramework/Supporting Files/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.40.1 + 0.40.2 CFBundleSignature ???? CFBundleVersion diff --git a/Source/swiftlint/Supporting Files/Info.plist b/Source/swiftlint/Supporting Files/Info.plist index 9e45e913fab..8709c6c44b3 100644 --- a/Source/swiftlint/Supporting Files/Info.plist +++ b/Source/swiftlint/Supporting Files/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.40.1 + 0.40.2 CFBundleSignature ???? CFBundleVersion From d9ce579b38130bff9d109392a4b70db29a2b4a14 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Thu, 10 Sep 2020 22:41:13 -0400 Subject: [PATCH 7/8] Add empty changelog section --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 504c3f3d9cf..eac4db11b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## Master + +#### Breaking + +* None. + +#### Experimental + +* None. + +#### Enhancements + +* None. + +#### Bug Fixes + +* None. + ## 0.40.2: Demo Unit #### Breaking From ea311bab2311f93a5a844dfc107ad2104f3001ab Mon Sep 17 00:00:00 2001 From: JP Simard Date: Tue, 15 Sep 2020 10:33:27 -0700 Subject: [PATCH 8/8] [UnusedDeclarationRule] Speed up and detect more dead code (#3340) By using SourceKit's `index` request to index the entire source file, we can avoid having to make `cursor-info` requests for every candidate token in the file, which scales linearly with the number of candiate tokens. For the Yams project, this approach improved the total SwiftLint run time by 4.6x: 7.9 down from 36.8s. The SourceKit index response doesn't have everything we need to identify declarations, so we still need to make some `cursor-info` requests, mostly to detect overrides: protocol conformances and parent class overrides. This approach ends up finding more unused declarations because the index contains more declared USRs than can be found by calling `cursor-info` on candidate tokens in a file. --- Remove unused declaration in ArrayInitRule --- Update package versions --- CHANGELOG.md | 4 +- Package.resolved | 4 +- .../Models/RuleStorage.swift | 6 +- .../Rules/Lint/ArrayInitRule.swift | 6 - .../Rules/Lint/UnusedDeclarationRule.swift | 302 ++++++++++-------- 5 files changed, 171 insertions(+), 151 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eac4db11b95..87d9b0c8d92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ #### Enhancements -* None. +* Make the `unused_declaration` rule run 3-5 times faster, and + enable it to detect more occurrences of unused declarations. + [JP Simard](https://github.com/jpsim) #### Bug Fixes diff --git a/Package.resolved b/Package.resolved index 3de4c7a8ed1..97e74fc78b3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/Quick/Nimble.git", "state": { "branch": null, - "revision": "2b1809051b4a65c1d7f5233331daa24572cd7fca", - "version": "8.1.1" + "revision": "7a46a5fc86cb917f69e3daf79fcb045283d8f008", + "version": "8.1.2" } }, { diff --git a/Source/SwiftLintFramework/Models/RuleStorage.swift b/Source/SwiftLintFramework/Models/RuleStorage.swift index 8778e049458..caaa3db5e8d 100644 --- a/Source/SwiftLintFramework/Models/RuleStorage.swift +++ b/Source/SwiftLintFramework/Models/RuleStorage.swift @@ -1,10 +1,14 @@ import Dispatch /// A storage mechanism for aggregating the results of `CollectingRule`s. -public class RuleStorage { +public class RuleStorage: CustomStringConvertible { private var storage: [ObjectIdentifier: [SwiftLintFile: Any]] private let access = DispatchQueue(label: "io.realm.swiftlint.ruleStorageAccess", attributes: .concurrent) + public var description: String { + storage.description + } + /// Creates a `RuleStorage` with no initial stored data. public init() { storage = [:] diff --git a/Source/SwiftLintFramework/Rules/Lint/ArrayInitRule.swift b/Source/SwiftLintFramework/Rules/Lint/ArrayInitRule.swift index d0ecd9b7f09..43a589ad42c 100644 --- a/Source/SwiftLintFramework/Rules/Lint/ArrayInitRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/ArrayInitRule.swift @@ -208,9 +208,3 @@ public struct ArrayInitRule: ASTRule, ConfigurationProviderRule, OptInRule, Auto } } } - -private extension Array where Element == SyntaxKind { - static func ~= (array: [SyntaxKind], value: [SyntaxKind]) -> Bool { - return array == value - } -} diff --git a/Source/SwiftLintFramework/Rules/Lint/UnusedDeclarationRule.swift b/Source/SwiftLintFramework/Rules/Lint/UnusedDeclarationRule.swift index 6cad4a842c1..70173c7b78e 100644 --- a/Source/SwiftLintFramework/Rules/Lint/UnusedDeclarationRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/UnusedDeclarationRule.swift @@ -2,10 +2,16 @@ import Foundation import SourceKittenFramework public struct UnusedDeclarationRule: AutomaticTestableRule, ConfigurationProviderRule, AnalyzerRule, CollectingRule { - public struct FileUSRs { + public struct FileUSRs: Hashable { var referenced: Set - var declared: [(usr: String, nameOffset: ByteCount)] - var testCaseUSRs: Set + var declared: Set + + fileprivate static var empty: FileUSRs { FileUSRs(referenced: [], declared: []) } + } + + struct DeclaredUSR: Hashable { + let usr: String + let nameOffset: ByteCount } public typealias FileInfo = FileUSRs @@ -63,6 +69,9 @@ public struct UnusedDeclarationRule: AutomaticTestableRule, ConfigurationProvide } } _ = ResponseModel() + """), + Example(""" + public func foo() {} """) ], triggeringExamples: [ @@ -95,24 +104,40 @@ public struct UnusedDeclarationRule: AutomaticTestableRule, ConfigurationProvide Attempted to lint file at path '\(file.path ?? "...")' with the \ \(Self.description.identifier) rule without any compiler arguments. """) - return FileUSRs(referenced: [], declared: [], testCaseUSRs: []) + return .empty + } + + guard let index = file.index(compilerArguments: compilerArguments) else { + queuedPrintError(""" + Could not index file at path '\(file.path ?? "...")' with the \ + \(Self.description.identifier) rule. + """) + return .empty } - let allCursorInfo = file.allCursorInfo(compilerArguments: compilerArguments) - return FileUSRs(referenced: Set(SwiftLintFile.referencedUSRs(allCursorInfo: allCursorInfo)), - declared: SwiftLintFile.declaredUSRs(allCursorInfo: allCursorInfo, - includePublicAndOpen: configuration.includePublicAndOpen), - testCaseUSRs: SwiftLintFile.testCaseUSRs(allCursorInfo: allCursorInfo)) + guard let editorOpen = (try? Request.editorOpen(file: file.file).sendIfNotDisabled()) + .map(SourceKittenDictionary.init) else { + queuedPrintError(""" + Could not open file at path '\(file.path ?? "...")' with the \ + \(Self.description.identifier) rule. + """) + return .empty + } + + return FileUSRs( + referenced: file.referencedUSRs(index: index), + declared: file.declaredUSRs(index: index, + editorOpen: editorOpen, + compilerArguments: compilerArguments, + includePublicAndOpen: configuration.includePublicAndOpen) + ) } public func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: UnusedDeclarationRule.FileUSRs], compilerArguments: [String]) -> [StyleViolation] { let allReferencedUSRs = collectedInfo.values.reduce(into: Set()) { $0.formUnion($1.referenced) } - let allTestCaseUSRs = collectedInfo.values.reduce(into: Set()) { $0.formUnion($1.testCaseUSRs) } - return violationOffsets(in: file, compilerArguments: compilerArguments, - declaredUSRs: collectedInfo[file]?.declared ?? [], - allReferencedUSRs: allReferencedUSRs, - allTestCaseUSRs: allTestCaseUSRs) + return violationOffsets(declaredUSRs: collectedInfo[file]?.declared ?? [], + allReferencedUSRs: allReferencedUSRs) .map { StyleViolation(ruleDescription: Self.description, severity: configuration.severity, @@ -120,154 +145,127 @@ public struct UnusedDeclarationRule: AutomaticTestableRule, ConfigurationProvide } } - private func violationOffsets(in file: SwiftLintFile, compilerArguments: [String], - declaredUSRs: [(usr: String, nameOffset: ByteCount)], - allReferencedUSRs: Set, - allTestCaseUSRs: Set) -> [ByteCount] { + private func violationOffsets(declaredUSRs: Set, allReferencedUSRs: Set) -> [ByteCount] { // Unused declarations are: // 1. all declarations // 2. minus all references - // 3. minus all XCTestCase subclasses - // 4. minus all XCTest test functions - let unusedDeclarations = declaredUSRs + return declaredUSRs .filter { !allReferencedUSRs.contains($0.usr) } - .filter { !allTestCaseUSRs.contains($0.usr) } - .filter { declaredUSR in - return !allTestCaseUSRs.contains(where: { testCaseUSR in - return declaredUSR.usr.hasPrefix(testCaseUSR + "(im)test") || - declaredUSR.usr.hasPrefix( - testCaseUSR.replacingOccurrences(of: "@M@", with: "@CM@") + "(im)test" - ) - }) - } - return unusedDeclarations.map { $0.nameOffset } + .map { $0.nameOffset } + .sorted() } } // MARK: - File Extensions private extension SwiftLintFile { - func allCursorInfo(compilerArguments: [String]) -> [SourceKittenDictionary] { - guard let path = path, - let editorOpen = (try? Request.editorOpen(file: self.file).sendIfNotDisabled()) - .map(SourceKittenDictionary.init) else { - return [] - } - - return syntaxMap.tokens - .compactMap { token in - guard let kind = token.kind, !syntaxKindsToSkip.contains(kind) else { - return nil - } - - let offset = token.offset - let request = Request.cursorInfo(file: path, offset: offset, arguments: compilerArguments) - guard var cursorInfo = try? request.sendIfNotDisabled() else { - return nil - } - - if let acl = editorOpen.aclAtOffset(offset) { - cursorInfo["key.accessibility"] = acl.rawValue - } - cursorInfo["swiftlint.offset"] = Int64(offset.value) - return cursorInfo + func index(compilerArguments: [String]) -> SourceKittenDictionary? { + return path + .flatMap { path in + try? Request.index(file: path, arguments: compilerArguments) + .send() } .map(SourceKittenDictionary.init) } - static func declaredUSRs(allCursorInfo: [SourceKittenDictionary], includePublicAndOpen: Bool) - -> [(usr: String, nameOffset: ByteCount)] { - return allCursorInfo.compactMap { cursorInfo in - return declaredUSRAndOffset(cursorInfo: cursorInfo, includePublicAndOpen: includePublicAndOpen) - } - } + func referencedUSRs(index: SourceKittenDictionary) -> Set { + return Set(index.traverseEntities { entity -> String? in + if let usr = entity.usr, + let kind = entity.kind, + kind.starts(with: "source.lang.swift.ref") { + return usr + } - static func referencedUSRs(allCursorInfo: [SourceKittenDictionary]) -> [String] { - return allCursorInfo.compactMap(referencedUSR) + return nil + }) } - static func testCaseUSRs(allCursorInfo: [SourceKittenDictionary]) -> Set { - return Set(allCursorInfo.compactMap(testCaseUSR)) + func declaredUSRs(index: SourceKittenDictionary, editorOpen: SourceKittenDictionary, + compilerArguments: [String], includePublicAndOpen: Bool) + -> Set { + return Set(index.traverseEntities { indexEntity in + self.declaredUSR(indexEntity: indexEntity, editorOpen: editorOpen, compilerArguments: compilerArguments, + includePublicAndOpen: includePublicAndOpen) + }) } - private static func declaredUSRAndOffset(cursorInfo: SourceKittenDictionary, includePublicAndOpen: Bool) - -> (usr: String, nameOffset: ByteCount)? { - if let offset = cursorInfo.swiftlintOffset, - let usr = cursorInfo.usr, - let kind = cursorInfo.declarationKind, - !declarationKindsToSkip.contains(kind), - let acl = cursorInfo.accessibility, - 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.annotatedDeclaration, - ["@IBOutlet", "@IBAction", "@objc", "@IBInspectable"].contains(where: annotatedDecl.contains) { - return nil - } + func declaredUSR(indexEntity: SourceKittenDictionary, editorOpen: SourceKittenDictionary, + compilerArguments: [String], includePublicAndOpen: Bool) -> UnusedDeclarationRule.DeclaredUSR? { + guard let stringKind = indexEntity.kind, + stringKind.starts(with: "source.lang.swift.decl."), + !stringKind.contains(".accessor."), + let usr = indexEntity.usr, + let line = indexEntity.line.map(Int.init), + let column = indexEntity.column.map(Int.init), + let kind = indexEntity.declarationKind, + !declarationKindsToSkip.contains(kind) + else { + return nil + } - // Classes marked as @UIApplicationMain are used by the operating system as the entry point into the app. - if let annotatedDecl = cursorInfo.annotatedDeclaration, - annotatedDecl.contains("@UIApplicationMain") { - return nil - } + if indexEntity.enclosedSwiftAttributes.contains(where: declarationAttributesToSkip.contains) || + indexEntity.value["key.is_implicit"] as? Bool == true || + indexEntity.value["key.is_test_candidate"] as? Bool == true { + return nil + } - // Skip declarations that override another. This works for both subclass overrides & - // protocol extension overrides. - if cursorInfo.value["key.overrides"] != nil { - return nil - } + let nameOffset = stringView.byteOffset(forLine: line, column: column) - // Sometimes default protocol implementations don't have `key.overrides` set but they do have - // `key.related_decls`. - if cursorInfo.value["key.related_decls"] != nil { - return nil + if !includePublicAndOpen, + [.public, .open].contains(editorOpen.aclAtOffset(nameOffset)) { + return nil + } + + // Skip CodingKeys as they are used for Codable generation + if kind == .enum, + indexEntity.name == "CodingKeys", + case let allRelatedUSRs = indexEntity.traverseEntities(traverseBlock: { $0.usr }), + allRelatedUSRs.contains("s:s9CodingKeyP") { + return nil + } + + // Skip `static var allTests` members since those are used for Linux test discovery. + if kind == .varStatic, indexEntity.name == "allTests" { + let allTestCandidates = indexEntity.traverseEntities { subEntity -> Bool in + subEntity.value["key.is_test_candidate"] as? Bool == true } - // Skip CodingKeys as they are used - if kind == .enum, - cursorInfo.name == "CodingKeys", - let annotatedDecl = cursorInfo.annotatedDeclaration, - annotatedDecl.contains("usr=\"s:s9CodingKeyP\">CodingKey<") { + if allTestCandidates.contains(true) { return nil } - - return (usr, ByteCount(offset)) } - return nil - } + let cursorInfo = self.cursorInfo(at: nameOffset, compilerArguments: compilerArguments) - private static func referencedUSR(cursorInfo: SourceKittenDictionary) -> String? { - if let usr = cursorInfo.usr, - let kind = cursorInfo.kind, - kind.starts(with: "source.lang.swift.ref") { - if let synthesizedLocation = usr.range(of: "::SYNTHESIZED::")?.lowerBound { - return String(usr.prefix(upTo: synthesizedLocation)) - } - return usr + if let annotatedDecl = cursorInfo?.annotatedDeclaration, + ["@IBOutlet", "@IBAction", "@objc", "@IBInspectable"].contains(where: annotatedDecl.contains) { + return nil } - return nil - } + // This works for both subclass overrides & protocol extension overrides. + if cursorInfo?.value["key.overrides"] != nil { + return nil + } - private static func testCaseUSR(cursorInfo: SourceKittenDictionary) -> String? { - if let kind = cursorInfo.declarationKind, - kind == .class, - let annotatedDecl = cursorInfo.annotatedDeclaration, - annotatedDecl.contains("XCTestCase"), - let usr = cursorInfo.usr { - return usr + // Sometimes default protocol implementations don't have `key.overrides` set but they do have + // `key.related_decls`. The apparent exception is that related declarations also includes declarations + // with "related names", which appears to be similarly named declarations (i.e. overloads) that are + // programmatically unrelated to the current cursor-info declaration. Those similarly named declarations + // aren't in `key.related` so confirm that that one is also populated. + if cursorInfo?.value["key.related_decls"] != nil && indexEntity.value["key.related"] != nil { + return nil } - return nil + return .init(usr: usr, nameOffset: nameOffset) } -} -private extension SourceKittenDictionary { - var swiftlintOffset: Int64? { - return value["swiftlint.offset"] as? Int64 + func cursorInfo(at byteOffset: ByteCount, compilerArguments: [String]) -> SourceKittenDictionary? { + let request = Request.cursorInfo(file: path!, offset: byteOffset, arguments: compilerArguments) + return (try? request.sendIfNotDisabled()).map(SourceKittenDictionary.init) } +} +private extension SourceKittenDictionary { var usr: String? { return value["key.usr"] as? String } @@ -293,25 +291,47 @@ private extension SourceKittenDictionary { // Skip initializers, deinit, enum cases and subscripts since we can't reliably detect if they're used. private let declarationKindsToSkip: Set = [ + .enumelement, + .extensionProtocol, + .extension, + .extensionEnum, + .extensionClass, + .extensionStruct, .functionConstructor, .functionDestructor, - .enumelement, - .functionSubscript + .functionSubscript, + .genericTypeParam ] -/// Skip syntax kinds that won't respond to cursor info requests. -private let syntaxKindsToSkip: Set = [ - .attributeBuiltin, - .attributeID, - .comment, - .commentMark, - .commentURL, - .buildconfigID, - .buildconfigKeyword, - .docComment, - .docCommentField, - .keyword, - .number, - .string, - .stringInterpolationAnchor +private let declarationAttributesToSkip: Set = [ + .ibaction, + .ibinspectable, + .iboutlet, + .override ] + +private extension SourceKittenDictionary { + func traverseEntities(traverseBlock: (SourceKittenDictionary) -> T?) -> [T] { + var result: [T] = [] + traverseEntitiesDepthFirst(collectingValuesInto: &result, traverseBlock: traverseBlock) + return result + } + + private func traverseEntitiesDepthFirst(collectingValuesInto array: inout [T], + traverseBlock: (SourceKittenDictionary) -> T?) { + entities.forEach { subDict in + subDict.traverseEntitiesDepthFirst(collectingValuesInto: &array, traverseBlock: traverseBlock) + + if let collectedValue = traverseBlock(subDict) { + array.append(collectedValue) + } + } + } +} + +private extension StringView { + func byteOffset(forLine line: Int, column: Int) -> ByteCount { + guard line > 0 else { return ByteCount(column - 1) } + return lines[line - 1].byteRange.location + ByteCount(column - 1) + } +}