1- import SourceKittenFramework
1+ import SwiftSyntax
22
3- @DisabledWithoutSourceKit
4- struct QuickDiscouragedCallRule : OptInRule {
3+ @SwiftSyntaxRule ( foldExpressions : true , optIn : true )
4+ struct QuickDiscouragedCallRule : Rule {
55 var configuration = SeverityConfiguration < Self > ( . warning)
66
77 static let description = RuleDescription (
@@ -12,93 +12,94 @@ struct QuickDiscouragedCallRule: OptInRule {
1212 nonTriggeringExamples: QuickDiscouragedCallRuleExamples . nonTriggeringExamples,
1313 triggeringExamples: QuickDiscouragedCallRuleExamples . triggeringExamples
1414 )
15+ }
1516
16- func validate( file: SwiftLintFile ) -> [ StyleViolation ] {
17- let dict = file. structureDictionary
18- let testClasses = dict. substructure. filter {
19- $0. inheritedTypes. isNotEmpty &&
20- $0. declarationKind == . class
21- }
22-
23- let specDeclarations = testClasses. flatMap { classDict in
24- classDict. substructure. filter { structure in
25- structure. name == " spec() "
26- && structure. enclosedVarParameters. isEmpty
27- && [ . functionMethodInstance, . functionMethodStatic] . contains ( structure. declarationKind)
28- && structure. enclosedSwiftAttributes. contains ( . override)
29- }
30- }
17+ private typealias ScopeElement = ( kind: QuickCallKind , blockId: SyntaxIdentifier ) ?
3118
32- return specDeclarations. flatMap {
33- validate ( file: file, dictionary: $0)
34- }
35- }
19+ private extension QuickDiscouragedCallRule {
20+ final class Visitor : ViolationsSyntaxVisitor < ConfigurationType > {
21+ private var quickScope = Stack < ScopeElement > ( )
3622
37- private func validate( file: SwiftLintFile , dictionary: SourceKittenDictionary ) -> [ StyleViolation ] {
38- dictionary. traverseDepthFirst { subDict in
39- guard let kind = subDict. expressionKind else { return nil }
40- return validate ( file: file, kind: kind, dictionary: subDict)
23+ override var skippableDeclarations : [ any DeclSyntaxProtocol . Type ] {
24+ . allExcept( ClassDeclSyntax . self, FunctionDeclSyntax . self, VariableDeclSyntax . self)
4125 }
42- }
4326
44- private func validate( file: SwiftLintFile ,
45- kind: SwiftExpressionKind ,
46- dictionary: SourceKittenDictionary ) -> [ StyleViolation ] {
47- // is it a call to a restricted method?
48- guard kind == . call,
49- let name = dictionary. name,
50- let kindName = QuickCallKind ( rawValue: name) ,
51- QuickCallKind . restrictiveKinds. contains ( kindName) else {
52- return [ ]
27+ override func visit( _ node: ClassDeclSyntax ) -> SyntaxVisitorContinueKind {
28+ if node. inheritanceClause? . inheritedTypes. isNotEmpty == true {
29+ return . visitChildren
30+ }
31+ return . skipChildren
5332 }
5433
55- return violationOffsets ( in: dictionary. enclosedArguments) . map {
56- StyleViolation ( ruleDescription: Self . description,
57- severity: configuration. severity,
58- location: Location ( file: file, byteOffset: $0) ,
59- reason: " Discouraged call inside a ' \( name) ' block " )
34+ override func visit( _ node: FunctionDeclSyntax ) -> SyntaxVisitorContinueKind {
35+ if node. name. text == " spec " ,
36+ node. signature. parameterClause. parameters. isEmpty,
37+ node. signature. returnClause == nil {
38+ return . visitChildren
39+ }
40+ return . skipChildren
6041 }
61- }
6242
63- private func violationOffsets( in substructure: [ SourceKittenDictionary ] ) -> [ ByteCount ] {
64- substructure. flatMap { dictionary -> [ ByteCount ] in
65- let substructure = dictionary. substructure. flatMap { dict -> [ SourceKittenDictionary ] in
66- if dict. expressionKind == . closure {
67- return dict. substructure
43+ override func visit( _ node: FunctionCallExprSyntax ) -> SyntaxVisitorContinueKind {
44+ if let calledName = node. calledExpression. as ( DeclReferenceExprSyntax . self) ? . baseName. text {
45+ if let kind = QuickCallKind ( rawValue: calledName) {
46+ if let closure = node. trailingClosure {
47+ quickScope. push ( ( kind, closure. statements. id) )
48+ return . visitChildren
49+ }
50+ quickScope. push ( nil )
51+ return . skipChildren
6852 }
69- return [ dict]
7053 }
71-
72- return substructure. flatMap ( toViolationOffsets)
54+ if let scope = quickScope. lastSuspiciousScope ( node) {
55+ violations. append ( . violation( at: node. positionAfterSkippingLeadingTrivia, kind: scope. kind) )
56+ return . skipChildren
57+ }
58+ quickScope. push ( nil )
59+ return . visitChildren
7360 }
74- }
7561
76- private func toViolationOffsets( dictionary: SourceKittenDictionary ) -> [ ByteCount ] {
77- guard dictionary. kind != nil ,
78- let offset = dictionary. offset else {
79- return [ ]
62+ override func visit( _ node: VariableDeclSyntax ) -> SyntaxVisitorContinueKind {
63+ for binding in node. bindings {
64+ if let scope = quickScope. lastSuspiciousScope ( node) ,
65+ let initializer = binding. initializer,
66+ FunctionCallFinder ( viewMode: . sourceAccurate) . walk ( tree: initializer. value, handler: \. found) {
67+ violations. append ( . violation(
68+ at: initializer. value. positionAfterSkippingLeadingTrivia,
69+ kind: scope. kind
70+ ) )
71+ }
72+ }
73+ return . skipChildren
8074 }
8175
82- if dictionary. expressionKind == . call,
83- let name = dictionary. name, QuickCallKind ( rawValue: name) == nil {
84- return [ offset]
76+ override func visit( _ node: InfixOperatorExprSyntax ) -> SyntaxVisitorContinueKind {
77+ guard let scope = quickScope. lastSuspiciousScope ( node) ,
78+ node. operator. is ( AssignmentExprSyntax . self) ,
79+ node. leftOperand. is ( DeclReferenceExprSyntax . self) || node. leftOperand. is ( MemberAccessExprSyntax . self) ,
80+ let call = node. rightOperand. as ( FunctionCallExprSyntax . self) else {
81+ return . visitChildren
82+ }
83+ violations. append ( . violation( at: call. positionAfterSkippingLeadingTrivia, kind: scope. kind) )
84+ return . skipChildren
8585 }
8686
87- guard dictionary . expressionKind != . call else {
88- return [ ]
87+ override func visitPost ( _ : FunctionCallExprSyntax ) {
88+ quickScope . pop ( )
8989 }
9090
91- return dictionary. substructure. compactMap ( toViolationOffset)
92- }
93-
94- private func toViolationOffset( dictionary: SourceKittenDictionary ) -> ByteCount ? {
95- guard let name = dictionary. name,
96- let offset = dictionary. offset,
97- dictionary. expressionKind == . call,
98- QuickCallKind ( rawValue: name) == nil else {
99- return nil
91+ override func visit( _ node: IfConfigClauseSyntax ) -> SyntaxVisitorContinueKind {
92+ if let elements = node. elements? . as ( CodeBlockItemListSyntax . self) {
93+ if let scope = quickScope. peek ( ) , let scope {
94+ quickScope. push ( ( kind: scope. kind, blockId: elements. id) )
95+ } else {
96+ quickScope. push ( nil )
97+ }
98+ walk ( elements)
99+ quickScope. pop ( )
100+ }
101+ return . skipChildren
100102 }
101- return offset
102103 }
103104}
104105
@@ -128,3 +129,30 @@ private enum QuickCallKind: String {
128129 . describe, . fdescribe, . xdescribe, . context, . fcontext, . xcontext, . sharedExamples
129130 ]
130131}
132+
133+ private extension Stack where Element == ScopeElement {
134+ func lastSuspiciousScope( _ node: any SyntaxProtocol ) -> ScopeElement {
135+ if let scope = peek ( ) , let scope,
136+ QuickCallKind . restrictiveKinds. contains ( scope. kind) ,
137+ node. parent? . is ( CodeBlockItemSyntax . self) == true ,
138+ node. parent? . parent? . id == scope. blockId {
139+ return scope
140+ }
141+ return nil
142+ }
143+ }
144+
145+ private extension ReasonedRuleViolation {
146+ static func violation( at position: AbsolutePosition , kind: QuickCallKind ) -> Self {
147+ . init( position: position, reason: " Discouraged call inside a ' \( kind) ' block " )
148+ }
149+ }
150+
151+ private final class FunctionCallFinder : SyntaxVisitor {
152+ private( set) var found = false
153+
154+ override func visit( _: FunctionCallExprSyntax ) -> SyntaxVisitorContinueKind {
155+ found = true
156+ return . skipChildren
157+ }
158+ }
0 commit comments