Compare commits
5 Commits
main
...
jp-deadcod
Author | SHA1 | Date |
---|---|---|
![]() |
d8096906f4 | |
![]() |
0ff0fc9c5c | |
![]() |
3a6436305b | |
![]() |
4388dd9658 | |
![]() |
388d16ec9b |
|
@ -1,4 +1,4 @@
|
||||||
module: SwiftLintFramework
|
module: SwiftLintCore
|
||||||
author: JP Simard, SwiftLint Contributors
|
author: JP Simard, SwiftLint Contributors
|
||||||
author_url: https://jpsim.com
|
author_url: https://jpsim.com
|
||||||
root_url: https://realm.github.io/SwiftLint/
|
root_url: https://realm.github.io/SwiftLint/
|
||||||
|
@ -13,7 +13,8 @@ documentation: rule_docs/*.md
|
||||||
hide_unlisted_documentation: true
|
hide_unlisted_documentation: true
|
||||||
custom_categories_unlisted_prefix: ''
|
custom_categories_unlisted_prefix: ''
|
||||||
exclude:
|
exclude:
|
||||||
- Source/SwiftLintFramework/Rules/**/*.swift
|
# TODO: Document extensions
|
||||||
|
- Source/SwiftLintCore/Extensions/*.swift
|
||||||
custom_categories:
|
custom_categories:
|
||||||
- name: Rules
|
- name: Rules
|
||||||
children:
|
children:
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
|
|
||||||
/// The rule list containing all available rules built into SwiftLint.
|
/// The rule list containing all available rules built into SwiftLint.
|
||||||
let builtInRules: [Rule.Type] = [
|
public let builtInRules: [Rule.Type] = [
|
||||||
{% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
|
{% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
|
||||||
{% endfor %}]
|
{% endfor %}]
|
||||||
|
|
||||||
/// The rule list containing all available rules built into SwiftLint as well as native custom rules.
|
|
||||||
public let primaryRuleList = RuleList(rules: builtInRules + extraRules())
|
|
|
@ -1,13 +1,13 @@
|
||||||
|
@testable import SwiftLintBuiltInRules
|
||||||
@_spi(TestHelper)
|
@_spi(TestHelper)
|
||||||
@testable import SwiftLintFramework
|
@testable import SwiftLintCore
|
||||||
import SwiftLintTestHelpers
|
import SwiftLintTestHelpers
|
||||||
import XCTest
|
|
||||||
|
|
||||||
// swiftlint:disable file_length single_test_class type_name
|
// swiftlint:disable file_length type_name
|
||||||
|
|
||||||
{% for rule in types.structs %}
|
{% for rule in types.structs %}
|
||||||
{% if rule.name|hasSuffix:"Rule" %}
|
{% if rule.name|hasSuffix:"Rule" %}
|
||||||
class {{ rule.name }}GeneratedTests: XCTestCase {
|
class {{ rule.name }}GeneratedTests: SwiftLintTestCase {
|
||||||
func testWithDefaultConfiguration() {
|
func testWithDefaultConfiguration() {
|
||||||
verifyRule({{ rule.name }}.description)
|
verifyRule({{ rule.name }}.description)
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,8 +86,10 @@ number_separator:
|
||||||
minimum_length: 5
|
minimum_length: 5
|
||||||
file_name:
|
file_name:
|
||||||
excluded:
|
excluded:
|
||||||
- SwiftSyntax+SwiftLint.swift
|
- Exports.swift
|
||||||
- GeneratedTests.swift
|
- GeneratedTests.swift
|
||||||
|
- IndexStore+Visitors.swift
|
||||||
|
- SwiftSyntax+SwiftLint.swift
|
||||||
- TestHelpers.swift
|
- TestHelpers.swift
|
||||||
|
|
||||||
function_body_length: 60
|
function_body_length: 60
|
||||||
|
@ -118,3 +120,4 @@ custom_rules:
|
||||||
unused_import:
|
unused_import:
|
||||||
always_keep_imports:
|
always_keep_imports:
|
||||||
- SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes
|
- SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes
|
||||||
|
- SwiftLintFramework # now that this is a wrapper around other modules, don't treat as unused
|
||||||
|
|
59
BUILD
59
BUILD
|
@ -13,12 +13,9 @@ load(
|
||||||
# Targets
|
# Targets
|
||||||
|
|
||||||
swift_library(
|
swift_library(
|
||||||
name = "SwiftLintFramework",
|
name = "SwiftLintCore",
|
||||||
srcs = glob(
|
srcs = glob(["Source/SwiftLintCore/**/*.swift"]),
|
||||||
["Source/SwiftLintFramework/**/*.swift"],
|
module_name = "SwiftLintCore",
|
||||||
exclude = ["Source/SwiftLintFramework/Rules/ExcludedFromBazel/ExtraRules.swift"],
|
|
||||||
) + ["@swiftlint_extra_rules//:extra_rules"],
|
|
||||||
module_name = "SwiftLintFramework",
|
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"@com_github_jpsim_sourcekitten//:SourceKittenFramework",
|
"@com_github_jpsim_sourcekitten//:SourceKittenFramework",
|
||||||
|
@ -30,12 +27,62 @@ swift_library(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "SwiftLintAnalyzerRules",
|
||||||
|
srcs = glob(["Source/SwiftLintAnalyzerRules/**/*.swift"]),
|
||||||
|
module_name = "SwiftLintAnalyzerRules",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":SwiftLintCore",
|
||||||
|
"@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit",
|
||||||
|
"@com_github_lyft_index_store//:IndexStore",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "SwiftLintBuiltInRules",
|
||||||
|
srcs = glob(["Source/SwiftLintBuiltInRules/**/*.swift"]),
|
||||||
|
module_name = "SwiftLintBuiltInRules",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":SwiftLintCore",
|
||||||
|
"@com_github_apple_swift_syntax//:optlibs",
|
||||||
|
"@com_github_jpsim_sourcekitten//:SourceKittenFramework",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "SwiftLintExtraRules",
|
||||||
|
srcs = glob(
|
||||||
|
["Source/SwiftLintExtraRules/**/*.swift"],
|
||||||
|
exclude = ["Source/SwiftLintExtraRules/ExtraRules.swift"],
|
||||||
|
) + ["@swiftlint_extra_rules//:extra_rules"],
|
||||||
|
module_name = "SwiftLintExtraRules",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":SwiftLintCore",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "SwiftLintFramework",
|
||||||
|
srcs = glob(["Source/SwiftLintFramework/**/*.swift"]),
|
||||||
|
module_name = "SwiftLintFramework",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":SwiftLintBuiltInRules",
|
||||||
|
":SwiftLintCore",
|
||||||
|
":SwiftLintExtraRules",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
swift_library(
|
swift_library(
|
||||||
name = "swiftlint.library",
|
name = "swiftlint.library",
|
||||||
srcs = glob(["Source/swiftlint/**/*.swift"]),
|
srcs = glob(["Source/swiftlint/**/*.swift"]),
|
||||||
module_name = "swiftlint",
|
module_name = "swiftlint",
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
|
":SwiftLintAnalyzerRules",
|
||||||
":SwiftLintFramework",
|
":SwiftLintFramework",
|
||||||
"@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit",
|
"@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit",
|
||||||
"@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser",
|
"@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser",
|
||||||
|
|
|
@ -20,6 +20,8 @@ use_repo(
|
||||||
"com_github_johnsundell_collectionconcurrencykit",
|
"com_github_johnsundell_collectionconcurrencykit",
|
||||||
"com_github_krzyzanowskim_cryptoswift",
|
"com_github_krzyzanowskim_cryptoswift",
|
||||||
"swiftlint_com_github_scottrhoyt_swifty_text_table",
|
"swiftlint_com_github_scottrhoyt_swifty_text_table",
|
||||||
|
"com_github_keith_static_index_store",
|
||||||
|
"com_github_lyft_index_store",
|
||||||
)
|
)
|
||||||
|
|
||||||
extra_rules = use_extension("//bazel:extensions.bzl", "extra_rules")
|
extra_rules = use_extension("//bazel:extensions.bzl", "extra_rules")
|
||||||
|
|
12
Makefile
12
Makefile
|
@ -30,14 +30,14 @@ VERSION_STRING=$(shell ./tools/get-version)
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
sourcery: Source/SwiftLintFramework/Models/PrimaryRuleList.swift Tests/GeneratedTests/GeneratedTests.swift
|
sourcery: Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift Tests/GeneratedTests/GeneratedTests.swift
|
||||||
|
|
||||||
Source/SwiftLintFramework/Models/PrimaryRuleList.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/PrimaryRuleList.stencil
|
Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift: Source/SwiftLintBuiltInRules/Rules/**/*.swift .sourcery/BuiltInRules.stencil
|
||||||
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/PrimaryRuleList.stencil --output .sourcery
|
sourcery --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery
|
||||||
mv .sourcery/PrimaryRuleList.generated.swift Source/SwiftLintFramework/Models/PrimaryRuleList.swift
|
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
|
||||||
|
|
||||||
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/GeneratedTests.stencil
|
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLintBuiltInRules/Rules/**/*.swift .sourcery/GeneratedTests.stencil
|
||||||
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
|
sourcery --sources Source/SwiftLintCore/Rules --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
|
||||||
mv .sourcery/GeneratedTests.generated.swift Tests/GeneratedTests/GeneratedTests.swift
|
mv .sourcery/GeneratedTests.generated.swift Tests/GeneratedTests/GeneratedTests.swift
|
||||||
|
|
||||||
test: clean_xcode
|
test: clean_xcode
|
||||||
|
|
|
@ -27,6 +27,15 @@
|
||||||
"version" : "1.2.1"
|
"version" : "1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-index-store",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/lyft/swift-index-store.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c8df3645ac28b9527cd5157a30f909d69a4b7e2d",
|
||||||
|
"version" : "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-syntax",
|
"identity" : "swift-syntax",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|
|
@ -31,6 +31,7 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.1")),
|
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.1")),
|
||||||
.package(url: "https://github.com/apple/swift-syntax.git", exact: "0.50800.0-SNAPSHOT-2022-12-29-a"),
|
.package(url: "https://github.com/apple/swift-syntax.git", exact: "0.50800.0-SNAPSHOT-2022-12-29-a"),
|
||||||
|
.package(url: "https://github.com/lyft/swift-index-store.git", from: "1.0.0"),
|
||||||
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.34.0")),
|
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.34.0")),
|
||||||
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.3"),
|
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.3"),
|
||||||
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
|
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
|
||||||
|
@ -49,6 +50,7 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
"CollectionConcurrencyKit",
|
"CollectionConcurrencyKit",
|
||||||
|
"SwiftLintAnalyzerRules",
|
||||||
"SwiftLintFramework",
|
"SwiftLintFramework",
|
||||||
"SwiftyTextTable",
|
"SwiftyTextTable",
|
||||||
]
|
]
|
||||||
|
@ -60,9 +62,29 @@ let package = Package(
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SwiftLintFramework",
|
name: "SwiftLintCore",
|
||||||
dependencies: frameworkDependencies
|
dependencies: frameworkDependencies
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "SwiftLintAnalyzerRules",
|
||||||
|
dependencies: frameworkDependencies + [
|
||||||
|
"SwiftLintCore",
|
||||||
|
"libIndexStore",
|
||||||
|
.product(name: "IndexStore", package: "swift-index-store")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "SwiftLintBuiltInRules",
|
||||||
|
dependencies: frameworkDependencies + ["SwiftLintCore"]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "SwiftLintExtraRules",
|
||||||
|
dependencies: frameworkDependencies + ["SwiftLintCore"]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "SwiftLintFramework",
|
||||||
|
dependencies: frameworkDependencies + ["SwiftLintBuiltInRules", "SwiftLintCore", "SwiftLintExtraRules"]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SwiftLintTestHelpers",
|
name: "SwiftLintTestHelpers",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
@ -101,6 +123,11 @@ let package = Package(
|
||||||
"SwiftLintTestHelpers"
|
"SwiftLintTestHelpers"
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.binaryTarget(
|
||||||
|
name: "libIndexStore",
|
||||||
|
url: "https://github.com/keith/StaticIndexStore/releases/download/5.7/libIndexStore.xcframework.zip",
|
||||||
|
checksum: "da69bab932357a817aa0756e400be86d7156040bfbea8eded7a3acc529320731"
|
||||||
|
),
|
||||||
.binaryTarget(
|
.binaryTarget(
|
||||||
name: "SwiftLintBinary",
|
name: "SwiftLintBinary",
|
||||||
url: "https://github.com/realm/SwiftLint/releases/download/0.50.3/SwiftLintBinary-macos.artifactbundle.zip",
|
url: "https://github.com/realm/SwiftLint/releases/download/0.50.3/SwiftLintBinary-macos.artifactbundle.zip",
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
import IndexStore
|
||||||
|
|
||||||
|
// MARK: - DeclarationCollectionException
|
||||||
|
|
||||||
|
/// A rule determining that an occurrence should not be collected as a declaration because it should not be
|
||||||
|
/// treated as unused if there are no references to it.
|
||||||
|
struct DeclarationCollectionException {
|
||||||
|
/// Whether an occurrence should be skipped when collecting declarations.
|
||||||
|
let skipCollectingOccurrence: (SymbolOccurrence) -> Bool
|
||||||
|
|
||||||
|
/// All exceptions that should be applied when collecting declarations, in order they should be checked.
|
||||||
|
/// Order should generally be computationally cheapest to most expensive.
|
||||||
|
static var all: [DeclarationCollectionException] {
|
||||||
|
[
|
||||||
|
.notADefinition,
|
||||||
|
.isStaticAllTests,
|
||||||
|
.isGenericParameter,
|
||||||
|
.hasKindsToSkip,
|
||||||
|
.hasSymbolPropertiesToSkip,
|
||||||
|
.hasRolesToSkip,
|
||||||
|
// The following are fuzzy checks. It would be nice to more formally identify these cases.
|
||||||
|
.isLikelyCodableCodingKeys,
|
||||||
|
.isLikelySR11985FalsePositive,
|
||||||
|
.isLikelyProtocolRequirement,
|
||||||
|
.isLikelyResultBuilder,
|
||||||
|
.isLikelyDynamicMemberLookup,
|
||||||
|
.isLikelyPropertyWrapperProjectedValue
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private extension DeclarationCollectionException {
|
||||||
|
/// The occurrence is not a definition, so it should be skipped when collecting declaration occurrences.
|
||||||
|
static let notADefinition = DeclarationCollectionException { occurrence in
|
||||||
|
!occurrence.roles.contains(.definition)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is a static `allTests` variable, so it should be skipped when collecting declaration occurrences.
|
||||||
|
static let isStaticAllTests = DeclarationCollectionException { occurrence in
|
||||||
|
occurrence.symbol.kind == .staticProperty &&
|
||||||
|
occurrence.symbol.roles.contains(.childOf) &&
|
||||||
|
occurrence.symbol.name == "allTests"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is a generic parameter.
|
||||||
|
static let isGenericParameter = DeclarationCollectionException { occurrence in
|
||||||
|
occurrence.symbol.subkind == .swiftGenericParameter
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is of a kind that should not be surfaced as unused, such as:
|
||||||
|
///
|
||||||
|
/// * deinitializers
|
||||||
|
/// * function parameters
|
||||||
|
/// * type extensions
|
||||||
|
static let hasKindsToSkip = DeclarationCollectionException { occurrence in
|
||||||
|
let kindsToSkip: [SymbolKind] = [
|
||||||
|
.destructor,
|
||||||
|
.parameter,
|
||||||
|
.extension,
|
||||||
|
]
|
||||||
|
|
||||||
|
return kindsToSkip.contains(occurrence.symbol.kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence has associated symbol properties that should not be surfaced as unused, such as:
|
||||||
|
///
|
||||||
|
/// * having Interface Builder annotations, or
|
||||||
|
/// * being part of the unit test machinery
|
||||||
|
static let hasSymbolPropertiesToSkip = DeclarationCollectionException { occurrence in
|
||||||
|
#if os(macOS)
|
||||||
|
let propertiesToSkip: SymbolProperty = [
|
||||||
|
.IBAnnotated,
|
||||||
|
.unitTest
|
||||||
|
]
|
||||||
|
|
||||||
|
return !occurrence.symbol.properties.isDisjoint(with: propertiesToSkip)
|
||||||
|
#else
|
||||||
|
let properties = occurrence.symbol.properties
|
||||||
|
let ibannotated = SymbolProperty(SymbolProperty.IBAnnotated)
|
||||||
|
let unittest = SymbolProperty(SymbolProperty.unitTest)
|
||||||
|
return properties & ibannotated == ibannotated ||
|
||||||
|
properties & unittest == unittest
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence has associated symbol roles that should not be surfaced as unused, such as:
|
||||||
|
///
|
||||||
|
/// * being an accessor
|
||||||
|
/// * being implicit (not source code)
|
||||||
|
/// * being a superclass or protocol override
|
||||||
|
/// * being marked as "dynamic", meaning it can be accessed dynamically by the runtime even if not
|
||||||
|
/// explicitly referenced in source code
|
||||||
|
static let hasRolesToSkip = DeclarationCollectionException { occurrence in
|
||||||
|
let rolesToSkip: SymbolRoles = [
|
||||||
|
.accessorOf,
|
||||||
|
.implicit,
|
||||||
|
.overrideOf
|
||||||
|
]
|
||||||
|
|
||||||
|
return !occurrence.roles.isDisjoint(with: rolesToSkip) ||
|
||||||
|
!occurrence.symbol.roles.isDisjoint(with: rolesToSkip)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is likely to be the `CodingKeys` enum used by Codable.
|
||||||
|
static let isLikelyCodableCodingKeys = DeclarationCollectionException { occurrence in
|
||||||
|
return occurrence.symbol.name == "CodingKeys" &&
|
||||||
|
occurrence.symbol.kind == .enum &&
|
||||||
|
occurrence.symbol.roles.contains(.childOf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is likely to be a UIKit delegate protocol function, which don't get indexed properly:
|
||||||
|
/// https://bugs.swift.org/browse/SR-11985
|
||||||
|
static let isLikelySR11985FalsePositive = DeclarationCollectionException { occurrence in
|
||||||
|
/// Not an exhaustive list, add as needed.
|
||||||
|
let functionsToSkip = [
|
||||||
|
"navigationBar(_:didPop:)",
|
||||||
|
"position(for:)",
|
||||||
|
"scrollViewDidEndDecelerating(_:)",
|
||||||
|
"scrollViewDidEndDragging(_:willDecelerate:)",
|
||||||
|
"scrollViewDidScroll(_:)",
|
||||||
|
"scrollViewDidScrollToTop(_:)",
|
||||||
|
"scrollViewWillBeginDragging(_:)",
|
||||||
|
"scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)",
|
||||||
|
"tableView(_:canEditRowAt:)",
|
||||||
|
"tableView(_:commit:forRowAt:)",
|
||||||
|
"tableView(_:editingStyleForRowAt:)",
|
||||||
|
"tableView(_:willDisplayHeaderView:forSection:)",
|
||||||
|
"tableView(_:willSelectRowAt:)"
|
||||||
|
]
|
||||||
|
|
||||||
|
return occurrence.symbol.kind == .instanceMethod &&
|
||||||
|
functionsToSkip.contains(occurrence.symbol.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is likely to be a protocol requirement because it is a child of a protocol definition.
|
||||||
|
static let isLikelyProtocolRequirement = DeclarationCollectionException { occurrence in
|
||||||
|
guard SymbolKind.protocolRequirementKinds.contains(occurrence.symbol.kind) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return occurrence.mapFirstRelation(
|
||||||
|
matching: { $0.kind == .protocol && $1.contains(.childOf) },
|
||||||
|
transform: { _, _ in true }
|
||||||
|
) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is likely to be defined to implement a result builder. These static
|
||||||
|
/// functions are typically never referenced explicitly but implicitly referenced by the compiler.
|
||||||
|
static let isLikelyResultBuilder = DeclarationCollectionException { occurrence in
|
||||||
|
guard occurrence.symbol.kind == .staticMethod else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md#result-building-methods
|
||||||
|
let resultBuilderStaticMethods = [
|
||||||
|
"buildBlock(_:)",
|
||||||
|
"buildIf(_:)",
|
||||||
|
"buildOptional(_:)",
|
||||||
|
"buildEither(_:)",
|
||||||
|
"buildArray(_:)",
|
||||||
|
"buildExpression(_:)",
|
||||||
|
"buildFinalResult(_:)",
|
||||||
|
"buildLimitedAvailability(_:)",
|
||||||
|
// https://github.com/apple/swift-evolution/blob/main/proposals/0348-buildpartialblock.md
|
||||||
|
"buildPartialBlock(first:)",
|
||||||
|
"buildPartialBlock(accumulated:next:)"
|
||||||
|
]
|
||||||
|
|
||||||
|
return resultBuilderStaticMethods.contains(occurrence.symbol.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is likely to be used for `@dynamicMemberLookup`, which are typically never referenced
|
||||||
|
/// explicitly but used by the dynamic member lookup syntax.
|
||||||
|
static let isLikelyDynamicMemberLookup = DeclarationCollectionException { occurrence in
|
||||||
|
return occurrence.symbol.kind == .instanceProperty &&
|
||||||
|
occurrence.symbol.name == "subscript(dynamicMember:)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is likely to be used for `@propertyWrapper`, which are typically never referenced
|
||||||
|
/// explicitly but required to implement property wrappers.
|
||||||
|
static let isLikelyPropertyWrapperProjectedValue = DeclarationCollectionException { occurrence in
|
||||||
|
return occurrence.symbol.kind == .instanceProperty &&
|
||||||
|
occurrence.symbol.name == "projectedValue"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
import Foundation
|
||||||
|
import IndexStore
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
// MARK: - UnusedDeclarationException
|
||||||
|
|
||||||
|
/// A rule determining that a declaration should not be reported as unused.
|
||||||
|
struct UnusedDeclarationException {
|
||||||
|
/// Whether a declaration should not be reported as unused.
|
||||||
|
let skipReportingUnusedDeclaration: (Declaration, SourceFileSyntax) -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Exceptions
|
||||||
|
|
||||||
|
extension UnusedDeclarationException {
|
||||||
|
/// All exceptions that should be applied when calculating unused declarations, in the order they should
|
||||||
|
/// be checked. The order should generally be computationally cheapest to most expensive.
|
||||||
|
static var all: [UnusedDeclarationException] {
|
||||||
|
[
|
||||||
|
.hasAttributesToSkip,
|
||||||
|
.isAppDelegate,
|
||||||
|
.isSceneDelegate,
|
||||||
|
.isUNNotificationContentExtension,
|
||||||
|
.isPreviewProvider,
|
||||||
|
.isDisabledByCommentCommand,
|
||||||
|
.isRawValueEnumCase,
|
||||||
|
.isSkippableInitializer
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private extension UnusedDeclarationException {
|
||||||
|
/// The declaration has some attributes that shouldn't be reported as unused because they can be
|
||||||
|
/// referenced from interface builder or accessed at runtime.
|
||||||
|
static let hasAttributesToSkip = UnusedDeclarationException { declaration, tree in
|
||||||
|
let attributes = declaration.attributes(in: tree)
|
||||||
|
if attributes.contains("main") {
|
||||||
|
return true
|
||||||
|
} else if declaration.kind == .instanceProperty && attributes.contains("IBInspectable") {
|
||||||
|
return true
|
||||||
|
} else if declaration.kind == .instanceMethod && attributes.contains("objc") {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is a UIKit app delegate, which are typically never referenced explicitly but looked up
|
||||||
|
/// at runtime.
|
||||||
|
static let isAppDelegate = UnusedDeclarationException { declaration, tree in
|
||||||
|
let visitor = ConformanceVisitor(symbolName: declaration.name)
|
||||||
|
visitor.walk(tree)
|
||||||
|
return visitor.conformances.contains("UIApplicationDelegate")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is a UIKit scene delegate, which are typically never referenced explicitly but
|
||||||
|
/// referenced in the target's Info.plist.
|
||||||
|
static let isSceneDelegate = UnusedDeclarationException { declaration, tree in
|
||||||
|
let visitor = ConformanceVisitor(symbolName: declaration.name)
|
||||||
|
visitor.walk(tree)
|
||||||
|
return visitor.conformances.contains("UISceneDelegate")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is a `UNNotificationContentExtension`, which are typically never referenced explicitly
|
||||||
|
/// but specified in the extension's `Info.plist`.
|
||||||
|
static let isUNNotificationContentExtension = UnusedDeclarationException { declaration, tree in
|
||||||
|
let visitor = ConformanceVisitor(symbolName: declaration.name)
|
||||||
|
visitor.walk(tree)
|
||||||
|
return visitor.conformances.contains("UNNotificationContentExtension")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The occurrence is a SwiftUI Preview Provider, which are typically never referenced explicitly but
|
||||||
|
/// loaded by Xcode Live Previews.
|
||||||
|
static let isPreviewProvider = UnusedDeclarationException { declaration, tree in
|
||||||
|
guard declaration.name.hasSuffix("_Previews") else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let visitor = ConformanceVisitor(symbolName: declaration.name)
|
||||||
|
visitor.walk(tree)
|
||||||
|
return visitor.conformances.contains("PreviewProvider")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The declaration is in a SwiftLint-style disabled region: `// swiftlint:disable unused_declaration`.
|
||||||
|
static let isDisabledByCommentCommand = UnusedDeclarationException { declaration, tree in
|
||||||
|
return declaration.isDisabled(in: tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The declaration is an enum case backed by a raw value which can be constructed indirectly.
|
||||||
|
static let isRawValueEnumCase = UnusedDeclarationException { declaration, tree in
|
||||||
|
guard declaration.kind == .enumCase else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let locationConverter = SourceLocationConverter(file: declaration.file, tree: tree)
|
||||||
|
let visitor = RawValueEnumCaseVisitor(line: declaration.line, locationConverter: locationConverter)
|
||||||
|
return visitor.walk(tree: tree, handler: \.isRawValueEnumCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
static let isSkippableInitializer = UnusedDeclarationException { declaration, tree in
|
||||||
|
guard declaration.kind == .initializer else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let locationConverter = SourceLocationConverter(file: declaration.file, tree: tree)
|
||||||
|
let visitor = SkippableInitVisitor(line: declaration.line, locationConverter: locationConverter)
|
||||||
|
return visitor.walk(tree: tree, handler: \.unusedDeclarationException)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Declaration {
|
||||||
|
func attributes(in tree: SourceFileSyntax) -> [String] {
|
||||||
|
AttributeVisitor(line: line, locationConverter: SourceLocationConverter(file: file, tree: tree))
|
||||||
|
.walk(tree: tree, handler: \.attributes)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import SwiftLintCore
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
extension Declaration {
|
||||||
|
/// Returns if the current declaration should not be reported as unused because it is disabled
|
||||||
|
/// by a SwiftLint-style comment command.
|
||||||
|
///
|
||||||
|
/// - parameter tree: The source file syntax tree obtained by SwiftSyntax.
|
||||||
|
///
|
||||||
|
/// - returns: True if the declaration is disabled.
|
||||||
|
func isDisabled(in tree: SourceFileSyntax) -> Bool {
|
||||||
|
let location = Location(file: file, line: line, character: column)
|
||||||
|
let file = SwiftLintFile(pathDeferringReading: file)
|
||||||
|
let regionContainingLine = file.regions().first { $0.contains(location) }
|
||||||
|
return regionContainingLine?.isRuleIdentifierDisabled("unused_declaration") ?? false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import IndexStore
|
||||||
|
|
||||||
|
// MARK: - RecordReader
|
||||||
|
|
||||||
|
extension RecordReader {
|
||||||
|
/// Visits all the symbol occurrences referenced by this record reader that match the `matching`
|
||||||
|
/// predicate.
|
||||||
|
///
|
||||||
|
/// - parameter matching: Matching predicate. The visitor block will only be called if this returns true.
|
||||||
|
/// - parameter visitor: The block to run for occurrences passing the `matching` test.
|
||||||
|
func visitOccurrences(matching: (SymbolOccurrence) -> Bool = { _ in true },
|
||||||
|
visitor: (SymbolOccurrence) -> Void) {
|
||||||
|
forEach(occurrence: { occurrence in
|
||||||
|
if matching(occurrence) {
|
||||||
|
visitor(occurrence)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SymbolOccurrence
|
||||||
|
|
||||||
|
extension SymbolOccurrence {
|
||||||
|
/// Visits all this occurrence's related symbols and transforms the first relation passing the `matching`
|
||||||
|
/// test with the `transform` closure, returning the result.
|
||||||
|
///
|
||||||
|
/// - parameter matching: Matching predicate. The transform block will only be called if this returns
|
||||||
|
/// true.
|
||||||
|
/// - parameter transform: The transformation to apply to the first relationship passing the `matching`
|
||||||
|
/// test.
|
||||||
|
///
|
||||||
|
/// - returns: The result of the transformation, or nil if no relationship passed the `matching` test.
|
||||||
|
func mapFirstRelation<T>(
|
||||||
|
matching: (Symbol, SymbolRoles) -> Bool,
|
||||||
|
transform: (Symbol, SymbolRoles) -> T
|
||||||
|
) -> T? {
|
||||||
|
var result: T?
|
||||||
|
forEach(relation: { symbol, roles in
|
||||||
|
if result == nil && matching(symbol, roles) {
|
||||||
|
result = transform(symbol, roles)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
extension SyntaxProtocol {
|
||||||
|
func includesLine(_ line: Int, sourceLocationConverter: SourceLocationConverter) -> Bool {
|
||||||
|
let start = self.startLocation(converter: sourceLocationConverter)
|
||||||
|
let end = self.endLocation(converter: sourceLocationConverter)
|
||||||
|
guard let startLine = start.line, let endLine = end.line else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (startLine...endLine).contains(line)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import IndexStore
|
||||||
|
|
||||||
|
extension SymbolKind {
|
||||||
|
/// All kinds that could be used to define or satisfy a protocol requirement.
|
||||||
|
static var protocolRequirementKinds: [SymbolKind] {
|
||||||
|
[
|
||||||
|
.instanceMethod, .classMethod, .staticMethod,
|
||||||
|
.instanceProperty, .classProperty, .staticProperty
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/// An error produced by the dead code tool.
|
||||||
|
enum DeadCodeError: Error, CustomStringConvertible {
|
||||||
|
/// Failed to load the index store.
|
||||||
|
case indexStoreLoadFailure(indexStoreError: Error)
|
||||||
|
/// Failed to load units from index store.
|
||||||
|
case noUnits
|
||||||
|
/// Failed to load record from index store.
|
||||||
|
case recordLoadFailure(recordName: String, recordReaderError: Error)
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .indexStoreLoadFailure(indexStoreError: let indexStoreError):
|
||||||
|
return "Failed to open index store: \(indexStoreError)"
|
||||||
|
case .noUnits:
|
||||||
|
return "Failed to load units from index store"
|
||||||
|
case let .recordLoadFailure(recordName: recordName, recordReaderError: recordReaderError):
|
||||||
|
return "Failed to load record from index store: \(recordName) \(recordReaderError)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/// A source code declaration.
|
||||||
|
struct Declaration: Comparable, Hashable {
|
||||||
|
enum Kind {
|
||||||
|
/// This declaration is an instance property.
|
||||||
|
case instanceProperty
|
||||||
|
/// This declaration is an instance method.
|
||||||
|
case instanceMethod
|
||||||
|
/// This declaration is a class.
|
||||||
|
case `class`
|
||||||
|
/// This declaration is an enum case.
|
||||||
|
case enumCase
|
||||||
|
/// This declaration is an initializer.
|
||||||
|
case initializer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The unique symbol resolution ID for this declaration.
|
||||||
|
let usr: String
|
||||||
|
/// The file path where this declaration is defined.
|
||||||
|
let file: String
|
||||||
|
/// The line where this declaration is defined.
|
||||||
|
let line: Int
|
||||||
|
/// The column where this declaration is defined.
|
||||||
|
let column: Int
|
||||||
|
/// The Swift name for this declaration.
|
||||||
|
let name: String
|
||||||
|
/// The Swift module for this declaration.
|
||||||
|
let module: String
|
||||||
|
/// This kind of the symbol for the declaration.
|
||||||
|
let kind: Kind?
|
||||||
|
|
||||||
|
static func < (lhs: Declaration, rhs: Declaration) -> Bool {
|
||||||
|
return (lhs.file, lhs.line, lhs.column, lhs.usr) < (rhs.file, rhs.line, rhs.column, rhs.usr)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
/// A collection of symbol occurrences, both declarations and references.
|
||||||
|
/// Acts as a container pairing what's defined with what's used.
|
||||||
|
struct Occurrences {
|
||||||
|
/// The source declarations defined.
|
||||||
|
var declarations: Set<Declaration>
|
||||||
|
/// The source symbols referenced.
|
||||||
|
var references: Set<String>
|
||||||
|
|
||||||
|
/// Creates an empty `Occurrences` value.
|
||||||
|
init() {
|
||||||
|
declarations = []
|
||||||
|
references = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combines the current set of occurrences with the other set of occurrences specified.
|
||||||
|
///
|
||||||
|
/// - parameter other: A different set of occurrences to combine with the current set.
|
||||||
|
mutating func formUnion(_ other: Occurrences) {
|
||||||
|
declarations.formUnion(other.declarations)
|
||||||
|
references.formUnion(other.references)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
/// Definition of a protocol in the `ProtocolGraph`. A reference type to enable efficient in-place merging
|
||||||
|
/// with other protocol definitions.
|
||||||
|
final class ProtocolDefinition {
|
||||||
|
/// This protocol's unique identifier.
|
||||||
|
let usr: String
|
||||||
|
/// This protocol's name.
|
||||||
|
let name: String
|
||||||
|
/// The set of type names that conform to this protocol.
|
||||||
|
/// May be incomplete unless accessed through a `ProtocolGraph`.
|
||||||
|
var conformingNames: [String]
|
||||||
|
/// The names of this protocol's requirements.
|
||||||
|
private(set) var childNames: [String]
|
||||||
|
|
||||||
|
/// The set of type names that conform to this protocol, plus this protocol's name.
|
||||||
|
/// May be incomplete unless accessed through a `ProtocolGraph`.
|
||||||
|
var conformingNamesIncludingSelf: [String] { conformingNames + [name] }
|
||||||
|
|
||||||
|
init(usr: String, name: String, conformingNames: [String], childNames: [String]) {
|
||||||
|
self.usr = usr
|
||||||
|
self.name = name
|
||||||
|
self.conformingNames = conformingNames
|
||||||
|
self.childNames = childNames
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the children of the specified protocol definition to the current definition.
|
||||||
|
///
|
||||||
|
/// - parameter definition: The other protocol definition to merge with `self`.
|
||||||
|
func merge(with definition: ProtocolDefinition) {
|
||||||
|
childNames.append(contentsOf: definition.childNames)
|
||||||
|
childNames = Set(childNames).sorted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Hashable
|
||||||
|
|
||||||
|
extension ProtocolDefinition: Hashable {
|
||||||
|
static func == (lhs: ProtocolDefinition, rhs: ProtocolDefinition) -> Bool {
|
||||||
|
return (lhs.usr, lhs.name, lhs.conformingNames, lhs.childNames) ==
|
||||||
|
(rhs.usr, rhs.name, rhs.conformingNames, rhs.childNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(usr)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import IndexStore
|
||||||
|
|
||||||
|
// MARK: - ProtocolGraph
|
||||||
|
|
||||||
|
/// Graph of protocol definitions, along with information about which types conform to which protocols.
|
||||||
|
struct ProtocolGraph {
|
||||||
|
/// Protocol definitions that compose this graph.
|
||||||
|
let protocols: [ProtocolDefinition]
|
||||||
|
|
||||||
|
/// Returns true if the specified occurrence satisfies a protocol requirement for any of the protocols in
|
||||||
|
/// this graph.
|
||||||
|
///
|
||||||
|
/// - parameter occurrence: The symbol occurrence definition to check.
|
||||||
|
///
|
||||||
|
/// - returns: True if the specified occurrence satisfies a protocol requirement for any of the protocols
|
||||||
|
/// in this graph.
|
||||||
|
func occurrenceSatisfiesProtocolRequirement(_ occurrence: SymbolOccurrence) -> Bool {
|
||||||
|
guard occurrence.roles.contains(.definition),
|
||||||
|
SymbolKind.protocolRequirementKinds.contains(occurrence.symbol.kind),
|
||||||
|
let occurrenceParentName = occurrence.parentName()
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocolsConformedByType(named: occurrenceParentName)
|
||||||
|
.flatMap(\.childNames)
|
||||||
|
.contains(occurrence.symbol.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func protocolsConformedByType(named typeName: String) -> Set<ProtocolDefinition> {
|
||||||
|
// First check protocols the type name directly conforms to.
|
||||||
|
var protocolsConformedByType = Set(protocols).filter { protocolDefinition in
|
||||||
|
return protocolDefinition.conformingNamesIncludingSelf
|
||||||
|
.contains(typeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !protocolsConformedByType.isEmpty else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all direct protocols, traverse their inheritance hierarchy in the graph, up to some limit to
|
||||||
|
// make sure we don't infinitely recurse. Type-checking Swift code shouldn't hit this limit.
|
||||||
|
var maxProtocolRecursion = 20
|
||||||
|
while maxProtocolRecursion > 0 {
|
||||||
|
let update = protocols.filter { protocolDefinition in
|
||||||
|
if protocolsConformedByType.map(\.name).contains(protocolDefinition.name) {
|
||||||
|
// Exclude protocols we've already collected.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocolDefinition.conformingNames
|
||||||
|
.contains(where: { newProtoConformingName in
|
||||||
|
protocolsConformedByType.map(\.name).contains(newProtoConformingName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.isEmpty {
|
||||||
|
// No more protocols to check in the hierarchy.
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
protocolsConformedByType.formUnion(update)
|
||||||
|
maxProtocolRecursion -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocolsConformedByType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
private extension SymbolOccurrence {
|
||||||
|
func parentName() -> String? {
|
||||||
|
return mapFirstRelation(
|
||||||
|
matching: { _, roles in roles.contains(.childOf) },
|
||||||
|
transform: { symbol, _ in symbol.name }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import IndexStore
|
||||||
|
|
||||||
|
/// Opens and parses index stores.
|
||||||
|
struct IndexStoreLoader {
|
||||||
|
/// The path on disk to the index store directory.
|
||||||
|
let indexStorePath: String
|
||||||
|
|
||||||
|
/// Open and parse the index store.
|
||||||
|
///
|
||||||
|
/// - throws: `DeadCodeError` if the loading fails.
|
||||||
|
///
|
||||||
|
/// - returns: The index store.
|
||||||
|
func load() throws -> IndexStore {
|
||||||
|
do {
|
||||||
|
return try IndexStore(path: indexStorePath)
|
||||||
|
} catch {
|
||||||
|
throw DeadCodeError.indexStoreLoadFailure(indexStoreError: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import IndexStore
|
||||||
|
|
||||||
|
/// Collects Swift symbol occurrences (declarations and references) across all source units.
|
||||||
|
struct OccurrenceCollector {
|
||||||
|
/// The reader interface for a Swift source code unit.
|
||||||
|
let units: [UnitReader]
|
||||||
|
/// A map from source file paths to their record reader interfaces.
|
||||||
|
let recordReaders: [String: [RecordReader]]
|
||||||
|
|
||||||
|
/// Collects occurrences for all units.
|
||||||
|
///
|
||||||
|
/// - returns: occurrences for all units.
|
||||||
|
func collect() -> Occurrences {
|
||||||
|
return units.map { unitReader -> Occurrences in
|
||||||
|
collectSingle(unitReader: unitReader)
|
||||||
|
}
|
||||||
|
.reduce(into: Occurrences()) { $0.formUnion($1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects occurrences for a single unit.
|
||||||
|
///
|
||||||
|
/// - parameter unitReader: The reader for the unit being collected.
|
||||||
|
///
|
||||||
|
/// - returns: Occurrences for the specified unit.
|
||||||
|
private func collectSingle(unitReader: UnitReader) -> Occurrences {
|
||||||
|
var occurrences = Occurrences()
|
||||||
|
// Empty source files have units but no records
|
||||||
|
guard let recordReader = recordReaders[unitReader.mainFile] else {
|
||||||
|
return occurrences
|
||||||
|
}
|
||||||
|
|
||||||
|
let exceptions = DeclarationCollectionException.all
|
||||||
|
|
||||||
|
for recordReader in recordReader {
|
||||||
|
recordReader.visitOccurrences { occurrence in
|
||||||
|
if occurrence.roles.contains(.reference) && !occurrence.roles.contains(.extendedBy) {
|
||||||
|
occurrences.references.insert(occurrence.symbol.usr)
|
||||||
|
return
|
||||||
|
} else if exceptions.skipCollecting(occurrence) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
occurrences.declarations.insert(
|
||||||
|
Declaration(
|
||||||
|
usr: occurrence.symbol.usr,
|
||||||
|
file: unitReader.mainFile,
|
||||||
|
line: occurrence.location.line,
|
||||||
|
column: occurrence.location.column,
|
||||||
|
name: occurrence.symbol.name,
|
||||||
|
module: unitReader.moduleName,
|
||||||
|
kind: .init(symbolKind: occurrence.symbol.kind)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return occurrences
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Declaration.Kind {
|
||||||
|
init?(symbolKind: SymbolKind) {
|
||||||
|
switch symbolKind {
|
||||||
|
case .instanceProperty:
|
||||||
|
self = .instanceProperty
|
||||||
|
case .instanceMethod:
|
||||||
|
self = .instanceMethod
|
||||||
|
case .class:
|
||||||
|
self = .class
|
||||||
|
case .enumConstant:
|
||||||
|
self = .enumCase
|
||||||
|
case .constructor:
|
||||||
|
self = .initializer
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Array where Element == DeclarationCollectionException {
|
||||||
|
func skipCollecting(_ occurrence: SymbolOccurrence) -> Bool {
|
||||||
|
contains(where: { $0.skipCollectingOccurrence(occurrence) })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
import IndexStore
|
||||||
|
import SwiftLintCore
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
// MARK: - ProtocolCollector
|
||||||
|
|
||||||
|
/// Collects Swift symbol occurrences (declarations and references) across all source units.
|
||||||
|
struct ProtocolCollector {
|
||||||
|
/// The reader interface for a Swift source code unit.
|
||||||
|
let units: [UnitReader]
|
||||||
|
/// A map from source file paths to their record reader interfaces.
|
||||||
|
let recordReaders: [String: [RecordReader]]
|
||||||
|
|
||||||
|
/// Builds a protocol graph.
|
||||||
|
///
|
||||||
|
/// - returns: The protocol graph.
|
||||||
|
func collect() async -> ProtocolGraph {
|
||||||
|
units
|
||||||
|
.compactMap(collectSingle(unitReader:))
|
||||||
|
.reduce(into: ProtocolCollectionResult()) { $0.merge(with: $1) }
|
||||||
|
.toProtocolGraph()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func collectSingle(unitReader: UnitReader) -> ProtocolCollectionResult? {
|
||||||
|
// Empty source files have units but no records
|
||||||
|
guard let recordReader = recordReaders[unitReader.mainFile] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordReader.reduce(into: ProtocolCollectionResult()) { result, recordReader in
|
||||||
|
recordReader.visitOccurrences(matching: { $0.roles.contains(.definition) }, visitor: { occurrence in
|
||||||
|
if let protocolDefinition = occurrence.protocolDefinition {
|
||||||
|
result.mergeOrAppend(protocolDefinition)
|
||||||
|
}
|
||||||
|
|
||||||
|
if occurrence.canAddProtocolConformance {
|
||||||
|
let tree = FileSyntaxTreeCache.getSyntaxTree(forFile: unitReader.mainFile)
|
||||||
|
for protocolName in occurrence.conformances(in: tree) {
|
||||||
|
result.conformances[protocolName, default: []].append(occurrence.symbol.name)
|
||||||
|
}
|
||||||
|
} else if let protocolDefinition = occurrence.protocolDefinitionForRequirement {
|
||||||
|
result.mergeOrAppend(protocolDefinition)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
private extension SymbolOccurrence {
|
||||||
|
func conformances(in tree: SourceFileSyntax) -> [String] {
|
||||||
|
ConformanceVisitor(symbolName: symbol.name)
|
||||||
|
.walk(tree: tree, handler: \.conformances)
|
||||||
|
}
|
||||||
|
|
||||||
|
var canAddProtocolConformance: Bool {
|
||||||
|
symbol.kind == .extension || symbol.kind == .protocol || symbol.kind == .typealias
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocolDefinition: ProtocolDefinition? {
|
||||||
|
guard symbol.kind == .protocol || symbol.kind == .typealias else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProtocolDefinition(
|
||||||
|
usr: symbol.usr, name: symbol.name,
|
||||||
|
conformingNames: [], // Added later
|
||||||
|
childNames: [] // Added later
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocolDefinitionForRequirement: ProtocolDefinition? {
|
||||||
|
guard SymbolKind.protocolRequirementKinds.contains(symbol.kind) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapFirstRelation(
|
||||||
|
matching: { $0.kind == .protocol && $1.contains(.childOf) },
|
||||||
|
transform: { symbol, _ in
|
||||||
|
ProtocolDefinition(
|
||||||
|
usr: symbol.usr, name: symbol.name,
|
||||||
|
conformingNames: [], // Added later
|
||||||
|
childNames: [self.symbol.name]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ProtocolCollectionResult {
|
||||||
|
var protocols: [ProtocolDefinition] = []
|
||||||
|
// Key is protocol name, value is set of type names that conform to that protocol
|
||||||
|
var conformances: [String: [String]] = [:]
|
||||||
|
|
||||||
|
mutating func merge(with other: ProtocolCollectionResult) {
|
||||||
|
other.protocols
|
||||||
|
.forEach { mergeOrAppend($0) }
|
||||||
|
other.conformances
|
||||||
|
.forEach { conformances[$0.key, default: []].append(contentsOf: $0.value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func mergeOrAppend(_ definition: ProtocolDefinition) {
|
||||||
|
if let existingDefinition = protocols.first(where: { $0.usr == definition.usr }) {
|
||||||
|
existingDefinition.merge(with: definition)
|
||||||
|
} else {
|
||||||
|
protocols.append(definition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProtocolGraph() -> ProtocolGraph {
|
||||||
|
protocols.forEach { $0.conformingNames = conformances[$0.name] ?? [] }
|
||||||
|
return ProtocolGraph(protocols: protocols)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import Foundation
|
||||||
|
import IndexStore
|
||||||
|
|
||||||
|
/// Collects Swift source units and their associated record readers.
|
||||||
|
struct UnitCollector {
|
||||||
|
/// The index store to traverse to collect units.
|
||||||
|
let indexStore: IndexStore
|
||||||
|
|
||||||
|
/// Collects all source units and record readers.
|
||||||
|
///
|
||||||
|
/// - throws: `DeadCodeError` if no units were found, or if a record reader could not be created.
|
||||||
|
///
|
||||||
|
/// - returns: All source units and record readers.
|
||||||
|
func collectUnitsAndRecords() throws -> ([UnitReader], [String: [RecordReader]]) {
|
||||||
|
let units = indexStore.units.filter(\.shouldCollectUnitsAndRecords)
|
||||||
|
|
||||||
|
if units.isEmpty {
|
||||||
|
throw DeadCodeError.noUnits
|
||||||
|
}
|
||||||
|
|
||||||
|
let recordReaders = try units.reduce(into: [String: [RecordReader]]()) { accumulator, unitReader in
|
||||||
|
guard let recordName = unitReader.recordName else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let recordReader: RecordReader
|
||||||
|
do {
|
||||||
|
recordReader = try RecordReader(indexStore: indexStore, recordName: recordName)
|
||||||
|
} catch {
|
||||||
|
throw DeadCodeError.recordLoadFailure(recordName: recordName, recordReaderError: error)
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulator[unitReader.mainFile, default: []].append(recordReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (units, recordReaders)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UnitReader {
|
||||||
|
var shouldCollectUnitsAndRecords: Bool {
|
||||||
|
!mainFile.contains("/.build/") &&
|
||||||
|
!mainFile.contains("/external/")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import CollectionConcurrencyKit
|
||||||
|
import IndexStore
|
||||||
|
|
||||||
|
// MARK: - UnusedDeclarationCalculator
|
||||||
|
|
||||||
|
/// Calculates which declarations are unused based on the input occurrences that were collected, applying
|
||||||
|
/// exceptions as defined in `UnusedDeclarationException.all`.
|
||||||
|
struct UnusedDeclarationCalculator {
|
||||||
|
/// The reader interface for a Swift source code unit.
|
||||||
|
let units: [UnitReader]
|
||||||
|
/// A map from source file paths to their record reader interfaces.
|
||||||
|
let recordReaders: [String: [RecordReader]]
|
||||||
|
/// The protocol graph.
|
||||||
|
let protocolGraph: ProtocolGraph
|
||||||
|
/// The collection of symbol occurrences, both declarations and references.
|
||||||
|
let occurrences: Occurrences
|
||||||
|
|
||||||
|
/// Calculates which declarations are unused based on the input occurrences.
|
||||||
|
///
|
||||||
|
/// - returns: All unused declarations based on the input occurrences.
|
||||||
|
func calculate() async throws -> [UnusedDeclaration] {
|
||||||
|
let exceptions = UnusedDeclarationException.all
|
||||||
|
|
||||||
|
var unusedDeclarations = await occurrences.declarations.concurrentCompactMap { declaration -> Declaration? in
|
||||||
|
if occurrences.references.contains(declaration.usr) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let tree = FileSyntaxTreeCache.getSyntaxTree(forFile: declaration.file)
|
||||||
|
if exceptions.contains(where: { $0.skipReportingUnusedDeclaration(declaration, tree) }) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return declaration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declarations that satisfy protocol requirements in files other than where the parent type adds
|
||||||
|
// the conformance to the protocol aren't detected as relationships in the index store.
|
||||||
|
// We can remove these cases by checking if a USR satisfies a protocol requirement from the protocol
|
||||||
|
// graph we built ourselves.
|
||||||
|
let usrsToRemove = usrsSatisfyingProtocolRequirements(Set(unusedDeclarations.map(\.usr)))
|
||||||
|
unusedDeclarations.removeAll { usrsToRemove.contains($0.usr) }
|
||||||
|
|
||||||
|
return unusedDeclarations
|
||||||
|
.sorted()
|
||||||
|
.map(UnusedDeclaration.init)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func usrsSatisfyingProtocolRequirements(_ usrsToCheck: Set<String>) -> Set<String> {
|
||||||
|
guard !usrsToCheck.isEmpty else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return units.compactMap { unitReader -> Set<String>? in
|
||||||
|
guard let recordReader = recordReaders[unitReader.mainFile] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordReader.reduce(into: Set<String>()) { usrsToRemove, recordReader in
|
||||||
|
recordReader.visitOccurrences(
|
||||||
|
matching: { occurrence in
|
||||||
|
usrsToCheck.contains(occurrence.symbol.usr) &&
|
||||||
|
protocolGraph.occurrenceSatisfiesProtocolRequirement(occurrence)
|
||||||
|
},
|
||||||
|
visitor: { usrsToRemove.insert($0.symbol.usr) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.reduce(into: []) { $0.formUnion($1) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import IndexStore
|
||||||
|
|
||||||
|
/// Entry point to the dead code tool. Finds and produces all unused declarations.
|
||||||
|
public enum UnusedDeclarationFinder {
|
||||||
|
/// Find and return all unused declarations from the index store.
|
||||||
|
///
|
||||||
|
/// - parameter indexStorePath: The path on disk to the index store directory.
|
||||||
|
///
|
||||||
|
/// - throws: `DeadCodeError` on failure.
|
||||||
|
///
|
||||||
|
/// - returns: All unused declarations found.
|
||||||
|
public static func find(indexStorePath: String) async throws -> [UnusedDeclaration] {
|
||||||
|
let indexStore = try IndexStoreLoader(indexStorePath: indexStorePath)
|
||||||
|
.load()
|
||||||
|
|
||||||
|
let (units, recordReaders) = try TimedStep("(1/4) Collecting units and records") {
|
||||||
|
try UnitCollector(indexStore: indexStore)
|
||||||
|
.collectUnitsAndRecords()
|
||||||
|
}
|
||||||
|
|
||||||
|
let protocolGraph = await TimedStep("(2/4) Collecting protocol conformances") {
|
||||||
|
await ProtocolCollector(units: units, recordReaders: recordReaders)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
let occurrences = TimedStep("(3/4) Collecting declarations and references") {
|
||||||
|
OccurrenceCollector(units: units, recordReaders: recordReaders)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await TimedStep("(4/4) Calculating unused declarations") {
|
||||||
|
let calculator = UnusedDeclarationCalculator(
|
||||||
|
units: units, recordReaders: recordReaders, protocolGraph: protocolGraph, occurrences: occurrences
|
||||||
|
)
|
||||||
|
return try await calculator.calculate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Foundation
|
||||||
|
import SwiftLintCore
|
||||||
|
import SwiftParser
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
/// Caches parsed file syntax tree by path.
|
||||||
|
enum FileSyntaxTreeCache {
|
||||||
|
/// Returns the parsed source syntax tree of the file at the specified path.
|
||||||
|
///
|
||||||
|
/// - parameter file: Path to file to read from disk.
|
||||||
|
///
|
||||||
|
/// - returns: Parsed source file syntax tree.
|
||||||
|
static func getSyntaxTree(forFile file: String) -> SourceFileSyntax {
|
||||||
|
SwiftLintFile(pathDeferringReading: file).syntaxTree
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import Foundation
|
||||||
|
#if os(macOS)
|
||||||
|
import Darwin.C
|
||||||
|
#else
|
||||||
|
import Glibc
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// A named unit of work that prints a message, runs the work and terminates the message with the time it took
|
||||||
|
/// to execute.
|
||||||
|
///
|
||||||
|
/// - parameter name: The step's name, printed before executing the work.
|
||||||
|
/// - parameter work: The work closure to execute and time.
|
||||||
|
///
|
||||||
|
/// - throws: Rethrows error thrown by the `work` closure.
|
||||||
|
///
|
||||||
|
/// - returns: The result of the `work` closure.
|
||||||
|
func TimedStep<Output>(_ name: String, work: () throws -> Output) rethrows -> Output {
|
||||||
|
// swiftlint:disable:previous identifier_name - This looks better capitalized like a type name
|
||||||
|
let start = Date()
|
||||||
|
print(name, terminator: "")
|
||||||
|
fflush(stdout)
|
||||||
|
|
||||||
|
defer {
|
||||||
|
let duration = String(format: "%.2fs", -start.timeIntervalSinceNow)
|
||||||
|
print(" (\(duration))")
|
||||||
|
}
|
||||||
|
|
||||||
|
return try work()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimedStep<Output>(_ name: String, work: () async throws -> Output) async rethrows -> Output {
|
||||||
|
// swiftlint:disable:previous identifier_name - This looks better capitalized like a type name
|
||||||
|
let start = Date()
|
||||||
|
print(name, terminator: "")
|
||||||
|
fflush(stdout)
|
||||||
|
|
||||||
|
defer {
|
||||||
|
let duration = String(format: "%.2fs", -start.timeIntervalSinceNow)
|
||||||
|
print(" (\(duration))")
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await work()
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
/// An unused Swift source code declaration.
|
||||||
|
public struct UnusedDeclaration {
|
||||||
|
/// The description to print when logging this unused declaration.
|
||||||
|
public let logDescription: String
|
||||||
|
|
||||||
|
/// Creates an `UnusedDeclaration` from the specified `Declaration` and a path prefix to use to truncate
|
||||||
|
/// the declaration's original file path.
|
||||||
|
///
|
||||||
|
/// - parameter declaration: The unused declaration.
|
||||||
|
init(_ declaration: Declaration) {
|
||||||
|
logDescription =
|
||||||
|
"""
|
||||||
|
\(declaration.file):\(declaration.line):\(declaration.column): \
|
||||||
|
error: Unused declaration named '\(declaration.name)'
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
/// Visits the source syntax tree to collect attributes at a given line.
|
||||||
|
final class AttributeVisitor: SyntaxVisitor {
|
||||||
|
private(set) var attributes = [String]()
|
||||||
|
private let line: Int
|
||||||
|
private let locationConverter: SourceLocationConverter
|
||||||
|
|
||||||
|
init(line: Int, locationConverter: SourceLocationConverter) {
|
||||||
|
self.line = line
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: AttributeSyntax) {
|
||||||
|
if nodeIncludesLine(node.parent?.parent) {
|
||||||
|
attributes.append(node.attributeName.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nodeIncludesLine(_ node: SyntaxProtocol?) -> Bool {
|
||||||
|
guard
|
||||||
|
let node = node,
|
||||||
|
let startLine = node.startLocation(converter: locationConverter).line,
|
||||||
|
let endLine = node.endLocation(converter: locationConverter).line
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (startLine...endLine).contains(line)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
// MARK: - ConformanceVisitor
|
||||||
|
|
||||||
|
/// Visits the source syntax tree to collect the types the specified `symbolName` conform to.
|
||||||
|
final class ConformanceVisitor: SyntaxVisitor {
|
||||||
|
var conformances = [String]()
|
||||||
|
let symbolName: String
|
||||||
|
|
||||||
|
init(symbolName: String) {
|
||||||
|
self.symbolName = symbolName
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: TypealiasDeclSyntax) {
|
||||||
|
if node.identifier.text == symbolName,
|
||||||
|
let compositionTypeNames = node.compositionTypeNames() {
|
||||||
|
conformances.append(contentsOf: compositionTypeNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: GenericWhereClauseSyntax) {
|
||||||
|
for child in node.requirementList.children(viewMode: .sourceAccurate) {
|
||||||
|
if let genericRequirement = child.as(GenericRequirementSyntax.self),
|
||||||
|
let conformanceRequirement = genericRequirement.body.as(ConformanceRequirementSyntax.self),
|
||||||
|
let leftTypeIdentifier = conformanceRequirement.leftTypeIdentifier
|
||||||
|
.as(SimpleTypeIdentifierSyntax.self),
|
||||||
|
leftTypeIdentifier.firstToken?.tokenKind == .capitalSelfKeyword,
|
||||||
|
let rightTypeName = conformanceRequirement.rightTypeIdentifier.simpleTypeName,
|
||||||
|
let parent = node.parent, let extensionDecl = parent.as(ExtensionDeclSyntax.self),
|
||||||
|
let extendedTypeName = extensionDecl.extendedType.simpleTypeName,
|
||||||
|
extendedTypeName == symbolName {
|
||||||
|
conformances.append(rightTypeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: InheritedTypeListSyntax) {
|
||||||
|
guard node.parent?.parent?.declIdentifierName == symbolName else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for child in node.children(viewMode: .sourceAccurate) {
|
||||||
|
if let inheritedTypeName = child.as(InheritedTypeSyntax.self)?.typeName.simpleTypeName {
|
||||||
|
conformances.append(inheritedTypeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
private extension TypealiasDeclSyntax {
|
||||||
|
func compositionTypeNames() -> [String]? {
|
||||||
|
guard let elements = initializer.value.as(CompositionTypeSyntax.self)?.elements else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements.children(viewMode: .sourceAccurate).compactMap { child in
|
||||||
|
child.as(CompositionTypeElementSyntax.self)?.type.simpleTypeName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TypeSyntax {
|
||||||
|
var simpleTypeName: String? {
|
||||||
|
self.as(SimpleTypeIdentifierSyntax.self)?.name.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Syntax {
|
||||||
|
var declIdentifierName: String? {
|
||||||
|
if let decl = self.as(ClassDeclSyntax.self) {
|
||||||
|
return decl.identifier.text
|
||||||
|
} else if let decl = self.as(ProtocolDeclSyntax.self) {
|
||||||
|
return decl.identifier.text
|
||||||
|
} else if let decl = self.as(StructDeclSyntax.self) {
|
||||||
|
return decl.identifier.text
|
||||||
|
} else if let decl = self.as(ExtensionDeclSyntax.self) {
|
||||||
|
return decl.extendedType.simpleTypeName
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
/// A SwiftSyntax visitor that detects if the case at the specified line number should be excluded
|
||||||
|
/// from being reported as dead code because it is backed by a raw value which can be constructed indirectly.
|
||||||
|
final class RawValueEnumCaseVisitor: SyntaxVisitor {
|
||||||
|
private(set) var isRawValueEnumCase = false
|
||||||
|
private let line: Int
|
||||||
|
private let locationConverter: SourceLocationConverter
|
||||||
|
|
||||||
|
init(line: Int, locationConverter: SourceLocationConverter) {
|
||||||
|
self.line = line
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: EnumDeclSyntax) {
|
||||||
|
let enumSourceRange = node.sourceRange(converter: self.locationConverter)
|
||||||
|
guard
|
||||||
|
let startLine = enumSourceRange.start.line,
|
||||||
|
let endLine = enumSourceRange.end.line,
|
||||||
|
startLine <= self.line,
|
||||||
|
endLine >= self.line
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let nestedEnumVisitor = EnumSourceRangeVisitor(locationConverter: self.locationConverter)
|
||||||
|
let nestedEnumRanges = nestedEnumVisitor
|
||||||
|
.walk(tree: node.members, handler: \.enumRanges)
|
||||||
|
|
||||||
|
let nestedEnumContainsLine = nestedEnumRanges.contains { range in
|
||||||
|
guard
|
||||||
|
let startLine = range.start.line,
|
||||||
|
let endLine = range.end.line,
|
||||||
|
startLine <= self.line,
|
||||||
|
endLine >= self.line
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !nestedEnumContainsLine, node.inheritanceClause?.supportsRawValue == true else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isRawValueEnumCase = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private final class EnumSourceRangeVisitor: SyntaxVisitor {
|
||||||
|
var enumRanges = [SourceRange]()
|
||||||
|
private let locationConverter: SourceLocationConverter
|
||||||
|
|
||||||
|
init(locationConverter: SourceLocationConverter) {
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: EnumDeclSyntax) {
|
||||||
|
self.enumRanges.append(
|
||||||
|
node.sourceRange(converter: self.locationConverter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TypeInheritanceClauseSyntax {
|
||||||
|
var supportsRawValue: Bool {
|
||||||
|
// Check if it's an enum which supports raw values
|
||||||
|
let implicitRawValueSet: Set<String> = [
|
||||||
|
"Int", "Int8", "Int16", "Int32", "Int64",
|
||||||
|
"UInt", "UInt8", "UInt16", "UInt32", "UInt64",
|
||||||
|
"Double", "Float", "Float80", "Decimal", "NSNumber",
|
||||||
|
"NSDecimalNumber", "NSInteger", "String", "CGFloat",
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.inheritedTypeCollection.contains { element in
|
||||||
|
guard let identifier = element.typeName.as(SimpleTypeIdentifierSyntax.self)?.name.text else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return implicitRawValueSet.contains(identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
// MARK: - SkippableInitVisitor
|
||||||
|
|
||||||
|
/// A SwiftSyntax visitor that detects if the initializer at the specified line number should be excluded
|
||||||
|
/// from being reported as dead code.
|
||||||
|
final class SkippableInitVisitor: SyntaxVisitor {
|
||||||
|
var unusedDeclarationException = false
|
||||||
|
private let line: Int
|
||||||
|
private let locationConverter: SourceLocationConverter
|
||||||
|
|
||||||
|
init(line: Int, locationConverter: SourceLocationConverter) {
|
||||||
|
self.line = line
|
||||||
|
self.locationConverter = locationConverter
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: InitializerDeclSyntax) {
|
||||||
|
guard node.includesLine(self.line, sourceLocationConverter: self.locationConverter) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove some of these exceptions
|
||||||
|
|
||||||
|
guard let parent = node.nearestNominalOrExtensionParent() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip initializers of types with generic parameters since Swift doesn't record references to these
|
||||||
|
// in the index store.
|
||||||
|
if parent.as(StructDeclSyntax.self)?.genericParameterClause != nil ||
|
||||||
|
parent.as(ClassDeclSyntax.self)?.genericParameterClause != nil
|
||||||
|
{
|
||||||
|
self.unusedDeclarationException = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip initializers of non-final classes.
|
||||||
|
if let classDecl = parent.as(ClassDeclSyntax.self),
|
||||||
|
classDecl.modifiers?.hasFinalKeyword != true
|
||||||
|
{
|
||||||
|
self.unusedDeclarationException = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip extensions of `Array`.
|
||||||
|
if let extensionDecl = parent.as(ExtensionDeclSyntax.self),
|
||||||
|
let extendedType = extensionDecl.extendedType.as(SimpleTypeIdentifierSyntax.self),
|
||||||
|
extendedType.name.text == "Array"
|
||||||
|
{
|
||||||
|
self.unusedDeclarationException = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip extensions with `where` clause.
|
||||||
|
if let extensionDecl = parent.as(ExtensionDeclSyntax.self),
|
||||||
|
extensionDecl.genericWhereClause != nil
|
||||||
|
{
|
||||||
|
self.unusedDeclarationException = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip initializers marked as `required`.
|
||||||
|
if node.modifiers?.hasRequiredKeyword == true {
|
||||||
|
self.unusedDeclarationException = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Helpers
|
||||||
|
|
||||||
|
private extension ModifierListSyntax {
|
||||||
|
var hasRequiredKeyword: Bool {
|
||||||
|
self.contains { $0.name.tokenKind == .contextualKeyword("required") }
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasFinalKeyword: Bool {
|
||||||
|
self.contains { $0.name.tokenKind == .contextualKeyword("final") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SyntaxProtocol {
|
||||||
|
func nearestNominalOrExtensionParent() -> Syntax? {
|
||||||
|
guard let parent else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent.isNominalTypeDeclOrExtensionDecl ? parent : parent.nearestNominalOrExtensionParent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Syntax {
|
||||||
|
var isNominalTypeDeclOrExtensionDecl: Bool {
|
||||||
|
self.is(StructDeclSyntax.self) ||
|
||||||
|
self.is(ClassDeclSyntax.self) ||
|
||||||
|
self.is(ActorDeclSyntax.self) ||
|
||||||
|
self.is(EnumDeclSyntax.self) ||
|
||||||
|
self.is(ExtensionDeclSyntax.self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
@_exported import SwiftLintCore // swiftlint:disable:this unused_import
|
|
@ -1,8 +1,7 @@
|
||||||
// Generated using Sourcery 1.9.2 — https://github.com/krzysztofzablocki/Sourcery
|
// Generated using Sourcery 2.0.0 — https://github.com/krzysztofzablocki/Sourcery
|
||||||
// DO NOT EDIT
|
// DO NOT EDIT
|
||||||
|
|
||||||
/// The rule list containing all available rules built into SwiftLint.
|
/// The rule list containing all available rules built into SwiftLint.
|
||||||
let builtInRules: [Rule.Type] = [
|
public let builtInRules: [Rule.Type] = [
|
||||||
AccessibilityLabelForImageRule.self,
|
AccessibilityLabelForImageRule.self,
|
||||||
AccessibilityTraitForButtonRule.self,
|
AccessibilityTraitForButtonRule.self,
|
||||||
AnonymousArgumentInMultilineClosureRule.self,
|
AnonymousArgumentInMultilineClosureRule.self,
|
||||||
|
@ -32,7 +31,6 @@ let builtInRules: [Rule.Type] = [
|
||||||
ContainsOverRangeNilComparisonRule.self,
|
ContainsOverRangeNilComparisonRule.self,
|
||||||
ControlStatementRule.self,
|
ControlStatementRule.self,
|
||||||
ConvenienceTypeRule.self,
|
ConvenienceTypeRule.self,
|
||||||
CustomRules.self,
|
|
||||||
CyclomaticComplexityRule.self,
|
CyclomaticComplexityRule.self,
|
||||||
DeploymentTargetRule.self,
|
DeploymentTargetRule.self,
|
||||||
DiscardedNotificationCenterObserverRule.self,
|
DiscardedNotificationCenterObserverRule.self,
|
||||||
|
@ -181,7 +179,6 @@ let builtInRules: [Rule.Type] = [
|
||||||
StaticOperatorRule.self,
|
StaticOperatorRule.self,
|
||||||
StrictFilePrivateRule.self,
|
StrictFilePrivateRule.self,
|
||||||
StrongIBOutletRule.self,
|
StrongIBOutletRule.self,
|
||||||
SuperfluousDisableCommandRule.self,
|
|
||||||
SwitchCaseAlignmentRule.self,
|
SwitchCaseAlignmentRule.self,
|
||||||
SwitchCaseOnNewlineRule.self,
|
SwitchCaseOnNewlineRule.self,
|
||||||
SyntacticSugarRule.self,
|
SyntacticSugarRule.self,
|
||||||
|
@ -225,6 +222,3 @@ let builtInRules: [Rule.Type] = [
|
||||||
XCTSpecificMatcherRule.self,
|
XCTSpecificMatcherRule.self,
|
||||||
YodaConditionRule.self
|
YodaConditionRule.self
|
||||||
]
|
]
|
||||||
|
|
||||||
/// The rule list containing all available rules built into SwiftLint as well as native custom rules.
|
|
||||||
public let primaryRuleList = RuleList(rules: builtInRules + extraRules())
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue