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_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:
|
||||
|
|
|
@ -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())
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
59
BUILD
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
|
|
12
Makefile
12
Makefile
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
/// 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
Loading…
Reference in New Issue