Browse Source

Added project menu entry to run static code analysis

This is currently only implemented for clang-based projects, where
clang --analyze is run for all source code files in an associated
compile_commands.json file.
merge-requests/429/head
doe300 3 weeks ago
parent
commit
3ea04e8715
  1. 11
      src/config.cpp
  2. 1
      src/config.hpp
  3. 4
      src/menu.cpp
  4. 156
      src/project.cpp
  5. 2
      src/project.hpp
  6. 13
      src/window.cpp
  7. 2
      tests/stubs/project.cpp

11
src/config.cpp

@ -198,6 +198,13 @@ void Config::read(const JSON &cfg) {
str.erase(str.begin()); str.erase(str.begin());
project.open_with_default_application.emplace_back(std::move(str)); project.open_with_default_application.emplace_back(std::move(str));
} }
ss = std::stringstream(project_json.string("clang_analyze_checks"));
project.clang_analyze_checks.clear();
while(getline(ss, str, ',')) {
while(!str.empty() && str.front() == ' ')
str.erase(str.begin());
project.clang_analyze_checks.emplace_back(std::move(str));
}
auto terminal_json = cfg.object("terminal"); auto terminal_json = cfg.object("terminal");
terminal.history_size = terminal_json.integer("history_size", JSON::ParseOptions::accept_string); terminal.history_size = terminal_json.integer("history_size", JSON::ParseOptions::accept_string);
@ -361,7 +368,9 @@ std::string Config::default_config() {
"python_command": "python -u", "python_command": "python -u",
"markdown_command": "grip -b", "markdown_command": "grip -b",
"open_with_default_application_comment": "Comma-separated list of file extensions that should be opened with system default applications", "open_with_default_application_comment": "Comma-separated list of file extensions that should be opened with system default applications",
"open_with_default_application": ".pdf,.png" "open_with_default_application": ".pdf,.png",
"clang_analyze_checks_comment": "Comma-separated list of checkers to enable for the clang static analyzer",
"clang_analyze_checks": "core,cplusplus,deadcode,nullability,security,unix,osx"
}, },
"keybindings": { "keybindings": {
"preferences": "<primary>comma", "preferences": "<primary>comma",

1
src/config.hpp

@ -56,6 +56,7 @@ public:
std::string python_command; std::string python_command;
std::string markdown_command; std::string markdown_command;
std::vector<boost::filesystem::path> open_with_default_application; std::vector<boost::filesystem::path> open_with_default_application;
std::vector<std::string> clang_analyze_checks;
}; };
class Source { class Source {

4
src/menu.cpp

@ -431,6 +431,10 @@ const Glib::ustring menu_xml = R"RAW(<interface>
<attribute name='label' translatable='yes'>_Recreate _Build</attribute> <attribute name='label' translatable='yes'>_Recreate _Build</attribute>
<attribute name='action'>app.project_recreate_build</attribute> <attribute name='action'>app.project_recreate_build</attribute>
</item> </item>
<item>
<attribute name='label' translatable='yes'>Run Static Analyzer</attribute>
<attribute name='action'>app.project_static_analyze</attribute>
</item>
</section> </section>
<section> <section>
<item> <item>

156
src/project.cpp

@ -13,8 +13,11 @@
#ifdef JUCI_ENABLE_DEBUG #ifdef JUCI_ENABLE_DEBUG
#include "debug_lldb.hpp" #include "debug_lldb.hpp"
#endif #endif
#include "compile_commands.hpp"
#include "ctags.hpp" #include "ctags.hpp"
#include "dialog.hpp"
#include "info.hpp" #include "info.hpp"
#include "sarif.hpp"
#include "snippets.hpp" #include "snippets.hpp"
#include "source_clang.hpp" #include "source_clang.hpp"
#include "usages_clang.hpp" #include "usages_clang.hpp"
@ -228,6 +231,10 @@ void Project::Base::recreate_build() {
Info::get().print("Could not find a supported project"); Info::get().print("Could not find a supported project");
} }
void Project::Base::analyze() {
Info::get().print("Could not find a supported project");
}
void Project::Base::show_symbols() { void Project::Base::show_symbols() {
Ctags ctags(get_preferably_view_folder()); Ctags ctags(get_preferably_view_folder());
if(!ctags) { if(!ctags) {
@ -859,6 +866,155 @@ void Project::Clang::recreate_build() {
} }
} }
void Project::Clang::analyze() {
auto default_build_path = build->get_default_path();
if(default_build_path.empty() || !build->update_default())
return;
compiling = true;
if(Config::get().terminal.clear_on_compile)
Terminal::get().clear();
Terminal::get()
.print("\e[37mAnalyzing project: " + filesystem::get_short_path(build->project_path).string() + "\e[m\n");
CompileCommands db{default_build_path};
if(db.commands.empty())
return;
const auto base_cmd = [] {
std::vector<std::string> analyzer_cmd = {"clang++", "--analyze", "-Xanalyzer", "-analyzer-output=sarif"};
for(const auto &checker : Config::get().project.clang_analyze_checks) {
analyzer_cmd.push_back("-Xanalyzer");
analyzer_cmd.push_back("-analyzer-checker=" + checker);
}
return analyzer_cmd;
}();
std::vector<boost::filesystem::path> result_files;
std::vector<std::pair<std::string, std::string>> command_lines;
for(const auto &file : db.commands) {
auto output_file = file.directory / (file.file.filename().string() + ".sarif.json");
auto cmd = base_cmd;
cmd.push_back("-o");
cmd.push_back(output_file.string());
auto compile_args = CompileCommands::get_arguments(build->get_default_path(), file.file);
for(const auto &arg : compile_args) {
cmd.push_back(arg);
}
cmd.push_back(file.file.string());
std::stringstream command_line;
for(const auto &arg : cmd) {
command_line << std::quoted(arg) << ' ';
}
command_lines.push_back(std::make_pair(file.file.string(), command_line.str()));
result_files.push_back(output_file);
}
std::size_t num_finished_jobs = 0;
bool canceled = false;
Dialog::Message message(
"Clang-analyzing project, this can take a while...", [&canceled] { canceled = true; }, true /* progress */);
const auto run_analyzer = [&](const std::string &file, const std::string &command_line) {
Terminal::get().print("Analyzing file: " + file + "...\n", true);
return Terminal::get().async_process(command_line, default_build_path, [&](int exit_status) {
// After one job finished, schedule the next or the final collection job
++num_finished_jobs;
message.set_fraction(static_cast<double>(num_finished_jobs) / static_cast<double>(command_lines.size()));
if(exit_status > 0) {
Terminal::get().print("Analyzing file '" + file + "' returned " + std::to_string(exit_status) + ", aborting!\n", true);
// Do not start any new analyzers
canceled = true;
}
});
};
const auto concurrent_processes = std::min(command_lines.size(), std::max(std::size_t{1}, std::size_t{std::thread::hardware_concurrency() - 1U}));
std::vector<std::pair<std::string, std::shared_ptr<TinyProcessLib::Process>>> processes;
processes.reserve(concurrent_processes);
// Schedule the first few jobs
auto next_job_it = command_lines.begin();
for(std::size_t i = 0; !canceled && i < concurrent_processes; ++i) {
processes.emplace_back(next_job_it->first, run_analyzer(next_job_it->first, next_job_it->second));
++next_job_it;
}
// Schedule remainder of jobs
while(next_job_it != command_lines.end()) {
if(canceled) {
for(auto &process : processes) {
if(process.second)
process.second->kill();
}
break;
}
while(Gtk::Main::events_pending())
Gtk::Main::iteration();
int exit_status = 0;
for(auto &process : processes) {
if(!process.second || process.second->try_get_exit_status(exit_status)) {
if(!canceled && next_job_it != command_lines.end()) {
process = std::make_pair(next_job_it->first, run_analyzer(next_job_it->first, next_job_it->second));
++next_job_it;
}
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// Wait for all jobs to finish
while(!processes.empty()) {
int exit_status = 0;
for(auto it = processes.begin(); it != processes.end();) {
if(!it->second || it->second->try_get_exit_status(exit_status))
it = processes.erase(it);
else
++it;
}
while(Gtk::Main::events_pending())
Gtk::Main::iteration();
}
// Combine results
if(!canceled) {
auto results = SARIF::merge_files(default_build_path / "clang-analyze.sarif.json", result_files);
if(!results.empty()) {
Terminal::get().print("Summary:\n", true);
for(const auto &result : results) {
for(const auto &location : result.locations) {
auto text = location.artifact_location.string() + ":";
if(location.region.start_line) {
text.append(std::to_string(location.region.start_line)).push_back(':');
if(location.region.start_column)
text.append(std::to_string(location.region.start_column)).push_back(':');
}
text.append(" ").append(to_string(result.level)).append(": ").append(result.get_message(location));
Terminal::get().print(text + "\n", true);
}
}
}
for(const auto &file : result_files) {
boost::system::error_code error{};
boost::filesystem::remove(file, error);
}
Terminal::get()
.print("\e[37mFinished analyzing with " + std::to_string(results.size()) + " findings!" + "\e[m\n");
}
Project::compiling = false;
message.hide();
if(auto view = Notebook::get().get_current_view())
view->soft_reparse(true);
}
void Project::Markdown::compile_and_run(const boost::filesystem::path &file_path) { void Project::Markdown::compile_and_run(const boost::filesystem::path &file_path) {
std::string command; std::string command;

2
src/project.hpp

@ -58,6 +58,7 @@ namespace Project {
virtual void compile(); virtual void compile();
virtual void compile_and_run(const boost::filesystem::path &file_path = {}); virtual void compile_and_run(const boost::filesystem::path &file_path = {});
virtual void recreate_build(); virtual void recreate_build();
virtual void analyze();
void show_symbols(); void show_symbols();
@ -112,6 +113,7 @@ namespace Project {
void compile() override; void compile() override;
void compile_and_run(const boost::filesystem::path &file_path = {}) override; void compile_and_run(const boost::filesystem::path &file_path = {}) override;
void recreate_build() override; void recreate_build() override;
void analyze() override;
}; };
class Markdown : public Base { class Markdown : public Base {

13
src/window.cpp

@ -1438,6 +1438,19 @@ void Window::set_menu_actions() {
Project::current->recreate_build(); Project::current->recreate_build();
}); });
menu.add_action("project_static_analyze", []() {
if(Project::compiling || Project::debugging) {
Info::get().print("Compile or debug in progress");
return;
}
Project::current = Project::create();
if(Config::get().project.save_on_compile_or_run)
Project::save_files(!Project::current->build->project_path.empty() ? Project::current->build->project_path : Project::get_preferably_view_folder());
Project::current->analyze();
});
menu.add_action("project_run_command", [this]() { menu.add_action("project_run_command", [this]() {
EntryBox::get().clear(); EntryBox::get().clear();

2
tests/stubs/project.cpp

@ -14,6 +14,8 @@ void Project::Base::compile_and_run(const boost::filesystem::path &file_path) {}
void Project::Base::recreate_build() {} void Project::Base::recreate_build() {}
void Project::Base::analyze() {}
std::pair<std::string, std::string> Project::Base::debug_get_run_arguments() { std::pair<std::string, std::string> Project::Base::debug_get_run_arguments() {
return std::make_pair<std::string, std::string>("", ""); return std::make_pair<std::string, std::string>("", "");
} }

Loading…
Cancel
Save