diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 9a32ee4065..8e16ef1c6c 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,3 +1,3 @@ add_subdirectory(clang-interpreter) add_subdirectory(PrintFunctionNames) - +add_subdirectory(Tooling) diff --git a/examples/Tooling/CMakeLists.txt b/examples/Tooling/CMakeLists.txt new file mode 100644 index 0000000000..257d3ea8ea --- /dev/null +++ b/examples/Tooling/CMakeLists.txt @@ -0,0 +1,6 @@ +set(LLVM_USED_LIBS clangTooling clangBasic) + +add_clang_executable(clang-check + ClangCheck.cpp + ) + diff --git a/examples/Tooling/ClangCheck.cpp b/examples/Tooling/ClangCheck.cpp new file mode 100644 index 0000000000..9f0b1dde70 --- /dev/null +++ b/examples/Tooling/ClangCheck.cpp @@ -0,0 +1,108 @@ +//===- examples/Tooling/ClangCheck.cpp - Clang check tool -----------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// This file implements a clang-check tool that runs the +// clang::SyntaxOnlyAction over a number of translation units. +// +// Usage: +// clang-check ... +// +// Where is a CMake build directory in which a file named +// compile_commands.json exists (enable -DCMAKE_EXPORT_COMPILE_COMMANDS in +// CMake to get this output). +// +// ... specify the paths of files in the CMake source tree. This path +// is looked up in the compile command database. If the path of a file is +// absolute, it needs to point into CMake's source tree. If the path is +// relative, the current working directory needs to be in the CMake source +// tree and the file must be in a subdirectory of the current working +// directory. "./" prefixes in the relative files will be automatically +// removed, but the rest of a relative path must be a suffix of a path in +// the compile command line database. +// +// For example, to use clang-check on all files in a subtree of the source +// tree, use: +// /path/to/cmake/sources $ find . -name '*.cpp' \ +// |xargs clang-check /path/to/cmake/build +// +//===----------------------------------------------------------------------===// + +#include "clang/Frontend/FrontendActions.h" +#include "clang/Tooling/Tooling.h" +#include "llvm/ADT/OwningPtr.h" +#include "llvm/ADT/Twine.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" +#include "llvm/Support/system_error.h" + +/// \brief Returns the absolute path of 'File', by prepending it with +/// 'BaseDirectory' if 'File' is not absolute. Otherwise returns 'File'. +/// If 'File' starts with "./", the returned path will not contain the "./". +/// Otherwise, the returned path will contain the literal path-concatenation of +/// 'BaseDirectory' and 'File'. +/// +/// \param File Either an absolute or relative path. +/// \param BaseDirectory An absolute path. +/// +/// FIXME: Put this somewhere where it is more generally available. +static std::string GetAbsolutePath( + llvm::StringRef File, llvm::StringRef BaseDirectory) { + assert(llvm::sys::path::is_absolute(BaseDirectory)); + if (llvm::sys::path::is_absolute(File)) { + return File; + } + llvm::StringRef RelativePath(File); + if (RelativePath.startswith("./")) { + RelativePath = RelativePath.substr(strlen("./")); + } + llvm::SmallString<1024> AbsolutePath(BaseDirectory); + llvm::sys::path::append(AbsolutePath, RelativePath); + return AbsolutePath.str(); +} + +int main(int argc, char **argv) { + if (argc < 3) { + llvm::outs() << "Usage: " << argv[0] << " " + << " ...\n"; + return 1; + } + // FIXME: We should pull how to find the database into the Tooling package. + llvm::OwningPtr JsonDatabase; + llvm::SmallString<1024> JsonDatabasePath(argv[1]); + llvm::sys::path::append(JsonDatabasePath, "compile_commands.json"); + llvm::error_code Result = + llvm::MemoryBuffer::getFile(JsonDatabasePath, JsonDatabase); + if (Result != 0) { + llvm::outs() << "Error while opening JSON database: " << Result.message() + << "\n"; + return 1; + } + llvm::StringRef BaseDirectory(::getenv("PWD")); + for (int I = 2; I < argc; ++I) { + llvm::SmallString<1024> File(GetAbsolutePath(argv[I], BaseDirectory)); + llvm::outs() << "Processing " << File << ".\n"; + std::string ErrorMessage; + clang::tooling::CompileCommand LookupResult = + clang::tooling::FindCompileArgsInJsonDatabase( + File.str(), JsonDatabase->getBuffer(), ErrorMessage); + if (!LookupResult.CommandLine.empty()) { + if (!clang::tooling::RunToolWithFlags( + new clang::SyntaxOnlyAction, + LookupResult.CommandLine.size(), + clang::tooling::CommandLineToArgv( + &LookupResult.CommandLine).data())) { + llvm::outs() << "Error while processing " << File << ".\n"; + } + } else { + llvm::outs() << "Skipping " << File << ". Command line not found.\n"; + } + } + return 0; +} diff --git a/include/clang/Tooling/Tooling.h b/include/clang/Tooling/Tooling.h index f07adeb832..6ccccd0bff 100644 --- a/include/clang/Tooling/Tooling.h +++ b/include/clang/Tooling/Tooling.h @@ -16,6 +16,8 @@ #define LLVM_CLANG_TOOLING_TOOLING_H #include "llvm/ADT/StringRef.h" +#include +#include namespace clang { @@ -26,12 +28,53 @@ namespace tooling { /// \brief Runs (and deletes) the tool on 'Code' with the -fsynatx-only flag. /// /// \param ToolAction The action to run over the code. -// \param Code C++ code. +/// \param Code C++ code. /// /// \return - True if 'ToolAction' was successfully executed. bool RunSyntaxOnlyToolOnCode( clang::FrontendAction *ToolAction, llvm::StringRef Code); +/// \brief Runs (and deletes) the tool with the given Clang flags. +/// +/// \param ToolAction The action to run over the code. +/// \param Argc The number of elements in Argv. +/// \param Argv The command line arguments, including the path the binary +/// was started with (Argv[0]). +bool RunToolWithFlags( + clang::FrontendAction* ToolAction, int Argc, char *Argv[]); + +/// \brief Converts a vector into a vector suitable to pass +/// to main-style functions taking (int Argc, char *Argv[]). +std::vector CommandLineToArgv(const std::vector* Command); + +/// \brief Specifies the working directory and command of a compilation. +struct CompileCommand { + /// \brief The working directory the command was executed from. + std::string Directory; + + /// \brief The command line that was executed. + std::vector CommandLine; +}; + +/// \brief Looks up the compile command for 'FileName' in 'JsonDatabase'. +/// +/// \param FileName The path to an input file for which we want the compile +/// command line. If the 'JsonDatabase' was created by CMake, this must be +/// an absolute path inside the CMake source directory which does not have +/// symlinks resolved. +/// +/// \param JsonDatabase A JSON formatted list of compile commands. This lookup +/// command supports only a subset of the JSON standard as written by CMake. +/// +/// \param ErrorMessage If non-empty, an error occurred and 'ErrorMessage' will +/// be set to contain the error message. In this case CompileCommand will +/// contain an empty directory and command line. +/// +/// \see JsonCompileCommandLineDatabase +CompileCommand FindCompileArgsInJsonDatabase( + llvm::StringRef FileName, llvm::StringRef JsonDatabase, + std::string &ErrorMessage); + } // end namespace tooling } // end namespace clang diff --git a/lib/Tooling/CMakeLists.txt b/lib/Tooling/CMakeLists.txt index 6736d6537d..f52cf6c891 100644 --- a/lib/Tooling/CMakeLists.txt +++ b/lib/Tooling/CMakeLists.txt @@ -1,5 +1,6 @@ SET(LLVM_USED_LIBS clangBasic clangFrontend clangAST) add_clang_library(clangTooling + JsonCompileCommandLineDatabase.cpp Tooling.cpp ) diff --git a/lib/Tooling/JsonCompileCommandLineDatabase.cpp b/lib/Tooling/JsonCompileCommandLineDatabase.cpp new file mode 100644 index 0000000000..7f027cfbea --- /dev/null +++ b/lib/Tooling/JsonCompileCommandLineDatabase.cpp @@ -0,0 +1,214 @@ +//===--- JsonCompileCommandLineDatabase.cpp - Simple JSON database --------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// This file implements reading a compile command line database, as written +// out for example by CMake. +// +//===----------------------------------------------------------------------===// + +#include "JsonCompileCommandLineDatabase.h" +#include "llvm/ADT/Twine.h" + +namespace clang { +namespace tooling { + +namespace { + +// A parser for JSON escaped strings of command line arguments with \-escaping +// for quoted arguments (see the documentation of UnescapeJsonCommandLine(...)). +class CommandLineArgumentParser { + public: + CommandLineArgumentParser(llvm::StringRef CommandLine) + : Input(CommandLine), Position(Input.begin()-1) {} + + std::vector Parse() { + bool HasMoreInput = true; + while (HasMoreInput && NextNonWhitespace()) { + std::string Argument; + HasMoreInput = ParseStringInto(Argument); + CommandLine.push_back(Argument); + } + return CommandLine; + } + + private: + // All private methods return true if there is more input available. + + bool ParseStringInto(std::string &String) { + do { + if (*Position == '"') { + if (!ParseQuotedStringInto(String)) return false; + } else { + if (!ParseFreeStringInto(String)) return false; + } + } while (*Position != ' '); + return true; + } + + bool ParseQuotedStringInto(std::string &String) { + if (!Next()) return false; + while (*Position != '"') { + if (!SkipEscapeCharacter()) return false; + String.push_back(*Position); + if (!Next()) return false; + } + return Next(); + } + + bool ParseFreeStringInto(std::string &String) { + do { + if (!SkipEscapeCharacter()) return false; + String.push_back(*Position); + if (!Next()) return false; + } while (*Position != ' ' && *Position != '"'); + return true; + } + + bool SkipEscapeCharacter() { + if (*Position == '\\') { + return Next(); + } + return true; + } + + bool NextNonWhitespace() { + do { + if (!Next()) return false; + } while (*Position == ' '); + return true; + } + + bool Next() { + ++Position; + if (Position == Input.end()) return false; + // Remove the JSON escaping first. This is done unconditionally. + if (*Position == '\\') ++Position; + return Position != Input.end(); + } + + const llvm::StringRef Input; + llvm::StringRef::iterator Position; + std::vector CommandLine; +}; + +} // end namespace + +std::vector UnescapeJsonCommandLine( + llvm::StringRef JsonEscapedCommandLine) { + CommandLineArgumentParser parser(JsonEscapedCommandLine); + return parser.Parse(); +} + +JsonCompileCommandLineParser::JsonCompileCommandLineParser( + const llvm::StringRef Input, CompileCommandHandler *CommandHandler) + : Input(Input), Position(Input.begin()-1), CommandHandler(CommandHandler) {} + +bool JsonCompileCommandLineParser::Parse() { + NextNonWhitespace(); + return ParseTranslationUnits(); +} + +std::string JsonCompileCommandLineParser::GetErrorMessage() const { + return ErrorMessage; +} + +bool JsonCompileCommandLineParser::ParseTranslationUnits() { + if (!ConsumeOrError('[', "at start of compile command file")) return false; + if (!ParseTranslationUnit(/*First=*/true)) return false; + while (Consume(',')) { + if (!ParseTranslationUnit(/*First=*/false)) return false; + } + if (!ConsumeOrError(']', "at end of array")) return false; + if (CommandHandler != NULL) { + CommandHandler->EndTranslationUnits(); + } + return true; +} + +bool JsonCompileCommandLineParser::ParseTranslationUnit(bool First) { + if (First) { + if (!Consume('{')) return true; + } else { + if (!ConsumeOrError('{', "at start of object")) return false; + } + if (!Consume('}')) { + if (!ParseObjectKeyValuePairs()) return false; + if (!ConsumeOrError('}', "at end of object")) return false; + } + if (CommandHandler != NULL) { + CommandHandler->EndTranslationUnit(); + } + return true; +} + +bool JsonCompileCommandLineParser::ParseObjectKeyValuePairs() { + do { + llvm::StringRef Key; + if (!ParseString(Key)) return false; + if (!ConsumeOrError(':', "between name and value")) return false; + llvm::StringRef Value; + if (!ParseString(Value)) return false; + if (CommandHandler != NULL) { + CommandHandler->HandleKeyValue(Key, Value); + } + } while (Consume(',')); + return true; +} + +bool JsonCompileCommandLineParser::ParseString(llvm::StringRef &String) { + if (!ConsumeOrError('"', "at start of string")) return false; + llvm::StringRef::iterator First = Position; + llvm::StringRef::iterator Last = Position; + while (!Consume('"')) { + Consume('\\'); + ++Position; + // We need to store Position, as Consume will change Last before leaving + // the loop. + Last = Position; + } + String = llvm::StringRef(First, Last - First); + return true; +} + +bool JsonCompileCommandLineParser::Consume(char C) { + if (Position == Input.end()) return false; + if (*Position != C) return false; + NextNonWhitespace(); + return true; +} + +bool JsonCompileCommandLineParser::ConsumeOrError( + char C, llvm::StringRef Message) { + if (!Consume(C)) { + SetExpectError(C, Message); + return false; + } + return true; +} + +void JsonCompileCommandLineParser::SetExpectError( + char C, llvm::StringRef Message) { + ErrorMessage = (llvm::Twine("'") + llvm::StringRef(&C, 1) + + "' expected " + Message + ".").str(); +} + +void JsonCompileCommandLineParser::NextNonWhitespace() { + do { + ++Position; + } while (IsWhitespace()); +} + +bool JsonCompileCommandLineParser::IsWhitespace() { + if (Position == Input.end()) return false; + return (*Position == ' ' || *Position == '\t' || + *Position == '\n' || *Position == '\r'); +} + +} // end namespace tooling +} // end namespace clang diff --git a/lib/Tooling/JsonCompileCommandLineDatabase.h b/lib/Tooling/JsonCompileCommandLineDatabase.h new file mode 100644 index 0000000000..dcd1e4bbbb --- /dev/null +++ b/lib/Tooling/JsonCompileCommandLineDatabase.h @@ -0,0 +1,107 @@ +//===--- JsonCompileCommandLineDatabase - Simple JSON database --*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// This file implements reading a compile command line database, as written +// out for example by CMake. It only supports the subset of the JSON standard +// that is needed to parse the CMake output. +// See http://www.json.org/ for the full standard. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLING_JSON_COMPILE_COMMAND_LINE_DATABASE_H +#define LLVM_CLANG_TOOLING_JSON_COMPILE_COMMAND_LINE_DATABASE_H + +#include "llvm/ADT/StringRef.h" +#include +#include + +namespace clang { +namespace tooling { + +/// \brief Converts a JSON escaped command line to a vector of arguments. +/// +/// \param JsonEscapedCommandLine The escaped command line as a string. This +/// is assumed to be escaped as a JSON string (e.g. " and \ are escaped). +/// In addition, any arguments containing spaces are assumed to be \-escaped +/// +/// For example, the input (|| denoting non C-escaped strings): +/// |./call a \"b \\\" c \\\\ \" d| +/// would yield: +/// [ |./call|, |a|, |b " c \ |, |d| ]. +std::vector UnescapeJsonCommandLine( + llvm::StringRef JsonEscapedCommandLine); + +/// \brief Interface for users of the JsonCompileCommandLineParser. +class CompileCommandHandler { + public: + virtual ~CompileCommandHandler() {}; + + /// \brief Called after all translation units are parsed. + virtual void EndTranslationUnits() {} + + /// \brief Called at the end of a single translation unit. + virtual void EndTranslationUnit() {} + + /// \brief Called for every (Key, Value) pair in a translation unit + /// description. + virtual void HandleKeyValue(llvm::StringRef Key, llvm::StringRef Value) {} +}; + +/// \brief A JSON parser that supports the subset of JSON needed to parse +/// JSON compile command line databases as written out by CMake. +/// +/// The supported subset describes a list of compile command lines for +/// each processed translation unit. The translation units are stored in a +/// JSON array, where each translation unit is described by a JSON object +/// containing (Key, Value) pairs for the working directory the compile command +/// line was executed from, the main C/C++ input file of the translation unit +/// and the actual compile command line, for example: +/// [ +/// { +/// "file":"/file.cpp", +/// "directory":"/", +/// "command":"/cc /file.cpp" +/// } +/// ] +class JsonCompileCommandLineParser { + public: + /// \brief Create a parser on 'Input', calling 'CommandHandler' to handle the + /// parsed constructs. 'CommandHandler' may be NULL in order to just check + /// the validity of 'Input'. + JsonCompileCommandLineParser(const llvm::StringRef Input, + CompileCommandHandler *CommandHandler); + + /// \brief Parses the specified input. Returns true if no parsing errors were + /// foudn. + bool Parse(); + + /// \brief Returns an error message if Parse() returned false previously. + std::string GetErrorMessage() const; + + private: + bool ParseTranslationUnits(); + bool ParseTranslationUnit(bool First); + bool ParseObjectKeyValuePairs(); + bool ParseString(llvm::StringRef &String); + bool Consume(char C); + bool ConsumeOrError(char C, llvm::StringRef Message); + void NextNonWhitespace(); + bool IsWhitespace(); + void SetExpectError(char C, llvm::StringRef Message); + + const llvm::StringRef Input; + llvm::StringRef::iterator Position; + std::string ErrorMessage; + CompileCommandHandler * const CommandHandler; +}; + +} // end namespace tooling +} // end namespace clang + +#endif // LLVM_CLANG_TOOLING_JSON_COMPILE_COMMAND_LINE_DATABASE_H diff --git a/lib/Tooling/Tooling.cpp b/lib/Tooling/Tooling.cpp index a5bd1acf0e..8fc76b6b75 100644 --- a/lib/Tooling/Tooling.cpp +++ b/lib/Tooling/Tooling.cpp @@ -29,14 +29,41 @@ #include "clang/Frontend/FrontendAction.h" #include "clang/Frontend/FrontendDiagnostic.h" #include "clang/Frontend/TextDiagnosticPrinter.h" - -#include +#include "JsonCompileCommandLineDatabase.h" #include -#include +#include namespace clang { namespace tooling { +namespace { + +// Checks that the input conforms to the argv[] convention as in +// main(). Namely: +// - it must contain at least a program path, +// - argv[0], ..., and argv[argc - 1] mustn't be NULL, and +// - argv[argc] must be NULL. +void ValidateArgv(int argc, char* argv[]) { + if (argc < 1) { + fprintf(stderr, "ERROR: argc is %d. It must be >= 1.\n", argc); + abort(); + } + + for (int i = 0; i < argc; ++i) { + if (argv[i] == NULL) { + fprintf(stderr, "ERROR: argv[%d] is NULL.\n", i); + abort(); + } + } + + if (argv[argc] != NULL) { + fprintf(stderr, "ERROR: argv[argc] isn't NULL.\n"); + abort(); + } +} + +} // end namespace + // FIXME: This file contains structural duplication with other parts of the // code that sets up a compiler to run tools on it, and we should refactor // it to be based on the same framework. @@ -156,6 +183,25 @@ std::vector CommandLineToArgv(const std::vector* Command) { return Result; } +bool RunToolWithFlags( + clang::FrontendAction* ToolAction, int Args, char* Argv[]) { + ValidateArgv(Args, Argv); + const llvm::OwningPtr Diagnostics(NewTextDiagnostics()); + const llvm::OwningPtr Driver( + NewDriver(Diagnostics.get(), Argv[0])); + const llvm::OwningPtr Compilation( + Driver->BuildCompilation(llvm::ArrayRef(Argv, Args))); + const clang::driver::ArgStringList* const CC1Args = GetCC1Arguments( + Diagnostics.get(), Compilation.get()); + if (CC1Args == NULL) { + return false; + } + llvm::OwningPtr Invocation( + NewInvocation(Diagnostics.get(), *CC1Args)); + return RunInvocation(Argv[0], Compilation.get(), Invocation.take(), + *CC1Args, ToolAction); +} + /// \brief Runs 'ToolAction' on the code specified by 'FileContents'. /// /// \param FileContents A mapping from file name to source code. For each @@ -213,6 +259,64 @@ bool RunSyntaxOnlyToolOnCode( FileContents, ToolAction); } +namespace { + +// A CompileCommandHandler implementation that finds compile commands for a +// specific input file. +// +// FIXME: Implement early exit when JsonCompileCommandLineParser supports it. +class FindHandler : public clang::tooling::CompileCommandHandler { + public: + explicit FindHandler(llvm::StringRef File) + : FileToMatch(File), FoundMatchingCommand(false) {}; + + virtual void EndTranslationUnits() { + if (!FoundMatchingCommand && ErrorMessage.empty()) { + ErrorMessage = "ERROR: No matching command found."; + } + } + + virtual void EndTranslationUnit() { + if (File == FileToMatch) { + FoundMatchingCommand = true; + MatchingCommand.Directory = Directory; + MatchingCommand.CommandLine = UnescapeJsonCommandLine(Command); + } + } + + virtual void HandleKeyValue(llvm::StringRef Key, llvm::StringRef Value) { + if (Key == "directory") { Directory = Value; } + else if (Key == "file") { File = Value; } + else if (Key == "command") { Command = Value; } + else { + ErrorMessage = (llvm::Twine("Unknown key: \"") + Key + "\"").str(); + } + } + + const llvm::StringRef FileToMatch; + bool FoundMatchingCommand; + CompileCommand MatchingCommand; + std::string ErrorMessage; + + llvm::StringRef Directory; + llvm::StringRef File; + llvm::StringRef Command; +}; + +} // end namespace + +CompileCommand FindCompileArgsInJsonDatabase( + llvm::StringRef FileName, llvm::StringRef JsonDatabase, + std::string &ErrorMessage) { + FindHandler find_handler(FileName); + JsonCompileCommandLineParser parser(JsonDatabase, &find_handler); + if (!parser.Parse()) { + ErrorMessage = parser.GetErrorMessage(); + return CompileCommand(); + } + return find_handler.MatchingCommand; +} + } // end namespace tooling } // end namespace clang diff --git a/unittests/CMakeLists.txt b/unittests/CMakeLists.txt index dd6bad5fa9..f7f495ee9d 100644 --- a/unittests/CMakeLists.txt +++ b/unittests/CMakeLists.txt @@ -64,3 +64,8 @@ add_clang_unittest(Tooling Tooling/ToolingTest.cpp USED_LIBS gtest gtest_main clangTooling ) + +add_clang_unittest(JsonCompileCommandLineDatabase + Tooling/JsonCompileCommandLineDatabaseTest.cpp + USED_LIBS gtest gtest_main clangTooling + ) diff --git a/unittests/Tooling/JsonCompileCommandLineDatabaseTest.cpp b/unittests/Tooling/JsonCompileCommandLineDatabaseTest.cpp new file mode 100644 index 0000000000..d875293e5d --- /dev/null +++ b/unittests/Tooling/JsonCompileCommandLineDatabaseTest.cpp @@ -0,0 +1,232 @@ +//===- unittest/Tooling/JsonCompileCommandLineDatabaseTest ----------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "../../lib/Tooling/JsonCompileCommandLineDatabase.h" +#include "gtest/gtest.h" + +namespace clang { +namespace tooling { + +TEST(UnescapeJsonCommandLine, ReturnsEmptyArrayOnEmptyString) { + std::vector Result = UnescapeJsonCommandLine(""); + EXPECT_TRUE(Result.empty()); +} + +TEST(UnescapeJsonCommandLine, SplitsOnSpaces) { + std::vector Result = UnescapeJsonCommandLine("a b c"); + ASSERT_EQ(3ul, Result.size()); + EXPECT_EQ("a", Result[0]); + EXPECT_EQ("b", Result[1]); + EXPECT_EQ("c", Result[2]); +} + +TEST(UnescapeJsonCommandLine, MungesMultipleSpaces) { + std::vector Result = UnescapeJsonCommandLine(" a b "); + ASSERT_EQ(2ul, Result.size()); + EXPECT_EQ("a", Result[0]); + EXPECT_EQ("b", Result[1]); +} + +TEST(UnescapeJsonCommandLine, UnescapesBackslashCharacters) { + std::vector Backslash = UnescapeJsonCommandLine("a\\\\\\\\"); + ASSERT_EQ(1ul, Backslash.size()); + EXPECT_EQ("a\\", Backslash[0]); + std::vector Quote = UnescapeJsonCommandLine("a\\\\\\\""); + ASSERT_EQ(1ul, Quote.size()); + EXPECT_EQ("a\"", Quote[0]); +} + +TEST(UnescapeJsonCommandLine, DoesNotMungeSpacesBetweenQuotes) { + std::vector Result = UnescapeJsonCommandLine("\\\" a b \\\""); + ASSERT_EQ(1ul, Result.size()); + EXPECT_EQ(" a b ", Result[0]); +} + +TEST(UnescapeJsonCommandLine, AllowsMultipleQuotedArguments) { + std::vector Result = UnescapeJsonCommandLine( + " \\\" a \\\" \\\" b \\\" "); + ASSERT_EQ(2ul, Result.size()); + EXPECT_EQ(" a ", Result[0]); + EXPECT_EQ(" b ", Result[1]); +} + +TEST(UnescapeJsonCommandLine, AllowsEmptyArgumentsInQuotes) { + std::vector Result = UnescapeJsonCommandLine( + "\\\"\\\"\\\"\\\""); + ASSERT_EQ(1ul, Result.size()); + EXPECT_TRUE(Result[0].empty()) << Result[0]; +} + +TEST(UnescapeJsonCommandLine, ParsesEscapedQuotesInQuotedStrings) { + std::vector Result = UnescapeJsonCommandLine( + "\\\"\\\\\\\"\\\""); + ASSERT_EQ(1ul, Result.size()); + EXPECT_EQ("\"", Result[0]); +} + +TEST(UnescapeJsonCommandLine, ParsesMultipleArgumentsWithEscapedCharacters) { + std::vector Result = UnescapeJsonCommandLine( + " \\\\\\\" \\\"a \\\\\\\" b \\\" \\\"and\\\\\\\\c\\\" \\\\\\\""); + ASSERT_EQ(4ul, Result.size()); + EXPECT_EQ("\"", Result[0]); + EXPECT_EQ("a \" b ", Result[1]); + EXPECT_EQ("and\\c", Result[2]); + EXPECT_EQ("\"", Result[3]); +} + +TEST(UnescapeJsonCommandLine, ParsesStringsWithoutSpacesIntoSingleArgument) { + std::vector QuotedNoSpaces = UnescapeJsonCommandLine( + "\\\"a\\\"\\\"b\\\""); + ASSERT_EQ(1ul, QuotedNoSpaces.size()); + EXPECT_EQ("ab", QuotedNoSpaces[0]); + + std::vector MixedNoSpaces = UnescapeJsonCommandLine( + "\\\"a\\\"bcd\\\"ef\\\"\\\"\\\"\\\"g\\\""); + ASSERT_EQ(1ul, MixedNoSpaces.size()); + EXPECT_EQ("abcdefg", MixedNoSpaces[0]); +} + +TEST(JsonCompileCommandLineParser, FailsOnEmptyString) { + JsonCompileCommandLineParser Parser("", NULL); + EXPECT_FALSE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, DoesNotReadAfterInput) { + JsonCompileCommandLineParser Parser(llvm::StringRef(NULL, 0), NULL); + EXPECT_FALSE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, ParsesEmptyArray) { + JsonCompileCommandLineParser Parser("[]", NULL); + EXPECT_TRUE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, FailsIfNotClosingArray) { + JsonCompileCommandLineParser JustOpening("[", NULL); + EXPECT_FALSE(JustOpening.Parse()) << JustOpening.GetErrorMessage(); + JsonCompileCommandLineParser WithSpaces(" [ ", NULL); + EXPECT_FALSE(WithSpaces.Parse()) << WithSpaces.GetErrorMessage(); + JsonCompileCommandLineParser WithGarbage(" [x", NULL); + EXPECT_FALSE(WithGarbage.Parse()) << WithGarbage.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, ParsesEmptyArrayWithWhitespace) { + JsonCompileCommandLineParser Spaces(" [ ] ", NULL); + EXPECT_TRUE(Spaces.Parse()) << Spaces.GetErrorMessage(); + JsonCompileCommandLineParser AllWhites("\t\r\n[\t\n \t\r ]\t\r \n\n", NULL); + EXPECT_TRUE(AllWhites.Parse()) << AllWhites.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, FailsIfNotStartingArray) { + JsonCompileCommandLineParser ObjectStart("{", NULL); + EXPECT_FALSE(ObjectStart.Parse()) << ObjectStart.GetErrorMessage(); + // We don't implement a full JSON parser, and thus parse only a subset + // of valid JSON. + JsonCompileCommandLineParser Object("{}", NULL); + EXPECT_FALSE(Object.Parse()) << Object.GetErrorMessage(); + JsonCompileCommandLineParser Character("x", NULL); + EXPECT_FALSE(Character.Parse()) << Character.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, ParsesEmptyObject) { + JsonCompileCommandLineParser Parser("[{}]", NULL); + EXPECT_TRUE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, ParsesObject) { + JsonCompileCommandLineParser Parser("[{\"a\":\"/b\"}]", NULL); + EXPECT_TRUE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, ParsesMultipleKeyValuePairsInObject) { + JsonCompileCommandLineParser Parser( + "[{\"a\":\"/b\",\"c\":\"d\",\"e\":\"f\"}]", NULL); + EXPECT_TRUE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, FailsIfNotClosingObject) { + JsonCompileCommandLineParser MissingCloseOnEmpty("[{]", NULL); + EXPECT_FALSE(MissingCloseOnEmpty.Parse()) + << MissingCloseOnEmpty.GetErrorMessage(); + JsonCompileCommandLineParser MissingCloseAfterPair("[{\"a\":\"b\"]", NULL); + EXPECT_FALSE(MissingCloseAfterPair.Parse()) + << MissingCloseAfterPair.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, FailsIfMissingColon) { + JsonCompileCommandLineParser StringString("[{\"a\"\"/b\"}]", NULL); + EXPECT_FALSE(StringString.Parse()) << StringString.GetErrorMessage(); + JsonCompileCommandLineParser StringSpaceString("[{\"a\" \"b\"}]", NULL); + EXPECT_FALSE(StringSpaceString.Parse()) + << StringSpaceString.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, FailsOnMissingQuote) { + JsonCompileCommandLineParser OpenQuote("[{a\":\"b\"}]", NULL); + EXPECT_FALSE(OpenQuote.Parse()) << OpenQuote.GetErrorMessage(); + JsonCompileCommandLineParser CloseQuote("[{\"a\":\"b}]", NULL); + EXPECT_FALSE(CloseQuote.Parse()) << CloseQuote.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, ParsesEscapedQuotes) { + JsonCompileCommandLineParser Parser( + "[{\"a\":\"\\\"b\\\" \\\" \\\"\"}]", NULL); + EXPECT_TRUE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, ParsesEmptyString) { + JsonCompileCommandLineParser Parser("[{\"a\":\"\"}]", NULL); + EXPECT_TRUE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, FailsOnMissingString) { + JsonCompileCommandLineParser MissingValue("[{\"a\":}]", NULL); + EXPECT_FALSE(MissingValue.Parse()) << MissingValue.GetErrorMessage(); + JsonCompileCommandLineParser MissingKey("[{:\"b\"}]", NULL); + EXPECT_FALSE(MissingKey.Parse()) << MissingKey.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, ParsesMultipleObjects) { + JsonCompileCommandLineParser Parser( + "[" + " { \"a\" : \"b\" }," + " { \"a\" : \"b\" }," + " { \"a\" : \"b\" }" + "]", NULL); + EXPECT_TRUE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, FailsOnMissingComma) { + JsonCompileCommandLineParser Parser( + "[" + " { \"a\" : \"b\" }" + " { \"a\" : \"b\" }" + "]", NULL); + EXPECT_FALSE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, FailsOnSuperfluousComma) { + JsonCompileCommandLineParser Parser( + "[ { \"a\" : \"b\" }, ]", NULL); + EXPECT_FALSE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +TEST(JsonCompileCommandLineParser, ParsesSpacesInBetweenTokens) { + JsonCompileCommandLineParser Parser( + " \t \n\n \r [ \t \n\n \r" + " \t \n\n \r { \t \n\n \r\"a\"\t \n\n \r :" + " \t \n\n \r \"b\"\t \n\n \r } \t \n\n \r,\t \n\n \r" + " \t \n\n \r { \t \n\n \r\"a\"\t \n\n \r :" + " \t \n\n \r \"b\"\t \n\n \r } \t \n\n \r]\t \n\n \r", + NULL); + EXPECT_TRUE(Parser.Parse()) << Parser.GetErrorMessage(); +} + +} // end namespace tooling +} // end namespace clang diff --git a/unittests/Tooling/ToolingTest.cpp b/unittests/Tooling/ToolingTest.cpp index da89c0ba10..6e5bc6b613 100644 --- a/unittests/Tooling/ToolingTest.cpp +++ b/unittests/Tooling/ToolingTest.cpp @@ -7,6 +7,7 @@ // //===----------------------------------------------------------------------===// +#include "llvm/ADT/Twine.h" #include "clang/AST/ASTConsumer.h" #include "clang/AST/DeclCXX.h" #include "clang/AST/DeclGroup.h" @@ -86,6 +87,89 @@ TEST(RunSyntaxOnlyToolOnCode, FindsClassDecl) { EXPECT_FALSE(FoundClassDeclX); } +TEST(FindCompileArgsInJsonDatabase, FindsNothingIfEmpty) { + std::string ErrorMessage; + CompileCommand NotFound = FindCompileArgsInJsonDatabase( + "a-file.cpp", "", ErrorMessage); + EXPECT_TRUE(NotFound.CommandLine.empty()) << ErrorMessage; + EXPECT_TRUE(NotFound.Directory.empty()) << ErrorMessage; +} + +TEST(FindCompileArgsInJsonDatabase, ReadsSingleEntry) { + llvm::StringRef Directory("/some/directory"); + llvm::StringRef FileName("/path/to/a-file.cpp"); + llvm::StringRef Command("/path/to/compiler and some arguments"); + std::string ErrorMessage; + CompileCommand FoundCommand = FindCompileArgsInJsonDatabase( + FileName, + (llvm::Twine("[{\"directory\":\"") + Directory + "\"," + + "\"command\":\"" + Command + "\"," + "\"file\":\"" + FileName + "\"}]").str(), ErrorMessage); + EXPECT_EQ(Directory, FoundCommand.Directory) << ErrorMessage; + ASSERT_EQ(4u, FoundCommand.CommandLine.size()) << ErrorMessage; + EXPECT_EQ("/path/to/compiler", FoundCommand.CommandLine[0]) << ErrorMessage; + EXPECT_EQ("and", FoundCommand.CommandLine[1]) << ErrorMessage; + EXPECT_EQ("some", FoundCommand.CommandLine[2]) << ErrorMessage; + EXPECT_EQ("arguments", FoundCommand.CommandLine[3]) << ErrorMessage; + + CompileCommand NotFound = FindCompileArgsInJsonDatabase( + "a-file.cpp", + (llvm::Twine("[{\"directory\":\"") + Directory + "\"," + + "\"command\":\"" + Command + "\"," + "\"file\":\"" + FileName + "\"}]").str(), ErrorMessage); + EXPECT_TRUE(NotFound.Directory.empty()) << ErrorMessage; + EXPECT_TRUE(NotFound.CommandLine.empty()) << ErrorMessage; +} + +TEST(FindCompileArgsInJsonDatabase, ReadsCompileCommandLinesWithSpaces) { + llvm::StringRef Directory("/some/directory"); + llvm::StringRef FileName("/path/to/a-file.cpp"); + llvm::StringRef Command("\\\"/path to compiler\\\" \\\"and an argument\\\""); + std::string ErrorMessage; + CompileCommand FoundCommand = FindCompileArgsInJsonDatabase( + FileName, + (llvm::Twine("[{\"directory\":\"") + Directory + "\"," + + "\"command\":\"" + Command + "\"," + "\"file\":\"" + FileName + "\"}]").str(), ErrorMessage); + ASSERT_EQ(2u, FoundCommand.CommandLine.size()); + EXPECT_EQ("/path to compiler", FoundCommand.CommandLine[0]) << ErrorMessage; + EXPECT_EQ("and an argument", FoundCommand.CommandLine[1]) << ErrorMessage; +} + +TEST(FindCompileArgsInJsonDatabase, ReadsDirectoryWithSpaces) { + llvm::StringRef Directory("/some directory / with spaces"); + llvm::StringRef FileName("/path/to/a-file.cpp"); + llvm::StringRef Command("a command"); + std::string ErrorMessage; + CompileCommand FoundCommand = FindCompileArgsInJsonDatabase( + FileName, + (llvm::Twine("[{\"directory\":\"") + Directory + "\"," + + "\"command\":\"" + Command + "\"," + "\"file\":\"" + FileName + "\"}]").str(), ErrorMessage); + EXPECT_EQ(Directory, FoundCommand.Directory) << ErrorMessage; +} + +TEST(FindCompileArgsInJsonDatabase, FindsEntry) { + llvm::StringRef Directory("directory"); + llvm::StringRef FileName("file"); + llvm::StringRef Command("command"); + std::string JsonDatabase = "["; + for (int I = 0; I < 10; ++I) { + if (I > 0) JsonDatabase += ","; + JsonDatabase += (llvm::Twine( + "{\"directory\":\"") + Directory + llvm::Twine(I) + "\"," + + "\"command\":\"" + Command + llvm::Twine(I) + "\"," + "\"file\":\"" + FileName + llvm::Twine(I) + "\"}").str(); + } + JsonDatabase += "]"; + std::string ErrorMessage; + CompileCommand FoundCommand = FindCompileArgsInJsonDatabase( + "file4", JsonDatabase, ErrorMessage); + EXPECT_EQ("directory4", FoundCommand.Directory) << ErrorMessage; + ASSERT_EQ(1u, FoundCommand.CommandLine.size()) << ErrorMessage; + EXPECT_EQ("command4", FoundCommand.CommandLine[0]) << ErrorMessage; +} + } // end namespace tooling } // end namespace clang