From 69497b40a2cdac800fc2d6beba1bb3211c2a4f63 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Mon, 8 Jun 2026 14:41:56 -0400 Subject: [PATCH 1/3] [semantic-reference-resolution] Patch 8: resolve Swift qualified references semantically --- swift/Package.resolved | 29 ++- swift/Package.swift | 2 + swift/Sources/SwiftArchLint/main.swift | 326 ++++++++++++++++++++++++- swift/test.sh | 11 +- 4 files changed, 355 insertions(+), 13 deletions(-) diff --git a/swift/Package.resolved b/swift/Package.resolved index f08054c..78a894e 100644 --- a/swift/Package.resolved +++ b/swift/Package.resolved @@ -1,6 +1,33 @@ { - "originHash" : "43bbb60b6ab1d18c9b0e104073fb494b0cde6c43863845b048d206d2086b3a67", + "originHash" : "2406c40fcbb95137c75cafe8b9d120eac46499b6301d1a61512f110827bf7715", "pins" : [ + { + "identity" : "indexstore-db", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/indexstore-db.git", + "state" : { + "branch" : "main", + "revision" : "46a7e6467c7a3e706c57181f59bd0e08e9d14937" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", + "version" : "1.8.2" + } + }, + { + "identity" : "swift-lmdb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-lmdb.git", + "state" : { + "branch" : "main", + "revision" : "a4bc87807721c1fd114bf35464457e2db0d0e6c0" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", diff --git a/swift/Package.swift b/swift/Package.swift index b21e0c2..9811724 100644 --- a/swift/Package.swift +++ b/swift/Package.swift @@ -11,6 +11,7 @@ let package = Package( .executable(name: "SwiftArchLint", targets: ["SwiftArchLint"]) ], dependencies: [ + .package(url: "https://github.com/swiftlang/indexstore-db.git", branch: "main"), .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0"), .package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "603.0.1") ], @@ -18,6 +19,7 @@ let package = Package( .executableTarget( name: "SwiftArchLint", dependencies: [ + .product(name: "IndexStoreDB", package: "indexstore-db"), .product(name: "SwiftParser", package: "swift-syntax"), .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "Yams", package: "Yams"), diff --git a/swift/Sources/SwiftArchLint/main.swift b/swift/Sources/SwiftArchLint/main.swift index cda0c15..dd396ee 100644 --- a/swift/Sources/SwiftArchLint/main.swift +++ b/swift/Sources/SwiftArchLint/main.swift @@ -1,4 +1,5 @@ import Foundation +import IndexStoreDB import SwiftParser import SwiftSyntax import Yams @@ -31,6 +32,26 @@ struct SwiftFileInfo { let interfaceLogicEvidence: InterfaceLogicEvidence let sharedState: [SharedStateFact] let propertyChecks: [PropertyCheckFact] + + func withQualifiedReferences(_ references: Set) -> SwiftFileInfo { + SwiftFileInfo( + url: url, + source: source, + moduleName: moduleName, + metadata: metadata, + imports: imports, + identifiers: identifiers, + apiReferences: apiReferences, + qualifiedReferences: references, + decisionReferences: decisionReferences, + decisionSurface: decisionSurface, + propertyTestSurface: propertyTestSurface, + decisionProducts: decisionProducts, + interfaceLogicEvidence: interfaceLogicEvidence, + sharedState: sharedState, + propertyChecks: propertyChecks + ) + } } struct SwiftTarget { @@ -231,13 +252,24 @@ enum SwiftArchLint { private static func architectureFacts(xcodegenURL: URL) throws -> ArchitectureFacts { let targets: [SwiftTarget] = try swiftTargets(xcodegenURL: xcodegenURL) - let files: [SourceFact] = try targets.flatMap { target in + let fileInfosWithScopes: [(SwiftFileInfo, String)] = try targets.flatMap { target in try target.sourceRoots.flatMap { root in try swiftFileInfos(root: root).map { - sourceFact($0, testScope: target.isTestScope ? target.name : "") + ($0, target.isTestScope ? target.name : "") } } } + let qualifiedReferencesByPath: [String: Set] = try semanticQualifiedReferences( + for: fileInfosWithScopes.map(\.0), + indexableRoots: targets.filter { !$0.isTestScope }.flatMap(\.sourceRoots) + ) + let files: [SourceFact] = fileInfosWithScopes.map { fileInfo, testScope in + let semanticReferences: Set = qualifiedReferencesByPath[fileInfo.url.path] ?? [] + return sourceFact( + fileInfo.withQualifiedReferences(semanticReferences), + testScope: testScope + ) + } return ArchitectureFacts(files: files) } @@ -285,7 +317,8 @@ enum SwiftArchLint { } private static func swiftFileInfo(_ fileURL: URL) throws -> SwiftFileInfo { - let source: String = try String(contentsOf: fileURL, encoding: .utf8) + let canonicalFileURL: URL = fileURL.standardizedFileURL.resolvingSymlinksInPath() + let source: String = try String(contentsOf: canonicalFileURL, encoding: .utf8) let tree: SourceFileSyntax = Parser.parse(source: source) let metadata: ModuleMetadata = parseModuleMetadata(source) let functionBodyVisitor: FunctionBodyVisitor = FunctionBodyVisitor(viewMode: .sourceAccurate) @@ -297,9 +330,9 @@ enum SwiftArchLint { visitor.walk(tree) return SwiftFileInfo( - url: fileURL, + url: canonicalFileURL, source: source, - moduleName: capitalizedModuleName(for: fileURL), + moduleName: capitalizedModuleName(for: canonicalFileURL), metadata: metadata, imports: Set(visitor.importedModules), identifiers: Set(visitor.identifiers), @@ -323,6 +356,286 @@ enum SwiftArchLint { ) } + private static func semanticQualifiedReferences( + for fileInfos: [SwiftFileInfo], + indexableRoots: [URL] + ) throws -> [String: Set] { + let indexedFiles: [URL] = try indexableSwiftFiles(in: indexableRoots) + if indexedFiles.isEmpty { + return [:] + } + + let temporaryRoot: URL = FileManager.default.temporaryDirectory.appendingPathComponent( + "archlint-swift-index-\(UUID().uuidString)", + isDirectory: true + ) + let sourceCopies: URL = temporaryRoot.appending(path: "sources", directoryHint: .isDirectory) + let indexStore: URL = temporaryRoot.appending(path: "index-store", directoryHint: .isDirectory) + let indexDatabase: URL = temporaryRoot.appending(path: "index-db", directoryHint: .isDirectory) + try FileManager.default.createDirectory( + at: temporaryRoot, + withIntermediateDirectories: true + ) + defer { + try? FileManager.default.removeItem(at: temporaryRoot) + } + + try buildIndexStore(for: indexedFiles, sourceCopies: sourceCopies, indexStore: indexStore) + + let library: IndexStoreLibrary = try IndexStoreLibrary(dylibPath: try indexStoreLibraryPath()) + let index: IndexStoreDB = try IndexStoreDB( + storePath: indexStore.path, + databasePath: indexDatabase.path, + library: library, + waitUntilDoneInitializing: true, + prefixMappings: [ + PathMapping(original: sourceCopies.path + "/", replacement: "/") + ] + ) + index.pollForUnitChangesAndWait(isInitialScan: true) + var analyzedFileByPath: [String: SwiftFileInfo] = [:] + for fileInfo: SwiftFileInfo in fileInfos { + analyzedFileByPath[fileInfo.url.path] = fileInfo + analyzedFileByPath[privateVarAlias(fileInfo.url.path)] = fileInfo + analyzedFileByPath[varAlias(fileInfo.url.path)] = fileInfo + } + var qualifiedReferencesByPath: [String: Set] = [:] + for fileURL: URL in indexedFiles { + let filePath: String = fileURL.path + guard let sourceFile: SwiftFileInfo = analyzedFileByPath[filePath] else { + continue + } + let occurrences: [SymbolOccurrence] = uniqueOccurrences( + [filePath, privateVarAlias(filePath), varAlias(filePath)].flatMap { + index.symbolOccurrences(inFilePath: $0) + } + ) + for occurrence: SymbolOccurrence in occurrences { + guard occurrence.roles.contains(.reference), + occurrence.symbol.language == .swift, + !occurrence.location.isSystem + else { + continue + } + for definition: SymbolOccurrence in index.occurrences( + ofUSR: occurrence.symbol.usr, + roles: [.definition, .declaration] + ) { + let ownerPath: String = definition.location.path + guard analyzedFileByPath[ownerPath]?.url.path != sourceFile.url.path, + let ownerFile: SwiftFileInfo = analyzedFileByPath[ownerPath] + else { + continue + } + let symbolName: String = referenceSymbolName(occurrence.symbol.name) + if !symbolName.isEmpty { + qualifiedReferencesByPath[sourceFile.url.path, default: []].insert( + "\(ownerFile.moduleName).\(symbolName)" + ) + } + } + } + } + return qualifiedReferencesByPath + } + + private static func indexableSwiftFiles(in roots: [URL]) throws -> [URL] { + var files: Set = [] + for root: URL in roots { + guard + let enumerator: FileManager.DirectoryEnumerator = + FileManager.default.enumerator(at: root, includingPropertiesForKeys: [.isRegularFileKey]) + else { + continue + } + for item in enumerator { + guard let fileURL: URL = item as? URL, fileURL.pathExtension == "swift" else { + continue + } + files.insert(fileURL.standardizedFileURL.resolvingSymlinksInPath()) + } + } + return files.sorted { $0.path < $1.path } + } + + private static func buildIndexStore( + for files: [URL], + sourceCopies: URL?, + indexStore: URL + ) throws { + let swiftcPath: String = try developerToolPath("swiftc") + let compileFiles: [URL] + if let sourceCopies { + compileFiles = try files.map { fileURL in + let relativePath: String = fileURL.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + let copiedURL: URL = sourceCopies.appending(path: relativePath) + try FileManager.default.createDirectory( + at: copiedURL.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + let source: String = try String(contentsOf: fileURL, encoding: .utf8) + try "import Foundation\n\(source)".write(to: copiedURL, atomically: true, encoding: .utf8) + return copiedURL + } + } else { + compileFiles = files + } + var buildOutput: String = "" + for (index, indexedFile) in compileFiles.enumerated() { + var arguments: [String] = [ + "-typecheck", + "-parse-as-library", + "-continue-building-after-errors", + "-module-name", + "ArchLintIndexedSources", + "-index-store-path", + indexStore.path, + "-index-file", + "-index-file-path", + indexedFile.path, + "-index-unit-output-path", + "archlint-index-\(index).o", + ] + arguments.append(indexedFile.path) + arguments.append(contentsOf: compileFiles.filter { $0 != indexedFile }.map(\.path)) + let output: String = try runProcess( + executable: swiftcPath, + arguments: arguments, + failureMessage: "swift index build failed", + allowNonZeroExit: true + ) + if !output.isEmpty { + buildOutput.append(output) + buildOutput.append("\n") + } + } + guard containsIndexRecords(at: indexStore) else { + throw ArchLintError.semanticIndexUnavailable("swift index build failed: \(buildOutput)") + } + } + + private static func indexStoreLibraryPath() throws -> String { + let toolchainRoot: URL = URL(fileURLWithPath: try developerToolPath("swiftc")) + .deletingLastPathComponent() + .deletingLastPathComponent() + let libraryPath: String = toolchainRoot.appending(path: "lib/libIndexStore.dylib").path + guard FileManager.default.fileExists(atPath: libraryPath) else { + throw ArchLintError.semanticIndexUnavailable("missing libIndexStore.dylib at \(libraryPath)") + } + return libraryPath + } + + private static func developerToolPath(_ toolName: String) throws -> String { + let output: String = try runProcess( + executable: "/usr/bin/xcrun", + arguments: ["--find", toolName], + failureMessage: "unable to locate \(toolName)" + ) + return output.trimmingCharacters(in: .whitespacesAndNewlines) + } + + @discardableResult + private static func runProcess( + executable: String, + arguments: [String], + failureMessage: String, + allowNonZeroExit: Bool = false + ) throws -> String { + let process: Process = Process() + process.executableURL = URL(fileURLWithPath: executable) + process.arguments = arguments + let outputPipe: Pipe = Pipe() + let errorPipe: Pipe = Pipe() + process.standardOutput = outputPipe + process.standardError = errorPipe + do { + try process.run() + process.waitUntilExit() + } catch { + throw ArchLintError.semanticIndexUnavailable("\(failureMessage): \(error)") + } + let output: String = String( + data: outputPipe.fileHandleForReading.readDataToEndOfFile(), + encoding: .utf8 + ) ?? "" + let errorOutput: String = String( + data: errorPipe.fileHandleForReading.readDataToEndOfFile(), + encoding: .utf8 + ) ?? "" + let combinedOutput: String = [output, errorOutput] + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + .joined(separator: "\n") + guard process.terminationStatus == 0 || allowNonZeroExit else { + throw ArchLintError.semanticIndexUnavailable( + "\(failureMessage): \(combinedOutput)" + ) + } + return combinedOutput + } + + private static func containsIndexRecords(at indexStore: URL) -> Bool { + guard + let enumerator: FileManager.DirectoryEnumerator = + FileManager.default.enumerator(at: indexStore, includingPropertiesForKeys: [.isRegularFileKey]) + else { + return false + } + for item in enumerator { + guard let fileURL: URL = item as? URL else { + continue + } + let parentName: String = fileURL.deletingLastPathComponent().lastPathComponent + let grandparentName: String = fileURL + .deletingLastPathComponent() + .deletingLastPathComponent() + .lastPathComponent + if parentName == "units" || grandparentName == "records" { + return true + } + } + return false + } + + private static func referenceSymbolName(_ name: String) -> String { + if let parenIndex: String.Index = name.firstIndex(of: "(") { + return String(name[.. [SymbolOccurrence] { + var seen: Set = [] + var result: [SymbolOccurrence] = [] + for occurrence: SymbolOccurrence in occurrences { + let key: String = [ + occurrence.location.path, + String(occurrence.location.line), + String(occurrence.location.utf8Column), + occurrence.symbol.usr, + String(occurrence.roles.rawValue), + ].joined(separator: "|") + if seen.insert(key).inserted { + result.append(occurrence) + } + } + return result + } + + private static func privateVarAlias(_ path: String) -> String { + if path.hasPrefix("/var/") { + return "/private\(path)" + } + return path + } + + private static func varAlias(_ path: String) -> String { + if path.hasPrefix("/private/var/") { + return String(path.dropFirst("/private".count)) + } + return path + } + private static func parseModuleMetadata(_ source: String) -> ModuleMetadata { var moduleType: String = "" var domain: String = "" @@ -1006,6 +1319,7 @@ enum ArchLintError: Error, CustomStringConvertible { case missingArgumentValue(String) case missingRequiredArgument(String) case missingDirectory(String) + case semanticIndexUnavailable(String) var description: String { switch self { @@ -1015,6 +1329,8 @@ enum ArchLintError: Error, CustomStringConvertible { return "\(argument) is required" case .missingDirectory(let path): return "missing directory \(path)" + case .semanticIndexUnavailable(let message): + return "swift semantic reference resolution unavailable: \(message)" } } } diff --git a/swift/test.sh b/swift/test.sh index eda04e2..74aa343 100644 --- a/swift/test.sh +++ b/swift/test.sh @@ -90,8 +90,6 @@ struct AddAccountRequest: Equatable { EOF assert_passes "$passing_fixture" -# Patch 7 is schema-only for Swift: moduleName is present, while -# qualifiedReferences stays empty until semantic resolution lands in patch 8. run_adapter "$passing_fixture" > "$tmp_root/passing-facts.json" assert_facts "$tmp_root/passing-facts.json" <<'PY' import json @@ -100,7 +98,7 @@ import sys document = json.load(open(sys.argv[1], encoding="utf-8")) shell = [item for item in document["files"] if item["path"].endswith("HTTPMailBackendClient.swift")][0] assert shell["moduleName"] == "HTTPMailBackendClient", shell -assert shell["qualifiedReferences"] == [], shell +assert "HTTPMailBackendDecider.decidePath" in shell["qualifiedReferences"], shell core = [item for item in document["files"] if item["path"].endswith("HTTPMailBackendDecider.swift")][0] assert core["moduleName"] == "HTTPMailBackendDecider", core assert core["qualifiedReferences"] == [], core @@ -146,9 +144,6 @@ final class HTTPMailBackendClient { } } EOF -# PENDING: Patch 8 -# Semantic resolution should attribute the bare cross-file decidePathForRequest -# call back to HTTPMailBackendDecider.decidePath and emit it in qualifiedReferences. run_adapter "$cross_file_bare_reference_fixture" > "$tmp_root/cross-file-bare-reference-facts.json" assert_facts "$tmp_root/cross-file-bare-reference-facts.json" <<'PY' import json @@ -156,7 +151,9 @@ import sys document = json.load(open(sys.argv[1], encoding="utf-8")) shell = [item for item in document["files"] if item["path"].endswith("HTTPMailBackendClient.swift")][0] -assert shell["qualifiedReferences"] == [], shell +assert "HTTPMailBackendDecider.decidePath" in shell["qualifiedReferences"], shell +assert "HTTPMailBackendClient.decidePathForRequest" not in shell["qualifiedReferences"], shell +assert "HTTPMailBackendDecider.decidePathForRequest" not in shell["qualifiedReferences"], shell PY missing_core_reference_fixture="$(new_fixture missing-core-reference)" From ac440bfd2065c789e1e33d8a17120302d85b3f0f Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Mon, 8 Jun 2026 14:51:15 -0400 Subject: [PATCH 2/3] [semantic-reference-resolution] Patch 8: pin IndexStoreDB for CI --- swift/Package.resolved | 18 ++++-------------- swift/Package.swift | 2 +- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/swift/Package.resolved b/swift/Package.resolved index 78a894e..008a5e1 100644 --- a/swift/Package.resolved +++ b/swift/Package.resolved @@ -1,22 +1,12 @@ { - "originHash" : "2406c40fcbb95137c75cafe8b9d120eac46499b6301d1a61512f110827bf7715", + "originHash" : "f1e78049998c073f80a81b04d5488dfa89a1abf1f06f04f9da3aefbdd97ca310", "pins" : [ { "identity" : "indexstore-db", "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/indexstore-db.git", "state" : { - "branch" : "main", - "revision" : "46a7e6467c7a3e706c57181f59bd0e08e9d14937" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", - "version" : "1.8.2" + "revision" : "cf29ef4e5e5243a3b6f518d72c6e527b5290375a" } }, { @@ -24,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-lmdb.git", "state" : { - "branch" : "main", - "revision" : "a4bc87807721c1fd114bf35464457e2db0d0e6c0" + "branch" : "release/6.2", + "revision" : "1ad9a2d80b6fcde498c2242f509bd1be7d667ff8" } }, { diff --git a/swift/Package.swift b/swift/Package.swift index 9811724..0d11bad 100644 --- a/swift/Package.swift +++ b/swift/Package.swift @@ -11,7 +11,7 @@ let package = Package( .executable(name: "SwiftArchLint", targets: ["SwiftArchLint"]) ], dependencies: [ - .package(url: "https://github.com/swiftlang/indexstore-db.git", branch: "main"), + .package(url: "https://github.com/swiftlang/indexstore-db.git", revision: "cf29ef4e5e5243a3b6f518d72c6e527b5290375a"), .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0"), .package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "603.0.1") ], From aba3b7934aada511809881f3567ff992c45323d5 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Mon, 8 Jun 2026 14:58:09 -0400 Subject: [PATCH 3/3] [semantic-reference-resolution] Patch 8: complete OCaml QCheck fixture stub --- ocaml/test.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ocaml/test.sh b/ocaml/test.sh index 1726dfc..921a9f2 100644 --- a/ocaml/test.sh +++ b/ocaml/test.sh @@ -52,13 +52,16 @@ cat > "$TMPDIR/lib/qCheck2.ml" <<'ML' module Gen = struct type 'a t = unit - let list _ = () - let small_int = () - let unit = () + let list (_ : 'a t) : 'a list t = () + let map (_ : 'a -> 'b) (_ : 'a t) : 'b t = () + let int : int t = () + let small_int : int t = () + let unit : unit t = () end module Test = struct - let make ~name:_ _ f = f + let make ~name:_ (_ : 'a Gen.t) (_ : 'a -> bool) = () + let check_exn _ = () end ML