From 09a00c57db569a77239c0af67a0f22f2804d5d92 Mon Sep 17 00:00:00 2001 From: Kamaal Farah Date: Sun, 11 May 2025 22:37:27 +0200 Subject: [PATCH] Testing files --- src/prompts.ts | 1 + tests/samples/FileSystem.swift | 342 +++++++++++++++++++++++++ tests/{samples.ts => samples/index.ts} | 0 tests/samples/prompt.md | 133 ++++++++++ 4 files changed, 476 insertions(+) create mode 100644 tests/samples/FileSystem.swift rename tests/{samples.ts => samples/index.ts} (100%) create mode 100644 tests/samples/prompt.md diff --git a/src/prompts.ts b/src/prompts.ts index 4602d27..4bb40b9 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -2,6 +2,7 @@ export const SYSTEM_TRANSFORM_PROMPT = ` You are a code-transformation assistant. Reply with a JSON object: { "code": "" } Do not include anything else. +Make sure you always no matter what reply with a valid JSON object, with valid JSON parse-able characters. `; export function buildTransformUserPrompt(source: string, prompt: string) { diff --git a/tests/samples/FileSystem.swift b/tests/samples/FileSystem.swift new file mode 100644 index 0000000..1d84ffa --- /dev/null +++ b/tests/samples/FileSystem.swift @@ -0,0 +1,342 @@ +// +// FileSystem.swift +// FileSystem +// +// Created by Kamaal M Farah on 5/6/25. +// + +import Cocoa +import Foundation +import KamaalExtensions + +public enum FileSystemGetDirectoryInfoErrors: Error { + case notADirectory + case doesNotExist + case generalError(cause: Error) +} + +public struct FileSystemOpenFilePickerConfig { + public let allowsMultipleSelection: Bool + public let canChooseDirectories: Bool + public let canChooseFiles: Bool + + public init(allowsMultipleSelection: Bool, canChooseDirectories: Bool, canChooseFiles: Bool) { + self.allowsMultipleSelection = allowsMultipleSelection + self.canChooseDirectories = canChooseDirectories + self.canChooseFiles = canChooseFiles + } +} + +public actor FileSystem { + private let fileManager: FileManager + + public init(fileManager: FileManager) { + self.fileManager = fileManager + } + + public init() { + self.init(fileManager: .default) + } + + @MainActor + public func openFilePicker(config: FileSystemOpenFilePickerConfig) -> [URL]? { + let panel = NSOpenPanel() + panel.allowsMultipleSelection = config.allowsMultipleSelection + panel.canChooseDirectories = config.canChooseDirectories + panel.canChooseFiles = config.canChooseFiles + guard panel.runModal() == .OK else { return nil } + + return panel.urls + } + + public func getDirectoryInfo( + for url: URL, + ignoringRuleFilenames: [String] = [] + ) -> Result { + getDirectoryInfo(for: url, parent: nil, ignoringRuleFilenames: ignoringRuleFilenames) + } + + public func findDirectories(_ match: String, in directory: DirectoryInfo) -> [DirectoryInfo] { + let directoryURLString = directory.url.absoluteString + var matches: [DirectoryInfo] = [] + for currentDirectory in listNestedDirectories(in: directory) { + var currentDirectoryURLComponents = currentDirectory.url.absoluteString.split(separator: directoryURLString) + if currentDirectoryURLComponents.count > 1 { + currentDirectoryURLComponents.removeFirst() + } + let currentDirectoryURLStringWithoutSourceDirectory = currentDirectoryURLComponents.joined(separator: "/") + if currentDirectoryURLStringWithoutSourceDirectory.contains(match) { + matches.append(currentDirectory) + } + } + + return matches + } + + public func listNestedDirectories(in directory: DirectoryInfo) -> [DirectoryInfo] { + var directories: [DirectoryInfo] = [] + for currentDirectory in directory.directories { + directories.append(currentDirectory) + let nestedDirectories = listNestedDirectories(in: currentDirectory) + directories.append(contentsOf: nestedDirectories) + } + + return directories + } + + public func isDirectory(_ url: URL) -> Bool { + var isDirectory: ObjCBool = false + let itemExists = fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) + + return itemExists && isDirectory.boolValue + } + + public func isFile(_ url: URL) -> Bool { + itemExists(url) && !isDirectory(url) + } + + public func itemExists(_ url: URL) -> Bool { + fileManager.fileExists(atPath: url.path) + } + + public func getFileContent(_ url: URL) -> String? { + guard isFile(url) else { + assertionFailure("Get content on files") + return nil + } + + do { + return try String(contentsOf: url, encoding: .utf8) + } catch { + return nil + } + } + + private func getDirectoryInfo( + for url: URL, + parent: DirectoryInfo?, + ignoringRuleFilenames: [String] + ) -> Result { + guard itemExists(url) else { return .failure(.doesNotExist) } + guard isDirectory(url) else { return .failure(.notADirectory) } + + let itemURLs: [URL] + do { + itemURLs = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) + } catch { + return .failure(.generalError(cause: error)) + } + + let fetchingDirectoryInfo = DirectoryInfo(url: url, parent: parent, files: [], directories: []) + let ignores: Set + if ignoringRuleFilenames.isEmpty { + ignores = [] + } else { + ignores = itemURLs + .reduce(Set()) { partialResult, itemURL in + guard ignoringRuleFilenames.contains(itemURL.lastPathComponent) else { return partialResult } + guard isFile(itemURL) else { return partialResult } + guard let content = getFileContent(itemURL) else { return partialResult } + + return IgnoreSpec + .getFilePaths( + content, + previousIgnores: partialResult, + parent: fetchingDirectoryInfo + ) + } + } + let (fetchingDirectoryInfoFiles, fetchingDirectoryInfoDirectoriess) = itemURLs + .reduce((files: [FileInfo](), directories: [DirectoryInfo]())) { partialResult, itemURL in + guard itemExists(itemURL) else { + assertionFailure("Should exist at this point") + return partialResult + } + guard !itemURL.lastPathComponent.hasPrefix(".") else { return partialResult } + guard !IgnoreSpec.ignore(ignores: ignores, url: itemURL) else { return partialResult } + + if isDirectory(itemURL) { + let directoryResult = getDirectoryInfo( + for: itemURL, + parent: fetchingDirectoryInfo, + ignoringRuleFilenames: ignoringRuleFilenames + ) + switch directoryResult { + case .failure: return partialResult + case let .success(success): + return (files: partialResult.files, directories: partialResult.directories.appended(success)) + } + } + + if isFile(itemURL) { + let file = FileInfo(url: itemURL, parent: fetchingDirectoryInfo) + return (files: partialResult.files.appended(file), directories: partialResult.directories) + } + + return partialResult + } + + let fetchingDirectoriesWithItems = fetchingDirectoryInfo + .setFiles(fetchingDirectoryInfoFiles) + .setDirectories(fetchingDirectoryInfoDirectoriess) + + return .success(fetchingDirectoriesWithItems) + } +} + +public enum FileSystemTypes: Sendable { + case file + case directory +} + +public protocol FileSystemItemable: Hashable, Equatable, Sendable { + var url: URL { get } + var parent: DirectoryInfo? { get } + var type: FileSystemTypes { get } + var items: [FileSystemItem] { get } +} + +extension FileSystemItemable { + public var name: String { url.lastPathComponent } + + public var isDirectory: Bool { type == .directory } + + public var isFile: Bool { type == .file } + + public var asItem: FileSystemItem { .init(url: url, parent: parent, type: type, items: items) } +} + +public final class FileSystemItem: FileSystemItemable { + public let url: URL + public let parent: DirectoryInfo? + public let type: FileSystemTypes + public let items: [FileSystemItem] + + public init(url: URL, parent: DirectoryInfo?, type: FileSystemTypes, items: [FileSystemItem]) { + self.url = url + self.parent = parent + self.type = type + self.items = items + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(url) + } + + public static func == (lhs: FileSystemItem, rhs: FileSystemItem) -> Bool { + lhs.url == rhs.url + } +} + +public final class DirectoryInfo: FileSystemItemable { + public let url: URL + public let parent: DirectoryInfo? + public let files: [FileInfo] + public let directories: [DirectoryInfo] + + public init(url: URL, parent: DirectoryInfo?, files: [FileInfo], directories: [DirectoryInfo]) { + self.url = url + self.parent = parent + self.files = files + self.directories = directories + } + + public var type: FileSystemTypes { .directory } + + public var items: [FileSystemItem] { + (files.map(\.asItem) + directories.map(\.asItem)) + .sorted(by: \.name, using: .orderedAscending) + } + + public var fileCount: Int { files.count } + + public var directoryCount: Int { directories.count } + + public func setFiles(_ files: [FileInfo]) -> Self { + Self(url: url, parent: parent, files: files, directories: directories) + } + + public func setDirectories(_ directories: [DirectoryInfo]) -> Self { + Self(url: url, parent: parent, files: files, directories: directories) + } + + nonisolated public func hash(into hasher: inout Hasher) { + hasher.combine(url) + } + + public static func == (lhs: DirectoryInfo, rhs: DirectoryInfo) -> Bool { + lhs.url == rhs.url + } +} + +public struct FileInfo: FileSystemItemable { + public let url: URL + public let parent: DirectoryInfo? + + public init(url: URL, parent: DirectoryInfo?) { + self.url = url + self.parent = parent + } + + public var type: FileSystemTypes { .file } + + public var items: [FileSystemItem] { [] } +} + +enum IgnoreSpec { + static func getFilePaths(_ content: String, previousIgnores: Set, parent: DirectoryInfo?) -> Set { + content + .splitLines + .reduce(previousIgnores, { result, current in + guard let formatted = formatLine(current) else { return result } + + var result = result + if shouldAddLineToIgnores(formatted) { + result.insert(formatted) + } else if shouldRemoveFromIgnores(formatted, ignores: result) { + result.remove(formatted) + } + + return result + }) + } + + static func ignore(ignores: Set, url: URL) -> Bool { + ignores.contains(url.lastPathComponent) + } + + private static func shouldRemoveFromIgnores(_ line: String, ignores: Set) -> Bool { + assert(formatLine(line) == line, "Should be already formatted before evulating") + assert(!line.isEmpty, "Should not be empty at this point") + + return ignores.contains(line) && line.hasPrefix("#") + } + + private static func shouldAddLineToIgnores(_ line: String) -> Bool { + assert(formatLine(line) == line, "Should be already formatted before evulating") + assert(!line.isEmpty, "Should not be empty at this point") + + return !line.hasPrefix("#") + } + + private static func formatLine(_ line: some StringProtocol) -> String? { + var formatted = String(line.trimmingByWhitespacesAndNewLines) + guard !formatted.isEmpty else { return nil } + + if formatted.hasSuffix("/") { + formatted = String(formatted.dropLast()) + guard !formatted.isEmpty else { return nil } + } + if formatted.hasPrefix("./") { + formatted = String(formatted.range(from: 2)) + guard !formatted.isEmpty else { return nil } + } + + // Very optimistic, but will bother later when it gets in the way! + // TODO: Do something with parent probably + guard let name = formatted.split(separator: "/").last else { return nil } + guard !name.isEmpty else { return nil } + + return String(name) + } +} diff --git a/tests/samples.ts b/tests/samples/index.ts similarity index 100% rename from tests/samples.ts rename to tests/samples/index.ts diff --git a/tests/samples/prompt.md b/tests/samples/prompt.md new file mode 100644 index 0000000..94d3105 --- /dev/null +++ b/tests/samples/prompt.md @@ -0,0 +1,133 @@ +You are a Swift code assistant. You will receive: + +A list of SwiftLint issues, each with: +• line: the line number +• column: the exact column where the issue is +• message: a human-readable description of what's wrong + +[ +{ +"line": "115", +"column": "13", +"message": "Function should have complexity 10 or less; currently complexity is 14 (cyclomatic_complexity)" +}, +{ +"line": "67", +"column": "17", +"message": "Variable name 'currentDirectoryURLStringWithoutSourceDirectory' should be between 2 and 40 characters long (identifier_name)" +}, +{ +"line": "18", +"column": "8", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "19", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "20", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "21", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "23", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "30", +"column": "8", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "33", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "37", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "42", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "52", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "59", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "76", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "87", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "94", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "98", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "102", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "200", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "202", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "204", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "206", +"column": "12", +"message": "public declarations should be documented (missing_docs)" +}, +{ +"line": "336", +"column": "12", +"message": "TODOs should be resolved (Do something with parent proba...) (todo)" +} +] + +Your task: +• For each issue, read the “message” and make the minimal in-place change needed to satisfy it. +• Don't introduce new issues, and don't reformat code beyond what's required. +• If the message asks for documentation (“…should be documented”), insert a /// comment stub with a “<- TODO: Describe this ->” placeholder +• If the message asks to remove something (e.g. “unused import”), remove the import. +• If the message asks to rename or adjust code, do so directly on the indicated line. +• For any other message, apply the obvious minimal fix implied by its text. +• Whatever you do, don't remove the comment on top of the file indicating the creator of the file, the filename and maybe the LICENSE.