Skip to content

Add CompilationDatabase and CompileCommand. #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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:
130 changes: 130 additions & 0 deletions Sources/Clang/CompilationDatabase.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
29 changes: 29 additions & 0 deletions Sources/Clang/TranslationUnit.swift
Original file line number Diff line number Diff line change
@@ -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`.
106 changes: 105 additions & 1 deletion Tests/ClangTests/ClangTests.swift
Original file line number Diff line number Diff line change
@@ -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<URL> = [
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)
]
}
}
24 changes: 24 additions & 0 deletions input_tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
# )
93 changes: 93 additions & 0 deletions utils/make-compile_commands.swift
Original file line number Diff line number Diff line change
@@ -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)
}