diff --git a/.travis.yml b/.travis.yml index 5df3edb..c893d82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ matrix: - export PATH=/usr/local/opt/llvm/bin:"${PATH}" - brew install llvm - sudo swift utils/make-pkgconfig.swift + - swift utils/make-compile_commands.swift script: - swift test notifications: diff --git a/Sources/Clang/CompilationDatabase.swift b/Sources/Clang/CompilationDatabase.swift new file mode 100644 index 0000000..00eb7f3 --- /dev/null +++ b/Sources/Clang/CompilationDatabase.swift @@ -0,0 +1,130 @@ +#if SWIFT_PACKAGE +import cclang +#endif + +import Foundation + +/// Error code for Compilation Database +/// +/// - noError: no error. +/// - canNotLoadDatabase: failed to load database. +public enum CompilationDatabaseError: Error { + + case canNotLoadDatabase + + init?(clang: CXCompilationDatabase_Error) { + switch clang { + case CXCompilationDatabase_CanNotLoadDatabase: + self = .canNotLoadDatabase + default: + return nil + } + } +} + +/// Contains the results of a search in the compilation database. +public struct CompileCommand: Equatable { + + // the working directory where the CompileCommand was executed from. + public let directory: String + + // the filename associated with the CompileCommand. + public let filename: String + + // the array of argument value in the compiler invocations. + public let arguments: [String] + + fileprivate init(command: CXCompileCommand) { + // get directory and filename + self.directory = clang_CompileCommand_getDirectory(command).asSwift() + self.filename = clang_CompileCommand_getFilename(command).asSwift() + + // get arguments + let args = clang_CompileCommand_getNumArgs(command) + self.arguments = (0 ..< args).map { i in + return clang_CompileCommand_getArg(command, i).asSwift() + } + + // MARK: - unsupported api by cclang yet? + // let mappedSourcesCount = clang_CompileCommand_getNumMappedSources(command) + // (0 ..< mappedSourcesCount).forEach { i in + // let path = clang_CompileCommand_getMappedSourcePath(command, UInt32(i)).asSwift() + // let content = clang_CompileCommand_getMappedSourceContent(command, UInt32(i)).asSwift() + // } + } +} + +/// A compilation database holds all information used to compile files in a project. +public class CompilationDatabase { + let db: CXCompilationDatabase + private let owned: Bool + + public init(directory: String) throws { + var err = CXCompilationDatabase_NoError + + // check `compile_commands.json` file existence in directory folder. + let cmdFile = URL(fileURLWithPath: directory, isDirectory: true) + .appendingPathComponent("compile_commands.json").path + guard FileManager.default.fileExists(atPath: cmdFile) else { + throw CompilationDatabaseError.canNotLoadDatabase + } + + // initialize compilation db + self.db = clang_CompilationDatabase_fromDirectory(directory, &err) + if let error = CompilationDatabaseError(clang: err) { + throw error + } + + self.owned = true + } + + /// the array of all compile command in the compilation database. + public lazy private(set) var compileCommands: [CompileCommand] = { + guard let commands = clang_CompilationDatabase_getAllCompileCommands(self.db) else { + return [] + } + // the compileCommands needs to be disposed. + defer { + clang_CompileCommands_dispose(commands) + } + + let count = clang_CompileCommands_getSize(commands) + return (0 ..< count).map { i in + // get compile command + guard let cmd = clang_CompileCommands_getCommand(commands, UInt32(i)) else { + fatalError("Failed to get compile command for an index \(i)") + } + return CompileCommand(command: cmd) + } + }() + + + /// Returns the array of compile command for a file. + /// + /// - Parameter filename: a filename containing directory. + /// - Returns: the array of compile command. + public func compileCommands(forFile filename: String) -> [CompileCommand] { + guard let commands = clang_CompilationDatabase_getCompileCommands(self.db, filename) else { + fatalError("failed to load compileCommands for \(filename).") + } + // the compileCommands needs to be disposed. + defer { + clang_CompileCommands_dispose(commands) + } + + let size = clang_CompileCommands_getSize(commands) + + return (0 ..< size).map { i in + guard let cmd = clang_CompileCommands_getCommand(commands, UInt32(i)) else { + fatalError("Failed to get compile command for an index \(i)") + } + return CompileCommand(command: cmd) + } + } + + deinit { + if self.owned { + clang_CompilationDatabase_dispose(self.db) + } + } +} diff --git a/Sources/Clang/TranslationUnit.swift b/Sources/Clang/TranslationUnit.swift index ba4d73b..d8816d2 100644 --- a/Sources/Clang/TranslationUnit.swift +++ b/Sources/Clang/TranslationUnit.swift @@ -219,6 +219,35 @@ public class TranslationUnit { index: index, commandLineArgs: args, options: options) + } + + /// Creates a `TranslationUnit` using the CompileCommand. + /// the name of the source file is expected to reside in the command line arguments. + /// + /// - Parameters: + /// - command: The compile command initialized by the CompilationDatabase. + /// (load data from the `compile_commands.json` file generated by CMake.) + /// - index: The index (optional, will use a default index if not + /// provided) + /// - options: Options for how to handle the parsed file + /// - throws: `ClangError` if the translation unit could not be created + /// successfully. + public init(compileCommand command: CompileCommand, + index: Index = Index(), + options: TranslationUnitOptions = [], + unsavedFiles: [UnsavedFile] = []) throws { + self.clang = try command.arguments.withUnsafeCStringBuffer { argC in + var cxUnsavedFiles = unsavedFiles.map { $0.clang } + let unit: CXTranslationUnit? = clang_createTranslationUnitFromSourceFile(index.clang, + nil, + Int32(argC.count), argC.baseAddress, + UInt32(cxUnsavedFiles.count), &cxUnsavedFiles) + guard unit != nil else { + throw ClangError.astRead + } + return unit! + } + self.owned = true } /// Creates a `TranslationUnit` from an AST file generated by `-emit-ast`. diff --git a/Tests/ClangTests/ClangTests.swift b/Tests/ClangTests/ClangTests.swift index 8cab2d0..7d9b520 100644 --- a/Tests/ClangTests/ClangTests.swift +++ b/Tests/ClangTests/ClangTests.swift @@ -168,7 +168,7 @@ class ClangTests: XCTestCase { XCTFail("\(error)") } } - + func testParsingWithUnsavedFile() { do { let filename = "input_tests/unsaved-file.c" @@ -248,6 +248,106 @@ class ClangTests: XCTestCase { XCTFail("\(error)") } } + + // ${projectRoot}/ folder URL. + var projectRoot: URL { + return URL(fileURLWithPath: #file).appendingPathComponent("../../../", isDirectory: true).standardized + } + + // ${projectRoot}/input_tests folder URL. + var inputTestUrl: URL { + return projectRoot.appendingPathComponent("input_tests", isDirectory: true) + } + + // ${projectRoot}/.build/build.input_tests folder URL + var buildUrl: URL { + return projectRoot.appendingPathComponent(".build/build.input_tests", isDirectory: true) + } + + func testInitCompilationDB() { + do { + let db = try CompilationDatabase(directory: buildUrl.path) + XCTAssertNotNil(db) + XCTAssertEqual(db.compileCommands.count, 7) + + } catch { + XCTFail("\(error)") + } + } + + func testCompileCommand() { + do { + // intialize CompilationDatabase. + let db = try CompilationDatabase(directory: buildUrl.path) + XCTAssertNotNil(db) + + // test first compileCommand + let cmd = db.compileCommands[0] + XCTAssertEqual(cmd.directory, buildUrl.path) + XCTAssertGreaterThan(cmd.arguments.count, 0) + + // test all compileCommands + let filenames = db.compileCommands.map { URL(fileURLWithPath: $0.filename) } + + let expectation: Set = [ + inputTestUrl.appendingPathComponent("inclusion.c"), + inputTestUrl.appendingPathComponent("index-action.c"), + inputTestUrl.appendingPathComponent("init-ast.c"), + inputTestUrl.appendingPathComponent("is-from-main-file.c"), + inputTestUrl.appendingPathComponent("locations.c"), + inputTestUrl.appendingPathComponent("reparse.c"), + inputTestUrl.appendingPathComponent("unsaved-file.c"), + ] + XCTAssertEqual(Set(filenames), expectation) + } catch { + XCTFail("\(error)") + } + } + + func testCompileCommandForFile() { + do { + // intialize CompilationDatabase. + let db = try CompilationDatabase(directory: buildUrl.path) + XCTAssertNotNil(db) + + let inclusionFile = inputTestUrl.appendingPathComponent("inclusion.c") + + // test compileCommand for file `inclusion.c` + let cmds = db.compileCommands(forFile: inclusionFile.path) + XCTAssertEqual(cmds.count, 1) + XCTAssertEqual(cmds[0].filename, inclusionFile.path) + XCTAssertEqual(cmds[0].directory, buildUrl.path) + XCTAssertGreaterThan(cmds[0].arguments.count, 0) + } catch { + XCTFail("\(error)") + } + } + + func testInitTranslationUnitUsingCompileCommand() { + do { + // intialize CompilationDatabase. + let filename = inputTestUrl.path + "/locations.c" + let db = try CompilationDatabase(directory: buildUrl.path) + + // get first compile command and initialize TranslationUnit using it. + let cmd = db.compileCommands(forFile: filename).first! + let unit = try TranslationUnit(compileCommand: cmd) + + // verify. + let file = unit.getFile(for: unit.spelling)! + let start = SourceLocation(translationUnit: unit, file: file, offset: 19) + let end = SourceLocation(translationUnit: unit, file: file, offset: 59) + let range = SourceRange(start: start, end: end) + + XCTAssertEqual( + unit.tokens(in: range).map { $0.spelling(in: unit) }, + ["int", "a", "=", "1", ";", "int", "b", "=", "1", ";", "int", "c", "=", + "a", "+", "b", ";"] + ) + } catch { + XCTFail("\(error)") + } + } static var allTests : [(String, (ClangTests) -> () throws -> Void)] { return [ @@ -262,6 +362,10 @@ class ClangTests: XCTestCase { ("testIsFromMainFile", testIsFromMainFile), ("testVisitInclusion", testVisitInclusion), ("testGetFile", testGetFile), + ("testInitCompilationDB", testInitCompilationDB), + ("testCompileCommand", testCompileCommand), + ("testCompileCommandForFile", testCompileCommandForFile), + ("testInitTranslationUnitUsingCompileCommand", testInitTranslationUnitUsingCompileCommand) ] } } diff --git a/input_tests/CMakeLists.txt b/input_tests/CMakeLists.txt new file mode 100644 index 0000000..22b5906 --- /dev/null +++ b/input_tests/CMakeLists.txt @@ -0,0 +1,24 @@ +project(InputTests) +cmake_minimum_required(VERSION 3.3) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_library(InputTests + inclusion.c index-action.c init-ast.c is-from-main-file.c locations.c reparse.c unsaved-file.c + inclusion-header.h +) + +target_include_directories(InputTests +PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +PRIVATE +) + +# target_compile_features(InputTests +# PUBLIC +# PRIVATE +# ) +# target_link_libraries(InputTests +# PUBLIC +# PRIVATE +# ) \ No newline at end of file diff --git a/utils/make-compile_commands.swift b/utils/make-compile_commands.swift new file mode 100644 index 0000000..d5dacc2 --- /dev/null +++ b/utils/make-compile_commands.swift @@ -0,0 +1,93 @@ +#!/usr/bin/env swift +import Foundation + +#if os(Linux) + typealias Process = Task +#elseif os(macOS) +#endif + + /// Runs the specified program at the provided path. + /// + /// - Parameters: + /// - exec: The full path of the executable binary. + /// - dir: The process working directory. If this is nil, the current directory will be used. + /// - args: The arguments you wish to pass to the process. + /// - Returns: The standard output of the process, or nil if it was empty. +func run(exec: String, at dir: URL? = nil, args: [String] = []) -> String? { + let pipe = Pipe() + let process = Process() + + process.executableURL = URL(fileURLWithPath: exec) + process.arguments = args + process.standardOutput = pipe + + if let dir = dir { + print("Running \(dir.path) \(exec) \(args.joined(separator: " "))...") + process.currentDirectoryURL = dir + } else { + print("Running \(args.joined(separator: " "))...") + } + + + process.launch() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + guard let result = String(data: data, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines), + !result.isEmpty else { return nil } + return result +} + +/// Finds the location of the provided binary on your system. +func which(_ name: String) -> String? { + return run(exec: "/usr/bin/which", args: [name]) +} + +extension String: Error { + /// Replaces all occurrences of characters in the provided set with + /// the provided string. + func replacing(charactersIn characterSet: CharacterSet, + with separator: String) -> String { + let components = self.components(separatedBy: characterSet) + return components.joined(separator: separator) + } +} + +func build() throws { + let projectRoot = URL(fileURLWithPath: #file) + .deletingLastPathComponent() + .deletingLastPathComponent() + + // ${project_root}/.build/build.input_tests url + let buildURL = projectRoot.appendingPathComponent(".build/build.input_tests", isDirectory: true) + let sourceURL = projectRoot.appendingPathComponent("input_tests", isDirectory: true) + + print("project root \(projectRoot.path)") + print("build folder \(buildURL.path)") + // print(sourceURL.path) + + // make `${projectRoot}/.build.input_tests` folder if it doesn't exist. + if !FileManager.default.fileExists(atPath: buildURL.path) { + try FileManager.default.createDirectory(at: buildURL, withIntermediateDirectories: true) + } + + // get `cmake` command path. + guard let cmake = which("cmake") else { return } + + // run `cd {buildPath}; cmake ${sourcePath}` command. + let results = run(exec: cmake, at: buildURL, args: [sourceURL.path]) + print(results!) +} + +do { + try build() +} catch { +#if os(Linux) + // FIXME: Printing the thrown error that here crashes on Linux. + print("Unexpected error occured while writing the config file. Check permissions and try again.") +#else + print("error: \(error)") +#endif + exit(-1) +}