@@ -45,4 +45,164 @@ struct UnnecessaryTestableRewriterTests {
4545 #expect( updated. isEmpty)
4646 #expect( fileSystem. writtenFiles. isEmpty)
4747 }
48+
49+ @Test ( " preserves empty lines when rewriting @testable imports " )
50+ func test_preservesEmptyLines( ) async throws {
51+ // Given
52+ let filePath = " /mock/Test.swift "
53+ // File with empty lines at the beginning, middle, end, and multiple consecutive empty lines
54+ let originalContents = """
55+ import Foundation
56+
57+ @testable import ModuleA
58+
59+ import ModuleB
60+
61+ @testable import ModuleC
62+
63+ class TestClass {
64+ }
65+
66+ """
67+ let fileSystem = MockFileSystem ( readFileResults: [ filePath: originalContents] )
68+ let sut = UnnecessaryTestableRewriter ( fileSystem: fileSystem, print: { _ in } )
69+
70+ // When
71+ let updated = try await sut. rewriteFiles ( [ filePath: [ " ModuleA " , " ModuleC " ] ] )
72+
73+ // Then
74+ #expect( updated == [ filePath] )
75+ let written = try #require( fileSystem. writtenFiles [ filePath] )
76+
77+ // Split both original and written into lines to compare structure
78+ let originalLines = originalContents. components ( separatedBy: . newlines)
79+ let writtenLines = written. components ( separatedBy: . newlines)
80+
81+ // The number of lines should match (preserving empty lines)
82+ #expect( writtenLines. count == originalLines. count)
83+
84+ // Verify that empty lines are preserved at their original positions
85+ for (index, originalLine) in originalLines. enumerated ( ) {
86+ let writtenLine = writtenLines [ index]
87+ if originalLine. isEmpty {
88+ // Empty lines must remain empty
89+ #expect( writtenLine. isEmpty, " Empty line at index \( index) was not preserved " )
90+ } else if originalLine. trimmingCharacters ( in: . whitespaces) . hasPrefix ( " @testable import ModuleA " ) {
91+ // This line should be rewritten
92+ #expect( writtenLine. trimmingCharacters ( in: . whitespaces) == " import ModuleA " )
93+ } else if originalLine. trimmingCharacters ( in: . whitespaces) . hasPrefix ( " @testable import ModuleC " ) {
94+ // This line should be rewritten
95+ #expect( writtenLine. trimmingCharacters ( in: . whitespaces) == " import ModuleC " )
96+ } else {
97+ // All other lines should remain unchanged
98+ #expect( writtenLine == originalLine, " Line at index \( index) was modified: expected ' \( originalLine) ', got ' \( writtenLine) ' " )
99+ }
100+ }
101+
102+ // Verify the imports were changed
103+ #expect( written. contains ( " import ModuleA " ) )
104+ #expect( written. contains ( " import ModuleC " ) )
105+ #expect( !written. contains ( " @testable import ModuleA " ) )
106+ #expect( !written. contains ( " @testable import ModuleC " ) )
107+ }
108+
109+ @Test ( " preserves trailing empty lines and newlines " )
110+ func test_preservesTrailingEmptyLines( ) async throws {
111+ // Given
112+ let filePath = " /mock/Test.swift "
113+ // File ending with multiple empty lines and a newline
114+ let originalContents = """
115+ @testable import ModuleA
116+ class TestClass {
117+ }
118+
119+ """
120+ let fileSystem = MockFileSystem ( readFileResults: [ filePath: originalContents] )
121+ let sut = UnnecessaryTestableRewriter ( fileSystem: fileSystem, print: { _ in } )
122+
123+ // When
124+ let updated = try await sut. rewriteFiles ( [ filePath: [ " ModuleA " ] ] )
125+
126+ // Then
127+ #expect( updated == [ filePath] )
128+ let written = try #require( fileSystem. writtenFiles [ filePath] )
129+
130+ // Split both original and written into lines to compare structure
131+ let originalLines = originalContents. components ( separatedBy: . newlines)
132+ let writtenLines = written. components ( separatedBy: . newlines)
133+
134+ // The number of lines should match exactly (including trailing empty lines)
135+ #expect( writtenLines. count == originalLines. count,
136+ " Line count mismatch: original has \( originalLines. count) lines, written has \( writtenLines. count) lines " )
137+
138+ // Verify trailing empty lines are preserved
139+ // Original: ["@testable import ModuleA", "class TestClass {", "", ""]
140+ // Written should have the same structure
141+ for (index, originalLine) in originalLines. enumerated ( ) {
142+ let writtenLine = writtenLines [ index]
143+ if originalLine. isEmpty {
144+ #expect( writtenLine. isEmpty, " Empty line at index \( index) was not preserved " )
145+ }
146+ }
147+
148+ // Verify the last line is empty (trailing newline creates an empty line)
149+ if !originalLines. isEmpty {
150+ let lastOriginalLine = originalLines [ originalLines. count - 1 ]
151+ let lastWrittenLine = writtenLines [ writtenLines. count - 1 ]
152+ #expect( lastWrittenLine == lastOriginalLine,
153+ " Last line mismatch: expected ' \( lastOriginalLine) ', got ' \( lastWrittenLine) ' " )
154+ }
155+ }
156+
157+ @Test ( " preserves multiple consecutive empty lines " )
158+ func test_preservesMultipleConsecutiveEmptyLines( ) async throws {
159+ // Given
160+ let filePath = " /mock/Test.swift "
161+ // File with multiple consecutive empty lines
162+ let originalContents = """
163+ import Foundation
164+
165+
166+ @testable import ModuleA
167+
168+
169+ import ModuleB
170+ """
171+ let fileSystem = MockFileSystem ( readFileResults: [ filePath: originalContents] )
172+ let sut = UnnecessaryTestableRewriter ( fileSystem: fileSystem, print: { _ in } )
173+
174+ // When
175+ let updated = try await sut. rewriteFiles ( [ filePath: [ " ModuleA " ] ] )
176+
177+ // Then
178+ #expect( updated == [ filePath] )
179+ let written = try #require( fileSystem. writtenFiles [ filePath] )
180+
181+ // Split both original and written into lines
182+ let originalLines = originalContents. components ( separatedBy: . newlines)
183+ let writtenLines = written. components ( separatedBy: . newlines)
184+
185+ // Verify exact line count match
186+ #expect( writtenLines. count == originalLines. count,
187+ " Line count mismatch: original has \( originalLines. count) lines, written has \( writtenLines. count) lines " )
188+
189+ // Verify consecutive empty lines are preserved
190+ // Check that empty lines at specific indices are preserved
191+ for (index, originalLine) in originalLines. enumerated ( ) {
192+ let writtenLine = writtenLines [ index]
193+ if originalLine. isEmpty {
194+ #expect( writtenLine. isEmpty, " Empty line at index \( index) was not preserved " )
195+ } else if originalLine. trimmingCharacters ( in: . whitespaces) . hasPrefix ( " @testable import ModuleA " ) {
196+ #expect( writtenLine. trimmingCharacters ( in: . whitespaces) == " import ModuleA " )
197+ } else {
198+ #expect( writtenLine == originalLine, " Line at index \( index) was modified: expected ' \( originalLine) ', got ' \( writtenLine) ' " )
199+ }
200+ }
201+
202+ // Verify we have consecutive empty lines preserved
203+ let originalEmptyLineIndices = originalLines. enumerated ( ) . compactMap { $0. element. isEmpty ? $0. offset : nil }
204+ let writtenEmptyLineIndices = writtenLines. enumerated ( ) . compactMap { $0. element. isEmpty ? $0. offset : nil }
205+ #expect( originalEmptyLineIndices == writtenEmptyLineIndices,
206+ " Empty line positions don't match: original at \( originalEmptyLineIndices) , written at \( writtenEmptyLineIndices) " )
207+ }
48208}
0 commit comments