1- import Foundation
2- import SourceKittenFramework
1+ import SwiftLintCore
2+ import SwiftSyntax
33
4+ @SwiftSyntaxRule
45struct TrailingClosureRule : OptInRule {
56var configuration = TrailingClosureConfiguration ( )
67
@@ -18,127 +19,79 @@ struct TrailingClosureRule: OptInRule {
1819Example ( " offsets.sorted { $0.offset < $1.offset } " ) ,
1920Example ( " foo.something({ return 1 }()) " ) ,
2021Example ( " foo.something({ return $0 }(1)) " ) ,
21- Example ( " foo.something(0, { return 1 }()) " )
22+ Example ( " foo.something(0, { return 1 }()) " ) ,
23+ Example ( " for x in list.filter({ $0.isValid }) {} " ) ,
24+ Example ( " if list.allSatisfy({ $0.isValid }) {} " ) ,
25+ Example ( " foo(param1: 1, param2: { _ in true }, param3: 0) " ) ,
26+ Example ( " foo(param1: 1, param2: { _ in true }) { $0 + 1 } " ) ,
27+ Example ( " foo(param1: { _ in false }, param2: { _ in true }) " ) ,
28+ Example ( " foo(param1: { _ in false }, param2: { _ in true }, param3: { _ in false }) " ) ,
29+ Example ( """
30+ if f({ true }), g({ true }) {
31+ print( " Hello " )
32+ }
33+ """ ) ,
34+ Example ( """
35+ for i in h({ [1,2,3] }) {
36+ print(i)
37+ }
38+ """ )
2239] ,
2340 triggeringExamples: [
2441Example ( " ↓foo.map({ $0 + 1 }) " ) ,
2542Example ( " ↓foo.reduce(0, combine: { $0 + 1 }) " ) ,
2643Example ( " ↓offsets.sorted(by: { $0.offset < $1.offset }) " ) ,
27- Example ( " ↓foo.something(0, { $0 + 1 }) " )
44+ Example ( " ↓foo.something(0, { $0 + 1 }) " ) ,
45+ Example ( " ↓foo.something(param1: { _ in true }, param2: 0, param3: { _ in false }) " ) ,
46+ Example ( """
47+ for n in list {
48+ ↓n.forEach({ print($0) })
49+ }
50+ """ , excludeFromDocumentation: true )
2851]
2952)
53+ }
3054
31- func validate( file: SwiftLintFile ) -> [ StyleViolation ] {
32- let dict = file. structureDictionary
33- return violationOffsets ( for: dict, file: file) . map {
34- StyleViolation ( ruleDescription: Self . description,
35- severity: configuration. severityConfiguration. severity,
36- location: Location ( file: file, byteOffset: $0) )
37- }
38- }
39-
40- private func violationOffsets( for dictionary: SourceKittenDictionary , file: SwiftLintFile ) -> [ ByteCount ] {
41- var results = [ ByteCount] ( )
42-
43- if dictionary. expressionKind== . call,
44- shouldBeTrailingClosure ( dictionary: dictionary, file: file) ,
45- let offset= dictionary. offset{
46- results= [ offset]
47- }
55+ private extension TrailingClosureRule {
56+ final class Visitor : ViolationsSyntaxVisitor < ConfigurationType > {
57+ override func visitPost( _ node: FunctionCallExprSyntax ) {
58+ guard node. trailingClosure== nil else { return }
4859
49- if let kind= dictionary. statementKind, kind!= . brace{
50- // trailing closures are not allowed in `if`, `guard`, etc
51- results+= dictionary. substructure. flatMap { subDict-> [ ByteCount ] in
52- guard subDict. statementKind== . braceelse {
53- return [ ]
60+ if configuration. onlySingleMutedParameter{
61+ if node. containsOnlySingleMutedParameter{
62+ violations. append ( node. positionAfterSkippingLeadingTrivia)
5463}
55-
56- return violationOffsets ( for: subDict, file: file)
57- }
58- } else {
59- results+= dictionary. substructure. flatMap { subDictin
60- violationOffsets ( for: subDict, file: file)
64+ } else if node. shouldTrigger{
65+ violations. append ( node. positionAfterSkippingLeadingTrivia)
6166}
6267}
6368
64- return results
65- }
66-
67- private func shouldBeTrailingClosure( dictionary: SourceKittenDictionary , file: SwiftLintFile ) -> Bool {
68- func shouldTrigger( ) -> Bool {
69- return !isAlreadyTrailingClosure( dictionary: dictionary, file: file) &&
70- !isAnonymousClosureCall( dictionary: dictionary, file: file)
71- }
72-
73- let arguments = dictionary. enclosedArguments
74-
75- // check if last parameter should be trailing closure
76- if !configuration. onlySingleMutedParameter, arguments. isNotEmpty,
77- caselet closureArguments= filterClosureArguments ( arguments, file: file) ,
78- closureArguments. count== 1 ,
79- closureArguments. last? . offset== arguments. last? . offset{
80- return shouldTrigger ( )
69+ override func visit( _ node: ConditionElementListSyntax ) -> SyntaxVisitorContinueKind {
70+ . skipChildren
8171}
8272
83- let argumentsCountIsExpected : Bool = {
84- if SwiftVersion . current>= . fiveDotSix, arguments. count== 1 ,
85- arguments [ 0 ] . expressionKind== . argument{
86- return true
87- }
88-
89- return arguments. isEmpty
90- } ( )
91- // check if there's only one unnamed parameter that is a closure
92- if argumentsCountIsExpected,
93- let offset= dictionary. offset,
94- let totalLength= dictionary. length,
95- let nameOffset= dictionary. nameOffset,
96- let nameLength= dictionary. nameLength,
97- caselet start= nameOffset+ nameLength,
98- caselet length= totalLength+ offset- start,
99- caselet byteRange= ByteRange ( location: start, length: length) ,
100- let range= file. stringView. byteRangeToNSRange ( byteRange) ,
101- let match= regex ( " \\ s* \\ ( \\ s* \\ { " ) . firstMatch ( in: file. contents, options: [ ] , range: range) ? . range,
102- match. location== range. location{
103- return shouldTrigger ( )
73+ override func visit( _ node: ForStmtSyntax ) -> SyntaxVisitorContinueKind {
74+ walk ( node. body)
75+ return . skipChildren
10476}
105-
106- return false
10777}
78+ }
10879
109- private func filterClosureArguments( _ arguments: [ SourceKittenDictionary ] ,
110- file: SwiftLintFile ) -> [ SourceKittenDictionary ] {
111- return arguments. filter { argumentin
112- guard let bodyByteRange= argument. bodyByteRange,
113- let range= file. stringView. byteRangeToNSRange ( bodyByteRange) ,
114- let match= regex ( " \\ s* \\ { " ) . firstMatch ( in: file. contents, options: [ ] , range: range) ? . range,
115- match. location== range. location
116- else {
117- return false
118- }
119-
120- return true
121- }
80+ private extension FunctionCallExprSyntax {
81+ var containsOnlySingleMutedParameter : Bool {
82+ arguments. onlyElement? . isMutedClosure== true
12283}
12384
124- private func isAlreadyTrailingClosure( dictionary: SourceKittenDictionary , file: SwiftLintFile ) -> Bool {
125- guard let byteRange= dictionary. byteRange,
126- let text= file. stringView. substringWithByteRange ( byteRange)
127- else {
128- return false
129- }
130-
131- return !text. hasSuffix ( " ) " )
85+ var shouldTrigger : Bool {
86+ arguments. last? . expression. is ( ClosureExprSyntax . self) == true
87+ // If at least last two arguments were ClosureExprSyntax, a violation should not be triggered.
88+ &&( arguments. count<= 1
89+ || !arguments. dropFirst ( arguments. count- 2 ) . allSatisfy ( { $0. expression. is ( ClosureExprSyntax . self) } ) )
13290}
91+ }
13392
134- private func isAnonymousClosureCall( dictionary: SourceKittenDictionary , file: SwiftLintFile ) -> Bool {
135- guard let byteRange= dictionary. byteRange,
136- let range= file. stringView. byteRangeToNSRange ( byteRange)
137- else {
138- return false
139- }
140-
141- let pattern = regex ( " \\ ) \\ s* \\ ) \\ z " )
142- return pattern. numberOfMatches ( in: file. contents, range: range) > 0
93+ private extension LabeledExprSyntax {
94+ var isMutedClosure : Bool {
95+ label== nil && expression. is ( ClosureExprSyntax . self)
14396}
14497}