Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRule.swift

164 lines
6.0 KiB
Swift

import SwiftSyntax
@SwiftSyntaxRule(foldExpressions: true, optIn: true)
struct QuickDiscouragedCallRule: Rule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "quick_discouraged_call",
name: "Quick Discouraged Call",
description: "Discouraged call inside 'describe' and/or 'context' block.",
kind: .lint,
nonTriggeringExamples: QuickDiscouragedCallRuleExamples.nonTriggeringExamples,
triggeringExamples: QuickDiscouragedCallRuleExamples.triggeringExamples
)
}
private typealias ScopeElement = (kind: QuickCallKind, blockId: SyntaxIdentifier)?
private extension QuickDiscouragedCallRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
private var quickScope = Stack<ScopeElement>()
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] {
.allExcept(ClassDeclSyntax.self, FunctionDeclSyntax.self, VariableDeclSyntax.self)
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
if node.inheritanceClause?.inheritedTypes.isNotEmpty == true {
return .visitChildren
}
return .skipChildren
}
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
if node.name.text == "spec",
node.signature.parameterClause.parameters.isEmpty,
node.signature.returnClause == nil {
return .visitChildren
}
return .skipChildren
}
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
if let calledName = node.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text {
if let kind = QuickCallKind(rawValue: calledName) {
if let closure = node.trailingClosure {
quickScope.push((kind, closure.statements.id))
return .visitChildren
}
quickScope.push(nil)
return .skipChildren
}
}
if let scope = quickScope.lastSuspiciousScope(node) {
violations.append(.violation(at: node.positionAfterSkippingLeadingTrivia, kind: scope.kind))
return .skipChildren
}
quickScope.push(nil)
return .visitChildren
}
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
// Skip variables annotated with @TestState (Quick's property wrapper
// that resets state between tests), since the initializer is required.
if node.attributes.contains(attributeNamed: "TestState") {
return .skipChildren
}
for binding in node.bindings {
if let scope = quickScope.lastSuspiciousScope(node),
let initializer = binding.initializer,
FunctionCallFinder(viewMode: .sourceAccurate).walk(tree: initializer.value, handler: \.found) {
violations.append(.violation(
at: initializer.value.positionAfterSkippingLeadingTrivia,
kind: scope.kind
))
}
}
return .skipChildren
}
override func visit(_ node: InfixOperatorExprSyntax) -> SyntaxVisitorContinueKind {
guard let scope = quickScope.lastSuspiciousScope(node),
node.operator.is(AssignmentExprSyntax.self),
node.leftOperand.is(DeclReferenceExprSyntax.self) || node.leftOperand.is(MemberAccessExprSyntax.self),
let call = node.rightOperand.as(FunctionCallExprSyntax.self) else {
return .visitChildren
}
violations.append(.violation(at: call.positionAfterSkippingLeadingTrivia, kind: scope.kind))
return .skipChildren
}
override func visitPost(_: FunctionCallExprSyntax) {
quickScope.pop()
}
override func visit(_ node: IfConfigClauseSyntax) -> SyntaxVisitorContinueKind {
if let elements = node.elements?.as(CodeBlockItemListSyntax.self) {
if let scope = quickScope.peek(), let scope {
quickScope.push((kind: scope.kind, blockId: elements.id))
} else {
quickScope.push(nil)
}
walk(elements)
quickScope.pop()
}
return .skipChildren
}
}
}
private enum QuickCallKind: String {
case describe
case context
case sharedExamples
case itBehavesLike
case aroundEach
case beforeEach
case justBeforeEach
case beforeSuite
case afterEach
case afterSuite
case it // swiftlint:disable:this identifier_name
case pending
case xdescribe
case xcontext
case xit
case xitBehavesLike
case fdescribe
case fcontext
case fit
case fitBehavesLike
static let restrictiveKinds: Set<QuickCallKind> = [
.describe, .fdescribe, .xdescribe, .context, .fcontext, .xcontext, .sharedExamples
]
}
private extension Stack where Element == ScopeElement {
func lastSuspiciousScope(_ node: any SyntaxProtocol) -> ScopeElement {
if let scope = peek(), let scope,
QuickCallKind.restrictiveKinds.contains(scope.kind),
node.parent?.is(CodeBlockItemSyntax.self) == true,
node.parent?.parent?.id == scope.blockId {
return scope
}
return nil
}
}
private extension ReasonedRuleViolation {
static func violation(at position: AbsolutePosition, kind: QuickCallKind) -> Self {
.init(position: position, reason: "Discouraged call inside a '\(kind)' block")
}
}
private final class FunctionCallFinder: SyntaxVisitor {
private(set) var found = false
override func visit(_: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
found = true
return .skipChildren
}
}