Compare commits

...

1 Commits

Author SHA1 Message Date
Marcelo Fabri 0e94f03b0b Add testable_import opt-in rule 2022-04-20 11:06:53 -07:00
3 changed files with 98 additions and 0 deletions

View File

@ -178,6 +178,7 @@ public let primaryRuleList = RuleList(rules: [
SwitchCaseOnNewlineRule.self,
SyntacticSugarRule.self,
TestCaseAccessibilityRule.self,
TestableImportRule.self,
TodoRule.self,
ToggleBoolRule.self,
TrailingClosureRule.self,

View File

@ -0,0 +1,91 @@
import SourceKittenFramework
import SwiftSyntax
public struct TestableImportRule: ConfigurationProviderRule, OptInRule, AutomaticTestableRule {
public var configuration = SeverityConfiguration(.error)
public init() {}
public static let description = RuleDescription(
identifier: "testable_import",
name: "Testable Import",
description: "@testable import should only be used in test files",
kind: .lint,
nonTriggeringExamples: [
Example("import Foo"),
Example("""
@testable import Foo
import XCTest
"""),
Example("""
@testable import Foo
import class XCTest.XCTestCase
"""),
Example("""
@testable import Foo
import TestUtils
class FooTests: XCTestCase {}
"""),
],
triggeringExamples: [
Example("↓@testable import Foo"),
]
)
public func validate(file: SwiftLintFile) -> [StyleViolation] {
guard let tree = file.syntaxTree else { return [] }
let visitor = TestableImportRuleVisitor()
visitor.walk(tree)
if visitor.isTestFile {
return []
}
return visitor.positions.map { position in
StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: ByteCount(position)))
}
}
}
private final class TestableImportRuleVisitor: SyntaxVisitor {
private(set) var positions: [AbsolutePosition] = []
private(set) var isTestFile = false
override func visitPost(_ node: ImportDeclSyntax) {
if node.isTestableImport {
positions.append(node.positionAfterSkippingLeadingTrivia)
}
let testImports: Set = ["XCTest", "Quick", "Nimble"]
let components = node.path.withoutTrivia().map(\.name.text)
if !testImports.isDisjoint(with: components) {
isTestFile = true
}
}
override func visitPost(_ node: ClassDeclSyntax) {
let inheritedTypes = node.inheritanceClause?.inheritedTypeCollection.map {
$0.withoutTrivia().typeName.description
} ?? []
let testClasses: Set = ["XCTestCase", "QuickSpec"]
if !testClasses.isDisjoint(with: inheritedTypes) {
isTestFile = true
}
}
}
private extension ImportDeclSyntax {
var isTestableImport: Bool {
guard let attributes = self.attributes else {
return false
}
return attributes.contains { syntax in
syntax.as(AttributeSyntax.self)?.attributeName.tokenKind == .identifier("testable")
}
}
}

View File

@ -785,6 +785,12 @@ class TestCaseAccessibilityRuleTests: XCTestCase {
}
}
class TestableImportRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(TestableImportRule.description)
}
}
class ToggleBoolRuleTests: XCTestCase {
func testWithDefaultConfiguration() {
verifyRule(ToggleBoolRule.description)