Skip to content

Commit 6be632e

Browse files
Merge pull request #6 from michaelversus/fix/new_lines_removed_issue
fix empty lines issue
2 parents 7e5ee2d + 023a337 commit 6be632e

8 files changed

Lines changed: 196 additions & 20 deletions

File tree

Sources/SwiftFindRefs/FileSystem/FileSystem.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,11 @@ final class FileSystem: FileSystemProvider {
5959
try String(contentsOfFile: path)
6060
}
6161

62-
func readLines(atPath path: String) async throws -> [String] {
63-
let url = URL(fileURLWithPath: path)
64-
var lines: [String] = []
65-
for try await line in url.resourceBytes.lines {
66-
lines.append(line)
67-
}
68-
return lines
62+
func readLines(atPath path: String) throws -> [String] {
63+
// Read the file content first to preserve empty lines (including trailing ones)
64+
// URL.resourceBytes.lines strips trailing newlines, so we need to split manually
65+
let contents = try readFile(atPath: path)
66+
return contents.components(separatedBy: .newlines)
6967
}
7068

7169
func writeFile(_ contents: String, toPath path: String) throws {

Sources/SwiftFindRefs/FileSystem/FileSystemProvider.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ protocol FileSystemProvider {
3131
/// - Parameter path: A file path (absolute or relative).
3232
func readFile(atPath path: String) throws -> String
3333

34-
/// Reads the contents of a file as lines asynchronously.
34+
/// Reads the contents of a file as lines.
3535
/// - Parameter path: A file path (absolute or relative).
36-
func readLines(atPath path: String) async throws -> [String]
36+
func readLines(atPath path: String) throws -> [String]
3737

3838
/// Writes the contents to a file path.
3939
/// - Parameters:

Sources/SwiftFindRefs/RemoveUnnecessaryTestable/TestableImportExtractor.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ struct TestableImportExtractor: TestableImportExtracting {
1414
}
1515

1616
func testableImports(inFile path: String) async throws -> Set<String> {
17-
let lines = try await fileSystem.readLines(atPath: path)
17+
let lines = try fileSystem.readLines(atPath: path)
1818
var testableImports = Set<String>()
1919
var conditionalDepth = 0
2020

Sources/SwiftFindRefs/RemoveUnnecessaryTestable/UnnecessaryTestableAnalyzer.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ struct UnnecessaryTestableAnalyzer: UnnecessaryTestableAnalyzing {
2020
let fileSystemBox = FileSystemBox(fileSystem: fileSystem)
2121
let fileLinesCache = FileLinesCache(
2222
readLines: { path in
23-
try await fileSystemBox.fileSystem.readLines(atPath: path)
23+
try fileSystemBox.fileSystem.readLines(atPath: path)
2424
}
2525
)
2626
var mutableTestableImportsByFile: [String: Set<String>] = [:]
@@ -264,19 +264,19 @@ private struct RelatedSymbolSnapshot: Sendable {
264264

265265
private actor FileLinesCache {
266266
private var cache: [String: [String]] = [:]
267-
private let readLines: @Sendable (String) async throws -> [String]
267+
private let readLines: @Sendable (String) throws -> [String]
268268

269269
init(
270-
readLines: @escaping @Sendable (String) async throws -> [String]
270+
readLines: @escaping @Sendable (String) throws -> [String]
271271
) {
272272
self.readLines = readLines
273273
}
274274

275-
func lines(for file: String) async -> [String] {
275+
func lines(for file: String) -> [String] {
276276
if let cached = cache[file] {
277277
return cached
278278
}
279-
let lines = (try? await readLines(file)) ?? []
279+
let lines = (try? readLines(file)) ?? []
280280
cache[file] = lines
281281
return lines
282282
}

Sources/SwiftFindRefs/RemoveUnnecessaryTestable/UnnecessaryTestableRewriter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct UnnecessaryTestableRewriter: UnnecessaryTestableRewriting {
1717
return try await withThrowingTaskGroup(of: String?.self) { group in
1818
for (filePath, modules) in removalsByFile {
1919
group.addTask {
20-
let lines = try await fileSystem.fileSystem.readLines(atPath: filePath)
20+
let lines = try fileSystem.fileSystem.readLines(atPath: filePath)
2121
if let updated = Self.replaceTestableImports(in: lines, modules: modules) {
2222
try fileSystem.fileSystem.writeFile(updated, toPath: filePath)
2323
return filePath

Tests/SwiftFindRefs/FileSystem/FileSystemTests.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,39 @@ struct FileSystemTests {
136136
#expect(result == contents)
137137
}
138138

139-
@Test("test readLines returns lines asynchronously")
140-
func test_readLines_ReturnsLines() async throws {
139+
@Test("test readLines returns lines")
140+
func test_readLines_ReturnsLines() throws {
141141
// Given
142142
let fileURL = makeTempFileURL()
143143
let contents = "LineA\nLineB\nLineC"
144144
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
145145
let sut = makeSUT(fileManager: FileManager.default)
146146

147147
// When
148-
let lines = try await sut.readLines(atPath: fileURL.path)
148+
let lines = try sut.readLines(atPath: fileURL.path)
149149

150150
// Then
151151
#expect(lines == ["LineA", "LineB", "LineC"])
152152
}
153153

154+
@Test("test readLines preserves empty lines including trailing ones")
155+
func test_readLines_PreservesEmptyLines() throws {
156+
// Given
157+
let fileURL = makeTempFileURL()
158+
// File with empty lines in middle and trailing empty lines
159+
let contents = "LineA\n\nLineB\n\n\n"
160+
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
161+
let sut = makeSUT(fileManager: FileManager.default)
162+
163+
// When
164+
let lines = try sut.readLines(atPath: fileURL.path)
165+
166+
// Then
167+
// components(separatedBy: .newlines) preserves all empty lines including trailing ones
168+
// "LineA\n\nLineB\n\n\n" should split to ["LineA", "", "LineB", "", "", ""]
169+
#expect(lines == ["LineA", "", "LineB", "", "", ""])
170+
}
171+
154172
// MARK: - Helpers
155173

156174
private func makeSUT(fileManager: FileManager) -> FileSystem {

Tests/SwiftFindRefs/Mocks/MockFileSystem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ final class MockFileSystem: FileSystemProvider {
7070
return readFileResults[path] ?? ""
7171
}
7272

73-
func readLines(atPath path: String) async throws -> [String] {
73+
func readLines(atPath path: String) throws -> [String] {
7474
actions.append(.readLines(atPath: path))
7575
if let error = readFileError {
7676
throw error

Tests/SwiftFindRefs/RemoveUnnecessaryTestable/UnnecessaryTestableRewriterTests.swift

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)