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
Expand Up @@ -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:
Expand Down
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
Expand Up @@ -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`.
Expand Down
106 changes: 105 additions & 1 deletion Tests/ClangTests/ClangTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ class ClangTests: XCTestCase {
XCTFail("\(error)")
}
}

func testParsingWithUnsavedFile() {
do {
let filename = "input_tests/unsaved-file.c"
Expand Down Expand Up @@ -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 [
Expand All @@ -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
# )
Loading