@@ -47,6 +47,7 @@ extension SwiftLintDev.Rules {
4747 . sorted ( )
4848 try registerInRulesList ( rules)
4949 try registerInTests ( rules)
50+ try registerInTestsBzl ( rules)
5051 try registerInTestReference ( adding: newRule)
5152 print ( " (Re-)Registered \( rules. count) rules. " )
5253 }
@@ -74,53 +75,158 @@ struct NewRuleDetails: Equatable {
7475 }
7576}
7677
78+ /// Struct to hold processed rule information and shard calculations
79+ private struct ProcessedRulesContext {
80+ let baseRuleNames : [ String ]
81+ let totalShards : Int
82+ let shardSize : Int
83+
84+ init ( ruleFiles: [ String ] , shardSize: Int ) {
85+ self . baseRuleNames = ruleFiles. map { $0. replacingOccurrences ( of: " .swift " , with: " " ) }
86+ self . shardSize = shardSize
87+ guard shardSize > 0 , !baseRuleNames. isEmpty else {
88+ self . totalShards = baseRuleNames. isEmpty ? 0 : 1
89+ return
90+ }
91+ self . totalShards = ( baseRuleNames. count + shardSize - 1 ) / shardSize // Ceiling division
92+ }
93+
94+ /// Returns the rule names for a specific shard index
95+ func shardRules( forIndex shardIndex: Int ) -> ArraySlice < String > {
96+ let startIndex = shardIndex * shardSize
97+ let endIndex = min ( startIndex + shardSize, baseRuleNames. count)
98+ return baseRuleNames [ startIndex..< endIndex]
99+ }
100+
101+ /// Returns zero-padded shard numbers for all shards
102+ var shardNumbers : [ String ] {
103+ ( 1 ... totalShards) . map { String ( format: " %02d " , $0) }
104+ }
105+ }
106+
77107private extension SwiftLintDev . Rules . Register {
108+ /// Number of test classes per shard for optimal parallelization
109+ private static let shardSize = 25
110+
111+ /// Common parent directory of testsDirectory
112+ private var testsParentDirectory : URL {
113+ testsDirectory. deletingLastPathComponent ( )
114+ }
115+
116+ /// Generate content for BuiltInRules.swift file
117+ private func generateBuiltInRulesFileContent( rulesImportList: String ) -> String {
118+ """
119+ // GENERATED FILE. DO NOT EDIT!
120+
121+ /// The rule list containing all available rules built into SwiftLint.
122+ public let builtInRules: [any Rule.Type] = [
123+ \( rulesImportList. indent ( by: 4 ) ) ,
124+ ]
125+
126+ """
127+ }
128+
129+ /// Generate content for Swift test files
130+ private func generateSwiftTestFileContent( forTestClasses testClassesString: String ) -> String {
131+ """
132+ // GENERATED FILE. DO NOT EDIT!
133+ // swiftlint:disable:previous file_name
134+ // swiftlint:disable:previous blanket_disable_command superfluous_disable_command
135+ // swiftlint:disable:next blanket_disable_command superfluous_disable_command
136+ // swiftlint:disable file_length single_test_class type_name
137+
138+ @testable import SwiftLintBuiltInRules
139+ @testable import SwiftLintCore
140+ import TestHelpers
141+
142+ \( testClassesString)
143+
144+ """
145+ }
146+
147+ /// Generate content for Bazel .bzl files
148+ private func generateBzlFileContent( macroInvocations: String ) -> String {
149+ #"""
150+ # GENERATED FILE. DO NOT EDIT!
151+
152+ load(":test_macros.bzl", "generated_test_shard")
153+
154+ def generated_tests(copts, strict_concurrency_copts):
155+ """Creates all generated test targets for SwiftLint rules.
156+
157+ Args:
158+ copts: Common compiler options
159+ strict_concurrency_copts: Strict concurrency compiler options
160+ """
161+ \#( macroInvocations)
162+
163+ """#
164+ }
165+
78166 func registerInRulesList( _ ruleFiles: [ String ] ) throws {
79- let rules = ruleFiles
167+ let rulesImportString = ruleFiles
80168 . map { $0. replacingOccurrences ( of: " .swift " , with: " .self " ) }
81169 . joined ( separator: " , \n " )
82170 let builtInRulesFile = rulesDirectory. deletingLastPathComponent ( )
83171 . appendingPathComponent ( " Models " , isDirectory: true )
84172 . appendingPathComponent ( " BuiltInRules.swift " , isDirectory: false )
85- try """
86- // GENERATED FILE. DO NOT EDIT!
87173
88- /// The rule list containing all available rules built into SwiftLint.
89- public let builtInRules: [any Rule.Type] = [
90- \( rules. indent ( by: 4 ) ) ,
91- ]
92-
93- """ . write ( to: builtInRulesFile, atomically: true , encoding: . utf8)
174+ let fileContent = generateBuiltInRulesFileContent ( rulesImportList: rulesImportString)
175+ try fileContent. write ( to: builtInRulesFile, atomically: true , encoding: . utf8)
94176 }
95177
96178 func registerInTests( _ ruleFiles: [ String ] ) throws {
97- let testFile = testsDirectory
98- . appendingPathComponent ( " GeneratedTests.swift " , isDirectory: false )
99- let rules = ruleFiles
100- . map { $0. replacingOccurrences ( of: " .swift " , with: " " ) }
101- . map { testName in """
179+ let rulesContext = ProcessedRulesContext ( ruleFiles: ruleFiles, shardSize: Self . shardSize)
180+
181+ // Remove old generated files
182+ let existingFiles = try FileManager . default. contentsOfDirectory (
183+ at: testsDirectory,
184+ includingPropertiesForKeys: nil
185+ )
186+ for file in existingFiles where file. lastPathComponent. hasPrefix ( " GeneratedTests " ) &&
187+ file. pathExtension == " swift " {
188+ try FileManager . default. removeItem ( at: file)
189+ }
190+
191+ // Create sharded test files
192+ for shardIndex in 0 ..< rulesContext. totalShards {
193+ let shardRules = rulesContext. shardRules ( forIndex: shardIndex)
194+
195+ let testClasses = shardRules. map { testName in """
102196 final class \( testName) GeneratedTests: SwiftLintTestCase {
103197 func testWithDefaultConfiguration() {
104198 verifyRule( \( testName) .description)
105199 }
106200 }
107201 """
108- }
109- . joined ( separator: " \n \n " )
202+ } . joined ( separator: " \n \n " )
110203
111- try """
112- // GENERATED FILE. DO NOT EDIT!
204+ let shardNumber = rulesContext. shardNumbers [ shardIndex]
205+ let testFile = testsDirectory. appendingPathComponent (
206+ " GeneratedTests_ \( shardNumber) .swift " ,
207+ isDirectory: false
208+ )
113209
114- @testable import SwiftLintBuiltInRules
115- @testable import SwiftLintCore
116- import TestHelpers
210+ let fileContent = generateSwiftTestFileContent ( forTestClasses: testClasses)
211+ try fileContent. write ( to: testFile, atomically: true , encoding: . utf8)
212+ }
213+ }
117214
118- // swiftlint:disable:next blanket_disable_command
119- // swiftlint:disable file_length single_test_class type_name
215+ func registerInTestsBzl ( _ ruleFiles : [ String ] ) throws {
216+ let rulesContext = ProcessedRulesContext ( ruleFiles : ruleFiles , shardSize : Self . shardSize )
120217
121- \( rules)
218+ // Generate macro calls for each shard
219+ let macroInvocationsString = rulesContext. shardNumbers. map {
220+ #" generated_test_shard(" \#( $0) ", copts, strict_concurrency_copts)"#
221+ } . joined ( separator: " \n " )
222+
223+ let bzlFile = testsParentDirectory. appendingPathComponent (
224+ " generated_tests.bzl " ,
225+ isDirectory: false
226+ )
122227
123- """ . write ( to: testFile, atomically: true , encoding: . utf8)
228+ let fileContent = generateBzlFileContent ( macroInvocations: macroInvocationsString)
229+ try fileContent. write ( to: bzlFile, atomically: true , encoding: . utf8)
124230 }
125231
126232 func registerInTestReference( adding newRule: NewRuleDetails ? ) throws {
@@ -147,7 +253,7 @@ private extension SwiftLintDev.Rules.Register {
147253 . joined ( separator: " \n " )
148254 . appending ( " \n " )
149255 . write (
150- to: testsDirectory . deletingLastPathComponent ( )
256+ to: testsParentDirectory
151257 . appendingPathComponent ( " IntegrationTests " , isDirectory: true )
152258 . appendingPathComponent ( " default_rule_configurations.yml " , isDirectory: false ) ,
153259 atomically: true ,
0 commit comments