Swiftformat: [CRASH] - Timeout when many closures exist in function

Created on 13 Mar 2020  路  9Comments  路  Source: nicklockwood/SwiftFormat

With functions that contain a lot of closures, such as an init file using RxSwift for binding, when adding more closures, at a certain point SwiftFormat will just break. This occurs for us in an init function with 30+ closures.

Running SwiftFormat...
warning: trailingClosures rule timed out.
error: Failed to format any files.
Running SwiftFormat...
SwiftFormat completed in 0.05s.
0/3 files formatted.
Running SwiftFormat...
SwiftFormat completed in 0.09s.
0/1 files formatted.
Command PhaseScriptExecution emitted errors but did not return a nonzero exit code to indicate failure
bug

Most helpful comment

@bryanforbes I can confirm that @zizi4n5's fix works for your file. It's up on the develop branch now - I'll try to get a release out shortly.

All 9 comments

@realityworks can you provide a sample file that demonstrates the problem?

Same here. I have a warning: braces rule timed out here. Please, check out my example.

Update: my version is 0.44.5


Details

enum ManagersAssembly {
    // swiftlint:disable:next closure_body_length
    static let container: DependencyContainer = {
        let container = DependencyContainer()

        // MARK: - Cocoa

        container.register {
            UserDefaults.standard
        }

        container.register { _ -> JSONDecoder in
            let jsonDecoder = JSONDecoder()
            jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
            jsonDecoder.dateDecodingStrategy = .secondsSince1970
            return jsonDecoder
        }

        container.register { JSONEncoder() }

        // MARK: - Managers

        container.register(.singleton) {
            CartManager(cartService: $0, storageManager: $1) as CartManagerProtocol
        }

        container.register {
            KeyboardManager(notificationCenter: .default) as KeyboardManagerProtocol
        }

        container.register {
            RequestProvider(decoder: $0, requestInterceptor: $1) as RequestProviderProtocol
        }

        container.register {
            UserDefaultsManager(defaults: $0, encoder: $1, decoder: $2) as StorageProtocol
        }

        container.register {
            FirstLaunchManager(defaults: $0) as FirstLaunchManagerProtocol
        }

        container.register {
            PrecacheManager(requestProvider: $0, storage: $1, firstLaunchManager: $2) as PrecacheManagerProtocol
        }

        container.register {
            GeocoderManager() as GeocoderManagerProtocol
        }

        container.register(.singleton) {
            StoriesManager(storiesService: $0, storiesListService: $1) as StoriesManagerProtocol
        }

        // MARK: - Services

        container.register {
            CartService(requestProvider: $0) as CartServiceProtocol
        }

        container.register {
            BackendListService<ProductList>(requestProvider: $0) as ProductsServiceProtocol
        }

        container.register {
            BackendListService<ShopList>(requestProvider: $0) as ShopsServiceProtocol
        }

        container.register {
            BackendListService<Stories>(requestProvider: $0) as StoriesListServiceProtocol
        }

        container.register {
            ContactService(requestProvider: $0) as ContactServiceProtocol
        }

        container.register {
            BackendListService<CollectionMetadata>(requestProvider: $0) as CollectionsServiceProtocol
        }

        container.register {
            MetadataService(requestProvider: $0) as MetadataServiceProtocol
        }

        container.register {
            ProductService(requestProvider: $0) as ProductServiceProtocol
        }

        container.register {
            ProfileService(requestProvider: $0) as ProfileServiceProtocol
        }

        container.register {
            AuthManager(requestProvider: $0, tokenHolder: $1) as AuthManagerProtocol
        }

        container.register(.singleton) {
            AuthTokenHolderManager() as AuthTokenHolder
        }

        container.register(.singleton) {
            AuthRequestAdapter(tokenHolder: $0, decoder: $1) as RequestInterceptor
        }

        container.register {
            MetadataGetService(requestProvider: $0, request: MetadataEndpoint.sizes) as SizesServiceProtocol
        }

        container.register {
            StoriesService(requestProvider: $0) as StoriesServiceProtocol
        }

        container.register {
            OrderManager(requestProvider: $0) as OrderManagerProtocol
        }

        container.register {
            MetadataGetService(requestProvider: $0, request: MetadataEndpoint.banners) as BannersManagerProtocol
        }

        container.register {
            MetadataGetService(requestProvider: $0, request: MetadataEndpoint.cities) as CitiesManagerProtocol
        }

        container.register {
            MetadataGetService(requestProvider: $0, request: MetadataEndpoint.faq) as FaqServiceProtocol
        }

        container.register {
            SavedStoreManager() as SavedStoreManagerProtocol
        }

        container.register {
            ReviewsService(requestProvider: $0) as ReviewsServiceProtocol
        }

        container.register {
            MetadataGetService(requestProvider: $0, request: MetadataEndpoint.about) as AboutCompanyServiceProtocol
        }

        container.register { WishlistService(requestProvider: $0) as WishlistServiceProtocol }

        container.register { ContactsTransitionGenerator() as ContactsTransitionGeneratorProtocol }

        container.register { SubscriptionService(requestProvider: $0) as SubscriptionServiceProtocol }

        container.register {
            BackendListService<Suggests>(requestProvider: $0) as SuggestServiceProtocol
        }

        container.register { FeedbackService(requestProvider: $0) as FeedbackServiceProtocol }

        return container
    }()
}

In my program code, some of the Quick-based test code had similar problems.
This problem is most likely to occur when using frameworks that require heavy use of closures.

I'm also running into this. I use Combine a lot in my app and have a few files that fail to format. I can add them here if need be.

@bryanforbes Can you provide the code so I need it?
I want to check if it is fixed by https://github.com/nicklockwood/SwiftFormat/pull/579.

Here's one of the files I'm having an issue with:

PreferencesStoreTests.swift

//
//  PreferencesStoreTests.swift
//  SilenceMeTests
//
//  Created by Bryan Forbes on 2/25/20.
//  Copyright 漏 2020 Bryan Forbes. All rights reserved.
//

import EntwineTest
import Foundation
@testable import SilenceMe
import XCTest

class PreferencesStoreTests: XCTestCase {
    var storage: ConstantsPreferencesStorage!

    override func setUp() {
        super.setUp()

        storage = ConstantsPreferencesStorage()
        storage.shortcut = .init(key: nil, modifiers: [.control, .option])
        storage.firstRun = false
    }

    override func tearDown() {
        storage = nil

        super.tearDown()
    }

    func testInitFirstRun() {
        storage = ConstantsPreferencesStorage()
        let store = PreferencesStore(storage: storage)

        XCTAssertFalse(store.startAtLogin)
        XCTAssertTrue(store.showInMenuBar)
        XCTAssertTrue(store.pushToTalk)
        XCTAssertFalse(store.doubleTapToggle)
        XCTAssertEqual(store.shortcut, .init(key: nil, modifiers: [.control, .fn]))
        XCTAssertFalse(storage.firstRun)
    }

    func testInit() {
        storage.startAtLogin = true
        storage.showInMenuBar = false
        storage.pushToTalk = false
        storage.doubleTapToggle = true
        storage.shortcut = nil
        let store = PreferencesStore(storage: storage)

        XCTAssertTrue(store.startAtLogin)
        XCTAssertFalse(store.showInMenuBar)
        XCTAssertFalse(store.pushToTalk)
        XCTAssertTrue(store.doubleTapToggle)
        XCTAssertEqual(store.shortcut, nil)
        XCTAssertFalse(storage.firstRun)
    }

    func testSetters() {
        let store = PreferencesStore(storage: storage)

        store.startAtLogin = true
        store.showInMenuBar = false
        store.pushToTalk = false
        store.doubleTapToggle = true
        store.shortcut = nil

        XCTAssertTrue(storage.startAtLogin)
        XCTAssertFalse(storage.showInMenuBar)
        XCTAssertFalse(storage.pushToTalk)
        XCTAssertTrue(storage.doubleTapToggle)
        XCTAssertNil(storage.shortcut)
    }

    func testPublisher() {
        let store = PreferencesStore(storage: storage)
        let scheduler = TestScheduler(initialClock: 0)

        let showInMenuBar = scheduler.createTestableSubscriber(Bool.self, Never.self)
        let doubleTapToggle = scheduler.createTestableSubscriber(Bool.self, Never.self)
        let pushToTalk = scheduler.createTestableSubscriber(Bool.self, Never.self)
        let shortcut = scheduler.createTestableSubscriber(Shortcut?.self, Never.self)
        let startAtLogin = scheduler.createTestableSubscriber(Bool.self, Never.self)

        scheduler.schedule(after: 100) { store.publisher(for: \.showInMenuBar).subscribe(showInMenuBar) }
        scheduler.schedule(after: 110) { store.publisher(for: \.doubleTapToggle).subscribe(doubleTapToggle) }
        scheduler.schedule(after: 120) { store.publisher(for: \.pushToTalk).subscribe(pushToTalk) }
        scheduler.schedule(after: 130) { store.publisher(for: \.shortcut).subscribe(shortcut) }
        scheduler.schedule(after: 140) { store.publisher(for: \.startAtLogin).subscribe(startAtLogin) }
        scheduler.schedule(after: 200) { store.showInMenuBar = false }
        scheduler.schedule(after: 210) { store.showInMenuBar = false }
        scheduler.schedule(after: 220) { store.showInMenuBar = true }
        scheduler.schedule(after: 230) { store.doubleTapToggle = true }
        scheduler.schedule(after: 240) { store.doubleTapToggle = true }
        scheduler.schedule(after: 250) { store.doubleTapToggle = false }
        scheduler.schedule(after: 260) { store.pushToTalk = false }
        scheduler.schedule(after: 270) { store.pushToTalk = false }
        scheduler.schedule(after: 280) { store.pushToTalk = true }
        scheduler.schedule(after: 290) { store.shortcut = nil }
        scheduler.schedule(after: 300) { store.shortcut = .init(key: 24, modifiers: [.capsLock, .shift]) }
        scheduler.schedule(after: 310) { store.shortcut = .init(key: 24, modifiers: [.capsLock, .shift]) }
        scheduler.schedule(after: 320) { store.shortcut = .init(key: nil, modifiers: [.fn, .shift, .command]) }
        scheduler.schedule(after: 330) { store.startAtLogin = true }
        scheduler.schedule(after: 340) { store.startAtLogin = true }
        scheduler.schedule(after: 350) { store.startAtLogin = false }

        scheduler.resume()

        XCTAssertEqual([
            (100, .subscription),
            (200, .input(false)),
            (220, .input(true)),
        ], showInMenuBar.recordedOutput)
        XCTAssertEqual([
            (110, .subscription),
            (230, .input(true)),
            (250, .input(false)),
        ], doubleTapToggle.recordedOutput)
        XCTAssertEqual([
            (120, .subscription),
            (260, .input(false)),
            (280, .input(true)),
        ], pushToTalk.recordedOutput)
        XCTAssertEqual([
            (130, .subscription),
            (290, .input(nil)),
            (300, .input(.init(key: 24, modifiers: [.capsLock, .shift]))),
            (320, .input(.init(key: nil, modifiers: [.fn, .shift, .command]))),
        ], shortcut.recordedOutput)
        XCTAssertEqual([
            (140, .subscription),
            (330, .input(true)),
            (350, .input(false)),
        ], startAtLogin.recordedOutput)
    }

    func testPublisherCurrent() {
        let store = PreferencesStore(storage: storage)
        let scheduler = TestScheduler(initialClock: 0)

        let showInMenuBar = scheduler.createTestableSubscriber(Bool.self, Never.self)
        let doubleTapToggle = scheduler.createTestableSubscriber(Bool.self, Never.self)
        let pushToTalk = scheduler.createTestableSubscriber(Bool.self, Never.self)
        let shortcut = scheduler.createTestableSubscriber(Shortcut?.self, Never.self)
        let startAtLogin = scheduler.createTestableSubscriber(Bool.self, Never.self)

        scheduler.schedule(after: 100) { store.publisher(current: \.showInMenuBar).subscribe(showInMenuBar) }
        scheduler.schedule(after: 110) { store.publisher(current: \.doubleTapToggle).subscribe(doubleTapToggle) }
        scheduler.schedule(after: 120) { store.publisher(current: \.pushToTalk).subscribe(pushToTalk) }
        scheduler.schedule(after: 130) { store.publisher(current: \.shortcut).subscribe(shortcut) }
        scheduler.schedule(after: 140) { store.publisher(current: \.startAtLogin).subscribe(startAtLogin) }
        scheduler.schedule(after: 200) { store.showInMenuBar = false }
        scheduler.schedule(after: 210) { store.showInMenuBar = false }
        scheduler.schedule(after: 220) { store.showInMenuBar = true }
        scheduler.schedule(after: 230) { store.doubleTapToggle = true }
        scheduler.schedule(after: 240) { store.doubleTapToggle = true }
        scheduler.schedule(after: 250) { store.doubleTapToggle = false }
        scheduler.schedule(after: 260) { store.pushToTalk = false }
        scheduler.schedule(after: 270) { store.pushToTalk = false }
        scheduler.schedule(after: 280) { store.pushToTalk = true }
        scheduler.schedule(after: 290) { store.shortcut = nil }
        scheduler.schedule(after: 300) { store.shortcut = .init(key: 24, modifiers: [.capsLock, .shift]) }
        scheduler.schedule(after: 310) { store.shortcut = .init(key: 24, modifiers: [.capsLock, .shift]) }
        scheduler.schedule(after: 320) { store.shortcut = .init(key: nil, modifiers: [.fn, .shift, .command]) }
        scheduler.schedule(after: 330) { store.startAtLogin = true }
        scheduler.schedule(after: 340) { store.startAtLogin = true }
        scheduler.schedule(after: 350) { store.startAtLogin = false }

        scheduler.resume()

        XCTAssertEqual([
            (100, .subscription),
            (100, .input(true)),
            (200, .input(false)),
            (220, .input(true)),
        ], showInMenuBar.recordedOutput)
        XCTAssertEqual([
            (110, .subscription),
            (110, .input(false)),
            (230, .input(true)),
            (250, .input(false)),
        ], doubleTapToggle.recordedOutput)
        XCTAssertEqual([
            (120, .subscription),
            (120, .input(true)),
            (260, .input(false)),
            (280, .input(true)),
        ], pushToTalk.recordedOutput)
        XCTAssertEqual([
            (130, .subscription),
            (130, .input(.init(key: nil, modifiers: [.control, .option]))),
            (290, .input(nil)),
            (300, .input(.init(key: 24, modifiers: [.capsLock, .shift]))),
            (320, .input(.init(key: nil, modifiers: [.fn, .shift, .command]))),
        ], shortcut.recordedOutput)
        XCTAssertEqual([
            (140, .subscription),
            (140, .input(false)),
            (330, .input(true)),
            (350, .input(false)),
        ], startAtLogin.recordedOutput)
    }
}

If I take out lines 85-105 and 147-167, swiftformat has no issue with the file.

@bryanforbes I can confirm that @zizi4n5's fix works for your file. It's up on the develop branch now - I'll try to get a release out shortly.

Fixed in 0.44.6.

I just wanted to confirm that 0.44.6 fixes the crash for my source files. Thank you, @nicklockwood, for the quick turn around!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DavinAhn picture DavinAhn  路  4Comments

fellipecaetano picture fellipecaetano  路  4Comments

MaxDesiatov picture MaxDesiatov  路  3Comments

AniHovhannisyanAni picture AniHovhannisyanAni  路  3Comments

mdiep picture mdiep  路  3Comments