mirror of
https://github.com/realm/SwiftLint.git
synced 2026-06-06 20:18:40 +00:00
164 lines
6.0 KiB
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
|
|
}
|
|
}
|