CodeCompass
462 строки · 13.8 Кб
1#include <memory>2#include <string>3#include <vector>4#include <iostream>5#include <fstream>6
7#include <boost/log/expressions.hpp>8#include <boost/log/expressions/attr.hpp>9#include <boost/log/attributes.hpp>10#include <boost/property_tree/ptree.hpp>11#include <boost/property_tree/json_parser.hpp>12
13#include <boost/program_options.hpp>14#include <boost/filesystem.hpp>15
16#include <util/dbutil.h>17#include <util/filesystem.h>18#include <util/logutil.h>19#include <util/odbtransaction.h>20
21#include <parser/parsercontext.h>22#include <parser/pluginhandler.h>23#include <parser/sourcemanager.h>24
25namespace po = boost::program_options;26namespace fs = boost::filesystem;27namespace trivial = boost::log::trivial;28
29po::options_description commandLineArguments()30{
31po::options_description desc("CodeCompass options");32
33desc.add_options()34("help,h",35"Prints this help message.")36("workspace,w", po::value<std::string>()->required(),37"Path to a workspace directory. Project log files and plugin specific "38"databases go under this directory.")39("name,n", po::value<std::string>()->required(),40"Project name.")41("description", po::value<std::string>(),42"Short description of the project.")43("force,f",44"If the project already exists with the given name in the workspace, "45"then by default CodeCompass skips all parsing actions. Force parsing "46"removes all previous results from the workspace and from the database "47"for the project.")48("list,l",49"List available plugins. Plugins come from shared objects stored in the "50"lib/parserplugin directory.")51("input,i", po::value<std::vector<std::string>>(),52"The input of the parsers can be a compilation database (see: "53"http://clang.llvm.org/docs/JSONCompilationDatabase.html) or a path of a "54"directory under which the files will be consumed. Here you can list the "55"compilation databases and the paths: -i /path/one -i /path/two -i "56"compilation_database.json")57("database,d", po::value<std::string>()->required(),58// TODO: Provide a full connection string example.59"The parsers can use a relational database to store information. This "60"database can be given by a connection string. Keep in mind that the "61"database type depends on the CodeCompass executable. CodeCompass can be "62"build to support PostgreSQL or SQLite. Connection string has the "63"following format: 'pgsql:database=name;port=5432;user=user_name' or "64"'sqlite:database=~/cc/mydatabase.sqlite'.")65("label", po::value<std::vector<std::string>>(),66"The submodules of a large project can be labeled so it can be easier "67"later to locate them. With this flag you can provide a label list in "68"the following format: label1=/path/to/submodule1.")69("loglevel",70po::value<trivial::severity_level>()->default_value(trivial::info),71"Logging legel of the parser. Possible values are: debug, info, warning, "72"error, critical.")73("logtarget", po::value<std::string>(),74"This is the path to the folder where the logging output files will be written. "75"If omitted, the output will be on the console only.")76("jobs,j", po::value<int>()->default_value(4),77"Number of threads the parsers can use.")78("skip,s", po::value<std::vector<std::string>>(),79"This is a list of parsers which will be omitted during the parsing "80"process. The possible values are the plugin names which can be listed "81"by --list flag.")82("dry-run",83"Performs a dry on the incremental parsing maintenance, "84"listing the detected changes, but without executing any "85"further actions modifying the state of the database.")86("incremental-threshold", po::value<int>()->default_value(10),87"This is a threshold percentage. If the total ratio of changed files "88"is greater than this value, full parse is forced instead of incremental parsing.");89
90return desc;91}
92
93/**
94* This function checks the existence of the workspace and project directory
95* based on the given command line arguments.
96* @return Whether the project directory exists or not.
97*/
98bool checkProjectDir(const po::variables_map& vm_)99{
100const std::string projDir101= vm_["workspace"].as<std::string>() + '/'102+ vm_["name"].as<std::string>();103
104return fs::is_directory(projDir);105}
106
107/**
108* This function prepares the workspace and project directory based on the given
109* command line arguments. If the project directory can't be created because of
110* permission issues then empty string returns which indicates the problem.
111* @return The path of the project directory.
112*/
113std::string prepareProjectDir(const po::variables_map& vm_)114{
115const std::string projDir116= vm_["workspace"].as<std::string>() + '/'117+ vm_["name"].as<std::string>();118
119boost::system::error_code ec;120
121bool isNewProject = fs::create_directories(projDir, ec);122
123if (ec)124{125LOG(error) << "Permission denied to create " + projDir;126return std::string();127}128
129if (isNewProject)130return projDir;131
132if (vm_.count("force"))133{134fs::remove_all(projDir, ec);135
136if (ec)137{138LOG(error) << "Permission denied to remove " + projDir;139return std::string();140}141
142fs::create_directory(projDir, ec);143
144if (ec)145{146LOG(error) << "Permission denied to create " + projDir;147return std::string();148}149}150
151return projDir;152}
153
154/**
155* Lists the file statuses for added, modified and deleted files affected by
156* incremental parsing.
157* @param ctx_ Parser context.
158*/
159void incrementalList(cc::parser::ParserContext& ctx_)160{
161LOG(info) << "[Incremental parsing] Detected change list:";162for (const auto& item : ctx_.fileStatus)163{164switch (item.second)165{166case cc::parser::IncrementalStatus::ADDED:167LOG(info) << "ADDED file: " << item.first;168break;169case cc::parser::IncrementalStatus::MODIFIED:170LOG(info) << "MODIFIED file: " << item.first;171break;172case cc::parser::IncrementalStatus::DELETED:173LOG(info) << "DELETED file: " << item.first;174break;175case cc::parser::IncrementalStatus::ACTION_CHANGED:176LOG(info) << "BUILD ACTION CHANGED file: " << item.first;177break;178}179}180}
181
182/**
183* Maintains and cleans up the file entries from the database as part of
184* incremental parsing.
185* @param ctx_ Parser context.
186*/
187void incrementalCleanup(cc::parser::ParserContext& ctx_)188{
189for (auto& item : ctx_.fileStatus)190{191cc::util::OdbTransaction {ctx_.db} ([&]192{193switch (item.second)194{195case cc::parser::IncrementalStatus::MODIFIED:196case cc::parser::IncrementalStatus::DELETED:197case cc::parser::IncrementalStatus::ACTION_CHANGED:198{199LOG(info) << "Database cleanup: " << item.first;200
201// Fetch file from SourceManager by path202cc::model::FilePtr delFile = ctx_.srcMgr.getFile(item.first);203
204// Delete File and FileContent (only when no other File references it)205ctx_.srcMgr.removeFile(*delFile);206break;207}208case cc::parser::IncrementalStatus::ADDED:209// Empty deliberately210break;211}212});213}214}
215
216int main(int argc, char* argv[])217{
218std::string compassRoot = cc::util::binaryPathToInstallDir(argv[0]);219
220const std::string PARSER_PLUGIN_DIR = compassRoot + "/lib/parserplugin";221const std::string SQL_DIR = compassRoot + "/share/codecompass/sql";222
223cc::parser::PluginHandler pHandler(PARSER_PLUGIN_DIR);224
225cc::util::initConsoleLogger();226
227//--- Process command line arguments ---//228
229po::options_description desc = commandLineArguments();230
231po::variables_map vm;232po::store(po::command_line_parser(argc, argv)233.options(desc).allow_unregistered().run(), vm);234
235if (vm.count("logtarget"))236{237vm.at("logtarget").value() = cc::util::getLoggingBase( vm["logtarget"].as<std::string>()238, vm["name"].as<std::string>()239);240if (!cc::util::initFileLogger(vm["logtarget"].as<std::string>() + "parser.log"))241{242vm.at("logtarget").value() = std::string();243}244}245
246//--- Skip parser list ---//247
248std::vector<std::string> skipParserList;249if (vm.count("skip"))250skipParserList = vm["skip"].as<std::vector<std::string>>();251
252//--- Load parsers ---//253
254pHandler.loadPlugins(skipParserList);255
256//--- Add arguments of parsers ---//257
258po::options_description pluginOptions = pHandler.getOptions();259desc.add(pluginOptions);260
261po::store(po::parse_command_line(argc, argv, desc), vm);262
263if (argc < 2 || vm.count("help"))264{265std::cout << desc << std::endl;266return 0;267}268
269if (vm.count("loglevel"))270{271trivial::severity_level loglevel272= vm["loglevel"].as<trivial::severity_level>();273boost::shared_ptr<boost::log::core> logger = boost::log::core::get();274logger->set_filter(boost::log::expressions::attr<275trivial::severity_level>("Severity") >= loglevel);276logger->add_global_attribute("Severity",277boost::log::attributes::mutable_constant<trivial::severity_level>(278loglevel));279}280
281if (vm.count("list"))282{283std::cout << "Available plugins:" << std::endl;284
285for (const std::string& pluginName : pHandler.getPluginNames())286std::cout << " - " << pluginName << std::endl;287
288return 0;289}290
291try292{293po::notify(vm);294}295catch (const po::error& e)296{297LOG(error) << "Error in command line arguments: " << e.what();298return 1;299}300
301//--- Check database and project directory existence ---//302
303bool isNewDb = cc::util::connectDatabase(304vm["database"].as<std::string>(), false) == nullptr;305bool isNewProject = !checkProjectDir(vm);306
307if ((isNewProject ^ isNewDb) && !vm.count("force"))308{309LOG(error) << "Database and working directory existence are inconsistent. "310"Use -f for reparsing!";311return 1;312}313
314if (!isNewDb && !vm.count("force"))315{316LOG(info)317<< "Project already exists, incremental parsing in action"318<< (vm.count("dry-run") ? " (DRY RUN)" : "") << ".";319}320
321if (isNewDb && vm.count("dry-run"))322{323LOG(error) << "Dry-run can be only used with incremental parsing, "324"no project found. Try turning --dry-run off.";325return 1;326}327
328//--- Prepare workspace and project directory ---//329
330std::string projDir = prepareProjectDir(vm);331if (projDir.empty())332return 1;333
334//--- Create and init database ---//335
336std::shared_ptr<odb::database> db = cc::util::connectDatabase(337vm["database"].as<std::string>(), true);338
339std::unordered_map<std::string, cc::parser::IncrementalStatus> fileStatus;340
341if (!db)342{343LOG(error) << "Couldn't connect to database. Check the connection string. "344"Connection string format: \"" +345cc::util::getDbDriver() + ":<opt1>=<val1>;<opt2>=<val2>...\"";346return 1;347}348
349if (vm.count("force"))350cc::util::removeTables(db, SQL_DIR);351
352if (vm.count("force") || isNewDb)353cc::util::createTables(db, SQL_DIR);354
355//--- Start parsers ---//356
357/*358* Workflow for incremental parsing:
359* 1. directly modified files are detected by ParserContext.
360* 2. all plugin parsers mark the indirectly modified files.
361* 3. all plugin parsers perform a cleanup operation.
362* 4. global tables are cleaned up by parser.cpp.
363* 5. all plugin parsers perform a parsing operation.
364*
365* In case of an initial or forced parsing, only step 5 is executed.
366*/
367
368cc::parser::SourceManager srcMgr(db);369cc::parser::ParserContext ctx(db, srcMgr, compassRoot, vm);370pHandler.createPlugins(ctx);371
372std::vector<std::string> pluginNames = pHandler.getLoadedPluginNames();373for (const std::string& pluginName : pluginNames)374{375LOG(info) << "[" << pluginName << "] started to mark modified files!";376pHandler.getParser(pluginName)->markModifiedFiles();377}378
379if (vm.count("dry-run"))380{381incrementalList(ctx);382return 0;383}384
385if (ctx.fileStatus.size() >386ctx.srcMgr.numberOfFiles() * vm["incremental-threshold"].as<int>() / 100.0)387{388LOG(info) << "The number of changed files exceeds the given incremental "389"threshold ratio, full parse will be forced.";390vm.insert(std::make_pair("force", po::variable_value()));391
392cc::util::removeTables(db, SQL_DIR);393cc::util::createTables(db, SQL_DIR);394
395srcMgr.reloadCache();396ctx.fileStatus.clear();397}398
399if (!vm.count("force"))400{401for (const std::string& pluginName : pluginNames)402{403LOG(info) << "[" << pluginName << "] cleanup started!";404if (!pHandler.getParser(pluginName)->cleanupDatabase())405{406LOG(error) << "[" << pluginName << "] cleanup failed!";407return 2;408}409}410
411incrementalCleanup(ctx);412}413
414// TODO: Handle errors returned by parse().415for (const std::string& pluginName : pluginNames)416{417LOG(info) << "[" << pluginName << "] parse started!";418pHandler.getParser(pluginName)->parse();419}420
421//--- Add indexes to the database ---//422
423if (vm.count("force") || isNewDb)424cc::util::createIndexes(db, SQL_DIR);425
426//--- Create project config file ---//427
428boost::property_tree::ptree pt;429
430if (vm.count("label"))431{432boost::property_tree::ptree labels;433
434for (const std::string& label : vm["label"].as<std::vector<std::string>>())435{436std::size_t pos = label.find('=');437
438if (pos == std::string::npos)439LOG(warning)440<< "Label doesn't contain '=' for separating label and the path: "441<< label;442else443labels.put(label.substr(0, pos), label.substr(pos + 1));444}445
446pt.add_child("labels", labels);447}448
449std::string database450= cc::util::connStrComponent(vm["database"].as<std::string>(), "database");451
452pt.put("database", vm["database"].as<std::string>());453
454if (vm.count("description"))455pt.put("description", vm["description"].as<std::string>());456
457boost::property_tree::write_json(projDir + "/project_info.json", pt);458
459// TODO: Print statistics.460
461return 0;462}
463