import Foundation import Testing @Suite struct FileHandleLegacyAPIGuardTests { @Test func sourcesAvoidLegacyNonThrowingFileHandleReadAPIs() throws { let testFile = URL(fileURLWithPath: #filePath) let packageRoot = testFile .deletingLastPathComponent() // ClawdbotIPCTests .deletingLastPathComponent() // Tests .deletingLastPathComponent() // apps/macos let sourcesRoot = packageRoot.appendingPathComponent("Sources") let swiftFiles = try Self.swiftFiles(under: sourcesRoot) var offenders: [String] = [] for file in swiftFiles { let raw = try String(contentsOf: file, encoding: .utf8) let stripped = Self.stripCommentsAndStrings(from: raw) if stripped.contains("readDataToEndOfFile(") || stripped.contains(".availableData") { offenders.append(file.path) } } if !offenders.isEmpty { let message = "Found legacy FileHandle reads in:\n" + offenders.joined(separator: "\n") throw NSError( domain: "FileHandleLegacyAPIGuardTests", code: 1, userInfo: [NSLocalizedDescriptionKey: message]) } } private static func swiftFiles(under root: URL) throws -> [URL] { let fm = FileManager.default guard let enumerator = fm.enumerator(at: root, includingPropertiesForKeys: [.isRegularFileKey]) else { return [] } var files: [URL] = [] for case let url as URL in enumerator { guard url.pathExtension == "swift" else { continue } files.append(url) } return files } private static func stripCommentsAndStrings(from source: String) -> String { enum Mode { case code case lineComment case blockComment(depth: Int) case string(quoteCount: Int) // 1 = ", 3 = """ } var mode: Mode = .code var out = "" out.reserveCapacity(source.count) var index = source.startIndex func peek(_ offset: Int) -> Character? { guard let i = source.index(index, offsetBy: offset, limitedBy: source.endIndex), i < source.endIndex else { return nil } return source[i] } while index < source.endIndex { let ch = source[index] switch mode { case .code: if ch == "/", peek(1) == "/" { out.append(" ") index = source.index(index, offsetBy: 2) mode = .lineComment continue } if ch == "/", peek(1) == "*" { out.append(" ") index = source.index(index, offsetBy: 2) mode = .blockComment(depth: 1) continue } if ch == "\"" { let triple = (peek(1) == "\"") && (peek(2) == "\"") out.append(triple ? " " : " ") index = source.index(index, offsetBy: triple ? 3 : 1) mode = .string(quoteCount: triple ? 3 : 1) continue } out.append(ch) index = source.index(after: index) case .lineComment: if ch == "\n" { out.append(ch) index = source.index(after: index) mode = .code } else { out.append(" ") index = source.index(after: index) } case let .blockComment(depth): if ch == "/", peek(1) == "*" { out.append(" ") index = source.index(index, offsetBy: 2) mode = .blockComment(depth: depth + 1) continue } if ch == "*", peek(1) == "/" { out.append(" ") index = source.index(index, offsetBy: 2) let newDepth = depth - 1 mode = newDepth > 0 ? .blockComment(depth: newDepth) : .code continue } out.append(ch == "\n" ? "\n" : " ") index = source.index(after: index) case let .string(quoteCount): if ch == "\\", quoteCount == 1 { // Skip escaped character in normal strings. out.append(" ") index = source.index(after: index) if index < source.endIndex { out.append(" ") index = source.index(after: index) } continue } if ch == "\"" { if quoteCount == 3, peek(1) == "\"", peek(2) == "\"" { out.append(" ") index = source.index(index, offsetBy: 3) mode = .code continue } if quoteCount == 1 { out.append(" ") index = source.index(after: index) mode = .code continue } } out.append(ch == "\n" ? "\n" : " ") index = source.index(after: index) } } return out } }