Compare commits

...

5 Commits

Author SHA1 Message Date
JP Simard d8096906f4
Temporarily disable concurrent protocol collection 2023-01-27 15:01:16 -05:00
JP Simard 0ff0fc9c5c
Add `dead-code` command
This command should be considered experimental because there's only
partial integration with the rest of SwiftLint's infrastructure, and the
command is very likely to change in the near future.

This can effectively replace the `unused_declaration` analyzer rule and
in fact re-uses the same rule identifier to simplify migrations.

However, instead of using SourceKit requests, this parses the index
store generated by the Swift compiler, in addition to SwiftSyntax, which
means it runs it can run in seconds instead of hours when analyzing
projects with tens of thousands of Swift files.

In addition to Swift, this also catches dead C, C++, Objective-C
declarations, mostly by accident. There's no way to disable the rule for
those files at the moment.
2023-01-27 15:01:16 -05:00
JP Simard 3a6436305b
Split SwiftLintFramework into multiple modules
* SwiftLintCore
* SwiftLintBuiltInRules
* SwiftLintExtraRules

Keep SwiftLintFramework as a wrapper around SwiftLintCore,
SwiftLintBuiltInRules and SwiftLintExtraRules.

This required making a number of `SwiftLintCore` declarations public
that previously were internal in SwiftLintFramework.

This significantly reduces the amount of time spent rebuilding
SwiftLintFramework or the SwiftLint CLI when working on rules outside of
the core SwiftLint API. In my testing, incremental compilation is over
twice as fast, with adding new rules taking 33s before and 12s after
this modularization.

This also has the benefit of custom external rules not be able to access
internal SwiftLint APIs. Now all rules, built-in or external, depend on
the public API of the SwiftLintCore module.

The SwiftLintBuiltInRules and SwiftLintExtraRules modules both make
SwiftLintCore's public API available to all their source files without
requiring an explicit `import` statement in that file. This is because
you nearly always want access to the core APIs when defining rules. It
also makes the migration leaner, requiring fewer changes.
2023-01-27 14:40:26 -05:00
JP Simard 4388dd9658
Introduce SwiftLintTestCase
To consistently set up rules before tests
2023-01-27 14:05:46 -05:00
JP Simard 388d16ec9b
Introduce a "rule registry" concept
This will allow for registering rules that aren't compiled as part of
SwiftLintFramework.

Specifically this will allow us to split the built-in and extra rules
into separate modules, leading to faster incremental compilation when
working on rules since the rest of the framework won't need to be
rebuilt on every compilation.
2023-01-27 14:00:42 -05:00
564 changed files with 2357 additions and 745 deletions

View File

@ -1,4 +1,4 @@
module: SwiftLintFramework
module: SwiftLintCore
author: JP Simard, SwiftLint Contributors
author_url: https://jpsim.com
root_url: https://realm.github.io/SwiftLint/
@ -13,7 +13,8 @@ documentation: rule_docs/*.md
hide_unlisted_documentation: true
custom_categories_unlisted_prefix: ''
exclude:
- Source/SwiftLintFramework/Rules/**/*.swift
# TODO: Document extensions
- Source/SwiftLintCore/Extensions/*.swift
custom_categories:
- name: Rules
children:

View File

@ -1,8 +1,4 @@
/// 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 %}
{% endfor %}]
/// The rule list containing all available rules built into SwiftLint as well as native custom rules.
public let primaryRuleList = RuleList(rules: builtInRules + extraRules())

View File

@ -1,13 +1,13 @@
@testable import SwiftLintBuiltInRules
@_spi(TestHelper)
@testable import SwiftLintFramework
@testable import SwiftLintCore
import SwiftLintTestHelpers
import XCTest
// swiftlint:disable file_length single_test_class type_name
// swiftlint:disable file_length type_name
{% for rule in types.structs %}
{% if rule.name|hasSuffix:"Rule" %}
class {{ rule.name }}GeneratedTests: XCTestCase {
class {{ rule.name }}GeneratedTests: SwiftLintTestCase {
func testWithDefaultConfiguration() {
verifyRule({{ rule.name }}.description)
}

View File

@ -86,8 +86,10 @@ number_separator:
minimum_length: 5
file_name:
excluded:
- SwiftSyntax+SwiftLint.swift
- Exports.swift
- GeneratedTests.swift
- IndexStore+Visitors.swift
- SwiftSyntax+SwiftLint.swift
- TestHelpers.swift
function_body_length: 60
@ -118,3 +120,4 @@ custom_rules:
unused_import:
always_keep_imports:
- 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
View File

@ -13,12 +13,9 @@ load(
# Targets
swift_library(
name = "SwiftLintFramework",
srcs = glob(
["Source/SwiftLintFramework/**/*.swift"],
exclude = ["Source/SwiftLintFramework/Rules/ExcludedFromBazel/ExtraRules.swift"],
) + ["@swiftlint_extra_rules//:extra_rules"],
module_name = "SwiftLintFramework",
name = "SwiftLintCore",
srcs = glob(["Source/SwiftLintCore/**/*.swift"]),
module_name = "SwiftLintCore",
visibility = ["//visibility:public"],
deps = [
"@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(
name = "swiftlint.library",
srcs = glob(["Source/swiftlint/**/*.swift"]),
module_name = "swiftlint",
visibility = ["//visibility:public"],
deps = [
":SwiftLintAnalyzerRules",
":SwiftLintFramework",
"@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit",
"@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser",

View File

@ -20,6 +20,8 @@ use_repo(
"com_github_johnsundell_collectionconcurrencykit",
"com_github_krzyzanowskim_cryptoswift",
"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")

View File

@ -30,14 +30,14 @@ VERSION_STRING=$(shell ./tools/get-version)
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
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/PrimaryRuleList.stencil --output .sourcery
mv .sourcery/PrimaryRuleList.generated.swift Source/SwiftLintFramework/Models/PrimaryRuleList.swift
Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift: Source/SwiftLintBuiltInRules/Rules/**/*.swift .sourcery/BuiltInRules.stencil
sourcery --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/GeneratedTests.stencil
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLintBuiltInRules/Rules/**/*.swift .sourcery/GeneratedTests.stencil
sourcery --sources Source/SwiftLintCore/Rules --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
mv .sourcery/GeneratedTests.generated.swift Tests/GeneratedTests/GeneratedTests.swift
test: clean_xcode

View File

@ -27,6 +27,15 @@
"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",
"kind" : "remoteSourceControl",

View File

@ -31,6 +31,7 @@ let package = Package(
dependencies: [
.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/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/Yams.git", from: "5.0.3"),
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
@ -49,6 +50,7 @@ let package = Package(
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"CollectionConcurrencyKit",
"SwiftLintAnalyzerRules",
"SwiftLintFramework",
"SwiftyTextTable",
]
@ -60,9 +62,29 @@ let package = Package(
]
),
.target(
name: "SwiftLintFramework",
name: "SwiftLintCore",
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(
name: "SwiftLintTestHelpers",
dependencies: [
@ -101,6 +123,11 @@ let package = Package(
"SwiftLintTestHelpers"
]
),
.binaryTarget(
name: "libIndexStore",
url: "https://github.com/keith/StaticIndexStore/releases/download/5.7/libIndexStore.xcframework.zip",
checksum: "da69bab932357a817aa0756e400be86d7156040bfbea8eded7a3acc529320731"
),
.binaryTarget(
name: "SwiftLintBinary",
url: "https://github.com/realm/SwiftLint/releases/download/0.50.3/SwiftLintBinary-macos.artifactbundle.zip",

View File

@ -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"
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
]
}
}

View File

@ -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)"
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 }
)
}
}

View File

@ -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)
}
}
}

View File

@ -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) })
}
}

View File

@ -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)
}
}

View File

@ -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/")
}
}

View File

@ -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) }
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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)'
"""
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

View File

@ -0,0 +1 @@
@_exported import SwiftLintCore // swiftlint:disable:this unused_import

View File

@ -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
/// The rule list containing all available rules built into SwiftLint.
let builtInRules: [Rule.Type] = [
public let builtInRules: [Rule.Type] = [
AccessibilityLabelForImageRule.self,
AccessibilityTraitForButtonRule.self,
AnonymousArgumentInMultilineClosureRule.self,
@ -32,7 +31,6 @@ let builtInRules: [Rule.Type] = [
ContainsOverRangeNilComparisonRule.self,
ControlStatementRule.self,
ConvenienceTypeRule.self,
CustomRules.self,
CyclomaticComplexityRule.self,
DeploymentTargetRule.self,
DiscardedNotificationCenterObserverRule.self,
@ -181,7 +179,6 @@ let builtInRules: [Rule.Type] = [
StaticOperatorRule.self,
StrictFilePrivateRule.self,
StrongIBOutletRule.self,
SuperfluousDisableCommandRule.self,
SwitchCaseAlignmentRule.self,
SwitchCaseOnNewlineRule.self,
SyntacticSugarRule.self,
@ -225,6 +222,3 @@ let builtInRules: [Rule.Type] = [
XCTSpecificMatcherRule.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