Browse Source

Merge branch 'clang_static_analyzer' into 'master'

Add support for clang --analyze and displaying of results

See merge request cppit/jucipp!429
merge-requests/429/merge
doe300 3 weeks ago
parent
commit
8df655e657
  1. 1
      src/CMakeLists.txt
  2. 11
      src/config.cpp
  3. 1
      src/config.hpp
  4. 4
      src/menu.cpp
  5. 156
      src/project.cpp
  6. 2
      src/project.hpp
  7. 336
      src/sarif.cpp
  8. 121
      src/sarif.hpp
  9. 91
      src/source_clang.cpp
  10. 5
      src/source_clang.hpp
  11. 13
      src/window.cpp
  12. 4
      tests/CMakeLists.txt
  13. 72
      tests/sarif_test.cpp
  14. 353
      tests/sarif_test_files/example_run.sarif.json
  15. 111
      tests/sarif_test_files/example_run2.sarif.json
  16. 2
      tests/stubs/project.cpp

1
src/CMakeLists.txt

@ -16,6 +16,7 @@ set(JUCI_SHARED_FILES
meson.cpp
mutex.cpp
project_build.cpp
sarif.cpp
snippets.cpp
source.cpp
source_base.cpp

11
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": "<primary>comma",

1
src/config.hpp

@ -56,6 +56,7 @@ public:
std::string python_command;
std::string markdown_command;
std::vector<boost::filesystem::path> open_with_default_application;
std::vector<std::string> clang_analyze_checks;
};
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='action'>app.project_recreate_build</attribute>
</item>
<item>
<attribute name='label' translatable='yes'>Run Static Analyzer</attribute>
<attribute name='action'>app.project_static_analyze</attribute>
</item>
</section>
<section>
<item>

156
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<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) {
std::string command;

2
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 {

336
src/sarif.cpp

@ -0,0 +1,336 @@
#include "sarif.hpp"
#include "utility.hpp"
#define JSON_DISABLE_ENUM_SERIALIZATION 1
#include "nlohmann/json.hpp"
#include <algorithm>
#include <fstream>
#include <iomanip>
#include <unordered_set>
namespace SARIF {
std::string Message::to_string() const {
if(arguments.empty())
return text;
auto final_text = text;
for(std::size_t i = 0; i < arguments.size(); ++i) {
auto pattern = "{" + std::to_string(i) + "}";
for(auto pos = final_text.find(pattern); pos < final_text.size(); pos = final_text.find(pattern)) {
final_text.replace(pos, pattern.size(), arguments[i]);
}
}
return final_text;
}
void from_json(const nlohmann::json &json, Message &message) {
message.text = json.value("text", "");
message.arguments = json.value("arguments", std::vector<std::string>{});
}
bool Region::operator<(const Region &other) const noexcept {
#define COMPARE(one, other) \
if(one < other) \
return true; \
else if(one > other) \
return false;
COMPARE(start_line, other.start_line)
COMPARE(start_column, other.start_column)
COMPARE(end_line, other.end_line)
COMPARE(end_column, other.end_column)
return false;
#undef COMPARE
}
void from_json(const nlohmann::json &json, Region &region) {
region.start_line = json.value("startLine", 0U);
region.end_line = json.value("endLine", region.start_line);
region.start_column = json.value("startColumn", 1U);
region.end_column = json.value("endColumn", region.start_column);
region.snippet = json.value("snippet", nlohmann::json::object()).value("text", "");
}
void from_json(const nlohmann::json &json, Location &location) {
location.message = json.value("message", Message{});
auto physicalLocation = json.value("physicalLocation", nlohmann::json::object());
location.region = physicalLocation.value("region", Region{});
auto contextRegion = physicalLocation.value("contextRegion", nlohmann::json{});
if(!contextRegion.empty())
location.context_region = contextRegion.get<Region>();
auto uri = physicalLocation.value("artifactLocation", nlohmann::json::object()).value("uri", "");
if(starts_with(uri, "file://"))
location.artifact_location = boost::filesystem::path{uri.substr(7)};
}
using Importance = ThreadFlowLocation::Importance;
NLOHMANN_JSON_SERIALIZE_ENUM(Importance, {{Importance::important, "important"}, {Importance::essential, "essential"}, {Importance::unimportant, "unimportant"}})
void from_json(const nlohmann::json &json, ThreadFlowLocation &location) {
location.importance = json.value("importance", ThreadFlowLocation::Importance::important);
location.location = json.value("location", Location{});
}
void from_json(const nlohmann::json &json, ThreadFlow &flow) {
flow.locations = json.value("locations", std::vector<ThreadFlowLocation>{});
}
void from_json(const nlohmann::json &json, CodeFlow &flow) {
flow.thread_flows = json.value("threadFlows", std::vector<ThreadFlow>{});
}
using Level = Result::Level;
NLOHMANN_JSON_SERIALIZE_ENUM(Level, {{Level::none, "none"}, {Level::error, "error"}, {Level::note, "note"}, {Level::warning, "warning"}})
std::string Result::get_message(const Location &location, bool markdown) const {
auto text = location.message.to_string();
if(text.empty())
text = message.to_string();
if(markdown && !rule_id.empty() && !help_uri.empty())
text.append(" [(clang-analyze: ")
.append(rule_id)
.append(")](")
.append(help_uri)
.append(")");
else if(!rule_id.empty())
text.append(" (clang-analyze: ")
.append(rule_id)
.append(")");
return text;
}
std::string to_string(Result::Level level) {
switch(level) {
case Result::Level::error:
return "error";
case Result::Level::none:
return "none";
case Result::Level::note:
return "note";
case Result::Level::warning:
return "warning";
}
return "(unknown)";
}
void from_json(const nlohmann::json &json, Result &result) {
result.code_flows = json.value("codeFlows", std::vector<CodeFlow>{});
result.level = json.value("level", Result::Level::warning);
result.locations = json.value("locations", std::vector<Location>{});
result.message = json.value("message", Message{});
result.rule_id = json.value("ruleId", "");
}
void from_json(const nlohmann::json &json, ReportingDescriptor &descriptor) {
descriptor.id = json.value("id", "");
descriptor.name = json.value("name", "");
descriptor.help_uri = json.value("helpUri", "");
}
void from_json(const nlohmann::json &json, ToolComponent &component) {
component.rules = json.value("rules", std::vector<ReportingDescriptor>{});
}
void from_json(const nlohmann::json &json, Tool &tool) {
tool.driver = json.value("driver", ToolComponent{});
}
void from_json(const nlohmann::json &json, Run &run) {
run.results = json.value("results", std::vector<Result>{});
run.tool = json.value("tool", Tool{});
}
void from_json(const nlohmann::json &json, Log &log) {
log.runs = json.value("runs", std::vector<Run>{});
}
static void recursive_add_context(nlohmann::json &json) {
if(json.is_object()) {
auto file_uri = json.value("artifactLocation", nlohmann::json::object()).value("uri", "");
auto region = json.value("region", Region{});
if(starts_with(file_uri, "file://") && region.start_line && region.end_line) {
const std::size_t CONTEXT_LINES = 3;
auto context_start_line = std::max(region.start_line, CONTEXT_LINES) - CONTEXT_LINES;
auto context_num_desired_lines = region.end_line + 1 /* end is inclusive */ - region.start_line + 2 * CONTEXT_LINES;
std::ifstream fis{file_uri.substr(7 /* "file://" */)};
std::string line;
std::string context;
std::string text;
uint32_t context_num_lines = 0;
for(uint32_t i = 1; i < context_start_line; ++i)
std::getline(fis, line);
for(uint32_t i = 0; i < context_num_desired_lines; ++i) {
if(std::getline(fis, line)) {
if(!context.empty())
context.append("\n");
context.append(line);
++context_num_lines;
if(i >= CONTEXT_LINES && i + CONTEXT_LINES < context_num_desired_lines) {
if(i + CONTEXT_LINES + 1 == context_num_desired_lines && region.end_column > 1)
line = line.substr(0, region.end_column - 1U);
if(i == CONTEXT_LINES && region.start_column)
line = line.substr(region.start_column - 1);
if(!text.empty())
text.append("\n");
text.append(line);
}
}
}
if(!context.empty())
json["contextRegion"] = nlohmann::json::object({
{"startLine", context_start_line},
{"endLine", context_start_line + context_num_lines - 1},
{"snippet", nlohmann::json::object({{"text", context}})},
});
if(!text.empty() && !json["region"].contains("snippet"))
json["region"]["snippet"] = nlohmann::json::object({{"text", text}});
}
}
if(json.is_structured()) {
for(auto &entry : json) {
recursive_add_context(entry);
}
}
}
static void recursive_remove_index(nlohmann::json &json) {
if(json.is_object()) {
auto it = json.find("index");
if(it != json.end())
json.erase(it);
}
if(json.is_structured()) {
for(auto &entry : json) {
recursive_remove_index(entry);
}
}
}
std::vector<Result> merge_files(const boost::filesystem::path &output_file, std::vector<boost::filesystem::path> &input_files) {
input_files.erase(std::remove_if(input_files.begin(), input_files.end(), [](const boost::filesystem::path &file) {
return !boost::filesystem::exists(file);
}),
input_files.end());
// Deduplicate results added by multiple analyzes, e.g. for finding in headers
std::unordered_set<nlohmann::json> added_results;
if(input_files.empty()) {
boost::filesystem::remove(output_file);
return {};
}
std::ifstream fis{input_files.front().string()};
auto output = nlohmann::json::parse(fis);
recursive_remove_index(output);
auto &runs = output["runs"];
if(runs.is_null() || (runs.size() == 1 && runs[0]["results"].empty()))
runs = nlohmann::json::array();
auto it = input_files.begin();
for(++it; it < input_files.end(); ++it) {
fis = std::ifstream{it->string()};
auto part = nlohmann::json::parse(fis);
auto it = part.find("runs");
if(it != part.end() && it->is_array()) {
for(auto &run : *it) {
auto &results = run["results"];
for(auto result_it = results.begin(); result_it != results.end();) {
// Remove some values which we don't care about and might differ for otherwise identical results
auto it = result_it->find("ruleIndex");
if(it != result_it->end())
result_it->erase(it);
recursive_remove_index(*result_it);
if(added_results.find(*result_it) != added_results.end())
result_it = results.erase(result_it);
else {
added_results.emplace(*result_it);
++result_it;
}
}
if(results.empty())
// Do not merge runs without findings
continue;
runs.insert(runs.end(), std::move(run));
}
}
}
recursive_add_context(output);
std::ofstream fos{output_file.string()};
fos << std::setw(2) << output << std::endl;
std::vector<Result> results{};
for(const auto &run : runs) {
auto new_results = run.value("results", std::vector<Result>{});
results.insert(results.end(), std::make_move_iterator(new_results.begin()), std::make_move_iterator(new_results.end()));
}
return results;
}
std::vector<Result> results_for_file(const boost::filesystem::path &results_file, const boost::filesystem::path &source_file) {
if(!boost::filesystem::exists(results_file))
return {};
const auto find_help_uri = [](const Run &run, const std::string &id) {
for(const auto &rule : run.tool.driver.rules) {
if(rule.id == id)
return rule.help_uri;
}
return std::string{};
};
std::ifstream fis{results_file.string()};
auto log = nlohmann::json::parse(fis, nullptr).get<Log>();
std::vector<Result> results;
for(auto &run : log.runs) {
for(auto result : run.results) {
if(std::any_of(result.locations.begin(), result.locations.end(), [&](const auto &loc) {
return loc.artifact_location == source_file;
})) {
result.help_uri = find_help_uri(run, result.rule_id);
results.push_back(std::move(result));
continue;
}
bool result_added = false;
for(auto &code_flow : result.code_flows) {
if(result_added)
break;
for(auto &thread_flow : code_flow.thread_flows) {
if(result_added)
break;
if(std::any_of(thread_flow.locations.begin(), thread_flow.locations.end(), [&](const auto &loc) {
return loc.location.artifact_location == source_file;
})) {
result.help_uri = find_help_uri(run, result.rule_id);
results.push_back(std::move(result));
result_added = true;
break;
}
}
}
}
}
return results;
}
} // namespace SARIF

121
src/sarif.hpp

@ -0,0 +1,121 @@
#pragma once
#include <string>
#include <vector>
#include <boost/filesystem.hpp>
#include <boost/optional.hpp>
#include "nlohmann/json_fwd.hpp"
namespace SARIF {
struct Message {
std::string text;
std::vector<std::string> arguments;
std::string to_string() const;
};
void from_json(const nlohmann::json &json, Message &message);
struct Region {
std::size_t start_line;
std::size_t end_line;
std::size_t start_column;
std::size_t end_column;
std::string snippet;
bool operator<(const Region &other) const noexcept;
};
void from_json(const nlohmann::json &json, Region &region);
struct Location {
Message message;
boost::filesystem::path artifact_location;
Region region;
boost::optional<Region> context_region;
};
void from_json(const nlohmann::json &json, Location &location);
struct ThreadFlowLocation {
enum class Importance {
important,
essential,
unimportant,
};
Importance importance;
Location location;
};
void from_json(const nlohmann::json &json, ThreadFlowLocation &location);
struct ThreadFlow {
std::vector<ThreadFlowLocation> locations;
};
void from_json(const nlohmann::json &json, ThreadFlow &flow);
struct CodeFlow {
std::vector<ThreadFlow> thread_flows;
};
void from_json(const nlohmann::json &json, CodeFlow &flow);
struct Result {
enum class Level {
none,
note,
warning,
error,
};
Level level;
Message message;
std::vector<Location> locations;
std::vector<CodeFlow> code_flows;
std::string rule_id;
// Custom field
std::string help_uri;
std::string get_message(const Location &location, bool markdown = false) const;
};
std::string to_string(Result::Level level);
void from_json(const nlohmann::json &json, Result &result);
struct ReportingDescriptor {
std::string id;
std::string name;
std::string help_uri;
};
void from_json(const nlohmann::json &json, ReportingDescriptor &descriptor);
struct ToolComponent {
std::vector<ReportingDescriptor> rules;
};
void from_json(const nlohmann::json &json, ToolComponent &component);
struct Tool {
ToolComponent driver;
};
void from_json(const nlohmann::json &json, Tool &tool);
struct Run {
std::vector<Result> results;
Tool tool;
};
void from_json(const nlohmann::json &json, Run &run);
// According to Static Analysis Results Interchange Format (SARIF) specification version 2.1,
// see https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
struct Log {
std::vector<Run> runs;
};
void from_json(const nlohmann::json &json, Log &log);
/// Merges the results of all input files.
/// Removes duplicate entries (e.g. findings in headers included in multiple files).
/// Adds a context region for each location where possible.
/// Returns the list of merged results.
std::vector<Result> merge_files(const boost::filesystem::path &output_file, std::vector<boost::filesystem::path> &input_files);
/// Returns the list of results for the given file.
std::vector<Result> results_for_file(const boost::filesystem::path &results_file, const boost::filesystem::path &source_file);
} // namespace SARIF

91
src/source_clang.cpp

@ -12,6 +12,7 @@
#include "documentation.hpp"
#include "filesystem.hpp"
#include "info.hpp"
#include "sarif.hpp"
#include "selection_dialog.hpp"
#include "usages_clang.hpp"
#include "utility.hpp"
@ -171,6 +172,16 @@ void Source::ClangViewParse::parse(size_t count) {
for(auto &token : *clang_tokens)
clang_tokens_offsets.emplace_back(token.get_source_range().get_offsets());
clang_diagnostics = clang_tu->get_diagnostics();
try {
auto analyze_results_file = Project::Build::create(this->file_path)->get_default_path() / "clang-analyze.sarif.json";
analyze_diagnostics = SARIF::results_for_file(analyze_results_file, file_path);
}
catch(const std::exception &err) {
analyze_diagnostics.clear();
Terminal::get().async_print("\e[31mError\e[m: Parsing clang-analyze results failed: " + std::string{err.what()} + ".\n", true);
}
parse_mutex.unlock();
dispatcher.post([this, count] {
if(count != parse_count || parse_state != ParseState::processing)
@ -525,6 +536,86 @@ void Source::ClangViewParse::update_diagnostics() {
}
}
std::map<std::pair<Gtk::TextIter, Gtk::TextIter>, std::set<std::string>> added_messages;
const auto add_tooltip_for_location = [this, &added_messages](const SARIF::Location &location, const SARIF::Result &result) {
auto text = result.get_message(location, true /* markdown */);
int line = location.region.start_line - 1;
if(line < 0 || line >= get_buffer()->get_line_count())
line = get_buffer()->get_line_count() - 1;
auto start = get_iter_at_line_end(line);
int offset = location.region.start_column - 1;
if(offset >= 0 && offset < start.get_line_index())
start = get_buffer()->get_iter_at_line_offset(line, offset);
if(start.ends_line()) {
while(!start.is_start() && start.ends_line())
start.backward_char();
}
line = location.region.end_line - 1;
if(line < 0 || line >= get_buffer()->get_line_count())
line = get_buffer()->get_line_count() - 1;
auto end = get_iter_at_line_end(line);
offset = location.region.end_column - 1;
if(offset >= 0 && offset < end.get_line_index())
end = get_buffer()->get_iter_at_line_offset(line, offset);
// Check whether the source actually still matches the scan result
auto match = get_buffer()->get_text(start, end);
if(!location.region.snippet.empty() && match != location.region.snippet) {
bool context_found = false;
if(location.context_region && !location.context_region->snippet.empty()) {
// Try to find the context in case the matching source was only moved
Gtk::TextIter match_start, match_end;
if(start.forward_search(location.context_region->snippet, static_cast<Gtk::TextSearchFlags>(0), match_start, match_end))
context_found = true;
else if(end.backward_search(location.context_region->snippet, static_cast<Gtk::TextSearchFlags>(0), match_start, match_end))
context_found = true;
else if(get_buffer()->begin().forward_search(location.context_region->snippet, static_cast<Gtk::TextSearchFlags>(0), match_start, match_end))
context_found = true;
if(context_found) {
match_start.forward_search(location.region.snippet, static_cast<Gtk::TextSearchFlags>(0), start, end, match_end);
}
}
text.append(" (Source modified since scan, match ").append(context_found ? "may be" : "is").append(" inaccurate)");
}
auto added_it = added_messages.find(std::make_pair(start, end));
if(added_it != added_messages.end() && added_it->second.find(text) != added_it->second.end())
return false;
diagnostic_offsets.emplace(start.get_offset());
added_messages[std::make_pair(start, end)].emplace(text);
add_diagnostic_tooltip(start, end, result.level == SARIF::Result::Level::error, [text](Tooltip &tooltip) {
tooltip.insert_markdown(text);
});
return true;
};
for(const auto &diagnostic : analyze_diagnostics) {
for(const auto &location : diagnostic.locations) {
if(location.artifact_location == file_path) {
if(add_tooltip_for_location(location, diagnostic)) {
num_warnings += diagnostic.level == SARIF::Result::Level::warning;
num_errors += diagnostic.level == SARIF::Result::Level::error;
}
}
}
for(const auto &code_flow : diagnostic.code_flows) {
for(const auto &thread_flow : code_flow.thread_flows) {
for(const auto &entry : thread_flow.locations) {
if(entry.location.artifact_location == file_path && entry.importance != SARIF::ThreadFlowLocation::Importance::unimportant) {
if(add_tooltip_for_location(entry.location, diagnostic)) {
num_warnings += diagnostic.level == SARIF::Result::Level::warning;
num_errors += diagnostic.level == SARIF::Result::Level::error;
}
}
}
}
}
}
status_diagnostics = std::make_tuple(num_warnings, num_errors, num_fix_its);
if(update_status_diagnostics)
update_status_diagnostics(this);

5
src/source_clang.hpp

@ -11,6 +11,10 @@
#include <set>
#include <thread>
namespace SARIF {
struct Result;
} // namespace SARIF
namespace Source {
class ClangViewParse : public View {
protected:
@ -57,6 +61,7 @@ namespace Source {
void update_diagnostics() REQUIRES(parse_mutex);
std::vector<clangmm::Diagnostic> clang_diagnostics GUARDED_BY(parse_mutex);
std::vector<SARIF::Result> analyze_diagnostics GUARDED_BY(parse_mutex);
/// Removes for instance ::__1:: and ::__cxx11:: from type
void remove_internal_namespaces(std::string &type);

13
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();

4
tests/CMakeLists.txt

@ -99,6 +99,10 @@ if(BUILD_TESTING)
add_executable(json_test json_test.cpp $<TARGET_OBJECTS:test_stubs>)
target_link_libraries(json_test juci_shared)
add_test(json_test json_test)
add_executable(sarif_test sarif_test.cpp)
target_link_libraries(sarif_test juci_shared)
add_test(sarif_test sarif_test)
endif()
if(BUILD_FUZZING)

72
tests/sarif_test.cpp

@ -0,0 +1,72 @@
#include "sarif.hpp"
#include "nlohmann/json.hpp"
#include <glib.h>
#include <vector>
int main() {
const auto tests_path = boost::filesystem::canonical(JUCI_TESTS_PATH) / "sarif_test_files";
std::vector<boost::filesystem::path> example_files = {
tests_path / "example_run.sarif.json",
tests_path / "example_run2.sarif.json",
tests_path / "no_such_file.sarif.json",
};
const auto output_file = tests_path / "merged.sarif.json";
boost::filesystem::remove(output_file);
g_assert(!boost::filesystem::exists(output_file));
auto results = SARIF::merge_files(output_file, example_files);
g_assert_cmpuint(results.size(), ==, 2U);
results = SARIF::results_for_file(output_file, "/home/user/jucipp/src/source_base.cpp");
g_assert_cmpuint(results.size(), ==, 1U);
const auto &result = results.front();
g_assert(result.rule_id == "alpha.cplusplus.IteratorRange");
g_assert(result.message.text == "Past-the-end iterator dereferenced");
g_assert(result.message.arguments.empty());
g_assert(result.level == SARIF::Result::Level::warning);
g_assert(result.help_uri == "https://clang.llvm.org/docs/analyzer/checkers.html#alpha-cplusplus-iteratorrange");
g_assert_cmpuint(result.locations.size(), ==, 1);
const auto &loc = result.locations.front();
g_assert(loc.artifact_location == "/home/user/jucipp/src/source_base.cpp");
g_assert_cmpuint(loc.region.start_line, ==, 1940);
g_assert_cmpuint(loc.region.end_line, ==, 1940);
g_assert_cmpuint(loc.region.start_column, ==, 35);
g_assert_cmpuint(loc.region.end_column, ==, 57);
g_assert(loc.message.text.empty());
g_assert(loc.message.arguments.empty());
g_assert(result.get_message(loc, false) == "Past-the-end iterator dereferenced (clang-analyze: alpha.cplusplus.IteratorRange)");
g_assert(result.get_message(loc, true) == "Past-the-end iterator dereferenced [(clang-analyze: alpha.cplusplus.IteratorRange)](https://clang.llvm.org/docs/analyzer/checkers.html#alpha-cplusplus-iteratorrange)");
g_assert_cmpuint(result.code_flows.size(), ==, 1);
const auto &codeFlow = result.code_flows.front();
g_assert_cmpuint(codeFlow.thread_flows.size(), ==, 1);
const auto &threadFlow = codeFlow.thread_flows.front();
g_assert_cmpuint(threadFlow.locations.size(), ==, 14);
const auto &firstLoc = threadFlow.locations.front();
g_assert(firstLoc.importance == SARIF::ThreadFlowLocation::Importance::unimportant);
g_assert(firstLoc.location.message.text == "Taking false branch");
g_assert(firstLoc.location.message.arguments.empty());
g_assert(firstLoc.location.artifact_location == "/home/user/jucipp/src/source_base.cpp");
g_assert(firstLoc.location.region.start_line == 1875);
g_assert(firstLoc.location.region.end_line == 1875);
g_assert(firstLoc.location.region.start_column == 3);
g_assert(firstLoc.location.region.end_column == 3);
const auto &lastLoc = threadFlow.locations.back();
g_assert(lastLoc.importance == SARIF::ThreadFlowLocation::Importance::essential);
g_assert(lastLoc.location.message.text == "Past-the-end iterator dereferenced");
g_assert(lastLoc.location.message.arguments.empty());
g_assert(lastLoc.location.artifact_location == "/home/user/jucipp/src/source_base.cpp");
g_assert(lastLoc.location.region.start_line == 1940);
g_assert(lastLoc.location.region.end_line == 1940);
g_assert(lastLoc.location.region.start_column == 35);
g_assert(lastLoc.location.region.end_column == 57);
}

353
tests/sarif_test_files/example_run.sarif.json

@ -0,0 +1,353 @@
{
"$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json",
"runs": [
{
"artifacts": [
{
"length": 75606,
"location": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"mimeType": "text/plain",
"roles": [
"resultFile"
]
}
],
"columnKind": "unicodeCodePoints",
"results": [
{
"codeFlows": [
{
"threadFlows": [
{
"locations": [
{
"importance": "unimportant",
"location": {
"message": {
"text": "Taking false branch"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 3,
"startColumn": 3,
"startLine": 1875
}
}
}
},
{
"importance": "important",
"location": {
"message": {
"text": "Assuming the condition is false"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 52,
"endLine": 1884,
"startColumn": 6,
"startLine": 1884
}
}
}
},
{
"importance": "unimportant",
"location": {
"message": {
"text": "Taking false branch"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 3,
"startColumn": 3,
"startLine": 1884
}
}
}
},
{
"importance": "unimportant",
"location": {
"message": {
"text": "'erase_line' is false"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 11,
"startColumn": 11,
"startLine": 1888
}
}
}
},
{
"importance": "unimportant",
"location": {
"message": {
"text": "Taking false branch"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 8,
"startColumn": 8,
"startLine": 1888
}
}
}
},
{
"importance": "unimportant",
"location": {
"message": {
"text": "'erase_word' is false"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 11,
"startColumn": 11,
"startLine": 1894
}
}
}
},
{
"importance": "unimportant",
"location": {
"message": {
"text": "Left side of '&&' is false"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 22,
"startColumn": 22,
"startLine": 1894
}
}
}
},
{
"importance": "important",
"location": {
"message": {
"text": "Assuming the condition is true"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 46,
"endLine": 1914,
"startColumn": 6,
"startLine": 1914
}
}
}
},
{
"importance": "unimportant",
"location": {
"message": {
"text": "Taking true branch"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 3,
"startColumn": 3,
"startLine": 1914
}
}
}
},
{
"importance": "essential",
"location": {
"message": {
"text": "Calling 'BaseView::select_snippet_parameter'"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 31,
"endLine": 1915,
"startColumn": 5,
"startLine": 1915
}
}
}
},
{
"importance": "unimportant",
"location": {
"message": {
"text": "Loop condition is false. Execution continues on line 1926"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 3,
"startColumn": 3,
"startLine": 1919
}
}
}
},
{
"importance": "important",
"location": {
"message": {
"text": "Assuming the condition is true"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 38,
"endLine": 1937,
"startColumn": 6,
"startLine": 1937
}
}
}
},
{
"importance": "unimportant",
"location": {
"message": {
"text": "Taking true branch"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 3,
"startColumn": 3,
"startLine": 1937
}
}
}
},
{
"importance": "essential",
"location": {
"message": {
"text": "Past-the-end iterator dereferenced"
},
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 57,
"endLine": 1940,
"startColumn": 35,
"startLine": 1940
}
}
}
}
]
}
]
}
],
"level": "warning",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"index": 0,
"uri": "file:///home/user/jucipp/src/source_base.cpp"
},
"region": {
"endColumn": 57,
"endLine": 1940,
"startColumn": 35,
"startLine": 1940
}
}
}
],
"message": {
"text": "Past-the-end iterator dereferenced"
},
"ruleId": "alpha.cplusplus.IteratorRange",
"ruleIndex": 0
}
],
"tool": {
"driver": {
"fullName": "clang static analyzer",
"informationUri": "https://clang.llvm.org/docs/UsersManual.html",
"language": "en-US",
"name": "clang",
"rules": [
{
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"rank": -1
},
"fullDescription": {
"text": "Check for iterators used outside their valid ranges"
},
"helpUri": "https://clang.llvm.org/docs/analyzer/checkers.html#alpha-cplusplus-iteratorrange",
"id": "alpha.cplusplus.IteratorRange",
"name": "alpha.cplusplus.IteratorRange"
}
],
"version": "clang version 21.1.8 (Fedora 21.1.8-4.fc43)"
}
}
}
],
"version": "2.1.0"
}

111
tests/sarif_test_files/example_run2.sarif.json

@ -0,0 +1,111 @@
{
"$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json",
"runs": [
{
"artifacts": [
{
"length": 3814,
"location": {
"index": 0,
"uri": "file:///home/user/jucipp/src/config.hpp"
},
"mimeType": "text/plain",
"roles": [
"resultFile"
]
}
],
"columnKind": "unicodeCodePoints",
"results": [
{
"codeFlows": [
{
"threadFlows": [
{
"locations": [
{
"importance": "essential",
"location": {
"message": {
"text": "Excessive padding in 'class Config::Source' (35 padding bytes, where 3 is optimal). Optimal fields order: style, font, spellcheck_language, show_whitespace_characters, word_wrap, clang_format_style, clang_tidy_checks, documentation_searches, map_font_size, right_margin_position, pixels_between_lines, default_tab_size, tooltip_top_offset, clang_usages_threads, add_missing_newline_at_end_of_file_on_save, remove_trailing_whitespace_characters_on_save, format_style_on_save, format_style_on_save_if_style_file_found, smart_brackets, smart_inserts, show_map, show_git_diff, show_background_pattern, show_right_margin, auto_tab_char_and_size, default_tab_char, tab_indents_line, highlight_current_line, show_line_numbers, enable_multiple_cursors, auto_reload_changed_files, search_for_selection, clang_tidy_enable, clang_detailed_preprocessing_record, debug_place_cursor_at_stop, consider reordering the fields or adding explicit padding members"
},
"physicalLocation": {
"artifactLocation": {
"uri": "file:///home/user/jucipp/src/config.hpp"
},
"contextRegion": {
"endLine": 65,
"snippet": {
"text": " std::vector<std::string> clang_analyze_checks;\n };\n\n class Source {\n public:\n class DocumentationSearch {\n public:"
},
"startLine": 59
},
"region": {
"endColumn": 9,
"startColumn": 9,
"startLine": 62
}
}
}
}
]
}
]
}
],
"level": "warning",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "file:///home/user/jucipp/src/config.hpp"
},
"contextRegion": {
"endLine": 65,
"snippet": {
"text": " std::vector<std::string> clang_analyze_checks;\n };\n\n class Source {\n public:\n class DocumentationSearch {\n public:"
},
"startLine": 59
},
"region": {
"endColumn": 9,
"startColumn": 9,
"startLine": 62
}
}
}
],
"message": {
"text": "Excessive padding in 'class Config::Source' (35 padding bytes, where 3 is optimal). Optimal fields order: style, font, spellcheck_language, show_whitespace_characters, word_wrap, clang_format_style, clang_tidy_checks, documentation_searches, map_font_size, right_margin_position, pixels_between_lines, default_tab_size, tooltip_top_offset, clang_usages_threads, add_missing_newline_at_end_of_file_on_save, remove_trailing_whitespace_characters_on_save, format_style_on_save, format_style_on_save_if_style_file_found, smart_brackets, smart_inserts, show_map, show_git_diff, show_background_pattern, show_right_margin, auto_tab_char_and_size, default_tab_char, tab_indents_line, highlight_current_line, show_line_numbers, enable_multiple_cursors, auto_reload_changed_files, search_for_selection, clang_tidy_enable, clang_detailed_preprocessing_record, debug_place_cursor_at_stop, consider reordering the fields or adding explicit padding members"
},
"ruleId": "optin.performance.Padding"
}
],
"tool": {
"driver": {
"fullName": "clang static analyzer",
"informationUri": "https://clang.llvm.org/docs/UsersManual.html",
"language": "en-US",
"name": "clang",
"rules": [
{
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"rank": -1
},
"fullDescription": {
"text": "Check for excessively padded structs."
},
"helpUri": "https://clang.llvm.org/docs/analyzer/checkers.html#optin-performance-padding",
"id": "optin.performance.Padding",
"name": "optin.performance.Padding"
}
],
"version": "clang version 21.1.8 (Fedora 21.1.8-4.fc43)"
}
}
}
],
"version": "2.1.0"
}

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::analyze() {}
std::pair<std::string, std::string> Project::Base::debug_get_run_arguments() {
return std::make_pair<std::string, std::string>("", "");
}

Loading…
Cancel
Save