From 3ea04e8715a41065a9d5afeba5452d9781056a11 Mon Sep 17 00:00:00 2001 From: doe300 Date: Sat, 5 Sep 2026 09:46:08 +0200 Subject: [PATCH] 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. --- src/config.cpp | 11 ++- src/config.hpp | 1 + src/menu.cpp | 4 ++ src/project.cpp | 156 ++++++++++++++++++++++++++++++++++++++++ src/project.hpp | 2 + src/window.cpp | 13 ++++ tests/stubs/project.cpp | 2 + 7 files changed, 188 insertions(+), 1 deletion(-) diff --git a/src/config.cpp b/src/config.cpp index 86f3954..21035ca 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -198,6 +198,13 @@ void Config::read(const JSON &cfg) { str.erase(str.begin()); 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"); 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", "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": ".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": { "preferences": "comma", diff --git a/src/config.hpp b/src/config.hpp index eda7a29..4b1e30b 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -56,6 +56,7 @@ public: std::string python_command; std::string markdown_command; std::vector open_with_default_application; + std::vector clang_analyze_checks; }; class Source { diff --git a/src/menu.cpp b/src/menu.cpp index 46a9b72..11a184d 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -431,6 +431,10 @@ const Glib::ustring menu_xml = R"RAW( _Recreate _Build app.project_recreate_build + + Run Static Analyzer + app.project_static_analyze +
diff --git a/src/project.cpp b/src/project.cpp index f041dcb..4b95c57 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -13,8 +13,11 @@ #ifdef JUCI_ENABLE_DEBUG #include "debug_lldb.hpp" #endif +#include "compile_commands.hpp" #include "ctags.hpp" +#include "dialog.hpp" #include "info.hpp" +#include "sarif.hpp" #include "snippets.hpp" #include "source_clang.hpp" #include "usages_clang.hpp" @@ -228,6 +231,10 @@ void Project::Base::recreate_build() { 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() { Ctags ctags(get_preferably_view_folder()); 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 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 result_files; + std::vector> 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(num_finished_jobs) / static_cast(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>> 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) { std::string command; diff --git a/src/project.hpp b/src/project.hpp index 178bbb8..4215bc3 100644 --- a/src/project.hpp +++ b/src/project.hpp @@ -58,6 +58,7 @@ namespace Project { virtual void compile(); virtual void compile_and_run(const boost::filesystem::path &file_path = {}); virtual void recreate_build(); + virtual void analyze(); void show_symbols(); @@ -112,6 +113,7 @@ namespace Project { void compile() override; void compile_and_run(const boost::filesystem::path &file_path = {}) override; void recreate_build() override; + void analyze() override; }; class Markdown : public Base { diff --git a/src/window.cpp b/src/window.cpp index a664181..518e2e4 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1438,6 +1438,19 @@ void Window::set_menu_actions() { 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]() { EntryBox::get().clear(); diff --git a/tests/stubs/project.cpp b/tests/stubs/project.cpp index 6902f51..d4a0e02 100644 --- a/tests/stubs/project.cpp +++ b/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::analyze() {} + std::pair Project::Base::debug_get_run_arguments() { return std::make_pair("", ""); }