<commit_before>import Foundation
import SourceKittenFramework

public struct AnonymousArgumentInMultilineClosureRule: ASTRule, OptInRule, ConfigurationProviderRule {
    public var configuration = SeverityConfiguration(.warning)

    public init() {}

    public static let description = RuleDescription(
        identifier: "anonymous_argument_in_multiline_closure",
        name: "Anonymous Argument in Multiline Closure",
        description: "Use named arguments in multiline closures",
        kind: .idiomatic,
        nonTriggeringExamples: [
            Example("closure { $0 }"),
            Example("closure { print($0) }"),
            Example("""
            closure { arg in
                print(arg)
            }
            """),
            Example("""
            closure { arg in
                nestedClosure { $0 + arg }
            }
            """)
        ],
        triggeringExamples: [
            Example("""
            closure {
                print(↓$0)
            }
            """)
        ]
    )

    public func validate(file: SwiftLintFile, kind: SwiftExpressionKind,
                         dictionary: SourceKittenDictionary) -> [StyleViolation] {
        guard kind == .closure,
              dictionary.enclosedVarParameters.isEmpty,
              let range = dictionary.bodyByteRange,
              let (initialLine, _) = file.stringView.lineAndCharacter(forByteOffset: range.lowerBound),
              let (finalLine, _) = file.stringView.lineAndCharacter(forByteOffset: range.upperBound),
              initialLine != finalLine,
              let bodyNSRange = file.stringView.byteRangeToNSRange(range) else {
                return []
        }

        let matches = file.match(pattern: "\\$0", with: [.identifier], range: bodyNSRange).filter { range in
            guard range.length == 2,
                  let byteRange = file.stringView.NSRangeToByteRange(range) else {
                return false
            }

            // do not trigger for nested closures
            let expressions = closureExpressions(forByteOffset: byteRange.location, structureDictionary: dictionary)
            return expressions.isEmpty
        }

        return matches.map {
            StyleViolation(ruleDescription: Self.description,
                           severity: configuration.severity,
                           location: Location(file: file, characterOffset: $0.location))
        }
    }

    private func closureExpressions(forByteOffset byteOffset: ByteCount,
                                    structureDictionary: SourceKittenDictionary) -> [SourceKittenDictionary] {
        return structureDictionary.traverseBreadthFirst { dictionary in
            guard dictionary.expressionKind == .closure,
                let byteRange = dictionary.byteRange,
                byteRange.contains(byteOffset)
            else {
                return nil
            }
            return [dictionary]
        }
    }
}<commit_msg>Migrate `anonymous_argument_in_multiline_closure` to SwiftSyntax (#4279)<commit_after>import SwiftSyntax

public struct AnonymousArgumentInMultilineClosureRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
    public var configuration = SeverityConfiguration(.warning)

    public init() {}

    public static let description = RuleDescription(
        identifier: "anonymous_argument_in_multiline_closure",
        name: "Anonymous Argument in Multiline Closure",
        description: "Use named arguments in multiline closures",
        kind: .idiomatic,
        nonTriggeringExamples: [
            Example("closure { $0 }"),
            Example("closure { print($0) }"),
            Example("""
            closure { arg in
                print(arg)
            }
            """),
            Example("""
            closure { arg in
                nestedClosure { $0 + arg }
            }
            """)
        ],
        triggeringExamples: [
            Example("""
            closure {
                print(↓$0)
            }
            """)
        ]
    )

    public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor? {
        file.locationConverter.map {
            Visitor(locationConverter: $0)
        }
    }
}

private extension AnonymousArgumentInMultilineClosureRule {
    final class Visitor: SyntaxVisitor, ViolationsSyntaxVisitor {
        private(set) var violationPositions: [AbsolutePosition] = []
        private let locationConverter: SourceLocationConverter

        init(locationConverter: SourceLocationConverter) {
            self.locationConverter = locationConverter
            super.init(viewMode: .sourceAccurate)
        }

        override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
            let startLocation = locationConverter.location(for: node.leftBrace.positionAfterSkippingLeadingTrivia)
            let endLocation = locationConverter.location(for: node.rightBrace.endPositionBeforeTrailingTrivia)

            guard let startLine = startLocation.line, let endLine = endLocation.line, startLine != endLine else {
                return .skipChildren
            }

            return .visitChildren
        }

        override func visitPost(_ node: IdentifierExprSyntax) {
            if case .dollarIdentifier = node.identifier.tokenKind {
                violationPositions.append(node.positionAfterSkippingLeadingTrivia)
            }
        }
    }
}
{'fraction_non_alphanumeric': 0.08113069016152717, 'fraction_numerical': 0.002936857562408223, 'mean_word_length': 1.9988992845349478, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5047968', 'n_tokens_mistral': 1345, 'n_tokens_neox': 1265, 'n_words': 287}