From 5138b7d7c0e09b7c1829498bbfe68f9a41e10be3 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:53:09 +0900 Subject: [PATCH 01/19] feat(cli): add reusable argument parser with option suggestions --- include/cli/ArgParser.hpp | 49 +++ src/cli/ArgParser.cpp | 892 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 941 insertions(+) create mode 100644 include/cli/ArgParser.hpp create mode 100644 src/cli/ArgParser.cpp diff --git a/include/cli/ArgParser.hpp b/include/cli/ArgParser.hpp new file mode 100644 index 0000000..caa456e --- /dev/null +++ b/include/cli/ArgParser.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "StackUsageAnalyzer.hpp" + +#include +#include + +namespace ctrace::stack::cli +{ + + enum class OutputFormat + { + Human, + Json, + Sarif + }; + + struct ParsedArguments + { + AnalysisConfig config; + std::vector inputFilenames; + OutputFormat outputFormat = OutputFormat::Human; + std::string sarifBaseDir; + std::string compileCommandsPath; + bool compileCommandsExplicit = false; + bool analysisProfileExplicit = false; + bool includeCompdbDeps = false; + bool verbose = false; + }; + + enum class ParseStatus + { + Ok, + Help, + Error + }; + + struct ParseResult + { + ParseStatus status = ParseStatus::Ok; + ParsedArguments parsed; + std::string error; + }; + + ParseResult parseArguments(int argc, char** argv); + ParseResult parseArguments(const std::vector& analyzerArgs); + ParseResult parseCommandLine(const std::string& commandLine); + +} // namespace ctrace::stack::cli diff --git a/src/cli/ArgParser.cpp b/src/cli/ArgParser.cpp new file mode 100644 index 0000000..a53e778 --- /dev/null +++ b/src/cli/ArgParser.cpp @@ -0,0 +1,892 @@ +#include "cli/ArgParser.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ctrace::stack::cli +{ + namespace + { + struct OptionCandidate + { + std::string_view matcher; + std::string_view suggestion; + }; + + class UnknownOptionSuggester + { + public: + static std::optional suggest(std::string_view unknownOption) + { + if (unknownOption.empty() || unknownOption.front() != '-') + return std::nullopt; + + if (isLongOptionWithInlineValue(unknownOption)) + { + if (auto fixedValueSuggestion = suggestFixedValueOption(unknownOption)) + return fixedValueSuggestion; + return suggestLongOptionBase(unknownOption); + } + + return suggestByMatcher(unknownOption, false); + } + + private: + static constexpr std::array kCandidates = { + {{"-h", "-h"}, + {"--help", "--help"}, + {"--demangle", "--demangle"}, + {"--quiet", "--quiet"}, + {"--verbose", "--verbose"}, + {"--stl", "--STL"}, + {"--only-file", "--only-file"}, + {"--only-func", "--only-func"}, + {"--only-function", "--only-function"}, + {"--only-dir", "--only-dir"}, + {"--exclude-dir", "--exclude-dir"}, + {"--stack-limit", "--stack-limit"}, + {"--dump-filter", "--dump-filter"}, + {"--dump-ir", "--dump-ir"}, + {"-I", "-I"}, + {"-D", "-D"}, + {"--compile-arg", "--compile-arg"}, + {"--compdb-fast", "--compdb-fast"}, + {"--analysis-profile", "--analysis-profile"}, + {"--include-compdb-deps", "--include-compdb-deps"}, + {"--jobs", "--jobs"}, + {"--timing", "--timing"}, + {"--resource-model", "--resource-model"}, + {"--escape-model", "--escape-model"}, + {"--resource-cross-tu", "--resource-cross-tu"}, + {"--no-resource-cross-tu", "--no-resource-cross-tu"}, + {"--uninitialized-cross-tu", "--uninitialized-cross-tu"}, + {"--no-uninitialized-cross-tu", "--no-uninitialized-cross-tu"}, + {"--resource-summary-cache-dir", "--resource-summary-cache-dir"}, + {"--resource-summary-cache-memory-only", "--resource-summary-cache-memory-only"}, + {"--compile-commands", "--compile-commands"}, + {"--compdb", "--compdb"}, + {"--warnings-only", "--warnings-only"}, + {"--base-dir", "--base-dir"}, + {"--format", "--format=json"}, + {"--format=json", "--format=json"}, + {"--format=sarif", "--format=sarif"}, + {"--format=human", "--format=human"}, + {"--mode", "--mode=ir"}, + {"--mode=ir", "--mode=ir"}, + {"--mode=abi", "--mode=abi"}}}; + + struct CandidateScore + { + std::string_view suggestion; + std::size_t distance = std::numeric_limits::max(); + std::size_t queryLength = 0; + bool valid = false; + }; + + static std::optional suggestFixedValueOption(std::string_view unknownOption) + { + return suggestByMatcher(unknownOption, true); + } + + static std::optional suggestLongOptionBase(std::string_view unknownOption) + { + const std::size_t eqPos = unknownOption.find('='); + const std::string_view base = unknownOption.substr(0, eqPos); + return suggestByMatcher(base, false); + } + + static std::optional suggestByMatcher(std::string_view query, + bool onlyFixedValueCandidates) + { + CandidateScore best = findBestCandidate(query, onlyFixedValueCandidates); + if (!best.valid) + return std::nullopt; + + if (best.distance > maxAcceptedDistance(best.queryLength)) + return std::nullopt; + return std::string(best.suggestion); + } + + static CandidateScore findBestCandidate(std::string_view query, + bool onlyFixedValueCandidates) + { + const std::string loweredQuery = toLowerCopy(query); + CandidateScore best; + if (loweredQuery.empty()) + return best; + + for (const OptionCandidate& candidate : kCandidates) + { + const bool isFixedValue = candidate.matcher.find('=') != std::string_view::npos; + if (onlyFixedValueCandidates && !isFixedValue) + continue; + + const std::string loweredMatcher = toLowerCopy(candidate.matcher); + const std::size_t distance = levenshteinDistance(loweredQuery, loweredMatcher); + + if (!best.valid || distance < best.distance || + (distance == best.distance && candidate.suggestion < best.suggestion)) + { + best.valid = true; + best.distance = distance; + best.suggestion = candidate.suggestion; + best.queryLength = loweredQuery.size(); + } + } + return best; + } + + static bool isLongOptionWithInlineValue(std::string_view option) + { + return option.size() > 2 && option[0] == '-' && option[1] == '-' && + option.find('=') != std::string_view::npos; + } + + static std::size_t maxAcceptedDistance(std::size_t queryLength) + { + if (queryLength <= 4) + return 1; + if (queryLength <= 12) + return 2; + return 3; + } + + static std::string toLowerCopy(std::string_view input) + { + std::string lowered; + lowered.reserve(input.size()); + for (char c : input) + { + lowered.push_back(static_cast(std::tolower(static_cast(c)))); + } + return lowered; + } + + static std::size_t levenshteinDistance(std::string_view lhs, std::string_view rhs) + { + if (lhs.empty()) + return rhs.size(); + if (rhs.empty()) + return lhs.size(); + + std::vector prev(rhs.size() + 1); + std::vector cur(rhs.size() + 1); + for (std::size_t j = 0; j <= rhs.size(); ++j) + prev[j] = j; + + for (std::size_t i = 1; i <= lhs.size(); ++i) + { + cur[0] = i; + for (std::size_t j = 1; j <= rhs.size(); ++j) + { + const std::size_t substitutionCost = (lhs[i - 1] == rhs[j - 1]) ? 0 : 1; + cur[j] = + std::min({prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + substitutionCost}); + } + prev.swap(cur); + } + return prev[rhs.size()]; + } + }; + + std::string trimCopy(const std::string& input) + { + std::size_t start = 0; + while (start < input.size() && std::isspace(static_cast(input[start]))) + ++start; + std::size_t end = input.size(); + while (end > start && std::isspace(static_cast(input[end - 1]))) + --end; + return input.substr(start, end - start); + } + + bool parsePositiveUnsigned(const std::string& input, unsigned& out, std::string& error) + { + const std::string trimmed = trimCopy(input); + if (trimmed.empty()) + { + error = "value is empty"; + return false; + } + + unsigned long long parsed = 0; + const auto [ptr, ec] = + std::from_chars(trimmed.data(), trimmed.data() + trimmed.size(), parsed, 10); + if (ec != std::errc() || ptr != trimmed.data() + trimmed.size()) + { + error = "invalid numeric value"; + return false; + } + if (parsed == 0) + { + error = "value must be greater than zero"; + return false; + } + if (parsed > std::numeric_limits::max()) + { + error = "value is too large"; + return false; + } + out = static_cast(parsed); + return true; + } + + bool parseAnalysisProfile(const std::string& input, AnalysisProfile& out, + std::string& error) + { + std::string trimmed = trimCopy(input); + std::string lowered; + lowered.reserve(trimmed.size()); + for (char c : trimmed) + lowered.push_back(static_cast(std::tolower(static_cast(c)))); + + if (lowered == "fast") + { + out = AnalysisProfile::Fast; + return true; + } + if (lowered == "full") + { + out = AnalysisProfile::Full; + return true; + } + error = "expected 'fast' or 'full'"; + return false; + } + + bool parseStackLimitValue(const std::string& input, StackSize& out, std::string& error) + { + std::string trimmed = trimCopy(input); + if (trimmed.empty()) + { + error = "stack limit is empty"; + return false; + } + + std::size_t digitCount = 0; + while (digitCount < trimmed.size() && + std::isdigit(static_cast(trimmed[digitCount]))) + { + ++digitCount; + } + if (digitCount == 0) + { + error = "stack limit must start with a number"; + return false; + } + + const std::string numberPart = trimmed.substr(0, digitCount); + std::string suffix = trimCopy(trimmed.substr(digitCount)); + + unsigned long long base = 0; + auto [ptr, ec] = + std::from_chars(numberPart.data(), numberPart.data() + numberPart.size(), base, 10); + if (ec != std::errc() || ptr != numberPart.data() + numberPart.size()) + { + error = "invalid numeric value"; + return false; + } + if (base == 0) + { + error = "stack limit must be greater than zero"; + return false; + } + + StackSize multiplier = 1; + if (!suffix.empty()) + { + std::string lowered; + lowered.reserve(suffix.size()); + for (char c : suffix) + { + lowered.push_back( + static_cast(std::tolower(static_cast(c)))); + } + + if (lowered == "b") + { + multiplier = 1; + } + else if (lowered == "k" || lowered == "kb" || lowered == "kib") + { + multiplier = 1024ull; + } + else if (lowered == "m" || lowered == "mb" || lowered == "mib") + { + multiplier = 1024ull * 1024ull; + } + else if (lowered == "g" || lowered == "gb" || lowered == "gib") + { + multiplier = 1024ull * 1024ull * 1024ull; + } + else + { + error = "unsupported suffix (use bytes, KiB, MiB, or GiB)"; + return false; + } + } + + if (base > std::numeric_limits::max() / multiplier) + { + error = "stack limit is too large"; + return false; + } + + out = static_cast(base) * multiplier; + return true; + } + + void addCsvFilters(std::vector& dest, const std::string& input) + { + std::string current; + for (char c : input) + { + if (c == ',') + { + std::string trimmed = trimCopy(current); + if (!trimmed.empty()) + dest.push_back(trimmed); + current.clear(); + } + else + { + current.push_back(c); + } + } + std::string trimmed = trimCopy(current); + if (!trimmed.empty()) + dest.push_back(trimmed); + } + + bool consumeLongOptionValue(const std::string& argStr, const char* optionName, int& i, + int argc, char** argv, std::string& valueOut, + std::string& errorOut) + { + if (argStr == optionName) + { + if (i + 1 >= argc) + { + errorOut = "Missing argument for " + std::string(optionName); + return true; + } + valueOut = argv[++i]; + return true; + } + const std::string prefix = std::string(optionName) + "="; + if (argStr.rfind(prefix, 0) == 0) + { + valueOut = argStr.substr(prefix.size()); + return true; + } + return false; + } + + ParseResult makeError(std::string error) + { + ParseResult result; + result.status = ParseStatus::Error; + result.error = std::move(error); + return result; + } + + std::string unknownOptionErrorWithSuggestion(const std::string& argStr) + { + std::string error = "Unknown option: " + argStr; + if (auto suggestion = UnknownOptionSuggester::suggest(argStr)) + { + error += ". Did you mean '" + *suggestion + "'?"; + } + return error; + } + + bool splitCommandLine(const std::string& commandLine, std::vector& outArgs, + std::string& error) + { + outArgs.clear(); + std::string current; + + bool inSingleQuote = false; + bool inDoubleQuote = false; + bool escaping = false; + + auto flushCurrent = [&]() + { + if (!current.empty()) + { + outArgs.push_back(current); + current.clear(); + } + }; + + for (std::size_t i = 0; i < commandLine.size(); ++i) + { + const char ch = commandLine[i]; + + if (escaping) + { + current.push_back(ch); + escaping = false; + continue; + } + + if (inSingleQuote) + { + if (ch == '\'') + { + inSingleQuote = false; + } + else + { + current.push_back(ch); + } + continue; + } + + if (inDoubleQuote) + { + if (ch == '"') + { + inDoubleQuote = false; + } + else if (ch == '\\') + { + escaping = true; + } + else + { + current.push_back(ch); + } + continue; + } + + if (std::isspace(static_cast(ch))) + { + flushCurrent(); + continue; + } + + if (ch == '\'') + { + inSingleQuote = true; + continue; + } + + if (ch == '"') + { + inDoubleQuote = true; + continue; + } + + if (ch == '\\') + { + escaping = true; + continue; + } + + current.push_back(ch); + } + + if (escaping) + { + error = "Invalid command line: dangling escape at end of input"; + return false; + } + if (inSingleQuote || inDoubleQuote) + { + error = "Invalid command line: unterminated quoted string"; + return false; + } + + flushCurrent(); + return true; + } + } // namespace + + ParseResult parseArguments(int argc, char** argv) + { + ParseResult result; + ParsedArguments& parsed = result.parsed; + AnalysisConfig& cfg = parsed.config; + cfg.quiet = false; + cfg.warningsOnly = false; + cfg.extraCompileArgs.emplace_back("-O0"); + cfg.extraCompileArgs.emplace_back("--ct-optnone"); + + for (int i = 1; i < argc; ++i) + { + const char* arg = argv[i]; + std::string argStr{arg}; + + if (argStr == "-h" || argStr == "--help") + { + result.status = ParseStatus::Help; + return result; + } + if (argStr == "--demangle") + { + cfg.demangle = true; + continue; + } + if (argStr == "--quiet") + { + cfg.quiet = true; + continue; + } + if (argStr == "--verbose") + { + cfg.quiet = false; + parsed.verbose = true; + continue; + } + if (argStr == "--STL" || argStr == "--stl") + { + cfg.includeSTL = true; + continue; + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--only-file", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + cfg.onlyFiles.emplace_back(std::move(value)); + continue; + } + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--only-func", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + addCsvFilters(cfg.onlyFunctions, value); + continue; + } + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--only-function", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + addCsvFilters(cfg.onlyFunctions, value); + continue; + } + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--only-dir", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + cfg.onlyDirs.emplace_back(std::move(value)); + continue; + } + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--exclude-dir", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + addCsvFilters(cfg.excludeDirs, value); + continue; + } + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--stack-limit", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + StackSize parsedStackLimit = 0; + if (!parseStackLimitValue(value, parsedStackLimit, error)) + return makeError("Invalid --stack-limit value: " + error); + cfg.stackLimit = parsedStackLimit; + continue; + } + } + if (argStr == "--dump-filter") + { + cfg.dumpFilter = true; + continue; + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--dump-ir", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + cfg.dumpIRPath = std::move(value); + continue; + } + } + if (argStr == "-I") + { + if (i + 1 >= argc) + return makeError("Missing argument for -I"); + cfg.extraCompileArgs.emplace_back("-I" + std::string(argv[++i])); + continue; + } + if (argStr.rfind("-I", 0) == 0 && argStr.size() > 2) + { + cfg.extraCompileArgs.emplace_back(argStr); + continue; + } + if (argStr == "-D") + { + if (i + 1 >= argc) + return makeError("Missing argument for -D"); + cfg.extraCompileArgs.emplace_back("-D" + std::string(argv[++i])); + continue; + } + if (argStr.rfind("-D", 0) == 0 && argStr.size() > 2) + { + cfg.extraCompileArgs.emplace_back(argStr); + continue; + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--compile-arg", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + cfg.extraCompileArgs.emplace_back(std::move(value)); + continue; + } + } + if (argStr == "--compdb-fast") + { + cfg.compdbFast = true; + continue; + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--analysis-profile", i, argc, argv, value, + error)) + { + if (!error.empty()) + return makeError(error); + if (!parseAnalysisProfile(value, cfg.profile, error)) + return makeError("Invalid --analysis-profile value: " + error); + parsed.analysisProfileExplicit = true; + continue; + } + } + if (argStr == "--include-compdb-deps") + { + parsed.includeCompdbDeps = true; + continue; + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--jobs", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + unsigned parsedJobs = 0; + if (!parsePositiveUnsigned(value, parsedJobs, error)) + return makeError("Invalid --jobs value: " + error); + cfg.jobs = parsedJobs; + continue; + } + } + if (argStr == "--timing") + { + cfg.timing = true; + continue; + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--resource-model", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + cfg.resourceModelPath = std::move(value); + continue; + } + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--escape-model", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + cfg.escapeModelPath = std::move(value); + continue; + } + } + if (argStr == "--resource-cross-tu") + { + cfg.resourceCrossTU = true; + continue; + } + if (argStr == "--no-resource-cross-tu") + { + cfg.resourceCrossTU = false; + continue; + } + if (argStr == "--uninitialized-cross-tu") + { + cfg.uninitializedCrossTU = true; + continue; + } + if (argStr == "--no-uninitialized-cross-tu") + { + cfg.uninitializedCrossTU = false; + continue; + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--resource-summary-cache-dir", i, argc, argv, + value, error)) + { + if (!error.empty()) + return makeError(error); + cfg.resourceSummaryCacheDir = std::move(value); + continue; + } + } + if (argStr == "--resource-summary-cache-memory-only") + { + cfg.resourceSummaryMemoryOnly = true; + continue; + } + if (argStr == "--compile-commands" || argStr == "--compdb") + { + if (i + 1 >= argc) + return makeError("Missing argument for " + argStr); + parsed.compileCommandsPath = argv[++i]; + parsed.compileCommandsExplicit = true; + continue; + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--compile-commands", i, argc, argv, value, + error)) + { + if (!error.empty()) + return makeError(error); + parsed.compileCommandsPath = std::move(value); + parsed.compileCommandsExplicit = true; + continue; + } + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--compdb", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + parsed.compileCommandsPath = std::move(value); + parsed.compileCommandsExplicit = true; + continue; + } + } + if (argStr == "--warnings-only") + { + cfg.warningsOnly = true; + continue; + } + if (argStr == "--format=json") + { + parsed.outputFormat = OutputFormat::Json; + continue; + } + if (argStr == "--format=sarif") + { + parsed.outputFormat = OutputFormat::Sarif; + continue; + } + if (argStr == "--format=human") + { + parsed.outputFormat = OutputFormat::Human; + continue; + } + { + std::string value; + std::string error; + if (consumeLongOptionValue(argStr, "--base-dir", i, argc, argv, value, error)) + { + if (!error.empty()) + return makeError(error); + parsed.sarifBaseDir = std::move(value); + continue; + } + } + if (std::strncmp(arg, "--mode=", 7) == 0) + { + const char* modeStr = arg + 7; + if (std::strcmp(modeStr, "ir") == 0) + { + cfg.mode = AnalysisMode::IR; + } + else if (std::strcmp(modeStr, "abi") == 0) + { + cfg.mode = AnalysisMode::ABI; + } + else + { + return makeError("Unknown mode: " + std::string(modeStr) + + " (expected 'ir' or 'abi')"); + } + continue; + } + if (!argStr.empty() && argStr[0] == '-') + return makeError(unknownOptionErrorWithSuggestion(argStr)); + + parsed.inputFilenames.emplace_back(std::move(argStr)); + } + + return result; + } + + ParseResult parseArguments(const std::vector& analyzerArgs) + { + std::vector argvStorage; + argvStorage.reserve(analyzerArgs.size() + 1); + argvStorage.emplace_back("stack_usage_analyzer"); + argvStorage.insert(argvStorage.end(), analyzerArgs.begin(), analyzerArgs.end()); + + std::vector argvPointers; + argvPointers.reserve(argvStorage.size()); + for (auto& arg : argvStorage) + argvPointers.push_back(const_cast(arg.c_str())); + + return parseArguments(static_cast(argvPointers.size()), argvPointers.data()); + } + + ParseResult parseCommandLine(const std::string& commandLine) + { + std::vector args; + std::string splitError; + if (!splitCommandLine(commandLine, args, splitError)) + return makeError(splitError); + return parseArguments(args); + } + +} // namespace ctrace::stack::cli From 5c72165ba1e0fd442b08bd1c2818bdb4dfb8d735 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:53:31 +0900 Subject: [PATCH 02/19] feat(app): extract analyzer orchestration into AnalyzerApp service --- include/app/AnalyzerApp.hpp | 28 + src/app/AnalyzerApp.cpp | 1884 +++++++++++++++++++++++++++++++++++ 2 files changed, 1912 insertions(+) create mode 100644 include/app/AnalyzerApp.hpp create mode 100644 src/app/AnalyzerApp.cpp diff --git a/include/app/AnalyzerApp.hpp b/include/app/AnalyzerApp.hpp new file mode 100644 index 0000000..cca1f5f --- /dev/null +++ b/include/app/AnalyzerApp.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "cli/ArgParser.hpp" + +#include + +namespace llvm +{ + class LLVMContext; +} + +namespace ctrace::stack::app +{ + + struct RunResult + { + int exitCode = 1; + std::string error; + + bool isOk() const + { + return error.empty(); + } + }; + + RunResult runAnalyzerApp(cli::ParsedArguments parsedArgs, llvm::LLVMContext& context); + +} // namespace ctrace::stack::app diff --git a/src/app/AnalyzerApp.cpp b/src/app/AnalyzerApp.cpp new file mode 100644 index 0000000..ed3be99 --- /dev/null +++ b/src/app/AnalyzerApp.cpp @@ -0,0 +1,1884 @@ +#include "app/AnalyzerApp.hpp" + +#include "StackUsageAnalyzer.hpp" +#include "cli/ArgParser.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "analysis/CompileCommands.hpp" +#include "analysis/FunctionFilter.hpp" +#include "analysis/InputPipeline.hpp" +#include "analysis/ResourceLifetimeAnalysis.hpp" +#include "analysis/UninitializedVarAnalysis.hpp" +#include "mangle.hpp" + +#include + +using namespace ctrace::stack; + +static std::string normalizePath(const std::string& input) +{ + if (input.empty()) + return {}; + + std::string adjusted = input; + for (char& c : adjusted) + { + if (c == '\\') + c = '/'; + } + + std::filesystem::path path(adjusted); + std::error_code ec; + std::filesystem::path absPath = std::filesystem::absolute(path, ec); + if (ec) + absPath = path; + + std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(absPath, ec); + std::filesystem::path norm = ec ? absPath.lexically_normal() : canonicalPath; + std::string out = norm.generic_string(); + while (out.size() > 1 && out.back() == '/') + out.pop_back(); + return out; +} + +struct NormalizedPathFilters +{ + std::vector onlyFiles; + std::vector onlyDirs; + std::vector excludeDirs; +}; + +template struct AppResult +{ + std::optional value; + std::string error; + + static AppResult success(T v) + { + AppResult r; + r.value = std::move(v); + return r; + } + + static AppResult failure(std::string e) + { + AppResult r; + r.error = std::move(e); + return r; + } + + bool isOk() const + { + return error.empty(); + } +}; + +template <> struct AppResult +{ + std::string error; + + static AppResult success() + { + return {}; + } + + static AppResult failure(std::string e) + { + AppResult r; + r.error = std::move(e); + return r; + } + + bool isOk() const + { + return error.empty(); + } +}; + +using AppStatus = AppResult; + +static NormalizedPathFilters buildNormalizedPathFilters(const AnalysisConfig& cfg) +{ + NormalizedPathFilters filters; + filters.onlyFiles.reserve(cfg.onlyFiles.size()); + filters.onlyDirs.reserve(cfg.onlyDirs.size()); + filters.excludeDirs.reserve(cfg.excludeDirs.size()); + + for (const auto& file : cfg.onlyFiles) + filters.onlyFiles.push_back(normalizePath(file)); + for (const auto& dir : cfg.onlyDirs) + filters.onlyDirs.push_back(normalizePath(dir)); + for (const auto& dir : cfg.excludeDirs) + filters.excludeDirs.push_back(normalizePath(dir)); + + return filters; +} + +static std::string basenameOf(const std::string& path) +{ + std::size_t pos = path.find_last_of('/'); + if (pos == std::string::npos) + return path; + if (pos + 1 >= path.size()) + return {}; + return path.substr(pos + 1); +} + +static bool pathHasSuffix(const std::string& path, const std::string& suffix) +{ + if (suffix.empty()) + return false; + if (path.size() < suffix.size()) + return false; + if (path.compare(path.size() - suffix.size(), suffix.size(), suffix) != 0) + return false; + if (path.size() == suffix.size()) + return true; + return path[path.size() - suffix.size() - 1] == '/'; +} + +static bool pathHasPrefix(const std::string& path, const std::string& prefix) +{ + if (prefix.empty()) + return false; + if (path.size() < prefix.size()) + return false; + if (path.compare(0, prefix.size(), prefix) != 0) + return false; + if (path.size() == prefix.size()) + return true; + return path[prefix.size()] == '/'; +} + +static bool pathContainsSegment(const std::string& path, const std::string& segment) +{ + if (path.empty() || segment.empty()) + return false; + std::size_t start = 0; + while (start < path.size()) + { + while (start < path.size() && path[start] == '/') + ++start; + if (start >= path.size()) + break; + std::size_t end = path.find('/', start); + if (end == std::string::npos) + end = path.size(); + if (path.compare(start, end - start, segment) == 0) + return true; + start = end + 1; + } + return false; +} + +static bool shouldIncludePath(const std::string& path, const AnalysisConfig& cfg, + const NormalizedPathFilters& filters) +{ + if (cfg.onlyFiles.empty() && cfg.onlyDirs.empty()) + return true; + if (path.empty()) + return false; + + const std::string normPath = normalizePath(path); + + for (const auto& normFile : filters.onlyFiles) + { + if (normPath == normFile || pathHasSuffix(normPath, normFile)) + return true; + const std::string fileBase = basenameOf(normFile); + if (!fileBase.empty() && basenameOf(normPath) == fileBase) + return true; + } + + for (const auto& normDir : filters.onlyDirs) + { + if (pathHasPrefix(normPath, normDir) || pathHasSuffix(normPath, normDir)) + return true; + const std::string needle = "/" + normDir + "/"; + if (normPath.find(needle) != std::string::npos) + return true; + } + + return false; +} + +static bool shouldExcludePath(const std::string& path, const NormalizedPathFilters& filters) +{ + if (filters.excludeDirs.empty() || path.empty()) + return false; + + const std::string normPath = normalizePath(path); + for (const auto& normDir : filters.excludeDirs) + { + if (normDir.empty()) + continue; + if (pathHasPrefix(normPath, normDir) || pathHasSuffix(normPath, normDir)) + return true; + const std::string needle = "/" + normDir + "/"; + if (normPath.find(needle) != std::string::npos) + return true; + } + + return false; +} + +static bool functionNameMatches(const std::string& name, const AnalysisConfig& cfg) +{ + if (cfg.onlyFunctions.empty()) + return true; + + auto itaniumBaseName = [](const std::string& symbol) -> std::string + { + if (symbol.rfind("_Z", 0) != 0) + return {}; + std::size_t i = 2; + if (i < symbol.size() && symbol[i] == 'L') + ++i; + if (i >= symbol.size() || !std::isdigit(static_cast(symbol[i]))) + return {}; + std::size_t len = 0; + while (i < symbol.size() && std::isdigit(static_cast(symbol[i]))) + { + len = len * 10 + static_cast(symbol[i] - '0'); + ++i; + } + if (len == 0 || i + len > symbol.size()) + return {}; + return symbol.substr(i, len); + }; + + std::string demangledName; + if (ctrace_tools::isMangled(name) || name.rfind("_Z", 0) == 0) + demangledName = ctrace_tools::demangle(name.c_str()); + std::string demangledBase; + if (!demangledName.empty()) + { + std::size_t pos = demangledName.find('('); + if (pos != std::string::npos && pos > 0) + demangledBase = demangledName.substr(0, pos); + } + std::string itaniumBase = itaniumBaseName(name); + + for (const auto& filter : cfg.onlyFunctions) + { + if (name == filter) + return true; + if (!demangledName.empty() && demangledName == filter) + return true; + if (!demangledBase.empty() && demangledBase == filter) + return true; + if (!itaniumBase.empty() && itaniumBase == filter) + return true; + if (ctrace_tools::isMangled(filter)) + { + std::string demangledFilter = ctrace_tools::demangle(filter.c_str()); + if (!demangledName.empty() && demangledName == demangledFilter) + return true; + std::size_t pos = demangledFilter.find('('); + if (pos != std::string::npos && pos > 0) + { + if (demangledBase == demangledFilter.substr(0, pos)) + return true; + } + } + } + + return false; +} + +class FunctionReportSpecification +{ + public: + FunctionReportSpecification(const AnalysisConfig& cfg, const NormalizedPathFilters& filters) + : cfg_(cfg), filters_(filters) + { + } + + bool isSatisfiedBy(const FunctionResult& function) const + { + bool keep = functionNameMatches(function.name, cfg_); + if (keep && (!cfg_.onlyFiles.empty() || !cfg_.onlyDirs.empty())) + keep = shouldIncludePath(function.filePath, cfg_, filters_); + return keep; + } + + private: + const AnalysisConfig& cfg_; + const NormalizedPathFilters& filters_; +}; + +class InputExclusionSpecification +{ + public: + explicit InputExclusionSpecification(const NormalizedPathFilters& filters) : filters_(filters) + { + } + + bool isSatisfiedBy(const std::string& inputPath) const + { + return shouldExcludePath(inputPath, filters_); + } + + private: + const NormalizedPathFilters& filters_; +}; + +static AnalysisResult filterResult(const AnalysisResult& result, const AnalysisConfig& cfg, + const NormalizedPathFilters& filters) +{ + if (cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty()) + return result; + + AnalysisResult filtered; + filtered.config = result.config; + FunctionReportSpecification functionSpec(cfg, filters); + + std::unordered_set keepFuncs; + for (const auto& f : result.functions) + { + const bool keep = functionSpec.isSatisfiedBy(f); + if (keep) + { + filtered.functions.push_back(f); + keepFuncs.insert(f.name); + } + } + + if (!keepFuncs.empty()) + { + for (const auto& d : result.diagnostics) + { + if (keepFuncs.count(d.funcName) != 0) + { + filtered.diagnostics.push_back(d); + } + } + } + + return filtered; +} + +static AnalysisResult filterWarningsOnly(const AnalysisResult& result, const AnalysisConfig& cfg) +{ + if (!cfg.warningsOnly) + return result; + + AnalysisResult filtered; + filtered.config = result.config; + filtered.functions = result.functions; + for (const auto& d : result.diagnostics) + { + if (d.severity != DiagnosticSeverity::Info) + { + filtered.diagnostics.push_back(d); + } + } + return filtered; +} + +struct LoadedInputModule +{ + std::string filename; + std::unique_ptr context; + std::unique_ptr module; +}; + +using AnalysisEntry = std::pair; + +static std::shared_ptr +buildCrossTUSummaryIndex(const std::vector& loadedModules, + const AnalysisConfig& cfg); + +static std::shared_ptr +buildCrossTUUninitializedSummaryIndex(const std::vector& loadedModules, + const AnalysisConfig& cfg); + +struct DiagnosticSummary +{ + std::size_t info = 0; + std::size_t warning = 0; + std::size_t error = 0; +}; + +static void accumulateSummary(DiagnosticSummary& total, const DiagnosticSummary& add); +static DiagnosticSummary summarizeDiagnostics(const AnalysisResult& result); + +static void stampResultFilePaths(AnalysisResult& result, const std::string& inputFilename) +{ + for (auto& f : result.functions) + { + if (f.filePath.empty()) + f.filePath = inputFilename; + } + for (auto& d : result.diagnostics) + { + if (d.filePath.empty()) + d.filePath = inputFilename; + } +} + +static std::string noFunctionMessage(const AnalysisResult& result, const std::string& inputFilename, + bool hasFilter) +{ + if (!result.functions.empty()) + return {}; + if (hasFilter) + return "No functions matched filters for: " + inputFilename + "\n"; + return "[ !Info! ] No analyzable functions in: " + inputFilename + " (skipping)\n"; +} + +static void logText(coretrace::Level level, const std::string& text) +{ + if (text.empty()) + return; + if (text.back() == '\n') + { + coretrace::log(level, "{}", text); + } + else + { + coretrace::log(level, "{}\n", text); + } +} + +static AppStatus loadCompilationDatabase(const std::string& compileCommandsPath, + AnalysisConfig& cfg) +{ + if (compileCommandsPath.empty()) + { + return AppStatus::failure("Compile commands path is empty"); + } + + std::filesystem::path compdbPath = compileCommandsPath; + std::error_code fsErr; + if (std::filesystem::is_directory(compdbPath, fsErr)) + { + compdbPath /= "compile_commands.json"; + } + else if (fsErr) + { + return AppStatus::failure("Failed to inspect compile commands path: " + fsErr.message()); + } + + if (!std::filesystem::exists(compdbPath, fsErr)) + { + if (fsErr) + { + return AppStatus::failure("Failed to inspect compile commands path: " + + fsErr.message()); + } + return AppStatus::failure("Compile commands file not found: " + compdbPath.string()); + } + + std::string error; + auto db = + ctrace::stack::analysis::CompilationDatabase::loadFromFile(compdbPath.string(), error); + if (!db) + { + return AppStatus::failure("Failed to load compile commands: " + error); + } + + cfg.compilationDatabase = std::move(db); + cfg.requireCompilationDatabase = true; + return AppStatus::success(); +} + +static bool discoverInputsFromCompilationDatabase(std::vector& inputFilenames, + const AnalysisConfig& cfg, bool includeCompdbDeps) +{ + if (!inputFilenames.empty() || !cfg.compilationDatabase) + return false; + + std::vector compdbFiles = cfg.compilationDatabase->listSourceFiles(); + std::size_t skippedUnsupported = 0; + std::size_t skippedDeps = 0; + for (const std::string& file : compdbFiles) + { + const LanguageType lang = analysis::detectFromExtension(file); + if (lang == LanguageType::Unknown) + { + ++skippedUnsupported; + continue; + } + if (!includeCompdbDeps) + { + const std::string normalizedFile = normalizePath(file); + if (pathContainsSegment(normalizedFile, "_deps")) + { + ++skippedDeps; + continue; + } + } + inputFilenames.push_back(file); + } + + if (!inputFilenames.empty()) + { + std::string message = "No explicit input files provided: using " + + std::to_string(inputFilenames.size()) + + " supported file(s) from compile_commands.json"; + if (skippedUnsupported > 0) + message += + " (skipped " + std::to_string(skippedUnsupported) + " unsupported entry/entries)"; + if (skippedDeps > 0) + message += " (skipped " + std::to_string(skippedDeps) + " _deps entry/entries)"; + coretrace::log(coretrace::Level::Info, "{}\n", message); + } + else + { + std::string message = "No supported source files found in compile_commands.json"; + if (skippedUnsupported > 0) + message += " (all entries were unsupported for this analyzer)"; + if (skippedDeps > 0) + message += " (all supported entries were filtered from _deps)"; + coretrace::log(coretrace::Level::Error, "{}\n", message); + } + return true; +} + +static void excludeInputFiles(std::vector& inputFilenames, const AnalysisConfig& cfg, + const NormalizedPathFilters& normalizedFilters) +{ + if (cfg.excludeDirs.empty() || inputFilenames.empty()) + return; + + std::vector filteredInputs; + filteredInputs.reserve(inputFilenames.size()); + std::size_t excludedCount = 0; + InputExclusionSpecification excludeSpec(normalizedFilters); + for (const auto& file : inputFilenames) + { + if (excludeSpec.isSatisfiedBy(file)) + { + ++excludedCount; + continue; + } + filteredInputs.push_back(file); + } + if (excludedCount > 0) + { + coretrace::log(coretrace::Level::Info, + "Excluded {} input file(s) via --exclude-dir filters\n", excludedCount); + } + inputFilenames.swap(filteredInputs); +} + +static AppStatus configureDumpIRPath(const std::vector& inputFilenames, + AnalysisConfig& cfg) +{ + if (cfg.dumpIRPath.empty()) + return AppStatus::success(); + + const bool trailingSlash = + !cfg.dumpIRPath.empty() && (cfg.dumpIRPath.back() == '/' || cfg.dumpIRPath.back() == '\\'); + std::error_code fsErr; + std::filesystem::path dumpPath(cfg.dumpIRPath); + const bool exists = std::filesystem::exists(dumpPath, fsErr); + if (fsErr) + { + return AppStatus::failure("Failed to inspect dump IR path: " + fsErr.message()); + } + + bool isDir = false; + if (exists) + { + isDir = std::filesystem::is_directory(dumpPath, fsErr); + if (fsErr) + { + return AppStatus::failure("Failed to inspect dump IR path: " + fsErr.message()); + } + } + if (inputFilenames.size() > 1 && !isDir && !trailingSlash) + { + return AppStatus::failure( + "--dump-ir must point to a directory when analyzing multiple inputs"); + } + cfg.dumpIRIsDir = isDir || trailingSlash || inputFilenames.size() > 1; + return AppStatus::success(); +} + +static void printInterprocStatus(const AnalysisConfig& cfg, std::size_t inputCount, + bool needsCrossTUResourceSummaries, + bool needsCrossTUUninitializedSummaries) +{ + if (!cfg.resourceModelPath.empty()) + { + if (needsCrossTUResourceSummaries) + { + std::string cacheSuffix; + if (cfg.resourceSummaryMemoryOnly) + cacheSuffix = ", cache: memory-only"; + else if (!cfg.resourceSummaryCacheDir.empty()) + cacheSuffix = ", cache: " + cfg.resourceSummaryCacheDir; + coretrace::log(coretrace::Level::Info, + "Resource inter-procedural analysis: enabled (cross-TU summaries across " + "{} files, jobs: {}{})\n", + inputCount, std::max(1u, cfg.jobs), cacheSuffix); + } + else if (!cfg.resourceCrossTU) + { + coretrace::log(coretrace::Level::Warn, + "Resource inter-procedural analysis: disabled by " + "--no-resource-cross-tu (local TU only)\n"); + } + else if (inputCount <= 1) + { + coretrace::log(coretrace::Level::Warn, + "Resource inter-procedural analysis: unavailable " + "(need at least 2 input files; local TU only)\n"); + } + } + + if (inputCount > 1) + { + if (needsCrossTUUninitializedSummaries) + { + coretrace::log( + coretrace::Level::Info, + "Uninitialized inter-procedural analysis: enabled (cross-TU summaries across " + "{} files, jobs: {})\n", + inputCount, std::max(1u, cfg.jobs)); + } + else if (!cfg.uninitializedCrossTU) + { + coretrace::log(coretrace::Level::Warn, + "Uninitialized inter-procedural analysis: disabled by " + "--no-uninitialized-cross-tu (local TU only)\n"); + } + } +} + +static AppStatus analyzeWithSharedModuleLoading(const std::vector& inputFilenames, + AnalysisConfig& cfg, bool hasFilter, + bool needsCrossTUResourceSummaries, + bool needsCrossTUUninitializedSummaries, + std::vector& results) +{ + std::vector loadedModules(inputFilenames.size()); + std::vector loadErrors(inputFilenames.size()); + std::vector loadSucceeded(inputFilenames.size(), 0); + auto loadSingleModule = [&](std::size_t index) + { + const std::string& inputFilename = inputFilenames[index]; + auto moduleContext = std::make_unique(); + llvm::SMDiagnostic localErr; + analysis::ModuleLoadResult load = + analysis::loadModuleForAnalysis(inputFilename, cfg, *moduleContext, localErr); + if (!load.module) + { + std::string err; + if (!load.error.empty()) + err += load.error; + if (localErr.getLineNo() != 0 || !localErr.getFilename().empty()) + { + std::string diagText; + llvm::raw_string_ostream os(diagText); + localErr.print("stack_usage_analyzer", os); + os.flush(); + err += diagText; + } + loadErrors[index] = std::move(err); + return; + } + loadedModules[index] = {inputFilename, std::move(moduleContext), std::move(load.module)}; + loadSucceeded[index] = 1; + }; + + const unsigned loadJobs = std::max(1u, cfg.jobs); + if (loadJobs <= 1 || inputFilenames.size() <= 1) + { + for (std::size_t index = 0; index < inputFilenames.size(); ++index) + loadSingleModule(index); + } + else + { + std::atomic_size_t nextIndex{0}; + const unsigned workerCount = + std::min(loadJobs, static_cast(inputFilenames.size())); + std::vector workers; + workers.reserve(workerCount); + for (unsigned worker = 0; worker < workerCount; ++worker) + { + workers.emplace_back( + [&]() + { + while (true) + { + const std::size_t index = nextIndex.fetch_add(1); + if (index >= inputFilenames.size()) + break; + loadSingleModule(index); + } + }); + } + for (auto& worker : workers) + worker.join(); + } + + std::vector orderedLoadedModules; + orderedLoadedModules.reserve(inputFilenames.size()); + for (std::size_t index = 0; index < inputFilenames.size(); ++index) + { + if (!loadSucceeded[index]) + { + std::string message; + if (!loadErrors[index].empty()) + { + message = loadErrors[index]; + if (!message.empty() && message.back() != '\n') + message.push_back('\n'); + } + message += "Failed to analyze: " + inputFilenames[index]; + return AppStatus::failure(std::move(message)); + } + orderedLoadedModules.push_back(std::move(loadedModules[index])); + } + loadedModules.swap(orderedLoadedModules); + + if (needsCrossTUResourceSummaries) + cfg.resourceSummaryIndex = buildCrossTUSummaryIndex(loadedModules, cfg); + if (needsCrossTUUninitializedSummaries) + cfg.uninitializedSummaryIndex = buildCrossTUUninitializedSummaryIndex(loadedModules, cfg); + + for (auto& loaded : loadedModules) + { + AnalysisResult result = analyzeModule(*loaded.module, cfg); + stampResultFilePaths(result, loaded.filename); + const std::string emptyMsg = noFunctionMessage(result, loaded.filename, hasFilter); + if (!emptyMsg.empty()) + logText(coretrace::Level::Info, emptyMsg); + results.emplace_back(loaded.filename, std::move(result)); + } + return AppStatus::success(); +} + +static AppStatus analyzeWithoutSharedModuleLoading(const std::vector& inputFilenames, + const AnalysisConfig& cfg, + llvm::LLVMContext& context, bool hasFilter, + std::vector& results) +{ + const unsigned parallelJobs = std::max(1u, cfg.jobs); + if (parallelJobs <= 1 || inputFilenames.size() <= 1) + { + for (const auto& inputFilename : inputFilenames) + { + llvm::SMDiagnostic localErr; + analysis::ModuleLoadResult load = + analysis::loadModuleForAnalysis(inputFilename, cfg, context, localErr); + if (!load.module) + { + std::string message; + if (!load.error.empty()) + { + message += load.error; + if (!message.empty() && message.back() != '\n') + message.push_back('\n'); + } + std::string diagText; + llvm::raw_string_ostream os(diagText); + localErr.print("stack_usage_analyzer", os); + os.flush(); + message += diagText; + if (!message.empty() && message.back() != '\n') + message.push_back('\n'); + message += "Failed to analyze: " + inputFilename; + return AppStatus::failure(std::move(message)); + } + + AnalysisResult result = analyzeModule(*load.module, cfg); + stampResultFilePaths(result, inputFilename); + const std::string emptyMsg = noFunctionMessage(result, inputFilename, hasFilter); + if (!emptyMsg.empty()) + logText(coretrace::Level::Info, emptyMsg); + results.emplace_back(inputFilename, std::move(result)); + } + return AppStatus::success(); + } + + struct ParallelAnalysisSlot + { + AnalysisResult result; + std::string loadError; + std::string noFunctionMsg; + bool success = false; + }; + + std::vector slots(inputFilenames.size()); + std::atomic_size_t nextIndex{0}; + const unsigned workerCount = + std::min(parallelJobs, static_cast(inputFilenames.size())); + std::vector workers; + workers.reserve(workerCount); + for (unsigned worker = 0; worker < workerCount; ++worker) + { + workers.emplace_back( + [&]() + { + while (true) + { + const std::size_t index = nextIndex.fetch_add(1); + if (index >= inputFilenames.size()) + break; + + const std::string& inputFilename = inputFilenames[index]; + llvm::LLVMContext localContext; + llvm::SMDiagnostic localErr; + analysis::ModuleLoadResult load = + analysis::loadModuleForAnalysis(inputFilename, cfg, localContext, localErr); + if (!load.module) + { + std::string err; + if (!load.error.empty()) + err += load.error; + if (localErr.getLineNo() != 0 || !localErr.getFilename().empty()) + { + std::string diagText; + llvm::raw_string_ostream os(diagText); + localErr.print("stack_usage_analyzer", os); + os.flush(); + err += diagText; + } + slots[index].loadError = std::move(err); + continue; + } + + AnalysisResult result = analyzeModule(*load.module, cfg); + stampResultFilePaths(result, inputFilename); + slots[index].noFunctionMsg = + noFunctionMessage(result, inputFilename, hasFilter); + slots[index].result = std::move(result); + slots[index].success = true; + } + }); + } + for (auto& worker : workers) + worker.join(); + + for (std::size_t index = 0; index < inputFilenames.size(); ++index) + { + if (!slots[index].success) + { + std::string message; + if (!slots[index].loadError.empty()) + { + message = slots[index].loadError; + if (!message.empty() && message.back() != '\n') + message.push_back('\n'); + } + message += "Failed to analyze: " + inputFilenames[index]; + return AppStatus::failure(std::move(message)); + } + if (!slots[index].noFunctionMsg.empty()) + logText(coretrace::Level::Info, slots[index].noFunctionMsg); + results.emplace_back(inputFilenames[index], std::move(slots[index].result)); + } + return AppStatus::success(); +} + +static AnalysisResult mergeAnalysisResults(const std::vector& results, + const AnalysisConfig& cfg) +{ + AnalysisResult merged{}; + merged.config = cfg; + for (const auto& entry : results) + { + const auto& res = entry.second; + merged.functions.insert(merged.functions.end(), res.functions.begin(), res.functions.end()); + merged.diagnostics.insert(merged.diagnostics.end(), res.diagnostics.begin(), + res.diagnostics.end()); + } + return merged; +} + +static int emitJsonOutput(const std::vector& results, const AnalysisConfig& cfg, + const std::vector& inputFilenames, + const NormalizedPathFilters& normalizedFilters) +{ + const bool applyFilter = + cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty(); + if (results.size() == 1) + { + AnalysisResult filtered = applyFilter + ? filterResult(results[0].second, cfg, normalizedFilters) + : results[0].second; + filtered = filterWarningsOnly(filtered, cfg); + llvm::outs() << ctrace::stack::toJson(filtered, results[0].first); + return 0; + } + + AnalysisResult merged = mergeAnalysisResults(results, cfg); + AnalysisResult filtered = applyFilter ? filterResult(merged, cfg, normalizedFilters) : merged; + filtered = filterWarningsOnly(filtered, cfg); + llvm::outs() << ctrace::stack::toJson(filtered, inputFilenames); + return 0; +} + +static int emitSarifOutput(const std::vector& results, const AnalysisConfig& cfg, + const std::vector& inputFilenames, + const std::string& sarifBaseDir, + const NormalizedPathFilters& normalizedFilters) +{ + const bool applyFilter = + cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty(); + if (results.size() == 1) + { + AnalysisResult filtered = applyFilter + ? filterResult(results[0].second, cfg, normalizedFilters) + : results[0].second; + filtered = filterWarningsOnly(filtered, cfg); + llvm::outs() << ctrace::stack::toSarif(filtered, results[0].first, + "coretrace-stack-analyzer", "0.1.0", sarifBaseDir); + return 0; + } + + AnalysisResult merged = mergeAnalysisResults(results, cfg); + AnalysisResult filtered = applyFilter ? filterResult(merged, cfg, normalizedFilters) : merged; + filtered = filterWarningsOnly(filtered, cfg); + llvm::outs() << ctrace::stack::toSarif(filtered, inputFilenames.front(), + "coretrace-stack-analyzer", "0.1.0", sarifBaseDir); + return 0; +} + +static int emitHumanOutput(const std::vector& results, const AnalysisConfig& cfg, + const NormalizedPathFilters& normalizedFilters) +{ + const bool multiFile = results.size() > 1; + DiagnosticSummary totalSummary; + for (std::size_t r = 0; r < results.size(); ++r) + { + const auto& inputFilename = results[r].first; + const AnalysisResult result = + (cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty()) + ? filterResult(results[r].second, cfg, normalizedFilters) + : results[r].second; + + if (multiFile) + { + if (r > 0) + llvm::outs() << "\n"; + llvm::outs() << "File: " << inputFilename << "\n"; + } + + llvm::outs() << "Mode: " << (result.config.mode == AnalysisMode::IR ? "IR" : "ABI") + << "\n\n"; + + for (const auto& f : result.functions) + { + if (cfg.demangle) + { + llvm::outs() << "Function: " << ctrace_tools::demangle(f.name.c_str()) << "\n"; + } + else + { + llvm::outs() << "Function: " << f.name << " " + << ((ctrace_tools::isMangled(f.name)) + ? ctrace_tools::demangle(f.name.c_str()) + : "") + << "\n"; + } + if (f.localStackUnknown) + { + llvm::outs() << "\tlocal stack: unknown"; + if (f.localStack > 0) + llvm::outs() << " (>= " << f.localStack << " bytes)"; + llvm::outs() << "\n"; + } + else + { + llvm::outs() << "\tlocal stack: " << f.localStack << " bytes\n"; + } + + if (f.maxStackUnknown) + { + llvm::outs() << "\tmax stack (including callees): unknown"; + if (f.maxStack > 0) + llvm::outs() << " (>= " << f.maxStack << " bytes)"; + llvm::outs() << "\n"; + } + else + { + llvm::outs() << "\tmax stack (including callees): " << f.maxStack << " bytes\n"; + } + + if (!result.config.quiet) + { + for (const auto& d : result.diagnostics) + { + if (d.funcName != f.name) + continue; + if (result.config.warningsOnly && d.severity == DiagnosticSeverity::Info) + continue; + if (d.line != 0) + llvm::outs() << "\tat line " << d.line << ", column " << d.column << "\n"; + llvm::outs() << d.message << "\n"; + } + } + + llvm::outs() << "\n"; + } + + const DiagnosticSummary summary = summarizeDiagnostics(result); + accumulateSummary(totalSummary, summary); + llvm::outs() << "Diagnostics summary: info=" << summary.info + << ", warning=" << summary.warning << ", error=" << summary.error << "\n"; + } + + if (multiFile) + { + llvm::outs() << "\nTotal diagnostics summary: info=" << totalSummary.info + << ", warning=" << totalSummary.warning << ", error=" << totalSummary.error + << " (across " << results.size() << " files)\n"; + } + return 0; +} + +static std::string md5Hex(llvm::StringRef input) +{ + llvm::MD5 hasher; + hasher.update(input); + llvm::MD5::MD5Result out; + hasher.final(out); + llvm::SmallString<32> hex; + llvm::MD5::stringifyResult(out, hex); + return std::string(hex.str()); +} + +class MD5RawOStream final : public llvm::raw_ostream +{ + public: + MD5RawOStream() + { + SetUnbuffered(); + } + + llvm::MD5::MD5Result finalize() + { + flush(); + llvm::MD5::MD5Result out; + hasher.final(out); + return out; + } + + private: + void write_impl(const char* ptr, size_t size) override + { + hasher.update(llvm::StringRef(ptr, size)); + position += size; + } + + uint64_t current_pos() const override + { + return position; + } + + llvm::MD5 hasher; + uint64_t position = 0; +}; + +static std::string hashModuleIR(const llvm::Module& mod) +{ + MD5RawOStream os; + mod.print(os, nullptr); + llvm::MD5::MD5Result digest = os.finalize(); + llvm::SmallString<32> hex; + llvm::MD5::stringifyResult(digest, hex); + return std::string(hex.str()); +} + +static std::string readFileAsString(const std::string& path) +{ + std::ifstream in(path, std::ios::in | std::ios::binary); + if (!in) + return {}; + std::ostringstream ss; + ss << in.rdbuf(); + return ss.str(); +} + +static std::string computeCompileArgsSignature(const AnalysisConfig& cfg, const std::string& file) +{ + std::ostringstream oss; + if (cfg.compilationDatabase) + { + if (const auto* cmd = cfg.compilationDatabase->findCommandForFile(file)) + { + oss << cmd->directory << "\n"; + for (const std::string& arg : cmd->arguments) + oss << arg << "\n"; + } + } + for (const std::string& arg : cfg.extraCompileArgs) + oss << "extra:" << arg << "\n"; + return md5Hex(oss.str()); +} + +static std::string +encodeSummaryEffectKey(const ctrace::stack::analysis::ResourceSummaryEffect& effect) +{ + std::ostringstream oss; + oss << static_cast(effect.action) << "|" << effect.argIndex << "|" << effect.offset << "|" + << (effect.viaPointerSlot ? 1 : 0) << "|" << effect.resourceKind; + return oss.str(); +} + +static std::string hashSummaryIndex(const ctrace::stack::analysis::ResourceSummaryIndex& index) +{ + std::map> canonical; + for (const auto& entry : index.functions) + { + std::vector keys; + keys.reserve(entry.second.effects.size()); + for (const auto& effect : entry.second.effects) + keys.push_back(encodeSummaryEffectKey(effect)); + std::sort(keys.begin(), keys.end()); + canonical.emplace(entry.first, std::move(keys)); + } + + std::ostringstream oss; + for (const auto& entry : canonical) + { + oss << entry.first << "\n"; + for (const auto& effectKey : entry.second) + oss << " " << effectKey << "\n"; + } + return md5Hex(oss.str()); +} + +static std::string encodeSummaryActionName(ctrace::stack::analysis::ResourceSummaryAction action) +{ + using Action = ctrace::stack::analysis::ResourceSummaryAction; + switch (action) + { + case Action::AcquireOut: + return "acquire_out"; + case Action::AcquireRet: + return "acquire_ret"; + case Action::ReleaseArg: + return "release_arg"; + } + llvm::report_fatal_error("Unhandled ResourceSummaryAction in encodeSummaryActionName"); +} + +static std::optional +decodeSummaryActionName(llvm::StringRef value) +{ + using Action = ctrace::stack::analysis::ResourceSummaryAction; + if (value == "acquire_out") + return Action::AcquireOut; + if (value == "acquire_ret") + return Action::AcquireRet; + if (value == "release_arg") + return Action::ReleaseArg; + return std::nullopt; +} + +static bool writeSummaryCacheFile(const std::filesystem::path& cacheFile, + const ctrace::stack::analysis::ResourceSummaryIndex& index) +{ + std::error_code ec; + std::filesystem::create_directories(cacheFile.parent_path(), ec); + if (ec) + return false; + + llvm::json::Array functionArray; + for (const auto& entry : index.functions) + { + llvm::json::Array effectArray; + for (const auto& effect : entry.second.effects) + { + llvm::json::Object effectObj; + effectObj["action"] = encodeSummaryActionName(effect.action); + effectObj["argIndex"] = static_cast(effect.argIndex); + effectObj["offset"] = static_cast(effect.offset); + effectObj["viaPointerSlot"] = effect.viaPointerSlot; + effectObj["resourceKind"] = effect.resourceKind; + effectArray.push_back(std::move(effectObj)); + } + + llvm::json::Object fnObj; + fnObj["name"] = ctrace_tools::canonicalizeMangledName(entry.first); + fnObj["effects"] = std::move(effectArray); + functionArray.push_back(std::move(fnObj)); + } + + llvm::json::Object root; + root["schema"] = "resource-summary-cache-v1"; + root["functions"] = std::move(functionArray); + + std::ofstream out(cacheFile, std::ios::out | std::ios::trunc | std::ios::binary); + if (!out) + return false; + std::string payload; + llvm::raw_string_ostream os(payload); + os << llvm::formatv("{0:2}", llvm::json::Value(std::move(root))); + os.flush(); + out << payload; + return out.good(); +} + +static std::optional +readSummaryCacheFile(const std::filesystem::path& cacheFile) +{ + std::ifstream in(cacheFile, std::ios::in | std::ios::binary); + if (!in) + return std::nullopt; + + std::ostringstream ss; + ss << in.rdbuf(); + auto parsed = llvm::json::parse(ss.str()); + if (!parsed) + return std::nullopt; + + const auto* obj = parsed->getAsObject(); + if (!obj) + return std::nullopt; + auto schema = obj->getString("schema"); + if (!schema || *schema != "resource-summary-cache-v1") + return std::nullopt; + + const auto* functions = obj->getArray("functions"); + if (!functions) + return std::nullopt; + + ctrace::stack::analysis::ResourceSummaryIndex index; + for (const auto& fnValue : *functions) + { + const auto* fnObj = fnValue.getAsObject(); + if (!fnObj) + continue; + auto name = fnObj->getString("name"); + if (!name || name->empty()) + continue; + const auto* effects = fnObj->getArray("effects"); + if (!effects) + continue; + + ctrace::stack::analysis::ResourceSummaryFunction fnSummary; + for (const auto& effectValue : *effects) + { + const auto* effectObj = effectValue.getAsObject(); + if (!effectObj) + continue; + auto actionName = effectObj->getString("action"); + auto action = actionName ? decodeSummaryActionName(*actionName) : std::nullopt; + if (!action) + continue; + auto argIndex = effectObj->getInteger("argIndex"); + auto offset = effectObj->getInteger("offset"); + auto viaPointerSlot = effectObj->getBoolean("viaPointerSlot"); + auto resourceKind = effectObj->getString("resourceKind"); + if (!argIndex || !offset || !viaPointerSlot || !resourceKind) + continue; + + ctrace::stack::analysis::ResourceSummaryEffect effect; + effect.action = *action; + effect.argIndex = static_cast(*argIndex); + effect.offset = static_cast(*offset); + effect.viaPointerSlot = *viaPointerSlot; + effect.resourceKind = resourceKind->str(); + fnSummary.effects.push_back(std::move(effect)); + } + index.functions[ctrace_tools::canonicalizeMangledName(name->str())] = std::move(fnSummary); + } + + return index; +} + +static std::shared_ptr +buildCrossTUSummaryIndex(const std::vector& loadedModules, + const AnalysisConfig& cfg) +{ + if (!cfg.resourceCrossTU || cfg.resourceModelPath.empty() || loadedModules.size() < 2) + return nullptr; + + using Clock = std::chrono::steady_clock; + const auto buildStart = Clock::now(); + if (cfg.timing) + { + coretrace::log(coretrace::Level::Info, + "Building cross-TU resource summaries for {} module(s)...\n", + loadedModules.size()); + } + + const std::string modelContent = readFileAsString(cfg.resourceModelPath); + const std::string modelHash = + md5Hex(modelContent.empty() ? cfg.resourceModelPath : modelContent); + constexpr llvm::StringLiteral kCacheSchema = "cross-tu-resource-summary-v1"; + const bool allowDiskCache = + !cfg.resourceSummaryMemoryOnly && !cfg.resourceSummaryCacheDir.empty(); + const unsigned maxJobs = std::max(1u, cfg.jobs); + std::unordered_map memoryCache; + std::vector moduleIRHashes; + std::vector moduleCompileArgsHashes; + moduleIRHashes.reserve(loadedModules.size()); + moduleCompileArgsHashes.reserve(loadedModules.size()); + for (const LoadedInputModule& loaded : loadedModules) + { + moduleIRHashes.push_back(hashModuleIR(*loaded.module)); + moduleCompileArgsHashes.push_back(computeCompileArgsSignature(cfg, loaded.filename)); + } + + // Empirical safeguard: cross-TU summaries usually stabilize in a few rounds. + // Keep a bounded worst-case runtime on very large dependency graphs. + constexpr unsigned kCrossTUMaxIterations = 12; + ctrace::stack::analysis::ResourceSummaryIndex globalIndex; + unsigned iterationsRan = 0; + bool converged = false; + for (unsigned iter = 0; iter < kCrossTUMaxIterations; ++iter) + { + const auto iterStart = Clock::now(); + const std::string externalHash = hashSummaryIndex(globalIndex); + ctrace::stack::analysis::ResourceSummaryIndex nextGlobal; + std::vector moduleSummaries( + loadedModules.size()); + std::vector summaryReady(loadedModules.size(), 0); + std::vector cacheKeys(loadedModules.size()); + std::vector missingIndices; + missingIndices.reserve(loadedModules.size()); + + for (std::size_t moduleIndex = 0; moduleIndex < loadedModules.size(); ++moduleIndex) + { + const std::string cacheKeyPayload = + std::string(kCacheSchema) + "|" + modelHash + "|" + externalHash + "|" + + moduleCompileArgsHashes[moduleIndex] + "|" + moduleIRHashes[moduleIndex]; + const std::string cacheKey = md5Hex(cacheKeyPayload); + cacheKeys[moduleIndex] = cacheKey; + + bool loadedFromCache = false; + if (const auto memIt = memoryCache.find(cacheKey); memIt != memoryCache.end()) + { + moduleSummaries[moduleIndex] = memIt->second; + loadedFromCache = true; + } + else if (allowDiskCache) + { + const std::filesystem::path cacheFile = + std::filesystem::path(cfg.resourceSummaryCacheDir) / (cacheKey + ".json"); + auto cached = readSummaryCacheFile(cacheFile); + if (cached) + { + moduleSummaries[moduleIndex] = std::move(*cached); + memoryCache.emplace(cacheKey, moduleSummaries[moduleIndex]); + loadedFromCache = true; + } + } + + if (loadedFromCache) + { + summaryReady[moduleIndex] = 1; + } + else + { + missingIndices.push_back(moduleIndex); + } + } + + auto buildModuleSummary = + [&](std::size_t moduleIndex) -> ctrace::stack::analysis::ResourceSummaryIndex + { + const LoadedInputModule& loaded = loadedModules[moduleIndex]; + analysis::FunctionFilter filter = analysis::buildFunctionFilter(*loaded.module, cfg); + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return filter.shouldAnalyze(F); }; + return analysis::buildResourceLifetimeSummaryIndex(*loaded.module, shouldAnalyze, + cfg.resourceModelPath, &globalIndex); + }; + + if (!missingIndices.empty()) + { + if (maxJobs <= 1 || missingIndices.size() <= 1) + { + for (std::size_t moduleIndex : missingIndices) + { + moduleSummaries[moduleIndex] = buildModuleSummary(moduleIndex); + summaryReady[moduleIndex] = 1; + } + } + else + { + const unsigned workerCount = + std::min(maxJobs, static_cast(missingIndices.size())); + std::vector computed( + loadedModules.size()); + std::vector computedReady(loadedModules.size(), 0); + std::atomic_size_t nextMissing{0}; + std::vector workers; + workers.reserve(workerCount); + + for (unsigned worker = 0; worker < workerCount; ++worker) + { + workers.emplace_back( + [&]() + { + while (true) + { + const std::size_t slot = nextMissing.fetch_add(1); + if (slot >= missingIndices.size()) + break; + const std::size_t moduleIndex = missingIndices[slot]; + computed[moduleIndex] = buildModuleSummary(moduleIndex); + computedReady[moduleIndex] = 1; + } + }); + } + + for (auto& worker : workers) + worker.join(); + + for (std::size_t moduleIndex : missingIndices) + { + if (computedReady[moduleIndex] == 0) + continue; + moduleSummaries[moduleIndex] = std::move(computed[moduleIndex]); + summaryReady[moduleIndex] = 1; + } + } + + for (std::size_t moduleIndex : missingIndices) + { + if (summaryReady[moduleIndex] == 0) + continue; + memoryCache.emplace(cacheKeys[moduleIndex], moduleSummaries[moduleIndex]); + if (allowDiskCache) + { + const std::filesystem::path cacheFile = + std::filesystem::path(cfg.resourceSummaryCacheDir) / + (cacheKeys[moduleIndex] + ".json"); + (void)writeSummaryCacheFile(cacheFile, moduleSummaries[moduleIndex]); + } + } + } + + for (std::size_t moduleIndex = 0; moduleIndex < loadedModules.size(); ++moduleIndex) + { + if (summaryReady[moduleIndex] == 0) + continue; + (void)analysis::mergeResourceSummaryIndex(nextGlobal, moduleSummaries[moduleIndex]); + } + + const bool iterConverged = analysis::resourceSummaryIndexEquals(nextGlobal, globalIndex); + ++iterationsRan; + if (cfg.timing) + { + const auto iterEnd = Clock::now(); + const auto ms = + std::chrono::duration_cast(iterEnd - iterStart).count(); + coretrace::log(coretrace::Level::Info, + "Cross-TU summary iteration {} done in {} ms{}\n", iterationsRan, ms, + iterConverged ? " (converged)" : ""); + } + + if (iterConverged) + { + converged = true; + break; + } + globalIndex = std::move(nextGlobal); + } + + if (!converged) + { + coretrace::log(coretrace::Level::Warn, + "Resource inter-procedural analysis: reached fixed-point iteration cap " + "({}); summary may be non-converged and conservative\n", + kCrossTUMaxIterations); + } + + if (cfg.timing) + { + const auto buildEnd = Clock::now(); + const auto ms = + std::chrono::duration_cast(buildEnd - buildStart).count(); + coretrace::log(coretrace::Level::Info, + "Cross-TU summary build done in {} ms ({} iteration(s))\n", ms, + iterationsRan); + } + + return std::make_shared(std::move(globalIndex)); +} + +static std::shared_ptr +buildCrossTUUninitializedSummaryIndex(const std::vector& loadedModules, + const AnalysisConfig& cfg) +{ + if (!cfg.uninitializedCrossTU || loadedModules.size() < 2) + return nullptr; + + using Clock = std::chrono::steady_clock; + const auto buildStart = Clock::now(); + if (cfg.timing) + { + coretrace::log(coretrace::Level::Info, + "Building cross-TU uninitialized summaries for {} module(s)...\n", + loadedModules.size()); + } + + // Same fixed-point budget policy as resource summaries. + constexpr unsigned kCrossTUMaxIterations = 12; + const unsigned maxJobs = std::max(1u, cfg.jobs); + analysis::UninitializedSummaryIndex globalIndex; + unsigned iterationsRan = 0; + bool converged = false; + for (unsigned iter = 0; iter < kCrossTUMaxIterations; ++iter) + { + const auto iterStart = Clock::now(); + analysis::UninitializedSummaryIndex nextGlobal; + std::vector moduleSummaries(loadedModules.size()); + + auto buildModuleSummary = + [&](std::size_t moduleIndex) -> analysis::UninitializedSummaryIndex + { + const LoadedInputModule& loaded = loadedModules[moduleIndex]; + analysis::FunctionFilter filter = analysis::buildFunctionFilter(*loaded.module, cfg); + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return filter.shouldAnalyze(F); }; + return analysis::buildUninitializedSummaryIndex(*loaded.module, shouldAnalyze, + &globalIndex); + }; + + if (maxJobs <= 1 || loadedModules.size() <= 1) + { + for (std::size_t moduleIndex = 0; moduleIndex < loadedModules.size(); ++moduleIndex) + moduleSummaries[moduleIndex] = buildModuleSummary(moduleIndex); + } + else + { + const unsigned workerCount = + std::min(maxJobs, static_cast(loadedModules.size())); + std::atomic_size_t nextModule{0}; + std::vector workers; + workers.reserve(workerCount); + for (unsigned worker = 0; worker < workerCount; ++worker) + { + workers.emplace_back( + [&]() + { + while (true) + { + const std::size_t moduleIndex = nextModule.fetch_add(1); + if (moduleIndex >= loadedModules.size()) + break; + moduleSummaries[moduleIndex] = buildModuleSummary(moduleIndex); + } + }); + } + for (auto& worker : workers) + worker.join(); + } + + for (const auto& moduleSummary : moduleSummaries) + { + (void)analysis::mergeUninitializedSummaryIndex(nextGlobal, moduleSummary); + } + + const bool iterConverged = + analysis::uninitializedSummaryIndexEquals(nextGlobal, globalIndex); + ++iterationsRan; + globalIndex = std::move(nextGlobal); + + if (cfg.timing) + { + const auto iterEnd = Clock::now(); + const auto ms = + std::chrono::duration_cast(iterEnd - iterStart).count(); + coretrace::log(coretrace::Level::Info, + "Cross-TU uninitialized summary iteration {} done in {} ms{}\n", + iterationsRan, ms, iterConverged ? " (converged)" : ""); + } + + if (iterConverged) + { + converged = true; + break; + } + } + + if (!converged) + { + coretrace::log(coretrace::Level::Warn, + "Uninitialized inter-procedural analysis: reached fixed-point iteration " + "cap ({}); summary may be non-converged and conservative\n", + kCrossTUMaxIterations); + } + + if (cfg.timing) + { + const auto buildEnd = Clock::now(); + const auto ms = + std::chrono::duration_cast(buildEnd - buildStart).count(); + coretrace::log(coretrace::Level::Info, + "Cross-TU uninitialized summary build done in {} ms ({} iteration(s))\n", ms, + iterationsRan); + } + + return std::make_shared(std::move(globalIndex)); +} + +static void accumulateSummary(DiagnosticSummary& total, const DiagnosticSummary& add) +{ + total.info += add.info; + total.warning += add.warning; + total.error += add.error; +} + +static DiagnosticSummary summarizeDiagnostics(const AnalysisResult& result) +{ + DiagnosticSummary summary; + for (const auto& d : result.diagnostics) + { + switch (d.severity) + { + case DiagnosticSeverity::Info: + ++summary.info; + break; + case DiagnosticSeverity::Warning: + ++summary.warning; + break; + case DiagnosticSeverity::Error: + ++summary.error; + break; + } + } + return summary; +} + +struct RunPlan +{ + AnalysisConfig cfg; + std::vector inputFilenames; + NormalizedPathFilters normalizedFilters; + ctrace::stack::cli::OutputFormat outputFormat = ctrace::stack::cli::OutputFormat::Human; + std::string sarifBaseDir; + bool hasFilter = false; + bool needsCrossTUResourceSummaries = false; + bool needsCrossTUUninitializedSummaries = false; + bool needsSharedModuleLoading = false; +}; + +class RunPlanBuilder +{ + public: + explicit RunPlanBuilder(ctrace::stack::cli::ParsedArguments parsedArgs) + : parsedArgs_(std::move(parsedArgs)) + { + } + + AppResult build() + { + RunPlan plan; + plan.cfg = std::move(parsedArgs_.config); + plan.inputFilenames = std::move(parsedArgs_.inputFilenames); + plan.outputFormat = parsedArgs_.outputFormat; + plan.sarifBaseDir = std::move(parsedArgs_.sarifBaseDir); + + if (parsedArgs_.compileCommandsExplicit) + { + AppStatus loadStatus = + loadCompilationDatabase(parsedArgs_.compileCommandsPath, plan.cfg); + if (!loadStatus.isOk()) + return AppResult::failure(std::move(loadStatus.error)); + } + + const bool compdbInputsAutoDiscovered = discoverInputsFromCompilationDatabase( + plan.inputFilenames, plan.cfg, parsedArgs_.includeCompdbDeps); + + plan.normalizedFilters = buildNormalizedPathFilters(plan.cfg); + excludeInputFiles(plan.inputFilenames, plan.cfg, plan.normalizedFilters); + + if (compdbInputsAutoDiscovered && !parsedArgs_.analysisProfileExplicit && + plan.inputFilenames.size() > 1) + { + plan.cfg.profile = AnalysisProfile::Fast; + coretrace::log(coretrace::Level::Info, + "Auto-selected --analysis-profile=fast for compile_commands " + "batch analysis (override with --analysis-profile=full)\n"); + } + + if (plan.inputFilenames.empty()) + { + return AppResult::failure( + "Usage: stack_usage_analyzer [file2.ll ...] [options]\n" + "Try --help for more information.\n"); + } + + AppStatus dumpIRStatus = configureDumpIRPath(plan.inputFilenames, plan.cfg); + if (!dumpIRStatus.isOk()) + return AppResult::failure(std::move(dumpIRStatus.error)); + + std::sort(plan.inputFilenames.begin(), plan.inputFilenames.end()); + plan.hasFilter = !plan.cfg.onlyFiles.empty() || !plan.cfg.onlyDirs.empty() || + !plan.cfg.onlyFunctions.empty(); + plan.needsCrossTUResourceSummaries = plan.cfg.resourceCrossTU && + !plan.cfg.resourceModelPath.empty() && + plan.inputFilenames.size() > 1; + plan.needsCrossTUUninitializedSummaries = + plan.cfg.uninitializedCrossTU && plan.inputFilenames.size() > 1; + plan.needsSharedModuleLoading = + plan.needsCrossTUResourceSummaries || plan.needsCrossTUUninitializedSummaries; + return AppResult::success(std::move(plan)); + } + + private: + ctrace::stack::cli::ParsedArguments parsedArgs_; +}; + +class AnalysisExecutionStrategy +{ + public: + virtual ~AnalysisExecutionStrategy() = default; + + virtual AppStatus execute(RunPlan& plan, llvm::LLVMContext& context, + std::vector& results) const = 0; +}; + +class SharedModuleLoadingExecutionStrategy final : public AnalysisExecutionStrategy +{ + public: + AppStatus execute(RunPlan& plan, llvm::LLVMContext&, + std::vector& results) const override + { + return analyzeWithSharedModuleLoading(plan.inputFilenames, plan.cfg, plan.hasFilter, + plan.needsCrossTUResourceSummaries, + plan.needsCrossTUUninitializedSummaries, results); + } +}; + +class DirectModuleLoadingExecutionStrategy final : public AnalysisExecutionStrategy +{ + public: + AppStatus execute(RunPlan& plan, llvm::LLVMContext& context, + std::vector& results) const override + { + return analyzeWithoutSharedModuleLoading(plan.inputFilenames, plan.cfg, context, + plan.hasFilter, results); + } +}; + +class OutputStrategy +{ + public: + virtual ~OutputStrategy() = default; + virtual int emit(const RunPlan& plan, const std::vector& results) const = 0; +}; + +class JsonOutputStrategy final : public OutputStrategy +{ + public: + int emit(const RunPlan& plan, const std::vector& results) const override + { + return emitJsonOutput(results, plan.cfg, plan.inputFilenames, plan.normalizedFilters); + } +}; + +class SarifOutputStrategy final : public OutputStrategy +{ + public: + int emit(const RunPlan& plan, const std::vector& results) const override + { + return emitSarifOutput(results, plan.cfg, plan.inputFilenames, plan.sarifBaseDir, + plan.normalizedFilters); + } +}; + +class HumanOutputStrategy final : public OutputStrategy +{ + public: + int emit(const RunPlan& plan, const std::vector& results) const override + { + return emitHumanOutput(results, plan.cfg, plan.normalizedFilters); + } +}; + +static std::unique_ptr makeExecutionStrategy(const RunPlan& plan) +{ + if (plan.needsSharedModuleLoading) + return std::make_unique(); + return std::make_unique(); +} + +static std::unique_ptr +makeOutputStrategy(ctrace::stack::cli::OutputFormat outputFormat) +{ + switch (outputFormat) + { + case ctrace::stack::cli::OutputFormat::Json: + return std::make_unique(); + case ctrace::stack::cli::OutputFormat::Sarif: + return std::make_unique(); + case ctrace::stack::cli::OutputFormat::Human: + return std::make_unique(); + } + llvm::report_fatal_error("Unhandled output format in output strategy selection"); +} + +class AnalyzerApp +{ + public: + AppResult run(ctrace::stack::cli::ParsedArguments parsedArgs, + llvm::LLVMContext& context) const + { + RunPlanBuilder planBuilder(std::move(parsedArgs)); + AppResult planResult = planBuilder.build(); + if (!planResult.isOk()) + return AppResult::failure(std::move(planResult.error)); + + RunPlan plan = std::move(*planResult.value); + printInterprocStatus(plan.cfg, plan.inputFilenames.size(), + plan.needsCrossTUResourceSummaries, + plan.needsCrossTUUninitializedSummaries); + + std::vector results; + results.reserve(plan.inputFilenames.size()); + std::unique_ptr executionStrategy = makeExecutionStrategy(plan); + AppStatus executionStatus = executionStrategy->execute(plan, context, results); + if (!executionStatus.isOk()) + return AppResult::failure(std::move(executionStatus.error)); + + std::unique_ptr outputStrategy = makeOutputStrategy(plan.outputFormat); + return AppResult::success(outputStrategy->emit(plan, results)); + } +}; + +namespace ctrace::stack::app +{ + +RunResult runAnalyzerApp(cli::ParsedArguments parsedArgs, llvm::LLVMContext& context) +{ + AnalyzerApp app; + AppResult runResult = app.run(std::move(parsedArgs), context); + + RunResult result; + if (!runResult.isOk()) + { + result.error = std::move(runResult.error); + return result; + } + + result.exitCode = *runResult.value; + return result; +} + +} // namespace ctrace::stack::app From 3d4fee2ec85f4a5bb8d130a9ff60640f914e51c4 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:53:37 +0900 Subject: [PATCH 03/19] refactor(cli): slim main and delegate parsing/execution to services --- main.cpp | 2230 +----------------------------------------------------- 1 file changed, 31 insertions(+), 2199 deletions(-) diff --git a/main.cpp b/main.cpp index 247ed5c..2e64cb8 100644 --- a/main.cpp +++ b/main.cpp @@ -1,47 +1,29 @@ -#include "StackUsageAnalyzer.hpp" +#include "app/AnalyzerApp.hpp" +#include "cli/ArgParser.hpp" + +#include +#include -#include -#include -#include -#include -#include -#include // strncmp, strcmp -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include -#include "analysis/CompileCommands.hpp" -#include "analysis/FunctionFilter.hpp" -#include "analysis/InputPipeline.hpp" -#include "analysis/ResourceLifetimeAnalysis.hpp" -#include "analysis/UninitializedVarAnalysis.hpp" -#include "mangle.hpp" #include using namespace ctrace::stack; -enum class OutputFormat +static void logText(coretrace::Level level, const std::string& text) { - Human, - Json, - Sarif -}; + if (text.empty()) + return; + if (text.back() == '\n') + { + coretrace::log(level, "{}", text); + } + else + { + coretrace::log(level, "{}\n", text); + } +} static void printHelp() { @@ -102,1681 +84,6 @@ static void printHelp() << " stack_usage_analyzer input.ll --warnings-only\n"; } -static std::string normalizePath(const std::string& input) -{ - if (input.empty()) - return {}; - - std::string adjusted = input; - for (char& c : adjusted) - { - if (c == '\\') - c = '/'; - } - - std::filesystem::path path(adjusted); - std::error_code ec; - std::filesystem::path absPath = std::filesystem::absolute(path, ec); - if (ec) - absPath = path; - - std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(absPath, ec); - std::filesystem::path norm = ec ? absPath.lexically_normal() : canonicalPath; - std::string out = norm.generic_string(); - while (out.size() > 1 && out.back() == '/') - out.pop_back(); - return out; -} - -struct NormalizedPathFilters -{ - std::vector onlyFiles; - std::vector onlyDirs; - std::vector excludeDirs; -}; - -static NormalizedPathFilters buildNormalizedPathFilters(const AnalysisConfig& cfg) -{ - NormalizedPathFilters filters; - filters.onlyFiles.reserve(cfg.onlyFiles.size()); - filters.onlyDirs.reserve(cfg.onlyDirs.size()); - filters.excludeDirs.reserve(cfg.excludeDirs.size()); - - for (const auto& file : cfg.onlyFiles) - filters.onlyFiles.push_back(normalizePath(file)); - for (const auto& dir : cfg.onlyDirs) - filters.onlyDirs.push_back(normalizePath(dir)); - for (const auto& dir : cfg.excludeDirs) - filters.excludeDirs.push_back(normalizePath(dir)); - - return filters; -} - -static std::string basenameOf(const std::string& path) -{ - std::size_t pos = path.find_last_of('/'); - if (pos == std::string::npos) - return path; - if (pos + 1 >= path.size()) - return {}; - return path.substr(pos + 1); -} - -static bool pathHasSuffix(const std::string& path, const std::string& suffix) -{ - if (suffix.empty()) - return false; - if (path.size() < suffix.size()) - return false; - if (path.compare(path.size() - suffix.size(), suffix.size(), suffix) != 0) - return false; - if (path.size() == suffix.size()) - return true; - return path[path.size() - suffix.size() - 1] == '/'; -} - -static bool pathHasPrefix(const std::string& path, const std::string& prefix) -{ - if (prefix.empty()) - return false; - if (path.size() < prefix.size()) - return false; - if (path.compare(0, prefix.size(), prefix) != 0) - return false; - if (path.size() == prefix.size()) - return true; - return path[prefix.size()] == '/'; -} - -static bool pathContainsSegment(const std::string& path, const std::string& segment) -{ - if (path.empty() || segment.empty()) - return false; - std::size_t start = 0; - while (start < path.size()) - { - while (start < path.size() && path[start] == '/') - ++start; - if (start >= path.size()) - break; - std::size_t end = path.find('/', start); - if (end == std::string::npos) - end = path.size(); - if (path.compare(start, end - start, segment) == 0) - return true; - start = end + 1; - } - return false; -} - -static bool shouldIncludePath(const std::string& path, const AnalysisConfig& cfg, - const NormalizedPathFilters& filters) -{ - if (cfg.onlyFiles.empty() && cfg.onlyDirs.empty()) - return true; - if (path.empty()) - return false; - - const std::string normPath = normalizePath(path); - - for (const auto& normFile : filters.onlyFiles) - { - if (normPath == normFile || pathHasSuffix(normPath, normFile)) - return true; - const std::string fileBase = basenameOf(normFile); - if (!fileBase.empty() && basenameOf(normPath) == fileBase) - return true; - } - - for (const auto& normDir : filters.onlyDirs) - { - if (pathHasPrefix(normPath, normDir) || pathHasSuffix(normPath, normDir)) - return true; - const std::string needle = "/" + normDir + "/"; - if (normPath.find(needle) != std::string::npos) - return true; - } - - return false; -} - -static bool shouldExcludePath(const std::string& path, const NormalizedPathFilters& filters) -{ - if (filters.excludeDirs.empty() || path.empty()) - return false; - - const std::string normPath = normalizePath(path); - for (const auto& normDir : filters.excludeDirs) - { - if (normDir.empty()) - continue; - if (pathHasPrefix(normPath, normDir) || pathHasSuffix(normPath, normDir)) - return true; - const std::string needle = "/" + normDir + "/"; - if (normPath.find(needle) != std::string::npos) - return true; - } - - return false; -} - -static bool functionNameMatches(const std::string& name, const AnalysisConfig& cfg) -{ - if (cfg.onlyFunctions.empty()) - return true; - - auto itaniumBaseName = [](const std::string& symbol) -> std::string - { - if (symbol.rfind("_Z", 0) != 0) - return {}; - std::size_t i = 2; - if (i < symbol.size() && symbol[i] == 'L') - ++i; - if (i >= symbol.size() || !std::isdigit(static_cast(symbol[i]))) - return {}; - std::size_t len = 0; - while (i < symbol.size() && std::isdigit(static_cast(symbol[i]))) - { - len = len * 10 + static_cast(symbol[i] - '0'); - ++i; - } - if (len == 0 || i + len > symbol.size()) - return {}; - return symbol.substr(i, len); - }; - - std::string demangledName; - if (ctrace_tools::isMangled(name) || name.rfind("_Z", 0) == 0) - demangledName = ctrace_tools::demangle(name.c_str()); - std::string demangledBase; - if (!demangledName.empty()) - { - std::size_t pos = demangledName.find('('); - if (pos != std::string::npos && pos > 0) - demangledBase = demangledName.substr(0, pos); - } - std::string itaniumBase = itaniumBaseName(name); - - for (const auto& filter : cfg.onlyFunctions) - { - if (name == filter) - return true; - if (!demangledName.empty() && demangledName == filter) - return true; - if (!demangledBase.empty() && demangledBase == filter) - return true; - if (!itaniumBase.empty() && itaniumBase == filter) - return true; - if (ctrace_tools::isMangled(filter)) - { - std::string demangledFilter = ctrace_tools::demangle(filter.c_str()); - if (!demangledName.empty() && demangledName == demangledFilter) - return true; - std::size_t pos = demangledFilter.find('('); - if (pos != std::string::npos && pos > 0) - { - if (demangledBase == demangledFilter.substr(0, pos)) - return true; - } - } - } - - return false; -} - -static std::string trimCopy(const std::string& input) -{ - std::size_t start = 0; - while (start < input.size() && std::isspace(static_cast(input[start]))) - ++start; - std::size_t end = input.size(); - while (end > start && std::isspace(static_cast(input[end - 1]))) - --end; - return input.substr(start, end - start); -} - -static bool parsePositiveUnsigned(const std::string& input, unsigned& out, std::string& error) -{ - const std::string trimmed = trimCopy(input); - if (trimmed.empty()) - { - error = "value is empty"; - return false; - } - - unsigned long long parsed = 0; - const auto [ptr, ec] = - std::from_chars(trimmed.data(), trimmed.data() + trimmed.size(), parsed, 10); - if (ec != std::errc() || ptr != trimmed.data() + trimmed.size()) - { - error = "invalid numeric value"; - return false; - } - if (parsed == 0) - { - error = "value must be greater than zero"; - return false; - } - if (parsed > std::numeric_limits::max()) - { - error = "value is too large"; - return false; - } - out = static_cast(parsed); - return true; -} - -static bool parseAnalysisProfile(const std::string& input, AnalysisProfile& out, std::string& error) -{ - std::string trimmed = trimCopy(input); - std::string lowered; - lowered.reserve(trimmed.size()); - for (char c : trimmed) - lowered.push_back(static_cast(std::tolower(static_cast(c)))); - - if (lowered == "fast") - { - out = AnalysisProfile::Fast; - return true; - } - if (lowered == "full") - { - out = AnalysisProfile::Full; - return true; - } - error = "expected 'fast' or 'full'"; - return false; -} - -static bool parseStackLimitValue(const std::string& input, StackSize& out, std::string& error) -{ - std::string trimmed = trimCopy(input); - if (trimmed.empty()) - { - error = "stack limit is empty"; - return false; - } - - std::size_t digitCount = 0; - while (digitCount < trimmed.size() && - std::isdigit(static_cast(trimmed[digitCount]))) - { - ++digitCount; - } - if (digitCount == 0) - { - error = "stack limit must start with a number"; - return false; - } - - const std::string numberPart = trimmed.substr(0, digitCount); - std::string suffix = trimCopy(trimmed.substr(digitCount)); - - unsigned long long base = 0; - auto [ptr, ec] = - std::from_chars(numberPart.data(), numberPart.data() + numberPart.size(), base, 10); - if (ec != std::errc() || ptr != numberPart.data() + numberPart.size()) - { - error = "invalid numeric value"; - return false; - } - if (base == 0) - { - error = "stack limit must be greater than zero"; - return false; - } - - StackSize multiplier = 1; - if (!suffix.empty()) - { - std::string lowered; - lowered.reserve(suffix.size()); - for (char c : suffix) - { - lowered.push_back(static_cast(std::tolower(static_cast(c)))); - } - - if (lowered == "b") - { - multiplier = 1; - } - else if (lowered == "k" || lowered == "kb" || lowered == "kib") - { - multiplier = 1024ull; - } - else if (lowered == "m" || lowered == "mb" || lowered == "mib") - { - multiplier = 1024ull * 1024ull; - } - else if (lowered == "g" || lowered == "gb" || lowered == "gib") - { - multiplier = 1024ull * 1024ull * 1024ull; - } - else - { - error = "unsupported suffix (use bytes, KiB, MiB, or GiB)"; - return false; - } - } - - if (base > std::numeric_limits::max() / multiplier) - { - error = "stack limit is too large"; - return false; - } - - out = static_cast(base) * multiplier; - return true; -} - -static void addCsvFilters(std::vector& dest, const std::string& input) -{ - std::string current; - for (char c : input) - { - if (c == ',') - { - std::string trimmed = trimCopy(current); - if (!trimmed.empty()) - dest.push_back(trimmed); - current.clear(); - } - else - { - current.push_back(c); - } - } - std::string trimmed = trimCopy(current); - if (!trimmed.empty()) - dest.push_back(trimmed); -} - -static AnalysisResult filterResult(const AnalysisResult& result, const AnalysisConfig& cfg, - const NormalizedPathFilters& filters) -{ - if (cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty()) - return result; - - AnalysisResult filtered; - filtered.config = result.config; - - std::unordered_set keepFuncs; - for (const auto& f : result.functions) - { - bool keep = functionNameMatches(f.name, cfg); - if (keep && (!cfg.onlyFiles.empty() || !cfg.onlyDirs.empty())) - keep = shouldIncludePath(f.filePath, cfg, filters); - if (keep) - { - filtered.functions.push_back(f); - keepFuncs.insert(f.name); - } - } - - if (!keepFuncs.empty()) - { - for (const auto& d : result.diagnostics) - { - if (keepFuncs.count(d.funcName) != 0) - { - filtered.diagnostics.push_back(d); - } - } - } - - return filtered; -} - -static AnalysisResult filterWarningsOnly(const AnalysisResult& result, const AnalysisConfig& cfg) -{ - if (!cfg.warningsOnly) - return result; - - AnalysisResult filtered; - filtered.config = result.config; - filtered.functions = result.functions; - for (const auto& d : result.diagnostics) - { - if (d.severity != DiagnosticSeverity::Info) - { - filtered.diagnostics.push_back(d); - } - } - return filtered; -} - -struct LoadedInputModule -{ - std::string filename; - std::unique_ptr context; - std::unique_ptr module; -}; - -using AnalysisEntry = std::pair; - -static std::shared_ptr -buildCrossTUSummaryIndex(const std::vector& loadedModules, - const AnalysisConfig& cfg); - -static std::shared_ptr -buildCrossTUUninitializedSummaryIndex(const std::vector& loadedModules, - const AnalysisConfig& cfg); - -struct DiagnosticSummary -{ - std::size_t info = 0; - std::size_t warning = 0; - std::size_t error = 0; -}; - -static void accumulateSummary(DiagnosticSummary& total, const DiagnosticSummary& add); -static DiagnosticSummary summarizeDiagnostics(const AnalysisResult& result); - -static void stampResultFilePaths(AnalysisResult& result, const std::string& inputFilename) -{ - for (auto& f : result.functions) - { - if (f.filePath.empty()) - f.filePath = inputFilename; - } - for (auto& d : result.diagnostics) - { - if (d.filePath.empty()) - d.filePath = inputFilename; - } -} - -static std::string noFunctionMessage(const AnalysisResult& result, const std::string& inputFilename, - bool hasFilter) -{ - if (!result.functions.empty()) - return {}; - if (hasFilter) - return "No functions matched filters for: " + inputFilename + "\n"; - return "[ !Info! ] No analyzable functions in: " + inputFilename + " (skipping)\n"; -} - -static bool loadCompilationDatabase(const std::string& compileCommandsPath, AnalysisConfig& cfg) -{ - if (compileCommandsPath.empty()) - { - coretrace::log(coretrace::Level::Error, "Compile commands path is empty\n"); - return false; - } - - std::filesystem::path compdbPath = compileCommandsPath; - std::error_code fsErr; - if (std::filesystem::is_directory(compdbPath, fsErr)) - { - compdbPath /= "compile_commands.json"; - } - else if (fsErr) - { - coretrace::log(coretrace::Level::Error, "Failed to inspect compile commands path: {}\n", - fsErr.message()); - return false; - } - - if (!std::filesystem::exists(compdbPath, fsErr)) - { - if (fsErr) - { - coretrace::log(coretrace::Level::Error, "Failed to inspect compile commands path: {}\n", - fsErr.message()); - } - else - { - coretrace::log(coretrace::Level::Error, "Compile commands file not found: {}\n", - compdbPath.string()); - } - return false; - } - - std::string error; - auto db = - ctrace::stack::analysis::CompilationDatabase::loadFromFile(compdbPath.string(), error); - if (!db) - { - coretrace::log(coretrace::Level::Error, "Failed to load compile commands: {}\n", error); - return false; - } - - cfg.compilationDatabase = std::move(db); - cfg.requireCompilationDatabase = true; - return true; -} - -static bool discoverInputsFromCompilationDatabase(std::vector& inputFilenames, - const AnalysisConfig& cfg, bool includeCompdbDeps) -{ - if (!inputFilenames.empty() || !cfg.compilationDatabase) - return false; - - std::vector compdbFiles = cfg.compilationDatabase->listSourceFiles(); - std::size_t skippedUnsupported = 0; - std::size_t skippedDeps = 0; - for (const std::string& file : compdbFiles) - { - const LanguageType lang = analysis::detectFromExtension(file); - if (lang == LanguageType::Unknown) - { - ++skippedUnsupported; - continue; - } - if (!includeCompdbDeps) - { - const std::string normalizedFile = normalizePath(file); - if (pathContainsSegment(normalizedFile, "_deps")) - { - ++skippedDeps; - continue; - } - } - inputFilenames.push_back(file); - } - - if (!inputFilenames.empty()) - { - llvm::errs() << "[ !Info! ] No explicit input files provided: using " - << inputFilenames.size() << " supported file(s) from compile_commands.json"; - if (skippedUnsupported > 0) - llvm::errs() << " (skipped " << skippedUnsupported << " unsupported entry/entries)"; - if (skippedDeps > 0) - llvm::errs() << " (skipped " << skippedDeps << " _deps entry/entries)"; - llvm::errs() << "\n"; - } - else - { - llvm::errs() << "No supported source files found in compile_commands.json"; - if (skippedUnsupported > 0) - llvm::errs() << " (all entries were unsupported for this analyzer)"; - if (skippedDeps > 0) - llvm::errs() << " (all supported entries were filtered from _deps)"; - llvm::errs() << "\n"; - } - return true; -} - -static void excludeInputFiles(std::vector& inputFilenames, const AnalysisConfig& cfg, - const NormalizedPathFilters& normalizedFilters) -{ - if (cfg.excludeDirs.empty() || inputFilenames.empty()) - return; - - std::vector filteredInputs; - filteredInputs.reserve(inputFilenames.size()); - std::size_t excludedCount = 0; - for (const auto& file : inputFilenames) - { - if (shouldExcludePath(file, normalizedFilters)) - { - ++excludedCount; - continue; - } - filteredInputs.push_back(file); - } - if (excludedCount > 0) - { - llvm::errs() << "[ !Info! ] Excluded " << excludedCount - << " input file(s) via --exclude-dir filters\n"; - } - inputFilenames.swap(filteredInputs); -} - -static bool configureDumpIRPath(const std::vector& inputFilenames, AnalysisConfig& cfg) -{ - if (cfg.dumpIRPath.empty()) - return true; - - const bool trailingSlash = - !cfg.dumpIRPath.empty() && (cfg.dumpIRPath.back() == '/' || cfg.dumpIRPath.back() == '\\'); - std::error_code fsErr; - std::filesystem::path dumpPath(cfg.dumpIRPath); - const bool exists = std::filesystem::exists(dumpPath, fsErr); - if (fsErr) - { - llvm::errs() << "Failed to inspect dump IR path: " << fsErr.message() << "\n"; - return false; - } - - bool isDir = false; - if (exists) - { - isDir = std::filesystem::is_directory(dumpPath, fsErr); - if (fsErr) - { - llvm::errs() << "Failed to inspect dump IR path: " << fsErr.message() << "\n"; - return false; - } - } - if (inputFilenames.size() > 1 && !isDir && !trailingSlash) - { - llvm::errs() << "--dump-ir must point to a directory when analyzing multiple inputs\n"; - return false; - } - cfg.dumpIRIsDir = isDir || trailingSlash || inputFilenames.size() > 1; - return true; -} - -static void printInterprocStatus(const AnalysisConfig& cfg, std::size_t inputCount, - bool needsCrossTUResourceSummaries, - bool needsCrossTUUninitializedSummaries) -{ - if (!cfg.resourceModelPath.empty()) - { - if (needsCrossTUResourceSummaries) - { - llvm::errs() << "[ !Info! ] Resource inter-procedural analysis: enabled (cross-TU " - "summaries across " - << inputCount << " files" - << ", jobs: " << std::max(1u, cfg.jobs); - if (cfg.resourceSummaryMemoryOnly) - llvm::errs() << ", cache: memory-only"; - else if (!cfg.resourceSummaryCacheDir.empty()) - llvm::errs() << ", cache: " << cfg.resourceSummaryCacheDir; - llvm::errs() << ")\n"; - } - else if (!cfg.resourceCrossTU) - { - llvm::errs() << "[ !!Warn ] Resource inter-procedural analysis: disabled by " - "--no-resource-cross-tu (local TU only)\n"; - } - else if (inputCount <= 1) - { - llvm::errs() << "[ !!Warn ] Resource inter-procedural analysis: unavailable " - "(need at least 2 input files; local TU only)\n"; - } - } - - if (inputCount > 1) - { - if (needsCrossTUUninitializedSummaries) - { - llvm::errs() << "[ !Info! ] Uninitialized inter-procedural analysis: enabled " - "(cross-TU summaries across " - << inputCount << " files" - << ", jobs: " << std::max(1u, cfg.jobs) << ")\n"; - } - else if (!cfg.uninitializedCrossTU) - { - llvm::errs() << "[ !!Warn ] Uninitialized inter-procedural analysis: disabled by " - "--no-uninitialized-cross-tu (local TU only)\n"; - } - } -} - -static bool analyzeWithSharedModuleLoading(const std::vector& inputFilenames, - AnalysisConfig& cfg, bool hasFilter, - bool needsCrossTUResourceSummaries, - bool needsCrossTUUninitializedSummaries, - std::vector& results) -{ - std::vector loadedModules(inputFilenames.size()); - std::vector loadErrors(inputFilenames.size()); - std::vector loadSucceeded(inputFilenames.size(), 0); - auto loadSingleModule = [&](std::size_t index) - { - const std::string& inputFilename = inputFilenames[index]; - auto moduleContext = std::make_unique(); - llvm::SMDiagnostic localErr; - analysis::ModuleLoadResult load = - analysis::loadModuleForAnalysis(inputFilename, cfg, *moduleContext, localErr); - if (!load.module) - { - std::string err; - if (!load.error.empty()) - err += load.error; - if (localErr.getLineNo() != 0 || !localErr.getFilename().empty()) - { - std::string diagText; - llvm::raw_string_ostream os(diagText); - localErr.print("stack_usage_analyzer", os); - os.flush(); - err += diagText; - } - loadErrors[index] = std::move(err); - return; - } - loadedModules[index] = {inputFilename, std::move(moduleContext), std::move(load.module)}; - loadSucceeded[index] = 1; - }; - - const unsigned loadJobs = std::max(1u, cfg.jobs); - if (loadJobs <= 1 || inputFilenames.size() <= 1) - { - for (std::size_t index = 0; index < inputFilenames.size(); ++index) - loadSingleModule(index); - } - else - { - std::atomic_size_t nextIndex{0}; - const unsigned workerCount = - std::min(loadJobs, static_cast(inputFilenames.size())); - std::vector workers; - workers.reserve(workerCount); - for (unsigned worker = 0; worker < workerCount; ++worker) - { - workers.emplace_back( - [&]() - { - while (true) - { - const std::size_t index = nextIndex.fetch_add(1); - if (index >= inputFilenames.size()) - break; - loadSingleModule(index); - } - }); - } - for (auto& worker : workers) - worker.join(); - } - - std::vector orderedLoadedModules; - orderedLoadedModules.reserve(inputFilenames.size()); - for (std::size_t index = 0; index < inputFilenames.size(); ++index) - { - if (!loadSucceeded[index]) - { - if (!loadErrors[index].empty()) - llvm::errs() << loadErrors[index]; - llvm::errs() << "Failed to analyze: " << inputFilenames[index] << "\n"; - coretrace::log(coretrace::Level::Error, coretrace::Module("cli"), - "Failed to analyze:{}\n", inputFilenames[index]); - return false; - } - orderedLoadedModules.push_back(std::move(loadedModules[index])); - } - loadedModules.swap(orderedLoadedModules); - - if (needsCrossTUResourceSummaries) - cfg.resourceSummaryIndex = buildCrossTUSummaryIndex(loadedModules, cfg); - if (needsCrossTUUninitializedSummaries) - cfg.uninitializedSummaryIndex = buildCrossTUUninitializedSummaryIndex(loadedModules, cfg); - - for (auto& loaded : loadedModules) - { - AnalysisResult result = analyzeModule(*loaded.module, cfg); - stampResultFilePaths(result, loaded.filename); - const std::string emptyMsg = noFunctionMessage(result, loaded.filename, hasFilter); - if (!emptyMsg.empty()) - llvm::errs() << emptyMsg; - results.emplace_back(loaded.filename, std::move(result)); - } - return true; -} - -static bool analyzeWithoutSharedModuleLoading(const std::vector& inputFilenames, - const AnalysisConfig& cfg, llvm::LLVMContext& context, - bool hasFilter, std::vector& results) -{ - const unsigned parallelJobs = std::max(1u, cfg.jobs); - if (parallelJobs <= 1 || inputFilenames.size() <= 1) - { - for (const auto& inputFilename : inputFilenames) - { - llvm::SMDiagnostic localErr; - analysis::ModuleLoadResult load = - analysis::loadModuleForAnalysis(inputFilename, cfg, context, localErr); - if (!load.module) - { - if (!load.error.empty()) - llvm::errs() << load.error; - llvm::errs() << "Failed to analyze: " << inputFilename << "\n"; - coretrace::log(coretrace::Level::Error, coretrace::Module("cli"), - "Failed to analyze:{}\n", inputFilename); - localErr.print("stack_usage_analyzer", llvm::errs()); - return false; - } - - AnalysisResult result = analyzeModule(*load.module, cfg); - stampResultFilePaths(result, inputFilename); - const std::string emptyMsg = noFunctionMessage(result, inputFilename, hasFilter); - if (!emptyMsg.empty()) - llvm::errs() << emptyMsg; - results.emplace_back(inputFilename, std::move(result)); - } - return true; - } - - struct ParallelAnalysisSlot - { - AnalysisResult result; - std::string loadError; - std::string noFunctionMsg; - bool success = false; - }; - - std::vector slots(inputFilenames.size()); - std::atomic_size_t nextIndex{0}; - const unsigned workerCount = - std::min(parallelJobs, static_cast(inputFilenames.size())); - std::vector workers; - workers.reserve(workerCount); - for (unsigned worker = 0; worker < workerCount; ++worker) - { - workers.emplace_back( - [&]() - { - while (true) - { - const std::size_t index = nextIndex.fetch_add(1); - if (index >= inputFilenames.size()) - break; - - const std::string& inputFilename = inputFilenames[index]; - llvm::LLVMContext localContext; - llvm::SMDiagnostic localErr; - analysis::ModuleLoadResult load = - analysis::loadModuleForAnalysis(inputFilename, cfg, localContext, localErr); - if (!load.module) - { - std::string err; - if (!load.error.empty()) - err += load.error; - if (localErr.getLineNo() != 0 || !localErr.getFilename().empty()) - { - std::string diagText; - llvm::raw_string_ostream os(diagText); - localErr.print("stack_usage_analyzer", os); - os.flush(); - err += diagText; - } - slots[index].loadError = std::move(err); - continue; - } - - AnalysisResult result = analyzeModule(*load.module, cfg); - stampResultFilePaths(result, inputFilename); - slots[index].noFunctionMsg = - noFunctionMessage(result, inputFilename, hasFilter); - slots[index].result = std::move(result); - slots[index].success = true; - } - }); - } - for (auto& worker : workers) - worker.join(); - - for (std::size_t index = 0; index < inputFilenames.size(); ++index) - { - if (!slots[index].success) - { - if (!slots[index].loadError.empty()) - llvm::errs() << slots[index].loadError; - llvm::errs() << "Failed to analyze: " << inputFilenames[index] << "\n"; - coretrace::log(coretrace::Level::Error, coretrace::Module("cli"), - "Failed to analyze:{}\n", inputFilenames[index]); - return false; - } - if (!slots[index].noFunctionMsg.empty()) - llvm::errs() << slots[index].noFunctionMsg; - results.emplace_back(inputFilenames[index], std::move(slots[index].result)); - } - return true; -} - -static AnalysisResult mergeAnalysisResults(const std::vector& results, - const AnalysisConfig& cfg) -{ - AnalysisResult merged{}; - merged.config = cfg; - for (const auto& entry : results) - { - const auto& res = entry.second; - merged.functions.insert(merged.functions.end(), res.functions.begin(), res.functions.end()); - merged.diagnostics.insert(merged.diagnostics.end(), res.diagnostics.begin(), - res.diagnostics.end()); - } - return merged; -} - -static int emitJsonOutput(const std::vector& results, const AnalysisConfig& cfg, - const std::vector& inputFilenames, - const NormalizedPathFilters& normalizedFilters) -{ - const bool applyFilter = - cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty(); - if (results.size() == 1) - { - AnalysisResult filtered = applyFilter - ? filterResult(results[0].second, cfg, normalizedFilters) - : results[0].second; - filtered = filterWarningsOnly(filtered, cfg); - llvm::outs() << ctrace::stack::toJson(filtered, results[0].first); - return 0; - } - - AnalysisResult merged = mergeAnalysisResults(results, cfg); - AnalysisResult filtered = applyFilter ? filterResult(merged, cfg, normalizedFilters) : merged; - filtered = filterWarningsOnly(filtered, cfg); - llvm::outs() << ctrace::stack::toJson(filtered, inputFilenames); - return 0; -} - -static int emitSarifOutput(const std::vector& results, const AnalysisConfig& cfg, - const std::vector& inputFilenames, - const std::string& sarifBaseDir, - const NormalizedPathFilters& normalizedFilters) -{ - const bool applyFilter = - cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty(); - if (results.size() == 1) - { - AnalysisResult filtered = applyFilter - ? filterResult(results[0].second, cfg, normalizedFilters) - : results[0].second; - filtered = filterWarningsOnly(filtered, cfg); - llvm::outs() << ctrace::stack::toSarif(filtered, results[0].first, - "coretrace-stack-analyzer", "0.1.0", sarifBaseDir); - return 0; - } - - AnalysisResult merged = mergeAnalysisResults(results, cfg); - AnalysisResult filtered = applyFilter ? filterResult(merged, cfg, normalizedFilters) : merged; - filtered = filterWarningsOnly(filtered, cfg); - llvm::outs() << ctrace::stack::toSarif(filtered, inputFilenames.front(), - "coretrace-stack-analyzer", "0.1.0", sarifBaseDir); - return 0; -} - -static int emitHumanOutput(const std::vector& results, const AnalysisConfig& cfg, - const NormalizedPathFilters& normalizedFilters) -{ - const bool multiFile = results.size() > 1; - DiagnosticSummary totalSummary; - for (std::size_t r = 0; r < results.size(); ++r) - { - const auto& inputFilename = results[r].first; - const AnalysisResult result = - (cfg.onlyFiles.empty() && cfg.onlyDirs.empty() && cfg.onlyFunctions.empty()) - ? filterResult(results[r].second, cfg, normalizedFilters) - : results[r].second; - - if (multiFile) - { - if (r > 0) - llvm::outs() << "\n"; - llvm::outs() << "File: " << inputFilename << "\n"; - } - - llvm::outs() << "Mode: " << (result.config.mode == AnalysisMode::IR ? "IR" : "ABI") - << "\n\n"; - - for (const auto& f : result.functions) - { - if (cfg.demangle) - { - llvm::outs() << "Function: " << ctrace_tools::demangle(f.name.c_str()) << "\n"; - } - else - { - llvm::outs() << "Function: " << f.name << " " - << ((ctrace_tools::isMangled(f.name)) - ? ctrace_tools::demangle(f.name.c_str()) - : "") - << "\n"; - } - if (f.localStackUnknown) - { - llvm::outs() << "\tlocal stack: unknown"; - if (f.localStack > 0) - llvm::outs() << " (>= " << f.localStack << " bytes)"; - llvm::outs() << "\n"; - } - else - { - llvm::outs() << "\tlocal stack: " << f.localStack << " bytes\n"; - } - - if (f.maxStackUnknown) - { - llvm::outs() << "\tmax stack (including callees): unknown"; - if (f.maxStack > 0) - llvm::outs() << " (>= " << f.maxStack << " bytes)"; - llvm::outs() << "\n"; - } - else - { - llvm::outs() << "\tmax stack (including callees): " << f.maxStack << " bytes\n"; - } - - if (!result.config.quiet) - { - for (const auto& d : result.diagnostics) - { - if (d.funcName != f.name) - continue; - if (result.config.warningsOnly && d.severity == DiagnosticSeverity::Info) - continue; - if (d.line != 0) - llvm::outs() << "\tat line " << d.line << ", column " << d.column << "\n"; - llvm::outs() << d.message << "\n"; - } - } - - llvm::outs() << "\n"; - } - - const DiagnosticSummary summary = summarizeDiagnostics(result); - accumulateSummary(totalSummary, summary); - llvm::outs() << "Diagnostics summary: info=" << summary.info - << ", warning=" << summary.warning << ", error=" << summary.error << "\n"; - } - - if (multiFile) - { - llvm::outs() << "\nTotal diagnostics summary: info=" << totalSummary.info - << ", warning=" << totalSummary.warning << ", error=" << totalSummary.error - << " (across " << results.size() << " files)\n"; - } - return 0; -} - -static std::string md5Hex(llvm::StringRef input) -{ - llvm::MD5 hasher; - hasher.update(input); - llvm::MD5::MD5Result out; - hasher.final(out); - llvm::SmallString<32> hex; - llvm::MD5::stringifyResult(out, hex); - return std::string(hex.str()); -} - -class MD5RawOStream final : public llvm::raw_ostream -{ - public: - MD5RawOStream() - { - SetUnbuffered(); - } - - llvm::MD5::MD5Result finalize() - { - flush(); - llvm::MD5::MD5Result out; - hasher.final(out); - return out; - } - - private: - void write_impl(const char* ptr, size_t size) override - { - hasher.update(llvm::StringRef(ptr, size)); - position += size; - } - - uint64_t current_pos() const override - { - return position; - } - - llvm::MD5 hasher; - uint64_t position = 0; -}; - -static std::string hashModuleIR(const llvm::Module& mod) -{ - MD5RawOStream os; - mod.print(os, nullptr); - llvm::MD5::MD5Result digest = os.finalize(); - llvm::SmallString<32> hex; - llvm::MD5::stringifyResult(digest, hex); - return std::string(hex.str()); -} - -static std::string readFileAsString(const std::string& path) -{ - std::ifstream in(path, std::ios::in | std::ios::binary); - if (!in) - return {}; - std::ostringstream ss; - ss << in.rdbuf(); - return ss.str(); -} - -static std::string computeCompileArgsSignature(const AnalysisConfig& cfg, const std::string& file) -{ - std::ostringstream oss; - if (cfg.compilationDatabase) - { - if (const auto* cmd = cfg.compilationDatabase->findCommandForFile(file)) - { - oss << cmd->directory << "\n"; - for (const std::string& arg : cmd->arguments) - oss << arg << "\n"; - } - } - for (const std::string& arg : cfg.extraCompileArgs) - oss << "extra:" << arg << "\n"; - return md5Hex(oss.str()); -} - -static std::string -encodeSummaryEffectKey(const ctrace::stack::analysis::ResourceSummaryEffect& effect) -{ - std::ostringstream oss; - oss << static_cast(effect.action) << "|" << effect.argIndex << "|" << effect.offset << "|" - << (effect.viaPointerSlot ? 1 : 0) << "|" << effect.resourceKind; - return oss.str(); -} - -static std::string hashSummaryIndex(const ctrace::stack::analysis::ResourceSummaryIndex& index) -{ - std::map> canonical; - for (const auto& entry : index.functions) - { - std::vector keys; - keys.reserve(entry.second.effects.size()); - for (const auto& effect : entry.second.effects) - keys.push_back(encodeSummaryEffectKey(effect)); - std::sort(keys.begin(), keys.end()); - canonical.emplace(entry.first, std::move(keys)); - } - - std::ostringstream oss; - for (const auto& entry : canonical) - { - oss << entry.first << "\n"; - for (const auto& effectKey : entry.second) - oss << " " << effectKey << "\n"; - } - return md5Hex(oss.str()); -} - -static std::string encodeSummaryActionName(ctrace::stack::analysis::ResourceSummaryAction action) -{ - using Action = ctrace::stack::analysis::ResourceSummaryAction; - switch (action) - { - case Action::AcquireOut: - return "acquire_out"; - case Action::AcquireRet: - return "acquire_ret"; - case Action::ReleaseArg: - return "release_arg"; - } - llvm::report_fatal_error("Unhandled ResourceSummaryAction in encodeSummaryActionName"); -} - -static std::optional -decodeSummaryActionName(llvm::StringRef value) -{ - using Action = ctrace::stack::analysis::ResourceSummaryAction; - if (value == "acquire_out") - return Action::AcquireOut; - if (value == "acquire_ret") - return Action::AcquireRet; - if (value == "release_arg") - return Action::ReleaseArg; - return std::nullopt; -} - -static bool writeSummaryCacheFile(const std::filesystem::path& cacheFile, - const ctrace::stack::analysis::ResourceSummaryIndex& index) -{ - std::error_code ec; - std::filesystem::create_directories(cacheFile.parent_path(), ec); - if (ec) - return false; - - llvm::json::Array functionArray; - for (const auto& entry : index.functions) - { - llvm::json::Array effectArray; - for (const auto& effect : entry.second.effects) - { - llvm::json::Object effectObj; - effectObj["action"] = encodeSummaryActionName(effect.action); - effectObj["argIndex"] = static_cast(effect.argIndex); - effectObj["offset"] = static_cast(effect.offset); - effectObj["viaPointerSlot"] = effect.viaPointerSlot; - effectObj["resourceKind"] = effect.resourceKind; - effectArray.push_back(std::move(effectObj)); - } - - llvm::json::Object fnObj; - fnObj["name"] = ctrace_tools::canonicalizeMangledName(entry.first); - fnObj["effects"] = std::move(effectArray); - functionArray.push_back(std::move(fnObj)); - } - - llvm::json::Object root; - root["schema"] = "resource-summary-cache-v1"; - root["functions"] = std::move(functionArray); - - std::ofstream out(cacheFile, std::ios::out | std::ios::trunc | std::ios::binary); - if (!out) - return false; - std::string payload; - llvm::raw_string_ostream os(payload); - os << llvm::formatv("{0:2}", llvm::json::Value(std::move(root))); - os.flush(); - out << payload; - return out.good(); -} - -static std::optional -readSummaryCacheFile(const std::filesystem::path& cacheFile) -{ - std::ifstream in(cacheFile, std::ios::in | std::ios::binary); - if (!in) - return std::nullopt; - - std::ostringstream ss; - ss << in.rdbuf(); - auto parsed = llvm::json::parse(ss.str()); - if (!parsed) - return std::nullopt; - - const auto* obj = parsed->getAsObject(); - if (!obj) - return std::nullopt; - auto schema = obj->getString("schema"); - if (!schema || *schema != "resource-summary-cache-v1") - return std::nullopt; - - const auto* functions = obj->getArray("functions"); - if (!functions) - return std::nullopt; - - ctrace::stack::analysis::ResourceSummaryIndex index; - for (const auto& fnValue : *functions) - { - const auto* fnObj = fnValue.getAsObject(); - if (!fnObj) - continue; - auto name = fnObj->getString("name"); - if (!name || name->empty()) - continue; - const auto* effects = fnObj->getArray("effects"); - if (!effects) - continue; - - ctrace::stack::analysis::ResourceSummaryFunction fnSummary; - for (const auto& effectValue : *effects) - { - const auto* effectObj = effectValue.getAsObject(); - if (!effectObj) - continue; - auto actionName = effectObj->getString("action"); - auto action = actionName ? decodeSummaryActionName(*actionName) : std::nullopt; - if (!action) - continue; - auto argIndex = effectObj->getInteger("argIndex"); - auto offset = effectObj->getInteger("offset"); - auto viaPointerSlot = effectObj->getBoolean("viaPointerSlot"); - auto resourceKind = effectObj->getString("resourceKind"); - if (!argIndex || !offset || !viaPointerSlot || !resourceKind) - continue; - - ctrace::stack::analysis::ResourceSummaryEffect effect; - effect.action = *action; - effect.argIndex = static_cast(*argIndex); - effect.offset = static_cast(*offset); - effect.viaPointerSlot = *viaPointerSlot; - effect.resourceKind = resourceKind->str(); - fnSummary.effects.push_back(std::move(effect)); - } - index.functions[ctrace_tools::canonicalizeMangledName(name->str())] = std::move(fnSummary); - } - - return index; -} - -static std::shared_ptr -buildCrossTUSummaryIndex(const std::vector& loadedModules, - const AnalysisConfig& cfg) -{ - if (!cfg.resourceCrossTU || cfg.resourceModelPath.empty() || loadedModules.size() < 2) - return nullptr; - - using Clock = std::chrono::steady_clock; - const auto buildStart = Clock::now(); - if (cfg.timing) - { - llvm::errs() << "Building cross-TU resource summaries for " << loadedModules.size() - << " module(s)...\n"; - } - - const std::string modelContent = readFileAsString(cfg.resourceModelPath); - const std::string modelHash = - md5Hex(modelContent.empty() ? cfg.resourceModelPath : modelContent); - constexpr llvm::StringLiteral kCacheSchema = "cross-tu-resource-summary-v1"; - const bool allowDiskCache = - !cfg.resourceSummaryMemoryOnly && !cfg.resourceSummaryCacheDir.empty(); - const unsigned maxJobs = std::max(1u, cfg.jobs); - std::unordered_map memoryCache; - std::vector moduleIRHashes; - std::vector moduleCompileArgsHashes; - moduleIRHashes.reserve(loadedModules.size()); - moduleCompileArgsHashes.reserve(loadedModules.size()); - for (const LoadedInputModule& loaded : loadedModules) - { - moduleIRHashes.push_back(hashModuleIR(*loaded.module)); - moduleCompileArgsHashes.push_back(computeCompileArgsSignature(cfg, loaded.filename)); - } - - // Empirical safeguard: cross-TU summaries usually stabilize in a few rounds. - // Keep a bounded worst-case runtime on very large dependency graphs. - constexpr unsigned kCrossTUMaxIterations = 12; - ctrace::stack::analysis::ResourceSummaryIndex globalIndex; - unsigned iterationsRan = 0; - bool converged = false; - for (unsigned iter = 0; iter < kCrossTUMaxIterations; ++iter) - { - const auto iterStart = Clock::now(); - const std::string externalHash = hashSummaryIndex(globalIndex); - ctrace::stack::analysis::ResourceSummaryIndex nextGlobal; - std::vector moduleSummaries( - loadedModules.size()); - std::vector summaryReady(loadedModules.size(), 0); - std::vector cacheKeys(loadedModules.size()); - std::vector missingIndices; - missingIndices.reserve(loadedModules.size()); - - for (std::size_t moduleIndex = 0; moduleIndex < loadedModules.size(); ++moduleIndex) - { - const std::string cacheKeyPayload = - std::string(kCacheSchema) + "|" + modelHash + "|" + externalHash + "|" + - moduleCompileArgsHashes[moduleIndex] + "|" + moduleIRHashes[moduleIndex]; - const std::string cacheKey = md5Hex(cacheKeyPayload); - cacheKeys[moduleIndex] = cacheKey; - - bool loadedFromCache = false; - if (const auto memIt = memoryCache.find(cacheKey); memIt != memoryCache.end()) - { - moduleSummaries[moduleIndex] = memIt->second; - loadedFromCache = true; - } - else if (allowDiskCache) - { - const std::filesystem::path cacheFile = - std::filesystem::path(cfg.resourceSummaryCacheDir) / (cacheKey + ".json"); - auto cached = readSummaryCacheFile(cacheFile); - if (cached) - { - moduleSummaries[moduleIndex] = std::move(*cached); - memoryCache.emplace(cacheKey, moduleSummaries[moduleIndex]); - loadedFromCache = true; - } - } - - if (loadedFromCache) - { - summaryReady[moduleIndex] = 1; - } - else - { - missingIndices.push_back(moduleIndex); - } - } - - auto buildModuleSummary = - [&](std::size_t moduleIndex) -> ctrace::stack::analysis::ResourceSummaryIndex - { - const LoadedInputModule& loaded = loadedModules[moduleIndex]; - analysis::FunctionFilter filter = analysis::buildFunctionFilter(*loaded.module, cfg); - auto shouldAnalyze = [&](const llvm::Function& F) -> bool - { return filter.shouldAnalyze(F); }; - return analysis::buildResourceLifetimeSummaryIndex(*loaded.module, shouldAnalyze, - cfg.resourceModelPath, &globalIndex); - }; - - if (!missingIndices.empty()) - { - if (maxJobs <= 1 || missingIndices.size() <= 1) - { - for (std::size_t moduleIndex : missingIndices) - { - moduleSummaries[moduleIndex] = buildModuleSummary(moduleIndex); - summaryReady[moduleIndex] = 1; - } - } - else - { - const unsigned workerCount = - std::min(maxJobs, static_cast(missingIndices.size())); - std::vector computed( - loadedModules.size()); - std::vector computedReady(loadedModules.size(), 0); - std::atomic_size_t nextMissing{0}; - std::vector workers; - workers.reserve(workerCount); - - for (unsigned worker = 0; worker < workerCount; ++worker) - { - workers.emplace_back( - [&]() - { - while (true) - { - const std::size_t slot = nextMissing.fetch_add(1); - if (slot >= missingIndices.size()) - break; - const std::size_t moduleIndex = missingIndices[slot]; - computed[moduleIndex] = buildModuleSummary(moduleIndex); - computedReady[moduleIndex] = 1; - } - }); - } - - for (auto& worker : workers) - worker.join(); - - for (std::size_t moduleIndex : missingIndices) - { - if (computedReady[moduleIndex] == 0) - continue; - moduleSummaries[moduleIndex] = std::move(computed[moduleIndex]); - summaryReady[moduleIndex] = 1; - } - } - - for (std::size_t moduleIndex : missingIndices) - { - if (summaryReady[moduleIndex] == 0) - continue; - memoryCache.emplace(cacheKeys[moduleIndex], moduleSummaries[moduleIndex]); - if (allowDiskCache) - { - const std::filesystem::path cacheFile = - std::filesystem::path(cfg.resourceSummaryCacheDir) / - (cacheKeys[moduleIndex] + ".json"); - (void)writeSummaryCacheFile(cacheFile, moduleSummaries[moduleIndex]); - } - } - } - - for (std::size_t moduleIndex = 0; moduleIndex < loadedModules.size(); ++moduleIndex) - { - if (summaryReady[moduleIndex] == 0) - continue; - (void)analysis::mergeResourceSummaryIndex(nextGlobal, moduleSummaries[moduleIndex]); - } - - const bool iterConverged = analysis::resourceSummaryIndexEquals(nextGlobal, globalIndex); - ++iterationsRan; - if (cfg.timing) - { - const auto iterEnd = Clock::now(); - const auto ms = - std::chrono::duration_cast(iterEnd - iterStart).count(); - llvm::errs() << "Cross-TU summary iteration " << iterationsRan << " done in " << ms - << " ms" << (iterConverged ? " (converged)\n" : "\n"); - } - - if (iterConverged) - { - converged = true; - break; - } - globalIndex = std::move(nextGlobal); - } - - if (!converged) - { - llvm::errs() << "[ !!Warn ] Resource inter-procedural analysis: reached fixed-point " - "iteration cap (" - << kCrossTUMaxIterations - << "); summary may be non-converged and conservative\n"; - } - - if (cfg.timing) - { - const auto buildEnd = Clock::now(); - const auto ms = - std::chrono::duration_cast(buildEnd - buildStart).count(); - llvm::errs() << "Cross-TU summary build done in " << ms << " ms (" << iterationsRan - << " iteration(s))\n"; - } - - return std::make_shared(std::move(globalIndex)); -} - -static std::shared_ptr -buildCrossTUUninitializedSummaryIndex(const std::vector& loadedModules, - const AnalysisConfig& cfg) -{ - if (!cfg.uninitializedCrossTU || loadedModules.size() < 2) - return nullptr; - - using Clock = std::chrono::steady_clock; - const auto buildStart = Clock::now(); - if (cfg.timing) - { - llvm::errs() << "Building cross-TU uninitialized summaries for " << loadedModules.size() - << " module(s)...\n"; - } - - // Same fixed-point budget policy as resource summaries. - constexpr unsigned kCrossTUMaxIterations = 12; - const unsigned maxJobs = std::max(1u, cfg.jobs); - analysis::UninitializedSummaryIndex globalIndex; - unsigned iterationsRan = 0; - bool converged = false; - for (unsigned iter = 0; iter < kCrossTUMaxIterations; ++iter) - { - const auto iterStart = Clock::now(); - analysis::UninitializedSummaryIndex nextGlobal; - std::vector moduleSummaries(loadedModules.size()); - - auto buildModuleSummary = - [&](std::size_t moduleIndex) -> analysis::UninitializedSummaryIndex - { - const LoadedInputModule& loaded = loadedModules[moduleIndex]; - analysis::FunctionFilter filter = analysis::buildFunctionFilter(*loaded.module, cfg); - auto shouldAnalyze = [&](const llvm::Function& F) -> bool - { return filter.shouldAnalyze(F); }; - return analysis::buildUninitializedSummaryIndex(*loaded.module, shouldAnalyze, - &globalIndex); - }; - - if (maxJobs <= 1 || loadedModules.size() <= 1) - { - for (std::size_t moduleIndex = 0; moduleIndex < loadedModules.size(); ++moduleIndex) - moduleSummaries[moduleIndex] = buildModuleSummary(moduleIndex); - } - else - { - const unsigned workerCount = - std::min(maxJobs, static_cast(loadedModules.size())); - std::atomic_size_t nextModule{0}; - std::vector workers; - workers.reserve(workerCount); - for (unsigned worker = 0; worker < workerCount; ++worker) - { - workers.emplace_back( - [&]() - { - while (true) - { - const std::size_t moduleIndex = nextModule.fetch_add(1); - if (moduleIndex >= loadedModules.size()) - break; - moduleSummaries[moduleIndex] = buildModuleSummary(moduleIndex); - } - }); - } - for (auto& worker : workers) - worker.join(); - } - - for (const auto& moduleSummary : moduleSummaries) - { - (void)analysis::mergeUninitializedSummaryIndex(nextGlobal, moduleSummary); - } - - const bool iterConverged = - analysis::uninitializedSummaryIndexEquals(nextGlobal, globalIndex); - ++iterationsRan; - globalIndex = std::move(nextGlobal); - - if (cfg.timing) - { - const auto iterEnd = Clock::now(); - const auto ms = - std::chrono::duration_cast(iterEnd - iterStart).count(); - llvm::errs() << "Cross-TU uninitialized summary iteration " << iterationsRan - << " done in " << ms << " ms" << (iterConverged ? " (converged)\n" : "\n"); - } - - if (iterConverged) - { - converged = true; - break; - } - } - - if (!converged) - { - llvm::errs() << "[ !!Warn ] Uninitialized inter-procedural analysis: reached fixed-point " - "iteration cap (" - << kCrossTUMaxIterations - << "); summary may be non-converged and conservative\n"; - } - - if (cfg.timing) - { - const auto buildEnd = Clock::now(); - const auto ms = - std::chrono::duration_cast(buildEnd - buildStart).count(); - llvm::errs() << "Cross-TU uninitialized summary build done in " << ms << " ms (" - << iterationsRan << " iteration(s))\n"; - } - - return std::make_shared(std::move(globalIndex)); -} - -static void accumulateSummary(DiagnosticSummary& total, const DiagnosticSummary& add) -{ - total.info += add.info; - total.warning += add.warning; - total.error += add.error; -} - -static DiagnosticSummary summarizeDiagnostics(const AnalysisResult& result) -{ - DiagnosticSummary summary; - for (const auto& d : result.diagnostics) - { - switch (d.severity) - { - case DiagnosticSeverity::Info: - ++summary.info; - break; - case DiagnosticSeverity::Warning: - ++summary.warning; - break; - case DiagnosticSeverity::Error: - ++summary.error; - break; - } - } - return summary; -} - int main(int argc, char** argv) { coretrace::enable_logging(); @@ -1788,20 +95,6 @@ int main(int argc, char** argv) "Starting analysis for {} input(s)\n", argc - 1); llvm::LLVMContext context; - std::vector inputFilenames; - OutputFormat outputFormat = OutputFormat::Human; - std::string sarifBaseDir; - - AnalysisConfig cfg{}; // mode = IR, stackLimit = 8 MiB default - cfg.quiet = false; - cfg.warningsOnly = false; - std::string compileCommandsPath; - bool compileCommandsExplicit = false; - bool analysisProfileExplicit = false; - bool includeCompdbDeps = false; - - cfg.extraCompileArgs.emplace_back("-O0"); - cfg.extraCompileArgs.emplace_back("--ct-optnone"); if (argc < 2) { @@ -1809,489 +102,28 @@ int main(int argc, char** argv) return 1; } - for (int i = 1; i < argc; ++i) - { - const char* arg = argv[i]; - std::string argStr{arg}; - if (argStr == "-h" || argStr == "--help") - { - printHelp(); - return 0; - } - if (argStr == "--demangle") - { - cfg.demangle = true; - continue; - } - if (argStr == "--quiet") - { - cfg.quiet = true; - continue; - } - if (argStr == "--verbose") - { - cfg.quiet = false; - coretrace::set_min_level(coretrace::Level::Debug); - continue; - } - if (argStr == "--STL" || argStr == "--stl") - { - cfg.includeSTL = true; - continue; - } - if (argStr == "--only-file") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --only-file\n"; - return 1; - } - cfg.onlyFiles.emplace_back(argv[++i]); - continue; - } - if (argStr.rfind("--only-file=", 0) == 0) - { - cfg.onlyFiles.emplace_back(argStr.substr(std::strlen("--only-file="))); - continue; - } - if (argStr == "--only-func") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --only-func\n"; - return 1; - } - addCsvFilters(cfg.onlyFunctions, argv[++i]); - continue; - } - if (argStr.rfind("--only-func=", 0) == 0) - { - addCsvFilters(cfg.onlyFunctions, argStr.substr(std::strlen("--only-func="))); - continue; - } - if (argStr == "--only-function") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --only-function\n"; - return 1; - } - addCsvFilters(cfg.onlyFunctions, argv[++i]); - continue; - } - if (argStr.rfind("--only-function=", 0) == 0) - { - addCsvFilters(cfg.onlyFunctions, argStr.substr(std::strlen("--only-function="))); - continue; - } - if (argStr.rfind("--only-dir=", 0) == 0) - { - cfg.onlyDirs.emplace_back(argStr.substr(std::strlen("--only-dir="))); - continue; - } - if (argStr == "--only-dir") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --only-dir\n"; - return 1; - } - cfg.onlyDirs.emplace_back(argv[++i]); - continue; - } - if (argStr == "--exclude-dir") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --exclude-dir\n"; - return 1; - } - addCsvFilters(cfg.excludeDirs, argv[++i]); - continue; - } - if (argStr.rfind("--exclude-dir=", 0) == 0) - { - addCsvFilters(cfg.excludeDirs, argStr.substr(std::strlen("--exclude-dir="))); - continue; - } - if (argStr == "--stack-limit") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --stack-limit\n"; - return 1; - } - std::string error; - StackSize value = 0; - if (!parseStackLimitValue(argv[++i], value, error)) - { - llvm::errs() << "Invalid --stack-limit value: " << error << "\n"; - return 1; - } - cfg.stackLimit = value; - continue; - } - if (argStr.rfind("--stack-limit=", 0) == 0) - { - std::string error; - StackSize value = 0; - if (!parseStackLimitValue(argStr.substr(std::strlen("--stack-limit=")), value, error)) - { - llvm::errs() << "Invalid --stack-limit value: " << error << "\n"; - return 1; - } - cfg.stackLimit = value; - continue; - } - if (argStr == "--dump-filter") - { - cfg.dumpFilter = true; - continue; - } - if (argStr == "--dump-ir") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --dump-ir\n"; - return 1; - } - cfg.dumpIRPath = argv[++i]; - continue; - } - if (argStr.rfind("--dump-ir=", 0) == 0) - { - cfg.dumpIRPath = argStr.substr(std::strlen("--dump-ir=")); - continue; - } - if (argStr == "-I") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for -I\n"; - return 1; - } - cfg.extraCompileArgs.emplace_back("-I" + std::string(argv[++i])); - continue; - } - if (argStr.rfind("-I", 0) == 0 && argStr.size() > 2) - { - cfg.extraCompileArgs.emplace_back(argStr); - continue; - } - if (argStr == "-D") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for -D\n"; - return 1; - } - cfg.extraCompileArgs.emplace_back("-D" + std::string(argv[++i])); - continue; - } - if (argStr.rfind("-D", 0) == 0 && argStr.size() > 2) - { - cfg.extraCompileArgs.emplace_back(argStr); - continue; - } - if (argStr.rfind("--compile-arg=", 0) == 0) - { - cfg.extraCompileArgs.emplace_back(argStr.substr(std::strlen("--compile-arg="))); - continue; - } - if (argStr == "--compdb-fast") - { - cfg.compdbFast = true; - continue; - } - if (argStr == "--analysis-profile") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --analysis-profile\n"; - return 1; - } - std::string error; - if (!parseAnalysisProfile(argv[++i], cfg.profile, error)) - { - llvm::errs() << "Invalid --analysis-profile value: " << error << "\n"; - return 1; - } - analysisProfileExplicit = true; - continue; - } - if (argStr.rfind("--analysis-profile=", 0) == 0) - { - std::string error; - if (!parseAnalysisProfile(argStr.substr(std::strlen("--analysis-profile=")), - cfg.profile, error)) - { - llvm::errs() << "Invalid --analysis-profile value: " << error << "\n"; - return 1; - } - analysisProfileExplicit = true; - continue; - } - if (argStr == "--include-compdb-deps") - { - includeCompdbDeps = true; - continue; - } - if (argStr == "--jobs") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --jobs\n"; - return 1; - } - std::string error; - unsigned jobs = 0; - if (!parsePositiveUnsigned(argv[++i], jobs, error)) - { - llvm::errs() << "Invalid --jobs value: " << error << "\n"; - return 1; - } - cfg.jobs = jobs; - continue; - } - if (argStr.rfind("--jobs=", 0) == 0) - { - std::string error; - unsigned jobs = 0; - if (!parsePositiveUnsigned(argStr.substr(std::strlen("--jobs=")), jobs, error)) - { - llvm::errs() << "Invalid --jobs value: " << error << "\n"; - return 1; - } - cfg.jobs = jobs; - continue; - } - if (argStr == "--timing") - { - cfg.timing = true; - continue; - } - if (argStr == "--resource-model") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --resource-model\n"; - return 1; - } - cfg.resourceModelPath = argv[++i]; - continue; - } - if (argStr.rfind("--resource-model=", 0) == 0) - { - cfg.resourceModelPath = argStr.substr(std::strlen("--resource-model=")); - continue; - } - if (argStr == "--escape-model") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --escape-model\n"; - return 1; - } - cfg.escapeModelPath = argv[++i]; - continue; - } - if (argStr.rfind("--escape-model=", 0) == 0) - { - cfg.escapeModelPath = argStr.substr(std::strlen("--escape-model=")); - continue; - } - if (argStr == "--resource-cross-tu") - { - cfg.resourceCrossTU = true; - continue; - } - if (argStr == "--no-resource-cross-tu") - { - cfg.resourceCrossTU = false; - continue; - } - if (argStr == "--uninitialized-cross-tu") - { - cfg.uninitializedCrossTU = true; - continue; - } - if (argStr == "--no-uninitialized-cross-tu") - { - cfg.uninitializedCrossTU = false; - continue; - } - if (argStr == "--resource-summary-cache-dir") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --resource-summary-cache-dir\n"; - return 1; - } - cfg.resourceSummaryCacheDir = argv[++i]; - continue; - } - if (argStr == "--resource-summary-cache-memory-only") - { - cfg.resourceSummaryMemoryOnly = true; - continue; - } - if (argStr.rfind("--resource-summary-cache-dir=", 0) == 0) - { - cfg.resourceSummaryCacheDir = - argStr.substr(std::strlen("--resource-summary-cache-dir=")); - continue; - } - if (argStr == "--compile-commands" || argStr == "--compdb") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for " << argStr << "\n"; - return 1; - } - compileCommandsPath = argv[++i]; - compileCommandsExplicit = true; - continue; - } - if (argStr.rfind("--compile-commands=", 0) == 0) - { - compileCommandsPath = argStr.substr(std::strlen("--compile-commands=")); - compileCommandsExplicit = true; - continue; - } - if (argStr.rfind("--compdb=", 0) == 0) - { - compileCommandsPath = argStr.substr(std::strlen("--compdb=")); - compileCommandsExplicit = true; - continue; - } - if (argStr == "--warnings-only") - { - cfg.warningsOnly = true; - continue; - } - if (argStr == "--format=json") - { - outputFormat = OutputFormat::Json; - continue; - } - else if (argStr == "--format=sarif") - { - outputFormat = OutputFormat::Sarif; - continue; - } - else if (argStr == "--format=human") - { - outputFormat = OutputFormat::Human; - continue; - } - if (argStr.rfind("--base-dir=", 0) == 0) - { - sarifBaseDir = argStr.substr(std::strlen("--base-dir=")); - continue; - } - if (argStr == "--base-dir") - { - if (i + 1 >= argc) - { - llvm::errs() << "Missing argument for --base-dir\n"; - return 1; - } - sarifBaseDir = argv[++i]; - continue; - } - if (std::strncmp(arg, "--mode=", 7) == 0) - { - const char* modeStr = arg + 7; - if (std::strcmp(modeStr, "ir") == 0) - { - cfg.mode = AnalysisMode::IR; - } - else if (std::strcmp(modeStr, "abi") == 0) - { - cfg.mode = AnalysisMode::ABI; - } - else - { - llvm::errs() << "Unknown mode: " << modeStr << " (expected 'ir' or 'abi')\n"; - return 1; - } - } - else if (!argStr.empty() && argStr[0] == '-') - { - llvm::errs() << "Unknown option: " << arg << "\n"; - return 1; - } - else - { - inputFilenames.emplace_back(arg); - } - } - - if (compileCommandsExplicit) - { - if (!loadCompilationDatabase(compileCommandsPath, cfg)) - return 1; - } - - const bool compdbInputsAutoDiscovered = - discoverInputsFromCompilationDatabase(inputFilenames, cfg, includeCompdbDeps); - - const NormalizedPathFilters normalizedFilters = buildNormalizedPathFilters(cfg); - excludeInputFiles(inputFilenames, cfg, normalizedFilters); - - if (compdbInputsAutoDiscovered && !analysisProfileExplicit && inputFilenames.size() > 1) + ctrace::stack::cli::ParseResult parseResult = ctrace::stack::cli::parseArguments(argc, argv); + if (parseResult.status == ctrace::stack::cli::ParseStatus::Help) { - cfg.profile = AnalysisProfile::Fast; - llvm::errs() << "[ !Info! ] Auto-selected --analysis-profile=fast for compile_commands " - "batch analysis (override with --analysis-profile=full)\n"; + printHelp(); + return 0; } - - if (inputFilenames.empty()) + if (parseResult.status == ctrace::stack::cli::ParseStatus::Error) { - // llvm::errs() << "Usage: stack_usage_analyzer [file2.ll ...] [options]\n" - // << "Try --help for more information.\n"; - coretrace::log(coretrace::Level::Error, - "Usage: stack_usage_analyzer [file2.ll ...] [options]\n"); - coretrace::log(coretrace::Level::Error, "Try --help for more information.\n"); + logText(coretrace::Level::Error, parseResult.error); return 1; } - if (!configureDumpIRPath(inputFilenames, cfg)) - return 1; - - std::sort(inputFilenames.begin(), inputFilenames.end()); - std::vector> results; - results.reserve(inputFilenames.size()); - const bool hasFilter = - !cfg.onlyFiles.empty() || !cfg.onlyDirs.empty() || !cfg.onlyFunctions.empty(); - const bool needsCrossTUResourceSummaries = - cfg.resourceCrossTU && !cfg.resourceModelPath.empty() && inputFilenames.size() > 1; - const bool needsCrossTUUninitializedSummaries = - cfg.uninitializedCrossTU && inputFilenames.size() > 1; - const bool needsSharedModuleLoading = - needsCrossTUResourceSummaries || needsCrossTUUninitializedSummaries; + if (parseResult.parsed.verbose) + coretrace::set_min_level(coretrace::Level::Debug); - printInterprocStatus(cfg, inputFilenames.size(), needsCrossTUResourceSummaries, - needsCrossTUUninitializedSummaries); - - bool analysisSucceeded = false; - if (needsSharedModuleLoading) - { - analysisSucceeded = analyzeWithSharedModuleLoading( - inputFilenames, cfg, hasFilter, needsCrossTUResourceSummaries, - needsCrossTUUninitializedSummaries, results); - } - else + ctrace::stack::app::RunResult runResult = + ctrace::stack::app::runAnalyzerApp(std::move(parseResult.parsed), context); + if (!runResult.isOk()) { - analysisSucceeded = - analyzeWithoutSharedModuleLoading(inputFilenames, cfg, context, hasFilter, results); - } - if (!analysisSucceeded) + logText(coretrace::Level::Error, runResult.error); return 1; + } - if (outputFormat == OutputFormat::Json) - return emitJsonOutput(results, cfg, inputFilenames, normalizedFilters); - if (outputFormat == OutputFormat::Sarif) - return emitSarifOutput(results, cfg, inputFilenames, sarifBaseDir, normalizedFilters); - return emitHumanOutput(results, cfg, normalizedFilters); + return runResult.exitCode; } From e3fd7a51eb499bc4daf9f8743bb9b2b944a12dfc Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:53:43 +0900 Subject: [PATCH 04/19] feat(analyzer): add modular analysis pipeline and services --- include/analyzer/AnalysisPipeline.hpp | 24 + include/analyzer/DiagnosticEmitter.hpp | 92 ++ include/analyzer/LocationResolver.hpp | 29 + include/analyzer/ModulePreparationService.hpp | 52 + src/analyzer/AnalysisPipeline.cpp | 253 +++++ src/analyzer/DiagnosticEmitter.cpp | 969 ++++++++++++++++++ src/analyzer/LocationResolver.cpp | 142 +++ src/analyzer/ModulePreparationService.cpp | 122 +++ 8 files changed, 1683 insertions(+) create mode 100644 include/analyzer/AnalysisPipeline.hpp create mode 100644 include/analyzer/DiagnosticEmitter.hpp create mode 100644 include/analyzer/LocationResolver.hpp create mode 100644 include/analyzer/ModulePreparationService.hpp create mode 100644 src/analyzer/AnalysisPipeline.cpp create mode 100644 src/analyzer/DiagnosticEmitter.cpp create mode 100644 src/analyzer/LocationResolver.cpp create mode 100644 src/analyzer/ModulePreparationService.cpp diff --git a/include/analyzer/AnalysisPipeline.hpp b/include/analyzer/AnalysisPipeline.hpp new file mode 100644 index 0000000..6b68eb3 --- /dev/null +++ b/include/analyzer/AnalysisPipeline.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "StackUsageAnalyzer.hpp" + +namespace llvm +{ + class Module; +} + +namespace ctrace::stack::analyzer +{ + + class AnalysisPipeline + { + public: + explicit AnalysisPipeline(const AnalysisConfig& config); + + AnalysisResult run(llvm::Module& mod) const; + + private: + const AnalysisConfig& config_; + }; + +} // namespace ctrace::stack::analyzer diff --git a/include/analyzer/DiagnosticEmitter.hpp b/include/analyzer/DiagnosticEmitter.hpp new file mode 100644 index 0000000..39b7798 --- /dev/null +++ b/include/analyzer/DiagnosticEmitter.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include "StackUsageAnalyzer.hpp" +#include "analyzer/ModulePreparationService.hpp" + +#include "analysis/AllocaUsage.hpp" +#include "analysis/ConstParamAnalysis.hpp" +#include "analysis/DuplicateIfCondition.hpp" +#include "analysis/DynamicAlloca.hpp" +#include "analysis/InvalidBaseReconstruction.hpp" +#include "analysis/MemIntrinsicOverflow.hpp" +#include "analysis/ResourceLifetimeAnalysis.hpp" +#include "analysis/SizeMinusKWrites.hpp" +#include "analysis/StackBufferAnalysis.hpp" +#include "analysis/StackPointerEscape.hpp" +#include "analysis/UninitializedVarAnalysis.hpp" + +#include +#include +#include +#include + +#include + +namespace ctrace::stack::analyzer +{ + + struct SourceLocation + { + unsigned line = 0; + unsigned column = 0; + }; + + struct FunctionAuxData + { + llvm::DenseMap locations; + llvm::DenseMap callPaths; + llvm::DenseMap>> + localAllocas; + llvm::DenseMap indices; + }; + + AnalysisResult buildResults(const PreparedModule& prepared, FunctionAuxData& aux); + + void emitSummaryDiagnostics(AnalysisResult& result, const PreparedModule& prepared, + const FunctionAuxData& aux); + + void appendStackBufferDiagnostics( + AnalysisResult& result, + const std::vector& bufferIssues); + + void appendDynamicAllocaDiagnostics( + AnalysisResult& result, + const std::vector& issues); + + void appendAllocaUsageDiagnostics(AnalysisResult& result, const AnalysisConfig& config, + StackSize allocaLargeThreshold, + const std::vector& issues); + + void appendMemIntrinsicDiagnostics( + AnalysisResult& result, + const std::vector& issues); + + void appendSizeMinusKDiagnostics( + AnalysisResult& result, + const std::vector& issues); + + void appendMultipleStoreDiagnostics( + AnalysisResult& result, + const std::vector& issues); + + void appendDuplicateIfConditionDiagnostics( + AnalysisResult& result, const std::vector& issues); + + void appendUninitializedLocalReadDiagnostics( + AnalysisResult& result, + const std::vector& issues); + + void appendInvalidBaseReconstructionDiagnostics( + AnalysisResult& result, + const std::vector& issues); + + void appendStackPointerEscapeDiagnostics( + AnalysisResult& result, const std::vector& issues); + + void appendConstParamDiagnostics(AnalysisResult& result, + const std::vector& issues); + + void appendResourceLifetimeDiagnostics( + AnalysisResult& result, const std::vector& issues); + +} // namespace ctrace::stack::analyzer diff --git a/include/analyzer/LocationResolver.hpp b/include/analyzer/LocationResolver.hpp new file mode 100644 index 0000000..e14756f --- /dev/null +++ b/include/analyzer/LocationResolver.hpp @@ -0,0 +1,29 @@ +#pragma once + +namespace llvm +{ + class AllocaInst; + class Instruction; +} // namespace llvm + +namespace ctrace::stack::analyzer +{ + + struct ResolvedLocation + { + unsigned line = 0; + unsigned column = 0; + unsigned startLine = 0; + unsigned startColumn = 0; + unsigned endLine = 0; + unsigned endColumn = 0; + bool hasLocation = false; + }; + + ResolvedLocation resolveFromInstruction(const llvm::Instruction* inst, + bool includeRange = false); + + bool resolveAllocaSourceLocation(const llvm::AllocaInst* allocaInst, unsigned& line, + unsigned& column); + +} // namespace ctrace::stack::analyzer diff --git a/include/analyzer/ModulePreparationService.hpp b/include/analyzer/ModulePreparationService.hpp new file mode 100644 index 0000000..2f70756 --- /dev/null +++ b/include/analyzer/ModulePreparationService.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "StackUsageAnalyzer.hpp" +#include "analysis/FunctionFilter.hpp" +#include "analysis/StackComputation.hpp" + +#include +#include +#include + +namespace llvm +{ + class DataLayout; + class Function; + class Module; +} // namespace llvm + +namespace ctrace::stack::analyzer +{ + + struct ModuleAnalysisContext + { + llvm::Module& mod; + const AnalysisConfig& config; + const llvm::DataLayout* dataLayout = nullptr; + analysis::FunctionFilter filter; + std::vector functions; + std::unordered_set functionSet; + std::vector allDefinedFunctions; + std::unordered_set allDefinedSet; + + bool shouldAnalyze(const llvm::Function& F) const; + bool isDefined(const llvm::Function& F) const; + }; + + using LocalStackMap = std::map; + + struct PreparedModule + { + ModuleAnalysisContext ctx; + LocalStackMap localStack; + analysis::CallGraph callGraph; + analysis::InternalAnalysisState recursionState; + }; + + class ModulePreparationService + { + public: + PreparedModule prepare(llvm::Module& mod, const AnalysisConfig& config) const; + }; + +} // namespace ctrace::stack::analyzer diff --git a/src/analyzer/AnalysisPipeline.cpp b/src/analyzer/AnalysisPipeline.cpp new file mode 100644 index 0000000..a6dbc3a --- /dev/null +++ b/src/analyzer/AnalysisPipeline.cpp @@ -0,0 +1,253 @@ +#include "analyzer/AnalysisPipeline.hpp" + +#include "analyzer/DiagnosticEmitter.hpp" +#include "analyzer/ModulePreparationService.hpp" + +#include "analysis/AllocaUsage.hpp" +#include "analysis/ConstParamAnalysis.hpp" +#include "analysis/DuplicateIfCondition.hpp" +#include "analysis/DynamicAlloca.hpp" +#include "analysis/InvalidBaseReconstruction.hpp" +#include "analysis/MemIntrinsicOverflow.hpp" +#include "analysis/ResourceLifetimeAnalysis.hpp" +#include "analysis/SizeMinusKWrites.hpp" +#include "analysis/StackBufferAnalysis.hpp" +#include "analysis/StackComputation.hpp" +#include "analysis/StackPointerEscape.hpp" +#include "analysis/UninitializedVarAnalysis.hpp" +#include "passes/ModulePasses.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace ctrace::stack::analyzer +{ + namespace + { + struct PipelineData + { + llvm::Module& mod; + const AnalysisConfig& config; + ModulePreparationService preparation; + std::unique_ptr prepared; + FunctionAuxData aux; + AnalysisResult result; + StackSize allocaLargeThreshold = 0; + + PipelineData(llvm::Module& module, const AnalysisConfig& cfg) : mod(module), config(cfg) + { + } + }; + + struct PipelineStep + { + const char* label; + std::function run; + }; + } // namespace + + AnalysisPipeline::AnalysisPipeline(const AnalysisConfig& config) : config_(config) + { + } + + AnalysisResult AnalysisPipeline::run(llvm::Module& mod) const + { + using Clock = std::chrono::steady_clock; + + PipelineData data(mod, config_); + + auto logDuration = [&](const char* label, Clock::time_point start) + { + if (!config_.timing) + return; + const auto end = Clock::now(); + const auto ms = std::chrono::duration_cast(end - start).count(); + std::cerr << label << " done in " << ms << " ms\n"; + }; + + std::vector steps; + steps.push_back({"Function attrs pass", + [](PipelineData& state) + { + runFunctionAttrsPass(state.mod); + }}); + + steps.push_back({"Prepare module", + [](PipelineData& state) + { + state.prepared = std::make_unique( + state.preparation.prepare(state.mod, state.config)); + }}); + + steps.push_back({"Build results", + [](PipelineData& state) + { + state.result = buildResults(*state.prepared, state.aux); + }}); + + steps.push_back({"Emit summary diagnostics", + [](PipelineData& state) + { + emitSummaryDiagnostics(state.result, *state.prepared, state.aux); + }}); + + steps.push_back({"Compute alloca threshold", + [](PipelineData& state) + { + state.allocaLargeThreshold = analysis::computeAllocaLargeThreshold(state.config); + }}); + + steps.push_back({"Stack buffer overflows", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const std::vector issues = + analysis::analyzeStackBufferOverflows(state.mod, shouldAnalyze, + state.config); + appendStackBufferDiagnostics(state.result, issues); + }}); + + steps.push_back({"Dynamic allocas", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const std::vector issues = + analysis::analyzeDynamicAllocas(state.mod, shouldAnalyze); + appendDynamicAllocaDiagnostics(state.result, issues); + }}); + + steps.push_back({"Alloca usage", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const llvm::DataLayout& dataLayout = *state.prepared->ctx.dataLayout; + const std::vector issues = + analysis::analyzeAllocaUsage( + state.mod, dataLayout, state.prepared->recursionState.RecursiveFuncs, + state.prepared->recursionState.InfiniteRecursionFuncs, + shouldAnalyze); + appendAllocaUsageDiagnostics(state.result, state.config, + state.allocaLargeThreshold, issues); + }}); + + steps.push_back({"Mem intrinsic overflows", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const llvm::DataLayout& dataLayout = *state.prepared->ctx.dataLayout; + const std::vector issues = + analysis::analyzeMemIntrinsicOverflows(state.mod, dataLayout, + shouldAnalyze); + appendMemIntrinsicDiagnostics(state.result, issues); + }}); + + steps.push_back({"Size-minus-k writes", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const llvm::DataLayout& dataLayout = *state.prepared->ctx.dataLayout; + const std::vector issues = + analysis::analyzeSizeMinusKWrites(state.mod, dataLayout, + shouldAnalyze); + appendSizeMinusKDiagnostics(state.result, issues); + }}); + + steps.push_back({"Multiple stores", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const std::vector issues = + analysis::analyzeMultipleStores(state.mod, shouldAnalyze, + state.config); + appendMultipleStoreDiagnostics(state.result, issues); + }}); + + steps.push_back({"Duplicate if conditions", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const std::vector issues = + analysis::analyzeDuplicateIfConditions(state.mod, shouldAnalyze); + appendDuplicateIfConditionDiagnostics(state.result, issues); + }}); + + steps.push_back({"Uninitialized local reads", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const std::vector issues = + analysis::analyzeUninitializedLocalReads( + state.mod, shouldAnalyze, + state.config.uninitializedSummaryIndex.get()); + appendUninitializedLocalReadDiagnostics(state.result, issues); + }}); + + steps.push_back({"Invalid base reconstructions", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const llvm::DataLayout& dataLayout = *state.prepared->ctx.dataLayout; + const std::vector issues = + analysis::analyzeInvalidBaseReconstructions(state.mod, dataLayout, + shouldAnalyze); + appendInvalidBaseReconstructionDiagnostics(state.result, issues); + }}); + + steps.push_back({"Stack pointer escapes", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const std::vector issues = + analysis::analyzeStackPointerEscapes(state.mod, shouldAnalyze, + state.config.escapeModelPath); + appendStackPointerEscapeDiagnostics(state.result, issues); + }}); + + steps.push_back({"Const params", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const std::vector issues = + analysis::analyzeConstParams(state.mod, shouldAnalyze); + appendConstParamDiagnostics(state.result, issues); + }}); + + steps.push_back({"Resource lifetime", + [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const std::vector issues = + analysis::analyzeResourceLifetime( + state.mod, shouldAnalyze, state.config.resourceModelPath, + state.config.resourceSummaryIndex.get()); + appendResourceLifetimeDiagnostics(state.result, issues); + }}); + + for (const PipelineStep& step : steps) + { + const auto start = Clock::now(); + step.run(data); + logDuration(step.label, start); + } + + return data.result; + } + +} // namespace ctrace::stack::analyzer diff --git a/src/analyzer/DiagnosticEmitter.cpp b/src/analyzer/DiagnosticEmitter.cpp new file mode 100644 index 0000000..1ed4556 --- /dev/null +++ b/src/analyzer/DiagnosticEmitter.cpp @@ -0,0 +1,969 @@ +#include "analyzer/DiagnosticEmitter.hpp" + +#include "analyzer/LocationResolver.hpp" +#include "analysis/AnalyzerUtils.hpp" +#include "analysis/Reachability.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace ctrace::stack::analyzer +{ + namespace + { + constexpr std::string_view kInfoPrefix = "[ !Info! ]"; + constexpr std::string_view kWarnPrefix = "[ !!Warn ]"; + constexpr std::string_view kErrorPrefix = "[!!!Error]"; + constexpr std::string_view kDiagIndentArrow = "\t\t ↳ "; + + constexpr std::string_view prefixForSeverity(ctrace::stack::DiagnosticSeverity severity) noexcept + { + switch (severity) + { + case ctrace::stack::DiagnosticSeverity::Info: + return kInfoPrefix; + case ctrace::stack::DiagnosticSeverity::Warning: + return kWarnPrefix; + case ctrace::stack::DiagnosticSeverity::Error: + return kErrorPrefix; + } + return kWarnPrefix; + } + + class DiagnosticBuilder + { + public: + DiagnosticBuilder& function(std::string name) + { + diag_.funcName = std::move(name); + return *this; + } + + DiagnosticBuilder& filePath(std::string path) + { + diag_.filePath = std::move(path); + return *this; + } + + DiagnosticBuilder& severity(DiagnosticSeverity severity) + { + diag_.severity = severity; + return *this; + } + + DiagnosticBuilder& errCode(DescriptiveErrorCode code) + { + diag_.errCode = code; + return *this; + } + + DiagnosticBuilder& ruleId(std::string id) + { + diag_.ruleId = std::move(id); + return *this; + } + + DiagnosticBuilder& confidence(double value) + { + diag_.confidence = value; + return *this; + } + + DiagnosticBuilder& cwe(std::string id) + { + diag_.cweId = std::move(id); + return *this; + } + + DiagnosticBuilder& location(const ResolvedLocation& loc) + { + if (!loc.hasLocation) + { + diag_.line = 0; + diag_.column = 0; + diag_.startLine = 0; + diag_.startColumn = 0; + diag_.endLine = 0; + diag_.endColumn = 0; + return *this; + } + + diag_.line = loc.line; + diag_.column = loc.column; + diag_.startLine = loc.startLine; + diag_.startColumn = loc.startColumn; + diag_.endLine = loc.endLine; + diag_.endColumn = loc.endColumn; + return *this; + } + + DiagnosticBuilder& lineColumn(unsigned line, unsigned column) + { + diag_.line = line; + diag_.column = column; + diag_.startLine = line; + diag_.startColumn = column; + diag_.endLine = line; + diag_.endColumn = column; + return *this; + } + + DiagnosticBuilder& message(std::string text) + { + diag_.message = std::move(text); + return *this; + } + + DiagnosticBuilder& variableAliasing(std::vector aliasing) + { + diag_.variableAliasingVec = std::move(aliasing); + return *this; + } + + Diagnostic build() + { + return std::move(diag_); + } + + private: + Diagnostic diag_; + }; + } // namespace + + AnalysisResult buildResults(const PreparedModule& prepared, FunctionAuxData& aux) + { + AnalysisResult result; + result.config = prepared.ctx.config; + + for (llvm::Function* function : prepared.ctx.functions) + { + const llvm::Function* fn = function; + + analysis::LocalStackInfo localInfo; + analysis::StackEstimate totalInfo; + + if (auto itLocal = prepared.localStack.find(fn); itLocal != prepared.localStack.end()) + localInfo = itLocal->second; + + if (auto itTotal = prepared.recursionState.TotalStack.find(fn); + itTotal != prepared.recursionState.TotalStack.end()) + { + totalInfo = itTotal->second; + } + + FunctionResult functionResult; + functionResult.name = function->getName().str(); + functionResult.filePath = analysis::getFunctionSourcePath(*function); + if (functionResult.filePath.empty() && !prepared.ctx.filter.moduleSourcePath.empty()) + functionResult.filePath = prepared.ctx.filter.moduleSourcePath; + functionResult.localStack = localInfo.bytes; + functionResult.localStackUnknown = localInfo.unknown; + functionResult.maxStack = totalInfo.bytes; + functionResult.maxStackUnknown = totalInfo.unknown; + functionResult.hasDynamicAlloca = localInfo.hasDynamicAlloca; + functionResult.isRecursive = prepared.recursionState.RecursiveFuncs.count(fn) != 0; + functionResult.hasInfiniteSelfRecursion = + prepared.recursionState.InfiniteRecursionFuncs.count(fn) != 0; + functionResult.exceedsLimit = + (!functionResult.maxStackUnknown && totalInfo.bytes > prepared.ctx.config.stackLimit); + + unsigned line = 0; + unsigned column = 0; + if (analysis::getFunctionSourceLocation(*function, line, column)) + aux.locations[fn] = {line, column}; + + if (!functionResult.isRecursive && totalInfo.bytes > localInfo.bytes) + { + std::string path = + analysis::buildMaxStackCallPath(fn, prepared.callGraph, prepared.recursionState); + if (!path.empty()) + aux.callPaths[fn] = path; + } + if (!localInfo.localAllocas.empty()) + aux.localAllocas[fn] = localInfo.localAllocas; + + result.functions.push_back(std::move(functionResult)); + aux.indices[fn] = result.functions.size() - 1; + } + + return result; + } + + void emitSummaryDiagnostics(AnalysisResult& result, const PreparedModule& prepared, + const FunctionAuxData& aux) + { + for (const llvm::Function* function : prepared.ctx.functions) + { + const auto itIndex = aux.indices.find(function); + if (itIndex == aux.indices.end()) + continue; + + const std::size_t index = itIndex->second; + if (index >= result.functions.size()) + continue; + + const FunctionResult& functionResult = result.functions[index]; + SourceLocation functionLoc{}; + bool hasFunctionLoc = false; + if (const auto itLoc = aux.locations.find(function); itLoc != aux.locations.end()) + { + functionLoc = itLoc->second; + hasFunctionLoc = (functionLoc.line != 0); + } + + if (functionResult.isRecursive) + { + DiagnosticBuilder builder; + builder.function(functionResult.name) + .filePath(functionResult.filePath) + .severity(DiagnosticSeverity::Info) + .errCode(DescriptiveErrorCode::None) + .message("\t" + std::string(prefixForSeverity(DiagnosticSeverity::Info)) + + " recursive or mutually recursive function detected\n"); + if (hasFunctionLoc) + builder.lineColumn(functionLoc.line, functionLoc.column); + result.diagnostics.push_back(builder.build()); + } + + if (functionResult.hasInfiniteSelfRecursion) + { + DiagnosticBuilder builder; + builder.function(functionResult.name) + .filePath(functionResult.filePath) + .severity(DiagnosticSeverity::Error) + .errCode(DescriptiveErrorCode::None) + .message("\t" + std::string(prefixForSeverity(DiagnosticSeverity::Error)) + + " unconditional self recursion detected (no base case)\n" + "\t\t ↳ this will eventually overflow the stack at runtime\n"); + if (hasFunctionLoc) + builder.lineColumn(functionLoc.line, functionLoc.column); + result.diagnostics.push_back(builder.build()); + } + + if (!functionResult.exceedsLimit) + continue; + + DiagnosticBuilder builder; + builder.function(functionResult.name) + .filePath(functionResult.filePath) + .severity(DiagnosticSeverity::Error) + .errCode(DescriptiveErrorCode::StackFrameTooLarge); + if (hasFunctionLoc) + builder.lineColumn(functionLoc.line, functionLoc.column); + + std::string message; + bool suppressLocation = false; + const StackSize maxCallee = (functionResult.maxStack > functionResult.localStack) + ? (functionResult.maxStack - functionResult.localStack) + : 0; + + if (const auto itLocals = aux.localAllocas.find(function); + functionResult.localStack >= maxCallee && itLocals != aux.localAllocas.end()) + { + std::string localsDetails; + std::string singleName; + StackSize singleSize = 0; + for (const auto& entry : itLocals->second) + { + if (entry.first == "") + continue; + if (entry.second >= prepared.ctx.config.stackLimit && entry.second > singleSize) + { + singleName = entry.first; + singleSize = entry.second; + } + } + + std::string aliasLine; + if (!singleName.empty()) + { + aliasLine = "\t\t ↳ alias variable: " + singleName + "\n"; + } + else if (!itLocals->second.empty()) + { + localsDetails += "\t\t ↳ locals: " + std::to_string(itLocals->second.size()) + + " variables (total " + + std::to_string(functionResult.localStack) + " bytes)\n"; + + std::vector> named = itLocals->second; + named.erase(std::remove_if(named.begin(), named.end(), + [](const auto& value) + { return value.first == ""; }), + named.end()); + std::sort(named.begin(), named.end(), + [](const auto& lhs, const auto& rhs) + { + if (lhs.second != rhs.second) + return lhs.second > rhs.second; + return lhs.first < rhs.first; + }); + + if (!named.empty()) + { + constexpr std::size_t kMaxLocalsForLocation = 5; + if (named.size() > kMaxLocalsForLocation) + suppressLocation = true; + + std::string listLine = " locals list: "; + for (std::size_t i = 0; i < named.size(); ++i) + { + if (i > 0) + listLine += ", "; + listLine += named[i].first + "(" + std::to_string(named[i].second) + ")"; + } + localsDetails += listLine + "\n"; + } + } + + if (!localsDetails.empty()) + message += localsDetails; + message = aliasLine + message; + } + + std::string suffix; + if (const auto itPath = aux.callPaths.find(function); itPath != aux.callPaths.end()) + { + suffix += "\t\t ↳ path: " + itPath->second + "\n"; + } + + const std::string mainLine = + " potential stack overflow: exceeds limit of " + + std::to_string(prepared.ctx.config.stackLimit) + " bytes\n"; + + message = "\t" + std::string(prefixForSeverity(DiagnosticSeverity::Error)) + mainLine + + message + suffix; + + if (suppressLocation) + builder.lineColumn(0, 0); + + builder.message(std::move(message)); + result.diagnostics.push_back(builder.build()); + } + } + + void appendStackBufferDiagnostics( + AnalysisResult& result, + const std::vector& bufferIssues) + { + for (const auto& issue : bufferIssues) + { + const ResolvedLocation loc = resolveFromInstruction(issue.inst, true); + const bool isUnreachable = analysis::isStaticallyUnreachableStackAccess(issue); + + std::ostringstream body; + DiagnosticBuilder builder; + + if (issue.isLowerBoundViolation) + { + builder.errCode(DescriptiveErrorCode::NegativeStackIndex); + body << " [!!] potential negative index on variable '" << issue.varName + << "' (size " << issue.arraySize << ")\n"; + if (!issue.aliasPath.empty()) + body << "\t\t ↳ alias path: " << issue.aliasPath << "\n"; + body << "\t\t ↳ inferred lower bound for index expression: " << issue.lowerBound + << " (index may be < 0)\n"; + } + else + { + builder.errCode(DescriptiveErrorCode::StackBufferOverflow); + body << "\t[ !!Warn ] potential stack buffer overflow on variable '" + << issue.varName << "' (size " << issue.arraySize << ")\n"; + if (!issue.aliasPath.empty()) + body << "\t\t ↳ alias path: " << issue.aliasPath << "\n"; + if (issue.indexIsConstant) + { + body << "\t\t ↳ constant index " << issue.indexOrUpperBound + << " is out of bounds (0.." + << (issue.arraySize ? issue.arraySize - 1 : 0) << ")\n"; + } + else + { + body << "\t\t ↳ index variable may go up to " << issue.indexOrUpperBound + << " (array last valid index: " + << (issue.arraySize ? issue.arraySize - 1 : 0) << ")\n"; + } + } + + if (issue.isWrite) + body << "\t\t ↳ (this is a write access)\n"; + else + body << "\t\t ↳ (this is a read access)\n"; + + if (isUnreachable) + { + body << "\t\t ↳ [info] this access appears unreachable at runtime " + "(condition is always false for this branch)\n"; + } + + builder.function(issue.funcName) + .severity(DiagnosticSeverity::Warning) + .location(loc) + .message(body.str()) + .variableAliasing(issue.aliasPathVec); + + result.diagnostics.push_back(builder.build()); + } + } + + void appendDynamicAllocaDiagnostics( + AnalysisResult& result, + const std::vector& issues) + { + for (const auto& issue : issues) + { + const ResolvedLocation loc = resolveFromInstruction( + static_cast(issue.allocaInst)); + + std::ostringstream body; + body << "\t[ !!Warn ] dynamic stack allocation detected for variable '" + << issue.varName << "'\n"; + body << "\t\t ↳ allocated type: " << issue.typeName << "\n"; + body << "\t\t ↳ size of this allocation is not compile-time constant " + "(VLA / variable alloca) and may lead to unbounded stack usage\n"; + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .severity(DiagnosticSeverity::Warning) + .errCode(DescriptiveErrorCode::VLAUsage) + .location(loc) + .message(body.str()); + + result.diagnostics.push_back(builder.build()); + } + } + + void appendAllocaUsageDiagnostics(AnalysisResult& result, const AnalysisConfig& config, + StackSize allocaLargeThreshold, + const std::vector& issues) + { + for (const auto& issue : issues) + { + const ResolvedLocation loc = resolveFromInstruction( + static_cast(issue.allocaInst)); + + bool isOversized = false; + if (issue.sizeIsConst && issue.sizeBytes >= allocaLargeThreshold) + isOversized = true; + else if (issue.hasUpperBound && issue.upperBoundBytes >= allocaLargeThreshold) + isOversized = true; + else if (issue.sizeIsConst && config.stackLimit != 0 && + issue.sizeBytes >= config.stackLimit) + isOversized = true; + + std::ostringstream body; + DiagnosticBuilder builder; + builder.function(issue.funcName).location(loc); + + if (isOversized) + { + builder.severity(DiagnosticSeverity::Error) + .errCode(DescriptiveErrorCode::AllocaTooLarge); + body << "\t" << prefixForSeverity(DiagnosticSeverity::Error) + << " large alloca on the stack for variable '" << issue.varName << "'\n"; + } + else if (issue.userControlled) + { + builder.severity(DiagnosticSeverity::Warning) + .errCode(DescriptiveErrorCode::AllocaUserControlled); + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " user-controlled alloca size for variable '" << issue.varName << "'\n"; + } + else + { + builder.severity(DiagnosticSeverity::Warning) + .errCode(DescriptiveErrorCode::AllocaUsageWarning); + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " dynamic alloca on the stack for variable '" << issue.varName << "'\n"; + } + + body << "\t\t ↳ allocation performed via alloca/VLA; stack usage grows with runtime " + "value\n"; + + if (issue.sizeIsConst) + body << "\t\t ↳ requested stack size: " << issue.sizeBytes << " bytes\n"; + else if (issue.hasUpperBound) + body << "\t\t ↳ inferred upper bound for size: " << issue.upperBoundBytes + << " bytes\n"; + else + body << "\t\t ↳ size is unbounded at compile time\n"; + + if (issue.isInfiniteRecursive) + { + builder.severity(DiagnosticSeverity::Error); + body << "\t\t ↳ function is infinitely recursive; this alloca runs at every " + "frame and guarantees stack overflow\n"; + } + else if (issue.isRecursive) + { + if (isOversized || issue.userControlled) + builder.severity(DiagnosticSeverity::Error); + body << "\t\t ↳ function is recursive; this allocation repeats at each " + "recursion depth and can exhaust the stack\n"; + } + + if (isOversized) + { + body << "\t\t ↳ exceeds safety threshold of " << allocaLargeThreshold << " bytes"; + if (config.stackLimit != 0) + body << " (stack limit: " << config.stackLimit << " bytes)"; + body << "\n"; + } + else if (issue.userControlled) + { + body << "\t\t ↳ size depends on user-controlled input " + "(function argument or non-local value)\n"; + } + else + { + body << "\t\t ↳ size does not appear user-controlled but remains " + "runtime-dependent\n"; + } + + builder.message(body.str()); + result.diagnostics.push_back(builder.build()); + } + } + + void appendMemIntrinsicDiagnostics( + AnalysisResult& result, + const std::vector& issues) + { + for (const auto& issue : issues) + { + const ResolvedLocation loc = resolveFromInstruction(issue.inst); + + std::ostringstream body; + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " potential stack buffer overflow in " << issue.intrinsicName + << " on variable '" << issue.varName << "'\n"; + body << "\t\t ↳ destination stack buffer size: " << issue.destSizeBytes << " bytes\n"; + body << "\t\t ↳ requested " << issue.lengthBytes + << " bytes to be copied/initialized\n"; + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .severity(DiagnosticSeverity::Warning) + .location(loc) + .message(body.str()); + result.diagnostics.push_back(builder.build()); + } + } + + void appendSizeMinusKDiagnostics( + AnalysisResult& result, + const std::vector& issues) + { + for (const auto& issue : issues) + { + const ResolvedLocation loc = resolveFromInstruction(issue.inst); + + std::ostringstream body; + if (issue.hasPointerDest) + { + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " potential unsafe write with length (size - " << issue.k << ")"; + } + else + { + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " potential unsafe size-" << issue.k << " argument passed"; + } + if (!issue.sinkName.empty()) + body << " in " << issue.sinkName; + body << "\n"; + if (issue.hasPointerDest && !issue.ptrNonNull) + body << "\t\t ↳ destination pointer may be null\n"; + if (!issue.sizeAboveK) + body << "\t\t ↳ size operand may be <= " << issue.k << "\n"; + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .severity(DiagnosticSeverity::Warning) + .errCode(DescriptiveErrorCode::SizeMinusOneWrite) + .location(loc) + .message(body.str()); + result.diagnostics.push_back(builder.build()); + } + } + + void appendMultipleStoreDiagnostics( + AnalysisResult& result, + const std::vector& issues) + { + for (const auto& issue : issues) + { + unsigned line = 0; + unsigned column = 0; + const bool haveLoc = resolveAllocaSourceLocation(issue.allocaInst, line, column); + + std::ostringstream body; + body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) + << " multiple stores to stack buffer '" << issue.varName + << "' in this function (" << issue.storeCount << " store instruction(s)"; + if (issue.distinctIndexCount > 0) + body << ", " << issue.distinctIndexCount << " distinct index expression(s)"; + body << ")\n"; + + if (issue.distinctIndexCount == 1) + { + body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) + << " all stores use the same index expression " + "(possible redundant or unintended overwrite)\n"; + } + else if (issue.distinctIndexCount > 1) + { + body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) + << " stores use different index expressions; verify indices are " + "correct and non-overlapping\n"; + } + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .severity(DiagnosticSeverity::Info) + .errCode(DescriptiveErrorCode::MultipleStoresToStackBuffer) + .message(body.str()); + if (haveLoc) + builder.lineColumn(line, column); + result.diagnostics.push_back(builder.build()); + } + } + + void appendDuplicateIfConditionDiagnostics( + AnalysisResult& result, const std::vector& issues) + { + for (const auto& issue : issues) + { + const ResolvedLocation loc = resolveFromInstruction(issue.conditionInst, true); + + std::ostringstream body; + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " unreachable else-if branch: condition is equivalent to a previous " + "'if' condition\n"; + body << "\t\t ↳ else branch implies previous condition is false\n"; + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .severity(DiagnosticSeverity::Warning) + .errCode(DescriptiveErrorCode::DuplicateIfCondition) + .ruleId("DuplicateIfCondition") + .location(loc) + .message(body.str()); + result.diagnostics.push_back(builder.build()); + } + } + + void appendUninitializedLocalReadDiagnostics( + AnalysisResult& result, + const std::vector& issues) + { + for (const auto& issue : issues) + { + unsigned line = issue.line; + unsigned column = issue.column; + bool haveLoc = (line != 0); + + const ResolvedLocation locFromInst = resolveFromInstruction(issue.inst); + if (locFromInst.hasLocation) + { + line = locFromInst.line; + column = locFromInst.column; + haveLoc = true; + } + + std::ostringstream body; + if (issue.kind == analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInit) + { + body << "\t[ !!Warn ] potential read of uninitialized local variable '" + << issue.varName << "'\n"; + body << "\t\t ↳ this load may execute before any definite initialization on " + "all control-flow paths\n"; + } + else if (issue.kind == + analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInitViaCall) + { + body << "\t[ !!Warn ] potential read of uninitialized local variable '" + << issue.varName << "'\n"; + body << "\t\t ↳ this call may read the value before any definite initialization"; + if (!issue.calleeName.empty()) + body << " in '" << issue.calleeName << "'"; + body << "\n"; + } + else + { + body << "\t[ !!Warn ] local variable '" << issue.varName + << "' is never initialized\n"; + body << "\t\t ↳ declared without initializer and no definite write was found " + "in this function\n"; + } + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .severity(DiagnosticSeverity::Warning) + .errCode(DescriptiveErrorCode::UninitializedLocalRead) + .message(body.str()); + + if (haveLoc) + builder.lineColumn(line, column); + + builder.ruleId((issue.kind == analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInit || + issue.kind == + analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInitViaCall) + ? "UninitializedLocalRead" + : "UninitializedLocalVariable") + .confidence((issue.kind == analysis::UninitializedLocalIssueKind::NeverInitialized) + ? 0.75 + : 0.90) + .cwe("CWE-457"); + + result.diagnostics.push_back(builder.build()); + } + } + + void appendInvalidBaseReconstructionDiagnostics( + AnalysisResult& result, + const std::vector& issues) + { + for (const auto& issue : issues) + { + const ResolvedLocation loc = resolveFromInstruction(issue.inst, true); + + std::ostringstream body; + body << "\t[ !!Warn ] potential UB: invalid base reconstruction via " + "offsetof/container_of\n"; + body << "\t\t ↳ variable: '" << issue.varName << "'\n"; + body << "\t\t ↳ source member: " << issue.sourceMember << "\n"; + body << "\t\t ↳ offset applied: " << (issue.offsetUsed >= 0 ? "+" : "") + << issue.offsetUsed << " bytes\n"; + body << "\t\t ↳ target type: " << issue.targetType << "\n"; + + DiagnosticSeverity severity = DiagnosticSeverity::Warning; + if (issue.isOutOfBounds) + { + severity = DiagnosticSeverity::Error; + body << "\t[!!!Error] derived pointer points OUTSIDE the valid object range\n"; + body << "\t\t ↳ (this will cause undefined behavior if dereferenced)\n"; + } + else + { + body << "\t[ !!Warn ] unable to verify that derived pointer points to a " + "valid object\n"; + body << "\t\t ↳ (potential undefined behavior if offset is incorrect)\n"; + } + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .severity(severity) + .errCode(DescriptiveErrorCode::InvalidBaseReconstruction) + .location(loc) + .message(body.str()); + result.diagnostics.push_back(builder.build()); + } + } + + void appendStackPointerEscapeDiagnostics( + AnalysisResult& result, const std::vector& issues) + { + for (const auto& issue : issues) + { + const ResolvedLocation loc = resolveFromInstruction(issue.inst); + + std::ostringstream body; + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " stack pointer escape: address of variable '" << issue.varName + << "' escapes this function\n"; + + if (issue.escapeKind == "return") + { + body << "\t\t ↳ escape via return statement " + "(pointer to stack returned to caller)\n"; + } + else if (issue.escapeKind == "store_global") + { + if (!issue.targetName.empty()) + { + body << "\t\t ↳ stored into global variable '" << issue.targetName + << "' (pointer may be used after the function returns)\n"; + } + else + { + body << "\t\t ↳ stored into a global variable " + "(pointer may be used after the function returns)\n"; + } + } + else if (issue.escapeKind == "store_unknown") + { + body << "\t\t ↳ stored through a non-local pointer " + "(e.g. via an out-parameter; pointer may outlive this function)\n"; + if (!issue.targetName.empty()) + body << "\t\t ↳ destination pointer/value name: '" << issue.targetName << "'\n"; + } + else if (issue.escapeKind == "call_callback") + { + body << "\t\t ↳ address passed as argument to an indirect call " + "(callback may capture the pointer beyond this function)\n"; + } + else if (issue.escapeKind == "call_arg") + { + if (!issue.targetName.empty()) + { + body << "\t\t ↳ address passed as argument to function '" << issue.targetName + << "' (callee may capture the pointer beyond this function)\n"; + } + else + { + body << "\t\t ↳ address passed as argument to a function " + "(callee may capture the pointer beyond this function)\n"; + } + } + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .severity(DiagnosticSeverity::Warning) + .errCode(DescriptiveErrorCode::StackPointerEscape) + .location(loc) + .message(body.str()); + result.diagnostics.push_back(builder.build()); + } + } + + void appendConstParamDiagnostics(AnalysisResult& result, + const std::vector& issues) + { + for (const auto& issue : issues) + { + std::ostringstream body; + const std::string displayFuncName = + analysis::formatFunctionNameForMessage(issue.funcName); + + const char* subLabel = "Pointer"; + if (issue.pointerConstOnly) + subLabel = "PointerConstOnly"; + else if (issue.isReference) + subLabel = issue.isRvalueRef ? "ReferenceRvaluePreferValue" : "Reference"; + + if (issue.isRvalueRef) + { + body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) + << " ConstParameterNotModified." << subLabel << ": parameter '" + << issue.paramName << "' in function '" << displayFuncName + << "' is an rvalue reference and is never used to modify the referred " + "object\n"; + body << kDiagIndentArrow << "consider passing by value (" << issue.suggestedType + << ") or const reference (" << issue.suggestedTypeAlt << ")\n"; + body << kDiagIndentArrow << "current type: " << issue.currentType << "\n"; + } + else if (issue.pointerConstOnly) + { + body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) + << " ConstParameterNotModified." << subLabel << ": parameter '" + << issue.paramName << "' in function '" << displayFuncName + << "' is declared '" << issue.currentType + << "' but the pointed object is never modified\n"; + body << kDiagIndentArrow << "consider '" << issue.suggestedType + << "' for API const-correctness\n"; + } + else + { + body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) + << " ConstParameterNotModified." << subLabel << ": parameter '" + << issue.paramName << "' in function '" << displayFuncName + << "' is never used to modify the " + << (issue.isReference ? "referred" : "pointed") << " object\n"; + } + + if (!issue.isRvalueRef) + { + body << kDiagIndentArrow << "current type: " << issue.currentType << "\n"; + body << kDiagIndentArrow << "suggested type: " << issue.suggestedType << "\n"; + } + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .severity(DiagnosticSeverity::Info) + .errCode(DescriptiveErrorCode::ConstParameterNotModified) + .lineColumn(issue.line, issue.column) + .ruleId(std::string("ConstParameterNotModified.") + subLabel) + .message(body.str()); + result.diagnostics.push_back(builder.build()); + } + } + + void appendResourceLifetimeDiagnostics( + AnalysisResult& result, const std::vector& issues) + { + for (const auto& issue : issues) + { + const ResolvedLocation loc = resolveFromInstruction(issue.inst); + + DiagnosticBuilder builder; + builder.function(issue.funcName) + .errCode(DescriptiveErrorCode::ResourceLifetimeIssue) + .confidence(0.80) + .location(loc); + + std::ostringstream body; + switch (issue.kind) + { + case analysis::ResourceLifetimeIssueKind::MissingRelease: + builder.severity(DiagnosticSeverity::Warning) + .ruleId("ResourceLifetime.MissingRelease") + .cwe("CWE-772"); + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " potential resource leak: '" << issue.resourceKind + << "' acquired in handle '" << issue.handleName + << "' is not released in this function\n"; + body << kDiagIndentArrow + << "no matching release call was found for the tracked handle\n"; + break; + case analysis::ResourceLifetimeIssueKind::DoubleRelease: + builder.severity(DiagnosticSeverity::Error) + .ruleId("ResourceLifetime.DoubleRelease") + .cwe("CWE-415"); + body << "\t" << prefixForSeverity(DiagnosticSeverity::Error) + << " potential double release: '" << issue.resourceKind << "' handle '" + << issue.handleName + << "' is released without a matching acquire in this function\n"; + body << kDiagIndentArrow + << "this may indicate release-after-release or ownership mismatch\n"; + break; + case analysis::ResourceLifetimeIssueKind::MissingDestructorRelease: + builder.severity(DiagnosticSeverity::Warning) + .ruleId("ResourceLifetime.MissingDestructorRelease") + .cwe("CWE-772"); + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " resource acquired in constructor may leak: class '" << issue.className + << "' does not release '" << issue.resourceKind << "' field '" + << issue.handleName << "' in destructor\n"; + body << kDiagIndentArrow + << "tracked constructor acquisitions for this field have no matching " + "destructor release\n"; + break; + case analysis::ResourceLifetimeIssueKind::IncompleteInterproc: + builder.severity(DiagnosticSeverity::Warning) + .ruleId("ResourceLifetime.IncompleteInterproc"); + body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) + << " inter-procedural resource analysis incomplete: handle '" + << issue.handleName + << "' may be acquired by an unmodeled/external callee before release\n"; + body << kDiagIndentArrow + << "no matching resource model rule or cross-TU summary was found for at " + "least one related call\n"; + body << kDiagIndentArrow + << "include callee definitions in inputs or extend --resource-model to " + "improve precision\n"; + break; + } + + builder.message(body.str()); + result.diagnostics.push_back(builder.build()); + } + } + +} // namespace ctrace::stack::analyzer diff --git a/src/analyzer/LocationResolver.cpp b/src/analyzer/LocationResolver.cpp new file mode 100644 index 0000000..516d9ef --- /dev/null +++ b/src/analyzer/LocationResolver.cpp @@ -0,0 +1,142 @@ +#include "analyzer/LocationResolver.hpp" + +#include "analysis/AnalyzerUtils.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ctrace::stack::analyzer +{ + namespace + { + static bool fillFromDebugLoc(llvm::DebugLoc debugLoc, unsigned& line, unsigned& column) + { + if (!debugLoc) + return false; + + line = debugLoc.getLine(); + if (line == 0) + return false; + + column = debugLoc.getCol(); + if (column == 0) + column = 1; + return true; + } + + static bool fillFromVariableLine(const llvm::DILocalVariable* variable, unsigned& line, + unsigned& column) + { + if (!variable || variable->getLine() == 0) + return false; + line = variable->getLine(); + column = 1; + return true; + } + } // namespace + + ResolvedLocation resolveFromInstruction(const llvm::Instruction* inst, bool includeRange) + { + ResolvedLocation loc; + if (!inst) + return loc; + + const llvm::DebugLoc debugLoc = inst->getDebugLoc(); + if (!debugLoc) + return loc; + + const unsigned line = debugLoc.getLine(); + const unsigned column = debugLoc.getCol(); + if (line == 0) + return loc; + + loc.hasLocation = true; + loc.line = line; + loc.column = (column != 0) ? column : 1; + loc.startLine = loc.line; + loc.startColumn = loc.column; + loc.endLine = loc.line; + loc.endColumn = loc.column; + + if (!includeRange) + return loc; + + if (auto* rawLoc = debugLoc.get()) + { + if (auto* scope = llvm::dyn_cast(rawLoc)) + { + if (scope->getColumn() != 0) + loc.endColumn = scope->getColumn() + 1; + } + } + + return loc; + } + + bool resolveAllocaSourceLocation(const llvm::AllocaInst* allocaInst, unsigned& line, + unsigned& column) + { + line = 0; + column = 0; + if (!allocaInst) + return false; + + if (fillFromDebugLoc(allocaInst->getDebugLoc(), line, column)) + return true; + + auto* nonConstAlloca = const_cast(allocaInst); + + for (llvm::DbgDeclareInst* dbgDeclare : llvm::findDbgDeclares(nonConstAlloca)) + { + if (fillFromDebugLoc(llvm::getDebugValueLoc(dbgDeclare), line, column) || + fillFromVariableLine(dbgDeclare->getVariable(), line, column)) + { + return true; + } + } + + for (llvm::DbgVariableRecord* dbgRecord : llvm::findDVRDeclares(nonConstAlloca)) + { + if (fillFromDebugLoc(llvm::getDebugValueLoc(dbgRecord), line, column) || + fillFromVariableLine(dbgRecord->getVariable(), line, column)) + { + return true; + } + } + + llvm::SmallVector dbgUsers; + llvm::SmallVector dbgRecords; + llvm::findDbgUsers(dbgUsers, nonConstAlloca, &dbgRecords); + + for (llvm::DbgVariableIntrinsic* dbgUser : dbgUsers) + { + if (fillFromDebugLoc(llvm::getDebugValueLoc(dbgUser), line, column) || + fillFromVariableLine(dbgUser->getVariable(), line, column)) + { + return true; + } + } + + for (llvm::DbgVariableRecord* dbgRecord : dbgRecords) + { + if (fillFromDebugLoc(llvm::getDebugValueLoc(dbgRecord), line, column) || + fillFromVariableLine(dbgRecord->getVariable(), line, column)) + { + return true; + } + } + + if (const llvm::Function* function = allocaInst->getFunction()) + { + if (analysis::getFunctionSourceLocation(*function, line, column)) + return true; + } + + return false; + } + +} // namespace ctrace::stack::analyzer diff --git a/src/analyzer/ModulePreparationService.cpp b/src/analyzer/ModulePreparationService.cpp new file mode 100644 index 0000000..298be79 --- /dev/null +++ b/src/analyzer/ModulePreparationService.cpp @@ -0,0 +1,122 @@ +#include "analyzer/ModulePreparationService.hpp" + +#include "analysis/FunctionFilter.hpp" + +#include +#include +#include +#include + +namespace ctrace::stack::analyzer +{ + namespace + { + static ModuleAnalysisContext buildContext(llvm::Module& mod, const AnalysisConfig& config) + { + ModuleAnalysisContext ctx{mod, config, &mod.getDataLayout(), + analysis::buildFunctionFilter(mod, config)}; + + for (llvm::Function& F : mod) + { + if (F.isDeclaration()) + continue; + ctx.allDefinedFunctions.push_back(&F); + if (ctx.filter.shouldAnalyze(F)) + ctx.functions.push_back(&F); + } + + ctx.allDefinedSet.reserve(ctx.allDefinedFunctions.size()); + for (const llvm::Function* F : ctx.allDefinedFunctions) + ctx.allDefinedSet.insert(F); + + ctx.functionSet.reserve(ctx.functions.size()); + for (const llvm::Function* F : ctx.functions) + ctx.functionSet.insert(F); + + return ctx; + } + + static LocalStackMap computeLocalStacks(const ModuleAnalysisContext& ctx) + { + LocalStackMap localStack; + for (llvm::Function* F : ctx.allDefinedFunctions) + { + analysis::LocalStackInfo info = + analysis::computeLocalStack(*F, *ctx.dataLayout, ctx.config.mode); + localStack[F] = info; + } + return localStack; + } + + static analysis::CallGraph buildCallGraphFiltered(const ModuleAnalysisContext& ctx) + { + analysis::CallGraph graph; + for (llvm::Function* F : ctx.allDefinedFunctions) + { + auto& callees = graph[F]; + for (llvm::BasicBlock& BB : *F) + { + for (llvm::Instruction& I : BB) + { + const llvm::Function* callee = nullptr; + if (auto* CI = llvm::dyn_cast(&I)) + callee = CI->getCalledFunction(); + else if (auto* II = llvm::dyn_cast(&I)) + callee = II->getCalledFunction(); + + if (callee && !callee->isDeclaration() && ctx.isDefined(*callee)) + callees.push_back(callee); + } + } + } + return graph; + } + + static analysis::InternalAnalysisState + computeRecursionState(const ModuleAnalysisContext& ctx, const analysis::CallGraph& graph, + const LocalStackMap& localStack) + { + analysis::InternalAnalysisState state = + analysis::computeGlobalStackUsage(graph, localStack); + + std::vector nodes; + nodes.reserve(ctx.allDefinedFunctions.size()); + for (llvm::Function* F : ctx.allDefinedFunctions) + nodes.push_back(F); + + const auto recursiveComponents = analysis::computeRecursiveComponents(graph, nodes); + for (const auto& component : recursiveComponents) + { + if (!analysis::detectInfiniteRecursionComponent(component)) + continue; + for (const llvm::Function* Fn : component) + state.InfiniteRecursionFuncs.insert(Fn); + } + return state; + } + } // namespace + + bool ModuleAnalysisContext::shouldAnalyze(const llvm::Function& F) const + { + return functionSet.find(&F) != functionSet.end(); + } + + bool ModuleAnalysisContext::isDefined(const llvm::Function& F) const + { + return allDefinedSet.find(&F) != allDefinedSet.end(); + } + + PreparedModule ModulePreparationService::prepare(llvm::Module& mod, + const AnalysisConfig& config) const + { + ModuleAnalysisContext ctx = buildContext(mod, config); + LocalStackMap localStack = computeLocalStacks(ctx); + analysis::CallGraph callGraph = buildCallGraphFiltered(ctx); + analysis::InternalAnalysisState recursionState = + computeRecursionState(ctx, callGraph, localStack); + + return PreparedModule{std::move(ctx), std::move(localStack), std::move(callGraph), + std::move(recursionState)}; + } + +} // namespace ctrace::stack::analyzer From 56286320e91d3a047ad90ca4d83f59b0e820f9dd Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:53:50 +0900 Subject: [PATCH 05/19] feat(reachability): add static unreachable stack-access classification --- include/analysis/Reachability.hpp | 10 ++++ src/analysis/Reachability.cpp | 89 +++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 include/analysis/Reachability.hpp create mode 100644 src/analysis/Reachability.cpp diff --git a/include/analysis/Reachability.hpp b/include/analysis/Reachability.hpp new file mode 100644 index 0000000..6e273b5 --- /dev/null +++ b/include/analysis/Reachability.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "analysis/StackBufferAnalysis.hpp" + +namespace ctrace::stack::analysis +{ + + bool isStaticallyUnreachableStackAccess(const StackBufferOverflowIssue& issue); + +} // namespace ctrace::stack::analysis diff --git a/src/analysis/Reachability.cpp b/src/analysis/Reachability.cpp new file mode 100644 index 0000000..e9c94a2 --- /dev/null +++ b/src/analysis/Reachability.cpp @@ -0,0 +1,89 @@ +#include "analysis/Reachability.hpp" + +#include "analysis/IRValueUtils.hpp" + +#include +#include +#include +#include + +namespace ctrace::stack::analysis +{ + + bool isStaticallyUnreachableStackAccess(const StackBufferOverflowIssue& issue) + { + if (!issue.inst) + return false; + + auto* block = issue.inst->getParent(); + if (!block) + return false; + + using namespace llvm; + + for (auto* predecessor : predecessors(block)) + { + auto* branch = dyn_cast(predecessor->getTerminator()); + if (!branch || !branch->isConditional()) + continue; + + auto* compare = dyn_cast(branch->getCondition()); + if (!compare) + continue; + + const llvm::Function& function = *issue.inst->getFunction(); + auto* lhs = analysis::tryGetConstFromValue(compare->getOperand(0), function); + auto* rhs = analysis::tryGetConstFromValue(compare->getOperand(1), function); + if (!lhs || !rhs) + continue; + + bool condTrue = false; + const auto& lhsValue = lhs->getValue(); + const auto& rhsValue = rhs->getValue(); + + switch (compare->getPredicate()) + { + case ICmpInst::ICMP_EQ: + condTrue = (lhsValue == rhsValue); + break; + case ICmpInst::ICMP_NE: + condTrue = (lhsValue != rhsValue); + break; + case ICmpInst::ICMP_SLT: + condTrue = lhsValue.slt(rhsValue); + break; + case ICmpInst::ICMP_SLE: + condTrue = lhsValue.sle(rhsValue); + break; + case ICmpInst::ICMP_SGT: + condTrue = lhsValue.sgt(rhsValue); + break; + case ICmpInst::ICMP_SGE: + condTrue = lhsValue.sge(rhsValue); + break; + case ICmpInst::ICMP_ULT: + condTrue = lhsValue.ult(rhsValue); + break; + case ICmpInst::ICMP_ULE: + condTrue = lhsValue.ule(rhsValue); + break; + case ICmpInst::ICMP_UGT: + condTrue = lhsValue.ugt(rhsValue); + break; + case ICmpInst::ICMP_UGE: + condTrue = lhsValue.uge(rhsValue); + break; + default: + continue; + } + + if (block == branch->getSuccessor(0) && !condTrue) + return true; + if (block == branch->getSuccessor(1) && condTrue) + return true; + } + + return false; + } + +} // namespace ctrace::stack::analysis From 62eba3af0b00c840a3fbe2177cf41b7d1fcce962 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:54:05 +0900 Subject: [PATCH 06/19] refactor(core): delegate module analysis to analyzer pipeline --- src/StackUsageAnalyzer.cpp | 1611 +----------------------------------- 1 file changed, 17 insertions(+), 1594 deletions(-) diff --git a/src/StackUsageAnalyzer.cpp b/src/StackUsageAnalyzer.cpp index fd441a0..25292d2 100644 --- a/src/StackUsageAnalyzer.cpp +++ b/src/StackUsageAnalyzer.cpp @@ -1,1600 +1,20 @@ #include "StackUsageAnalyzer.hpp" +#include "analyzer/AnalysisPipeline.hpp" +#include "analysis/InputPipeline.hpp" + #include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include - -#include "analysis/AllocaUsage.hpp" -#include "analysis/AnalyzerUtils.hpp" -#include "analysis/ConstParamAnalysis.hpp" -#include "analysis/DuplicateIfCondition.hpp" -#include "analysis/DynamicAlloca.hpp" -#include "analysis/FunctionFilter.hpp" -#include "analysis/InputPipeline.hpp" -#include "analysis/IRValueUtils.hpp" -#include "analysis/InvalidBaseReconstruction.hpp" -#include "analysis/MemIntrinsicOverflow.hpp" -#include "analysis/ResourceLifetimeAnalysis.hpp" -#include "analysis/SizeMinusKWrites.hpp" -#include "analysis/StackBufferAnalysis.hpp" -#include "analysis/StackComputation.hpp" -#include "analysis/StackPointerEscape.hpp" -#include "analysis/UninitializedVarAnalysis.hpp" -#include "passes/ModulePasses.hpp" - -namespace -{ - constexpr std::string_view kInfoPrefix = "[ !Info! ]"; - constexpr std::string_view kWarnPrefix = "[ !!Warn ]"; - constexpr std::string_view kErrorPrefix = "[!!!Error]"; - constexpr std::string_view kDiagIndentArrow = "\t\t ↳ "; - - constexpr std::string_view prefixForSeverity(ctrace::stack::DiagnosticSeverity sev) noexcept - { - switch (sev) - { - case ctrace::stack::DiagnosticSeverity::Info: - return kInfoPrefix; - case ctrace::stack::DiagnosticSeverity::Warning: - return kWarnPrefix; - case ctrace::stack::DiagnosticSeverity::Error: - return kErrorPrefix; - } - return kWarnPrefix; - } -} // namespace namespace ctrace::stack { - namespace - { - struct SourceLocation - { - unsigned line = 0; - unsigned column = 0; - }; - - struct FunctionAuxData - { - llvm::DenseMap locations; - llvm::DenseMap callPaths; - llvm::DenseMap>> - localAllocas; - llvm::DenseMap indices; - }; - - struct ModuleAnalysisContext - { - llvm::Module& mod; - const AnalysisConfig& config; - const llvm::DataLayout* dataLayout = nullptr; - analysis::FunctionFilter filter; - std::vector functions; - std::unordered_set functionSet; - std::vector allDefinedFunctions; - std::unordered_set allDefinedSet; - - bool shouldAnalyze(const llvm::Function& F) const - { - return functionSet.find(&F) != functionSet.end(); - } - - bool isDefined(const llvm::Function& F) const - { - return allDefinedSet.find(&F) != allDefinedSet.end(); - } - }; - - using LocalStackMap = std::map; - - static ModuleAnalysisContext buildContext(llvm::Module& mod, const AnalysisConfig& config) - { - ModuleAnalysisContext ctx{mod, config, &mod.getDataLayout(), - analysis::buildFunctionFilter(mod, config)}; - - for (llvm::Function& F : mod) - { - if (F.isDeclaration()) - continue; - ctx.allDefinedFunctions.push_back(&F); - if (ctx.filter.shouldAnalyze(F)) - ctx.functions.push_back(&F); - } - ctx.allDefinedSet.reserve(ctx.allDefinedFunctions.size()); - for (const llvm::Function* F : ctx.allDefinedFunctions) - { - ctx.allDefinedSet.insert(F); - } - ctx.functionSet.reserve(ctx.functions.size()); - for (const llvm::Function* F : ctx.functions) - { - ctx.functionSet.insert(F); - } - - return ctx; - } - - static LocalStackMap computeLocalStacks(const ModuleAnalysisContext& ctx) - { - LocalStackMap localStack; - for (llvm::Function* F : ctx.allDefinedFunctions) - { - analysis::LocalStackInfo info = - analysis::computeLocalStack(*F, *ctx.dataLayout, ctx.config.mode); - localStack[F] = info; - } - return localStack; - } - - static bool fillFromDebugLoc(llvm::DebugLoc DL, unsigned& line, unsigned& column) - { - if (!DL) - return false; - line = DL.getLine(); - if (line == 0) - return false; - column = DL.getCol(); - if (column == 0) - column = 1; - return true; - } - - static bool fillFromVariableLine(const llvm::DILocalVariable* var, unsigned& line, - unsigned& column) - { - if (!var || var->getLine() == 0) - return false; - line = var->getLine(); - column = 1; - return true; - } - - static bool getAllocaSourceLocation(const llvm::AllocaInst* AI, unsigned& line, - unsigned& column) - { - line = 0; - column = 0; - if (!AI) - return false; - - if (fillFromDebugLoc(AI->getDebugLoc(), line, column)) - return true; - - auto* nonConstAI = const_cast(AI); - for (llvm::DbgDeclareInst* ddi : llvm::findDbgDeclares(nonConstAI)) - { - if (fillFromDebugLoc(llvm::getDebugValueLoc(ddi), line, column) || - fillFromVariableLine(ddi->getVariable(), line, column)) - { - return true; - } - } - - for (llvm::DbgVariableRecord* dvr : llvm::findDVRDeclares(nonConstAI)) - { - if (fillFromDebugLoc(llvm::getDebugValueLoc(dvr), line, column) || - fillFromVariableLine(dvr->getVariable(), line, column)) - { - return true; - } - } - - llvm::SmallVector dbgUsers; - llvm::SmallVector dbgRecords; - llvm::findDbgUsers(dbgUsers, nonConstAI, &dbgRecords); - - for (llvm::DbgVariableIntrinsic* dvi : dbgUsers) - { - if (fillFromDebugLoc(llvm::getDebugValueLoc(dvi), line, column) || - fillFromVariableLine(dvi->getVariable(), line, column)) - { - return true; - } - } - - for (llvm::DbgVariableRecord* dvr : dbgRecords) - { - if (fillFromDebugLoc(llvm::getDebugValueLoc(dvr), line, column) || - fillFromVariableLine(dvr->getVariable(), line, column)) - { - return true; - } - } - - if (const llvm::Function* F = AI->getFunction()) - { - if (analysis::getFunctionSourceLocation(*F, line, column)) - return true; - } - - return false; - } - - static analysis::CallGraph buildCallGraphFiltered(const ModuleAnalysisContext& ctx) - { - analysis::CallGraph CG; - for (llvm::Function* F : ctx.allDefinedFunctions) - { - auto& vec = CG[F]; - - for (llvm::BasicBlock& BB : *F) - { - for (llvm::Instruction& I : BB) - { - const llvm::Function* Callee = nullptr; - if (auto* CI = llvm::dyn_cast(&I)) - { - Callee = CI->getCalledFunction(); - } - else if (auto* II = llvm::dyn_cast(&I)) - { - Callee = II->getCalledFunction(); - } - - if (Callee && !Callee->isDeclaration() && ctx.isDefined(*Callee)) - { - vec.push_back(Callee); - } - } - } - } - - return CG; - } - - static analysis::InternalAnalysisState - computeRecursionState(const ModuleAnalysisContext& ctx, const analysis::CallGraph& CG, - const LocalStackMap& localStack) - { - analysis::InternalAnalysisState state = - analysis::computeGlobalStackUsage(CG, localStack); - - std::vector nodes; - nodes.reserve(ctx.allDefinedFunctions.size()); - for (llvm::Function* F : ctx.allDefinedFunctions) - { - nodes.push_back(F); - } - - const auto recursiveComponents = analysis::computeRecursiveComponents(CG, nodes); - for (const auto& component : recursiveComponents) - { - if (!analysis::detectInfiniteRecursionComponent(component)) - continue; - - for (const llvm::Function* Fn : component) - { - state.InfiniteRecursionFuncs.insert(Fn); - } - } - - return state; - } - - static AnalysisResult buildResults(const ModuleAnalysisContext& ctx, - const LocalStackMap& localStack, - const analysis::InternalAnalysisState& state, - const analysis::CallGraph& CG, FunctionAuxData& aux) - { - AnalysisResult result; - result.config = ctx.config; - - for (llvm::Function* F : ctx.functions) - { - const llvm::Function* Fn = F; - - analysis::LocalStackInfo localInfo; - analysis::StackEstimate totalInfo; - - auto itLocal = localStack.find(Fn); - if (itLocal != localStack.end()) - localInfo = itLocal->second; - - auto itTotal = state.TotalStack.find(Fn); - if (itTotal != state.TotalStack.end()) - totalInfo = itTotal->second; - - FunctionResult fr; - fr.name = F->getName().str(); - fr.filePath = analysis::getFunctionSourcePath(*F); - if (fr.filePath.empty() && !ctx.filter.moduleSourcePath.empty()) - fr.filePath = ctx.filter.moduleSourcePath; - fr.localStack = localInfo.bytes; - fr.localStackUnknown = localInfo.unknown; - fr.maxStack = totalInfo.bytes; - fr.maxStackUnknown = totalInfo.unknown; - fr.hasDynamicAlloca = localInfo.hasDynamicAlloca; - fr.isRecursive = state.RecursiveFuncs.count(Fn) != 0; - fr.hasInfiniteSelfRecursion = state.InfiniteRecursionFuncs.count(Fn) != 0; - fr.exceedsLimit = (!fr.maxStackUnknown && totalInfo.bytes > ctx.config.stackLimit); - - unsigned line = 0; - unsigned column = 0; - if (analysis::getFunctionSourceLocation(*F, line, column)) - { - aux.locations[Fn] = {line, column}; - } - if (!fr.isRecursive && totalInfo.bytes > localInfo.bytes) - { - std::string path = analysis::buildMaxStackCallPath(Fn, CG, state); - if (!path.empty()) - aux.callPaths[Fn] = path; - } - if (!localInfo.localAllocas.empty()) - { - aux.localAllocas[Fn] = localInfo.localAllocas; - } - - result.functions.push_back(std::move(fr)); - aux.indices[Fn] = result.functions.size() - 1; - } - - return result; - } - - static void emitSummaryDiagnostics(AnalysisResult& result, const ModuleAnalysisContext& ctx, - const FunctionAuxData& aux) - { - for (const llvm::Function* Fn : ctx.functions) - { - auto itIndex = aux.indices.find(Fn); - if (itIndex == aux.indices.end()) - continue; - const std::size_t index = itIndex->second; - if (index >= result.functions.size()) - continue; - const FunctionResult& fr = result.functions[index]; - SourceLocation functionLoc{}; - bool hasFunctionLoc = false; - auto itLoc = aux.locations.find(Fn); - if (itLoc != aux.locations.end()) - { - functionLoc = itLoc->second; - hasFunctionLoc = (functionLoc.line != 0); - } - - if (fr.isRecursive) - { - Diagnostic diag; - diag.funcName = fr.name; - diag.filePath = fr.filePath; - diag.severity = DiagnosticSeverity::Info; - diag.errCode = DescriptiveErrorCode::None; - if (hasFunctionLoc) - { - diag.line = functionLoc.line; - diag.column = functionLoc.column; - } - diag.message = "\t" + std::string(prefixForSeverity(diag.severity)) + - " recursive or mutually recursive function detected\n"; - result.diagnostics.push_back(std::move(diag)); - } - - if (fr.hasInfiniteSelfRecursion) - { - Diagnostic diag; - diag.funcName = fr.name; - diag.filePath = fr.filePath; - diag.severity = DiagnosticSeverity::Error; - diag.errCode = DescriptiveErrorCode::None; - if (hasFunctionLoc) - { - diag.line = functionLoc.line; - diag.column = functionLoc.column; - } - diag.message = "\t" + std::string(prefixForSeverity(diag.severity)) + - " unconditional self recursion detected (no base case)\n" - "\t\t ↳ this will eventually overflow the stack at runtime\n"; - result.diagnostics.push_back(std::move(diag)); - } - - if (fr.exceedsLimit) - { - Diagnostic diag; - diag.funcName = fr.name; - diag.filePath = fr.filePath; - diag.severity = DiagnosticSeverity::Error; - diag.errCode = DescriptiveErrorCode::StackFrameTooLarge; - if (hasFunctionLoc) - { - diag.line = functionLoc.line; - diag.column = functionLoc.column; - } - std::string message; - bool suppressLocation = false; - StackSize maxCallee = - (fr.maxStack > fr.localStack) ? (fr.maxStack - fr.localStack) : 0; - auto itLocals = aux.localAllocas.find(Fn); - std::string aliasLine; - if (fr.localStack >= maxCallee && itLocals != aux.localAllocas.end()) - { - std::string localsDetails; - std::string singleName; - StackSize singleSize = 0; - for (const auto& entry : itLocals->second) - { - if (entry.first == "") - continue; - if (entry.second >= ctx.config.stackLimit && entry.second > singleSize) - { - singleName = entry.first; - singleSize = entry.second; - } - } - if (!singleName.empty()) - { - aliasLine = "\t\t ↳ alias variable: " + singleName + "\n"; - } - else if (!itLocals->second.empty()) - { - localsDetails += - "\t\t ↳ locals: " + std::to_string(itLocals->second.size()) + - " variables (total " + std::to_string(fr.localStack) + " bytes)\n"; - - std::vector> named = itLocals->second; - named.erase(std::remove_if(named.begin(), named.end(), [](const auto& v) - { return v.first == ""; }), - named.end()); - std::sort(named.begin(), named.end(), - [](const auto& a, const auto& b) - { - if (a.second != b.second) - return a.second > b.second; - return a.first < b.first; - }); - if (!named.empty()) - { - constexpr std::size_t kMaxLocalsForLocation = 5; - if (named.size() > kMaxLocalsForLocation) - suppressLocation = true; - std::string listLine = " locals list: "; - for (std::size_t idx = 0; idx < named.size(); ++idx) - { - if (idx > 0) - listLine += ", "; - listLine += named[idx].first + "(" + - std::to_string(named[idx].second) + ")"; - } - localsDetails += listLine + "\n"; - } - } - if (!localsDetails.empty()) - message += localsDetails; - } - auto itPath = aux.callPaths.find(Fn); - std::string suffix; - if (itPath != aux.callPaths.end()) - { - suffix += "\t\t ↳ path: " + itPath->second + "\n"; - } - std::string mainLine = " potential stack overflow: exceeds limit of " + - std::to_string(ctx.config.stackLimit) + " bytes\n"; - message = "\t" + std::string(prefixForSeverity(diag.severity)) + mainLine + - aliasLine + suffix + message; - if (suppressLocation) - { - diag.line = 0; - diag.column = 0; - } - diag.message = std::move(message); - result.diagnostics.push_back(std::move(diag)); - } - } - } - - static void appendStackBufferDiagnostics( - AnalysisResult& result, - const std::vector& bufferIssues) - { - for (const auto& issue : bufferIssues) - { - unsigned line = 0; - unsigned column = 0; - unsigned startLine = 0; - unsigned startColumn = 0; - unsigned endLine = 0; - unsigned endColumn = 0; - bool haveLoc = false; - - if (issue.inst) - { - llvm::DebugLoc DL = issue.inst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - startLine = DL.getLine(); - - startColumn = DL.getCol(); - column = DL.getCol(); - - // By default, same as start - endLine = DL.getLine(); - endColumn = DL.getCol(); - haveLoc = true; - if (auto* loc = DL.get()) - { - if (auto* scope = llvm::dyn_cast(loc)) - { - if (scope->getColumn() != 0) - { - endColumn = scope->getColumn() + 1; - } - } - } - } - } - - bool isUnreachable = false; - { - using namespace llvm; - - if (issue.inst) - { - auto* BB = issue.inst->getParent(); - - // Walk block predecessors to see whether some - // have a conditional branch with a constant condition. - for (auto* Pred : predecessors(BB)) - { - auto* BI = dyn_cast(Pred->getTerminator()); - if (!BI || !BI->isConditional()) - continue; - - auto* CI = dyn_cast(BI->getCondition()); - if (!CI) - continue; - - const llvm::Function& Func = *issue.inst->getFunction(); - - auto* C0 = analysis::tryGetConstFromValue(CI->getOperand(0), Func); - auto* C1 = analysis::tryGetConstFromValue(CI->getOperand(1), Func); - if (!C0 || !C1) - continue; - - // Evaluate the ICmp result for these constants (homegrown implementation). - bool condTrue = false; - auto pred = CI->getPredicate(); - const auto& v0 = C0->getValue(); - const auto& v1 = C1->getValue(); - - switch (pred) - { - case ICmpInst::ICMP_EQ: - condTrue = (v0 == v1); - break; - case ICmpInst::ICMP_NE: - condTrue = (v0 != v1); - break; - case ICmpInst::ICMP_SLT: - condTrue = v0.slt(v1); - break; - case ICmpInst::ICMP_SLE: - condTrue = v0.sle(v1); - break; - case ICmpInst::ICMP_SGT: - condTrue = v0.sgt(v1); - break; - case ICmpInst::ICMP_SGE: - condTrue = v0.sge(v1); - break; - case ICmpInst::ICMP_ULT: - condTrue = v0.ult(v1); - break; - case ICmpInst::ICMP_ULE: - condTrue = v0.ule(v1); - break; - case ICmpInst::ICMP_UGT: - condTrue = v0.ugt(v1); - break; - case ICmpInst::ICMP_UGE: - condTrue = v0.uge(v1); - break; - default: - // Do not handle other exotic predicates here. - continue; - } - - // Branch of the form: - // br i1 %cond, label %then, label %else - // Successor 0 taken if condTrue == true - // Successor 1 taken if condTrue == false - if (BB == BI->getSuccessor(0) && condTrue == false) - { - // The "then" block is never reached. - isUnreachable = true; - } - else if (BB == BI->getSuccessor(1) && condTrue == true) - { - // The "else" block is never reached. - isUnreachable = true; - } - } - } - } - - std::ostringstream body; - Diagnostic diag; - - if (issue.isLowerBoundViolation) - { - diag.errCode = DescriptiveErrorCode::NegativeStackIndex; - body << " [!!] potential negative index on variable '" << issue.varName - << "' (size " << issue.arraySize << ")\n"; - if (!issue.aliasPath.empty()) - { - body << "\t\t ↳ alias path: " << issue.aliasPath << "\n"; - } - body << "\t\t ↳ inferred lower bound for index expression: " << issue.lowerBound - << " (index may be < 0)\n"; - } - else - { - diag.errCode = DescriptiveErrorCode::StackBufferOverflow; - body << "\t[ !!Warn ] potential stack buffer overflow on variable '" - << issue.varName << "' (size " << issue.arraySize << ")\n"; - if (!issue.aliasPath.empty()) - { - body << "\t\t ↳ alias path: " << issue.aliasPath << "\n"; - } - if (issue.indexIsConstant) - { - body << "\t\t ↳ constant index " << issue.indexOrUpperBound - << " is out of bounds (0.." - << (issue.arraySize ? issue.arraySize - 1 : 0) << ")\n"; - } - else - { - body << "\t\t ↳ index variable may go up to " << issue.indexOrUpperBound - << " (array last valid index: " - << (issue.arraySize ? issue.arraySize - 1 : 0) << ")\n"; - } - } - - if (issue.isWrite) - { - body << "\t\t ↳ (this is a write access)\n"; - } - else - { - body << "\t\t ↳ (this is a read access)\n"; - } - if (isUnreachable) - { - body << "\t\t ↳ [info] this access appears unreachable at runtime " - "(condition is always false for this branch)\n"; - } - - diag.funcName = issue.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.startLine = haveLoc ? startLine : 0; - diag.startColumn = haveLoc ? startColumn : 0; - diag.endLine = haveLoc ? endLine : 0; - diag.endColumn = haveLoc ? endColumn : 0; - diag.severity = DiagnosticSeverity::Warning; - diag.message = body.str(); - diag.variableAliasingVec = issue.aliasPathVec; - result.diagnostics.push_back(std::move(diag)); - } - } - - static void - appendDynamicAllocaDiagnostics(AnalysisResult& result, - const std::vector& issues) - { - for (const auto& d : issues) - { - unsigned line = 0; - unsigned column = 0; - bool haveLoc = false; - if (d.allocaInst) - { - llvm::DebugLoc DL = d.allocaInst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - column = DL.getCol(); - haveLoc = true; - } - } - - std::ostringstream body; - - body << "\t[ !!Warn ] dynamic stack allocation detected for variable '" << d.varName - << "'\n"; - body << "\t\t ↳ allocated type: " << d.typeName << "\n"; - body << "\t\t ↳ size of this allocation is not compile-time constant " - "(VLA / variable alloca) and may lead to unbounded stack usage\n"; - - Diagnostic diag; - diag.funcName = d.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.severity = DiagnosticSeverity::Warning; - diag.errCode = DescriptiveErrorCode::VLAUsage; - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - - static void - appendAllocaUsageDiagnostics(AnalysisResult& result, const AnalysisConfig& config, - StackSize allocaLargeThreshold, - const std::vector& issues) - { - for (const auto& a : issues) - { - unsigned line = 0; - unsigned column = 0; - bool haveLoc = false; - if (a.allocaInst) - { - llvm::DebugLoc DL = a.allocaInst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - column = DL.getCol(); - haveLoc = true; - } - } - - bool isOversized = false; - if (a.sizeIsConst && a.sizeBytes >= allocaLargeThreshold) - isOversized = true; - else if (a.hasUpperBound && a.upperBoundBytes >= allocaLargeThreshold) - isOversized = true; - else if (a.sizeIsConst && config.stackLimit != 0 && - a.sizeBytes >= config.stackLimit) - isOversized = true; - - std::ostringstream body; - Diagnostic diag; - diag.funcName = a.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - - if (isOversized) - { - diag.severity = DiagnosticSeverity::Error; - diag.errCode = DescriptiveErrorCode::AllocaTooLarge; - body << "\t" << prefixForSeverity(diag.severity) - << " large alloca on the stack for variable '" << a.varName << "'\n"; - } - else if (a.userControlled) - { - diag.severity = DiagnosticSeverity::Warning; - diag.errCode = DescriptiveErrorCode::AllocaUserControlled; - body << "\t" << prefixForSeverity(diag.severity) - << " user-controlled alloca size for variable '" << a.varName << "'\n"; - } - else - { - diag.severity = DiagnosticSeverity::Warning; - diag.errCode = DescriptiveErrorCode::AllocaUsageWarning; - body << "\t" << prefixForSeverity(diag.severity) - << " dynamic alloca on the stack for variable '" << a.varName << "'\n"; - } - - body - << "\t\t ↳ allocation performed via alloca/VLA; stack usage grows with runtime " - "value\n"; - - if (a.sizeIsConst) - { - body << "\t\t ↳ requested stack size: " << a.sizeBytes << " bytes\n"; - } - else if (a.hasUpperBound) - { - body << "\t\t ↳ inferred upper bound for size: " << a.upperBoundBytes - << " bytes\n"; - } - else - { - body << "\t\t ↳ size is unbounded at compile time\n"; - } - - if (a.isInfiniteRecursive) - { - // Any alloca inside infinite recursion will blow the stack. - diag.severity = DiagnosticSeverity::Error; - body << "\t\t ↳ function is infinitely recursive; this alloca runs at every " - "frame and guarantees stack overflow\n"; - } - else if (a.isRecursive) - { - // Controlled recursion still compounds stack usage across frames. - if (diag.severity != DiagnosticSeverity::Error && - (isOversized || a.userControlled)) - { - diag.severity = DiagnosticSeverity::Error; - } - body << "\t\t ↳ function is recursive; this allocation repeats at each " - "recursion " - "depth and can exhaust the stack\n"; - } - - if (isOversized) - { - body << "\t\t ↳ exceeds safety threshold of " << allocaLargeThreshold - << " bytes"; - if (config.stackLimit != 0) - { - body << " (stack limit: " << config.stackLimit << " bytes)"; - } - body << "\n"; - } - else if (a.userControlled) - { - body << "\t\t ↳ size depends on user-controlled input " - "(function argument or non-local value)\n"; - } - else - { - body << "\t\t ↳ size does not appear user-controlled but remains " - "runtime-dependent\n"; - } - - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - - static void - appendMemIntrinsicDiagnostics(AnalysisResult& result, - const std::vector& issues) - { - for (const auto& m : issues) - { - unsigned line = 0; - unsigned column = 0; - bool haveLoc = false; - if (m.inst) - { - llvm::DebugLoc DL = m.inst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - column = DL.getCol(); - haveLoc = true; - } - } - - std::ostringstream body; - - // body << "Function: " << m.funcName; - // if (haveLoc) - // { - // body << " (line " << line << ", column " << column << ")"; - // } - // body << "\n"; - - body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) - << " potential stack buffer overflow in " << m.intrinsicName - << " on variable '" << m.varName << "'\n"; - body << "\t\t ↳ destination stack buffer size: " << m.destSizeBytes << " bytes\n"; - body << "\t\t ↳ requested " << m.lengthBytes << " bytes to be copied/initialized\n"; - - Diagnostic diag; - diag.funcName = m.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.severity = DiagnosticSeverity::Warning; - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - - static void - appendSizeMinusKDiagnostics(AnalysisResult& result, - const std::vector& issues) - { - for (const auto& s : issues) - { - unsigned line = 0; - unsigned column = 0; - bool haveLoc = false; - if (s.inst) - { - llvm::DebugLoc DL = s.inst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - column = DL.getCol(); - haveLoc = true; - } - } - - std::ostringstream body; - if (s.hasPointerDest) - { - body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) - << " potential unsafe write with length (size - " << s.k << ")"; - } - else - { - body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) - << " potential unsafe size-" << s.k << " argument passed"; - } - if (!s.sinkName.empty()) - body << " in " << s.sinkName; - body << "\n"; - if (s.hasPointerDest && !s.ptrNonNull) - body << "\t\t ↳ destination pointer may be null\n"; - if (!s.sizeAboveK) - body << "\t\t ↳ size operand may be <= " << s.k << "\n"; - - Diagnostic diag; - diag.funcName = s.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.severity = DiagnosticSeverity::Warning; - diag.errCode = DescriptiveErrorCode::SizeMinusOneWrite; - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - - static void - appendMultipleStoreDiagnostics(AnalysisResult& result, - const std::vector& issues) - { - for (const auto& ms : issues) - { - unsigned line = 0; - unsigned column = 0; - bool haveLoc = getAllocaSourceLocation(ms.allocaInst, line, column); - - std::ostringstream body; - Diagnostic diag; - - body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) - << " multiple stores to stack buffer '" << ms.varName << "' in this function (" - << ms.storeCount << " store instruction(s)"; - diag.errCode = DescriptiveErrorCode::MultipleStoresToStackBuffer; - if (ms.distinctIndexCount > 0) - { - body << ", " << ms.distinctIndexCount << " distinct index expression(s)"; - } - body << ")\n"; - - if (ms.distinctIndexCount == 1) - { - body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) - << " all stores use the same index expression " - "(possible redundant or unintended overwrite)\n"; - } - else if (ms.distinctIndexCount > 1) - { - body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) - << " stores use different index expressions; verify indices are " - "correct and non-overlapping\n"; - } - - diag.funcName = ms.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.severity = DiagnosticSeverity::Info; - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - - static void appendDuplicateIfConditionDiagnostics( - AnalysisResult& result, const std::vector& issues) - { - for (const auto& issue : issues) - { - unsigned line = 0; - unsigned column = 0; - unsigned startLine = 0; - unsigned startColumn = 0; - unsigned endLine = 0; - unsigned endColumn = 0; - bool haveLoc = false; - - if (issue.conditionInst) - { - llvm::DebugLoc DL = issue.conditionInst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - startLine = DL.getLine(); - column = DL.getCol(); - startColumn = DL.getCol(); - endLine = DL.getLine(); - endColumn = DL.getCol(); - haveLoc = true; - - if (auto* loc = DL.get()) - { - if (auto* scope = llvm::dyn_cast(loc)) - { - if (scope->getColumn() != 0) - { - endColumn = scope->getColumn() + 1; - } - } - } - } - } - - std::ostringstream body; - body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) - << " unreachable else-if branch: condition is equivalent to a " - "previous " - "'if' condition\n"; - body << "\t\t ↳ else branch implies previous condition is false\n"; - - Diagnostic diag; - diag.funcName = issue.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.startLine = haveLoc ? startLine : 0; - diag.startColumn = haveLoc ? startColumn : 0; - diag.endLine = haveLoc ? endLine : 0; - diag.endColumn = haveLoc ? endColumn : 0; - diag.severity = DiagnosticSeverity::Warning; - diag.errCode = DescriptiveErrorCode::DuplicateIfCondition; - diag.ruleId = "DuplicateIfCondition"; - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - - static void appendUninitializedLocalReadDiagnostics( - AnalysisResult& result, - const std::vector& issues) - { - for (const auto& issue : issues) - { - unsigned line = issue.line; - unsigned column = issue.column; - bool haveLoc = (line != 0); - if (issue.inst) - { - llvm::DebugLoc DL = issue.inst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - column = DL.getCol(); - haveLoc = true; - } - } - - std::ostringstream body; - if (issue.kind == analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInit) - { - body << "\t[ !!Warn ] potential read of uninitialized local variable '" - << issue.varName << "'\n"; - body << "\t\t ↳ this load may execute before any definite initialization on " - "all control-flow paths\n"; - } - else if (issue.kind == - analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInitViaCall) - { - body << "\t[ !!Warn ] potential read of uninitialized local variable '" - << issue.varName << "'\n"; - body - << "\t\t ↳ this call may read the value before any definite initialization"; - if (!issue.calleeName.empty()) - { - body << " in '" << issue.calleeName << "'"; - } - body << "\n"; - } - else - { - body << "\t[ !!Warn ] local variable '" << issue.varName - << "' is never initialized\n"; - body << "\t\t ↳ declared without initializer and no definite write was found " - "in this function\n"; - } - - Diagnostic diag; - diag.funcName = issue.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.severity = DiagnosticSeverity::Warning; - diag.errCode = DescriptiveErrorCode::UninitializedLocalRead; - diag.ruleId = - (issue.kind == analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInit || - issue.kind == - analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInitViaCall) - ? "UninitializedLocalRead" - : "UninitializedLocalVariable"; - diag.confidence = - (issue.kind == analysis::UninitializedLocalIssueKind::NeverInitialized) ? 0.75 - : 0.90; - diag.cweId = "CWE-457"; - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - - static void appendInvalidBaseReconstructionDiagnostics( - AnalysisResult& result, - const std::vector& issues) - { - for (const auto& br : issues) - { - unsigned line = 0; - unsigned column = 0; - unsigned startLine = 0; - unsigned startColumn = 0; - unsigned endLine = 0; - unsigned endColumn = 0; - bool haveLoc = false; - - if (br.inst) - { - llvm::DebugLoc DL = br.inst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - startLine = DL.getLine(); - startColumn = DL.getCol(); - column = DL.getCol(); - endLine = DL.getLine(); - endColumn = DL.getCol(); - haveLoc = true; - - if (auto* loc = DL.get()) - { - if (auto* scope = llvm::dyn_cast(loc)) - { - if (scope->getColumn() != 0) - { - endColumn = scope->getColumn() + 1; - } - } - } - } - } - - std::ostringstream body; - - body << "\t[ !!Warn ] potential UB: invalid base reconstruction via " - "offsetof/container_of\n"; - body << "\t\t ↳ variable: '" << br.varName << "'\n"; - body << "\t\t ↳ source member: " << br.sourceMember << "\n"; - body << "\t\t ↳ offset applied: " << (br.offsetUsed >= 0 ? "+" : "") - << br.offsetUsed << " bytes\n"; - body << "\t\t ↳ target type: " << br.targetType << "\n"; - - if (br.isOutOfBounds) - { - body << "\t[!!!Error] derived pointer points OUTSIDE the valid object range\n"; - body << "\t\t ↳ (this will cause undefined behavior if dereferenced)\n"; - } - else - { - body << "\t[ !!Warn ] unable to verify that derived pointer points to a " - "valid " - "object\n"; - body << "\t\t ↳ (potential undefined behavior if offset is " - "incorrect)\n"; - } - - Diagnostic diag; - diag.funcName = br.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.startLine = haveLoc ? startLine : 0; - diag.startColumn = haveLoc ? startColumn : 0; - diag.endLine = haveLoc ? endLine : 0; - diag.endColumn = haveLoc ? endColumn : 0; - diag.severity = - br.isOutOfBounds ? DiagnosticSeverity::Error : DiagnosticSeverity::Warning; - diag.errCode = DescriptiveErrorCode::InvalidBaseReconstruction; - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - - static void appendStackPointerEscapeDiagnostics( - AnalysisResult& result, const std::vector& issues) - { - for (const auto& e : issues) - { - unsigned line = 0; - unsigned column = 0; - bool haveLoc = false; - if (e.inst) - { - llvm::DebugLoc DL = e.inst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - column = DL.getCol(); - haveLoc = true; - } - } - - std::ostringstream body; - - body << "\t" << prefixForSeverity(DiagnosticSeverity::Warning) - << " stack pointer escape: address of variable '" << e.varName - << "' escapes this function\n"; - - if (e.escapeKind == "return") - { - body << "\t\t ↳ escape via return statement " - "(pointer to stack returned to caller)\n"; - } - else if (e.escapeKind == "store_global") - { - if (!e.targetName.empty()) - { - body << "\t\t ↳ stored into global variable '" << e.targetName - << "' (pointer may be used after the function returns)\n"; - } - else - { - body << "\t\t ↳ stored into a global variable " - "(pointer may be used after the function returns)\n"; - } - } - else if (e.escapeKind == "store_unknown") - { - body << "\t\t ↳ stored through a non-local pointer " - "(e.g. via an out-parameter; pointer may outlive this function)\n"; - if (!e.targetName.empty()) - { - body << "\t\t ↳ destination pointer/value name: '" << e.targetName << "'\n"; - } - } - else if (e.escapeKind == "call_callback") - { - body << "\t\t ↳ address passed as argument to an indirect call " - "(callback may capture the pointer beyond this function)\n"; - } - else if (e.escapeKind == "call_arg") - { - if (!e.targetName.empty()) - { - body << "\t\t ↳ address passed as argument to function '" << e.targetName - << "' (callee may capture the pointer beyond this function)\n"; - } - else - { - body << "\t\t ↳ address passed as argument to a function " - "(callee may capture the pointer beyond this function)\n"; - } - } - - Diagnostic diag; - diag.funcName = e.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.severity = DiagnosticSeverity::Warning; - diag.errCode = DescriptiveErrorCode::StackPointerEscape; - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - - static void - appendConstParamDiagnostics(AnalysisResult& result, - const std::vector& issues) - { - for (const auto& cp : issues) - { - std::ostringstream body; - Diagnostic diag; - std::string displayFuncName = analysis::formatFunctionNameForMessage(cp.funcName); - - diag.severity = DiagnosticSeverity::Info; - diag.errCode = DescriptiveErrorCode::ConstParameterNotModified; - - const std::string_view prefix = prefixForSeverity(diag.severity); - - const char* subLabel = "Pointer"; - if (cp.pointerConstOnly) - { - subLabel = "PointerConstOnly"; - } - else if (cp.isReference) - { - subLabel = cp.isRvalueRef ? "ReferenceRvaluePreferValue" : "Reference"; - } - - if (cp.isRvalueRef) - { - body << "\t" << prefix << " ConstParameterNotModified." << subLabel - << ": parameter '" << cp.paramName << "' in function '" << displayFuncName - << "' is an rvalue reference and is never used to modify the referred " - "object\n"; - body << kDiagIndentArrow << "consider passing by value (" << cp.suggestedType - << ") or const reference (" << cp.suggestedTypeAlt << ")\n"; - body << kDiagIndentArrow << "current type: " << cp.currentType << "\n"; - } - else if (cp.pointerConstOnly) - { - body << "\t" << prefix << " ConstParameterNotModified." << subLabel - << ": parameter '" << cp.paramName << "' in function '" << displayFuncName - << "' is declared '" << cp.currentType - << "' but the pointed object is never modified\n"; - body << kDiagIndentArrow << "consider '" << cp.suggestedType - << "' for API const-correctness\n"; - } - else - { - body << "\t" << prefix << " ConstParameterNotModified." << subLabel - << ": parameter '" << cp.paramName << "' in function '" << displayFuncName - << "' is never used to modify the " - << (cp.isReference ? "referred" : "pointed") << " object\n"; - } - - if (!cp.isRvalueRef) - { - body << kDiagIndentArrow << "current type: " << cp.currentType << "\n"; - body << kDiagIndentArrow << "suggested type: " << cp.suggestedType << "\n"; - } - - diag.funcName = cp.funcName; - diag.line = cp.line; - diag.column = cp.column; - diag.startLine = cp.line; - diag.startColumn = cp.column; - diag.endLine = cp.line; - diag.endColumn = cp.column; - diag.message = body.str(); - diag.ruleId = std::string("ConstParameterNotModified.") + subLabel; - result.diagnostics.push_back(std::move(diag)); - } - } - - static void appendResourceLifetimeDiagnostics( - AnalysisResult& result, const std::vector& issues) - { - for (const auto& issue : issues) - { - unsigned line = 0; - unsigned column = 0; - bool haveLoc = false; - if (issue.inst) - { - llvm::DebugLoc DL = issue.inst->getDebugLoc(); - if (DL) - { - line = DL.getLine(); - column = DL.getCol(); - haveLoc = (line != 0); - } - } - - Diagnostic diag; - diag.funcName = issue.funcName; - diag.line = haveLoc ? line : 0; - diag.column = haveLoc ? column : 0; - diag.errCode = DescriptiveErrorCode::ResourceLifetimeIssue; - diag.confidence = 0.80; - - std::ostringstream body; - switch (issue.kind) - { - case analysis::ResourceLifetimeIssueKind::MissingRelease: - diag.severity = DiagnosticSeverity::Warning; - diag.ruleId = "ResourceLifetime.MissingRelease"; - diag.cweId = "CWE-772"; - body << "\t" << prefixForSeverity(diag.severity) - << " potential resource leak: '" << issue.resourceKind - << "' acquired in handle '" << issue.handleName - << "' is not released in this function\n"; - body << kDiagIndentArrow - << "no matching release call was found for the tracked " - "handle\n"; - break; - case analysis::ResourceLifetimeIssueKind::DoubleRelease: - diag.severity = DiagnosticSeverity::Error; - diag.ruleId = "ResourceLifetime.DoubleRelease"; - diag.cweId = "CWE-415"; - body << "\t" << prefixForSeverity(diag.severity) - << " potential double release: '" << issue.resourceKind << "' handle '" - << issue.handleName - << "' is released without a matching acquire in this function\n"; - body << kDiagIndentArrow - << "this may indicate release-after-release or ownership mismatch\n"; - break; - case analysis::ResourceLifetimeIssueKind::MissingDestructorRelease: - diag.severity = DiagnosticSeverity::Warning; - diag.ruleId = "ResourceLifetime.MissingDestructorRelease"; - diag.cweId = "CWE-772"; - body << "\t" << prefixForSeverity(diag.severity) - << " resource acquired in constructor may leak: class '" << issue.className - << "' does not release '" << issue.resourceKind << "' field '" - << issue.handleName << "' in destructor\n"; - body << kDiagIndentArrow - << "tracked constructor acquisitions for this field have " - "no matching destructor release\n"; - break; - case analysis::ResourceLifetimeIssueKind::IncompleteInterproc: - diag.severity = DiagnosticSeverity::Warning; - diag.ruleId = "ResourceLifetime.IncompleteInterproc"; - body << "\t" << prefixForSeverity(diag.severity) - << " inter-procedural resource analysis incomplete: handle '" - << issue.handleName - << "' may be acquired by an unmodeled/external callee before release\n"; - body << kDiagIndentArrow - << "no matching resource model rule or cross-TU summary was found for at " - "least one related call\n"; - body << kDiagIndentArrow - << "include callee definitions in inputs or extend --resource-model to " - "improve precision\n"; - break; - } - - diag.message = body.str(); - result.diagnostics.push_back(std::move(diag)); - } - } - } // namespace - - // ============================================================================ - // Types internes - // ============================================================================ - - // ============================================================================ - // API publique : analyzeModule / analyzeFile - // ============================================================================ - AnalysisResult analyzeModule(llvm::Module& mod, const AnalysisConfig& config) { - using Clock = std::chrono::steady_clock; - auto logDuration = [&](const char* label, Clock::time_point start) - { - if (!config.timing) - return; - auto end = Clock::now(); - auto ms = std::chrono::duration_cast(end - start).count(); - std::cerr << label << " done in " << ms << " ms\n"; - }; - - auto t0 = Clock::now(); - runFunctionAttrsPass(mod); - logDuration("Function attrs pass", t0); - - t0 = Clock::now(); - ModuleAnalysisContext ctx = buildContext(mod, config); - logDuration("Build context", t0); - const llvm::DataLayout& DL = *ctx.dataLayout; - auto shouldAnalyzeFunction = [&](const llvm::Function& F) -> bool - { return ctx.shouldAnalyze(F); }; - - // 1) Local stack per function - t0 = Clock::now(); - LocalStackMap localStack = computeLocalStacks(ctx); - logDuration("Compute local stacks", t0); - - // 2) Call graph - t0 = Clock::now(); - analysis::CallGraph CG = buildCallGraphFiltered(ctx); - logDuration("Build call graph", t0); - - // 3) Propagation + recursion detection - t0 = Clock::now(); - analysis::InternalAnalysisState state = computeRecursionState(ctx, CG, localStack); - logDuration("Compute recursion state", t0); - - // 4) Build public result - FunctionAuxData aux; - t0 = Clock::now(); - AnalysisResult result = buildResults(ctx, localStack, state, CG, aux); - logDuration("Build results", t0); - - // 4b) Emit summary diagnostics for recursion/overflow flags (for JSON parity) - t0 = Clock::now(); - emitSummaryDiagnostics(result, ctx, aux); - logDuration("Emit summary diagnostics", t0); - - t0 = Clock::now(); - StackSize allocaLargeThreshold = analysis::computeAllocaLargeThreshold(config); - logDuration("Compute alloca threshold", t0); - - // 6) Detect stack buffer overflows (intra-function analysis) - t0 = Clock::now(); - std::vector bufferIssues = - analysis::analyzeStackBufferOverflows(mod, shouldAnalyzeFunction, config); - appendStackBufferDiagnostics(result, bufferIssues); - logDuration("Stack buffer overflows", t0); - - // 8) Detect dynamic stack allocations (VLA / variable alloca) - t0 = Clock::now(); - std::vector dynAllocaIssues = - analysis::analyzeDynamicAllocas(mod, shouldAnalyzeFunction); - appendDynamicAllocaDiagnostics(result, dynAllocaIssues); - logDuration("Dynamic allocas", t0); - - // 10) Analyze alloca usage (tainted / excessive size) - t0 = Clock::now(); - std::vector allocaUsageIssues = analysis::analyzeAllocaUsage( - mod, DL, state.RecursiveFuncs, state.InfiniteRecursionFuncs, shouldAnalyzeFunction); - appendAllocaUsageDiagnostics(result, config, allocaLargeThreshold, allocaUsageIssues); - logDuration("Alloca usage", t0); - - // 11) Detect overflows via memcpy/memset on stack buffers - t0 = Clock::now(); - std::vector memIssues = - analysis::analyzeMemIntrinsicOverflows(mod, DL, shouldAnalyzeFunction); - appendMemIntrinsicDiagnostics(result, memIssues); - logDuration("Mem intrinsic overflows", t0); - - // 11b) Detect writes with "size-k" length - t0 = Clock::now(); - std::vector sizeMinusKIssues = - analysis::analyzeSizeMinusKWrites(mod, DL, shouldAnalyzeFunction); - appendSizeMinusKDiagnostics(result, sizeMinusKIssues); - logDuration("Size-minus-k writes", t0); - - // 12) Detect multiple stores into the same stack buffer - t0 = Clock::now(); - std::vector multiStoreIssues = - analysis::analyzeMultipleStores(mod, shouldAnalyzeFunction, config); - appendMultipleStoreDiagnostics(result, multiStoreIssues); - - // 12b) Détection de branches else-if inatteignables (condition dupliquée) - std::vector duplicateIfIssues = - analysis::analyzeDuplicateIfConditions(mod, shouldAnalyzeFunction); - appendDuplicateIfConditionDiagnostics(result, duplicateIfIssues); - logDuration("Multiple stores", t0); - - // 12c) Detect potential reads from uninitialized local stack variables - t0 = Clock::now(); - std::vector uninitializedReadIssues = - analysis::analyzeUninitializedLocalReads(mod, shouldAnalyzeFunction, - config.uninitializedSummaryIndex.get()); - appendUninitializedLocalReadDiagnostics(result, uninitializedReadIssues); - logDuration("Uninitialized local reads", t0); - - // 13) Detect invalid base pointer reconstructions (offsetof/container_of) - t0 = Clock::now(); - std::vector baseReconIssues = - analysis::analyzeInvalidBaseReconstructions(mod, DL, shouldAnalyzeFunction); - appendInvalidBaseReconstructionDiagnostics(result, baseReconIssues); - logDuration("Invalid base reconstructions", t0); - - // 14) Detect stack pointer escapes (potential use-after-return) - t0 = Clock::now(); - std::vector escapeIssues = - analysis::analyzeStackPointerEscapes(mod, shouldAnalyzeFunction, - config.escapeModelPath); - appendStackPointerEscapeDiagnostics(result, escapeIssues); - logDuration("Stack pointer escapes", t0); - - // 15) Const-correctness: parameters that can be made const - t0 = Clock::now(); - std::vector constParamIssues = - analysis::analyzeConstParams(mod, shouldAnalyzeFunction); - appendConstParamDiagnostics(result, constParamIssues); - logDuration("Const params", t0); - - // 16) Generic resource lifetime checks (model-driven acquire/release) - t0 = Clock::now(); - std::vector resourceLifetimeIssues = - analysis::analyzeResourceLifetime(mod, shouldAnalyzeFunction, config.resourceModelPath, - config.resourceSummaryIndex.get()); - appendResourceLifetimeDiagnostics(result, resourceLifetimeIssues); - logDuration("Resource lifetime", t0); - - return result; + analyzer::AnalysisPipeline pipeline(config); + return pipeline.run(mod); } AnalysisResult analyzeFile(const std::string& filename, const AnalysisConfig& config, @@ -1612,26 +32,29 @@ namespace ctrace::stack using Clock = std::chrono::steady_clock; if (config.timing) std::cerr << "Analyzing " << filename << "...\n"; - auto analyzeStart = Clock::now(); + + const auto analyzeStart = Clock::now(); AnalysisResult result = analyzeModule(*load.module, config); if (config.timing) { - auto analyzeEnd = Clock::now(); - auto ms = + const auto analyzeEnd = Clock::now(); + const auto ms = std::chrono::duration_cast(analyzeEnd - analyzeStart) .count(); std::cerr << "Analysis done in " << ms << " ms\n"; } - for (auto& f : result.functions) + + for (auto& function : result.functions) { - if (f.filePath.empty()) - f.filePath = filename; + if (function.filePath.empty()) + function.filePath = filename; } - for (auto& d : result.diagnostics) + for (auto& diagnostic : result.diagnostics) { - if (d.filePath.empty()) - d.filePath = filename; + if (diagnostic.filePath.empty()) + diagnostic.filePath = filename; } + return result; } From 1fc9cb4741b688286775650d0adf4fc89d747beb Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:54:09 +0900 Subject: [PATCH 07/19] refactor(logging): route analyzer traces through coretrace logger --- src/analysis/FunctionFilter.cpp | 27 +++++++++-------- src/analysis/InputPipeline.cpp | 35 ++++++++++++++--------- src/analysis/ResourceLifetimeAnalysis.cpp | 20 +++++++------ src/analysis/StackPointerEscape.cpp | 14 +++++---- src/passes/ModulePasses.cpp | 12 ++++---- 5 files changed, 61 insertions(+), 47 deletions(-) diff --git a/src/analysis/FunctionFilter.cpp b/src/analysis/FunctionFilter.cpp index 20d4a69..4ba6eac 100644 --- a/src/analysis/FunctionFilter.cpp +++ b/src/analysis/FunctionFilter.cpp @@ -8,7 +8,8 @@ #include #include #include -#include + +#include #include "analysis/AnalyzerUtils.hpp" @@ -194,6 +195,13 @@ namespace ctrace::stack::analysis name.starts_with("__asan_") || name.starts_with("__ubsan_") || name.starts_with("__tsan_") || name.starts_with("__msan_"); } + + static void logFilterDecision(const llvm::Function& F, const std::string& file, + bool keep) + { + coretrace::log(coretrace::Level::Info, "[filter] func={} file={} keep={}\n", + F.getName().str(), file, keep ? "yes" : "no"); + } } // namespace FunctionFilter buildFunctionFilter(const llvm::Module& mod, const AnalysisConfig& config) @@ -220,7 +228,7 @@ namespace ctrace::stack::analysis { if (cfg.dumpFilter) { - llvm::errs() << "[filter] func=" << F.getName() << " file= keep=no\n"; + logFilterDecision(F, "", false); } return false; } @@ -249,12 +257,8 @@ namespace ctrace::stack::analysis if (cfg.dumpFilter) { - llvm::errs() << "[filter] func=" << F.getName() << " file="; - if (usedPath.empty()) - llvm::errs() << ""; - else - llvm::errs() << usedPath; - llvm::errs() << " keep=" << (decision ? "yes" : "no") << "\n"; + logFilterDecision(F, usedPath.empty() ? std::string("") : usedPath, + decision); } return decision; } @@ -288,12 +292,7 @@ namespace ctrace::stack::analysis if (cfg.dumpFilter) { - llvm::errs() << "[filter] func=" << F.getName() << " file="; - if (usedPath.empty()) - llvm::errs() << ""; - else - llvm::errs() << usedPath; - llvm::errs() << " keep=" << (decision ? "yes" : "no") << "\n"; + logFilterDecision(F, usedPath.empty() ? std::string("") : usedPath, decision); } return decision; diff --git a/src/analysis/InputPipeline.cpp b/src/analysis/InputPipeline.cpp index e1b47b3..5dc52ae 100644 --- a/src/analysis/InputPipeline.cpp +++ b/src/analysis/InputPipeline.cpp @@ -19,6 +19,7 @@ #include #include +#include namespace ctrace::stack::analysis { @@ -129,6 +130,20 @@ namespace ctrace::stack::analysis args.swap(filtered); } + static void logText(coretrace::Level level, const std::string& text) + { + if (text.empty()) + return; + if (text.back() == '\n') + { + coretrace::log(level, "{}", text); + } + else + { + coretrace::log(level, "{}\n", text); + } + } + static bool resolveDumpIRPath(const AnalysisConfig& config, const std::string& inputPath, const std::filesystem::path& baseDir, std::filesystem::path& outPath, std::string& error) @@ -360,7 +375,7 @@ namespace ctrace::stack::analysis } if (config.timing) - llvm::errs() << "Compiling " << filename << "...\n"; + coretrace::log(coretrace::Level::Info, "Compiling {}...\n", filename); compilerlib::OutputMode mode = compilerlib::OutputMode::ToMemory; bool retriedWithWorkingDir = false; auto compileWithOptionalWorkingDir = @@ -409,11 +424,7 @@ namespace ctrace::stack::analysis } if (!res->diagnostics.empty() && !config.quiet) { - llvm::errs() << res->diagnostics; - if (res->diagnostics.back() != '\n') - { - llvm::errs() << '\n'; - } + logText(coretrace::Level::Warn, res->diagnostics); } if (res->llvmIR.empty()) @@ -428,10 +439,8 @@ namespace ctrace::stack::analysis auto ms = std::chrono::duration_cast(compileEnd - compileStart) .count(); - llvm::errs() << "Compilation done in " << ms << " ms"; - if (retriedWithWorkingDir) - llvm::errs() << " (retry with working directory)"; - llvm::errs() << "\n"; + coretrace::log(coretrace::Level::Info, "Compilation done in {} ms{}\n", ms, + retriedWithWorkingDir ? " (retry with working directory)" : ""); } auto buffer = llvm::MemoryBuffer::getMemBuffer(res->llvmIR, "in_memory_ll"); @@ -445,7 +454,7 @@ namespace ctrace::stack::analysis auto ms = std::chrono::duration_cast(parseEnd - parseStart) .count(); - llvm::errs() << "IR parse done in " << ms << " ms\n"; + coretrace::log(coretrace::Level::Info, "IR parse done in {} ms\n", ms); } if (!result.module) @@ -464,7 +473,7 @@ namespace ctrace::stack::analysis } if (config.timing) - llvm::errs() << "Parsing IR " << filename << "...\n"; + coretrace::log(coretrace::Level::Info, "Parsing IR {}...\n", filename); auto parseStart = Clock::now(); result.module = llvm::parseIRFile(filename, err, ctx); if (config.timing) @@ -472,7 +481,7 @@ namespace ctrace::stack::analysis auto parseEnd = Clock::now(); auto ms = std::chrono::duration_cast(parseEnd - parseStart) .count(); - llvm::errs() << "IR parse done in " << ms << " ms\n"; + coretrace::log(coretrace::Level::Info, "IR parse done in {} ms\n", ms); } if (result.module) { diff --git a/src/analysis/ResourceLifetimeAnalysis.cpp b/src/analysis/ResourceLifetimeAnalysis.cpp index 0787f94..4782b66 100644 --- a/src/analysis/ResourceLifetimeAnalysis.cpp +++ b/src/analysis/ResourceLifetimeAnalysis.cpp @@ -25,6 +25,8 @@ #include #include +#include + #include "analysis/IRValueUtils.hpp" #include "mangle.hpp" @@ -2191,9 +2193,9 @@ namespace ctrace::stack::analysis { if (interprocUncertaintyReported.insert(stateKey).second) { - llvm::errs() - << "[DEBUG-INTERPROC] PATH=fromSummary func=" << F.getName() - << " handle=" << storage.displayName << "\n"; + coretrace::log(coretrace::Level::Info, + "[DEBUG-INTERPROC] PATH=fromSummary func={} handle={}\n", + F.getName().str(), storage.displayName); ResourceLifetimeIssue issue; issue.funcName = F.getName().str(); issue.resourceKind = resourceKind; @@ -2213,9 +2215,9 @@ namespace ctrace::stack::analysis { if (interprocUncertaintyReported.insert(stateKey).second) { - llvm::errs() - << "[DEBUG-INTERPROC] PATH=externalStore func=" << F.getName() - << " handle=" << storage.displayName << "\n"; + coretrace::log(coretrace::Level::Info, + "[DEBUG-INTERPROC] PATH=externalStore func={} handle={}\n", + F.getName().str(), storage.displayName); ResourceLifetimeIssue issue; issue.funcName = F.getName().str(); issue.resourceKind = resourceKind; @@ -2251,9 +2253,9 @@ namespace ctrace::stack::analysis { if (interprocUncertaintyReported.insert(stateKey).second) { - llvm::errs() - << "[DEBUG-INTERPROC] PATH=escapeUnmodeled func=" << F.getName() - << " handle=" << storage.displayName << "\n"; + coretrace::log(coretrace::Level::Info, + "[DEBUG-INTERPROC] PATH=escapeUnmodeled func={} handle={}\n", + F.getName().str(), storage.displayName); ResourceLifetimeIssue issue; issue.funcName = F.getName().str(); issue.resourceKind = resourceKind; diff --git a/src/analysis/StackPointerEscape.cpp b/src/analysis/StackPointerEscape.cpp index ade4d92..35ad3dd 100644 --- a/src/analysis/StackPointerEscape.cpp +++ b/src/analysis/StackPointerEscape.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -17,6 +16,8 @@ #include #include +#include + namespace ctrace::stack::analysis { namespace @@ -650,10 +651,10 @@ namespace ctrace::stack::analysis if (changed) { - llvm::errs() << "[ !!Warn ] Stack escape inter-procedural analysis: reached " - "fixed-point iteration cap (" - << kEscapeSummaryMaxIterations - << "); summary may be non-converged and conservative\n"; + coretrace::log(coretrace::Level::Warn, + "Stack escape inter-procedural analysis: reached fixed-point " + "iteration cap ({}); summary may be non-converged and conservative\n", + kEscapeSummaryMaxIterations); } return summaries; @@ -953,7 +954,8 @@ namespace ctrace::stack::analysis std::string parseError; if (!parseStackEscapeModel(escapeModelPath, model, parseError)) { - llvm::errs() << "[ !!Warn ] stack escape model ignored: " << parseError << "\n"; + coretrace::log(coretrace::Level::Warn, "stack escape model ignored: {}\n", + parseError); } } diff --git a/src/passes/ModulePasses.cpp b/src/passes/ModulePasses.cpp index 606c7c2..82264bb 100644 --- a/src/passes/ModulePasses.cpp +++ b/src/passes/ModulePasses.cpp @@ -5,9 +5,10 @@ #include #include #include -#include #include +#include + namespace ctrace::stack { static llvm::DenseSet collectNoCaptureArgs(const llvm::Module& mod) @@ -60,11 +61,12 @@ namespace ctrace::stack { if (A.hasNoCaptureAttr() && !before.contains(&A)) { - // llvm::errs() << "[stack-analyzer] nocapture added: " << F.getName() - // << " arg#" << idx; + std::string suffix; if (A.hasName()) - llvm::errs() << " (" << A.getName() << ")"; - llvm::errs() << "\n"; + suffix = " (" + A.getName().str() + ")"; + coretrace::log(coretrace::Level::Info, + "[stack-analyzer] nocapture added: {} arg#{}{}\n", + F.getName().str(), idx, suffix); ++added; } ++idx; From f8cfbf2d0900ce8160f9eaa491a014b06272865e Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:54:15 +0900 Subject: [PATCH 08/19] build(core): register modular sources and add opt-in analyzer unit test target --- CMakeLists.txt | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3da53d9..0083692 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,12 @@ option(ENABLE_STACK_USAGE "Emit per-function stack usage (.su) files" OFF) # Communs Sources # =========================== set(STACK_ANALYZER_SOURCES + src/analyzer/AnalysisPipeline.cpp + src/analyzer/DiagnosticEmitter.cpp + src/analyzer/LocationResolver.cpp + src/analyzer/ModulePreparationService.cpp + src/app/AnalyzerApp.cpp + src/cli/ArgParser.cpp src/StackUsageAnalyzer.cpp src/analysis/AllocaUsage.cpp src/analysis/AnalyzerUtils.cpp @@ -71,6 +77,7 @@ set(STACK_ANALYZER_SOURCES src/analysis/InvalidBaseReconstruction.cpp src/analysis/MemIntrinsicOverflow.cpp src/analysis/ResourceLifetimeAnalysis.cpp + src/analysis/Reachability.cpp src/analysis/SizeMinusKWrites.cpp src/analysis/StackBufferAnalysis.cpp src/analysis/StackComputation.cpp @@ -187,6 +194,30 @@ if(BUILD_CLI) endif() endif() +# ========= +# TESTING +# ========= +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + include(CTest) + + option(BUILD_ANALYZER_UNIT_TESTS "Build fine-grained analyzer module unit tests" OFF) + if(BUILD_ANALYZER_UNIT_TESTS) + add_executable(stack_usage_analyzer_unit_tests + test/unit/analyzer_module_unit_tests.cpp + ) + + target_link_libraries(stack_usage_analyzer_unit_tests + PRIVATE + stack_usage_analyzer_lib + ) + + add_test( + NAME analyzer_module_unit_tests + COMMAND stack_usage_analyzer_unit_tests ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() +endif() + # ============ # FORMATTING # ============ From fa0f6da4c50229b824c49cb54a45792c19c034fc Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:54:20 +0900 Subject: [PATCH 09/19] test(harness): extend CLI coverage and integrate optional analyzer unit checks --- run_test.py | 411 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 340 insertions(+), 71 deletions(-) diff --git a/run_test.py b/run_test.py index a9a9b67..b7d13ff 100755 --- a/run_test.py +++ b/run_test.py @@ -16,6 +16,7 @@ from dataclasses import dataclass from concurrent.futures import ThreadPoolExecutor from pathlib import Path +from typing import Optional DEFAULT_ANALYZER = Path("./build/stack_usage_analyzer") DEFAULT_TEST_DIR = Path("test") @@ -36,6 +37,27 @@ class TestRunConfig: _MEM_CACHE = {} +def is_fixture_source(path: Path) -> bool: + """ + Return True if this source file should be analyzed as a regression fixture. + """ + try: + rel = path.resolve().relative_to(RUN_CONFIG.test_dir.resolve()) + except Exception: + rel = path + return not (len(rel.parts) > 0 and rel.parts[0] == "unit") + + +def collect_fixture_sources(): + """ + Collect C/C++ fixtures under test/, excluding helper/unit-test sources. + """ + c_files = sorted( + list(RUN_CONFIG.test_dir.glob("**/*.c")) + list(RUN_CONFIG.test_dir.glob("**/*.cpp")) + ) + return [path for path in c_files if is_fixture_source(path)] + + def parse_args(): parser = argparse.ArgumentParser( description="Run analyzer regression tests with optional parallelism and caching." @@ -732,10 +754,7 @@ def check_human_vs_json_parity() -> bool: Fails if information present in one view is missing in the other. """ print("=== Testing human vs JSON parity ===") - samples = [] - for ext in ("*.c", "*.cpp"): - samples.extend(RUN_CONFIG.test_dir.glob(f"**/{ext}")) - samples = sorted(samples) + samples = collect_fixture_sources() if not samples: print(" (no .c/.cpp files found, skipping)\n") return True @@ -970,15 +989,69 @@ def check_cli_parsing_and_filters() -> bool: ok = True sample = RUN_CONFIG.test_dir / "false-positif/unique_ptr_state.cpp" + sample_c = RUN_CONFIG.test_dir / "alloca/oversized-constant.c" + resource_model = Path("models/resource-lifetime/generic.txt") + escape_model = Path("models/stack-escape/generic.txt") + + def run_success_case(label: str, args: list[str], required: Optional[list[str]] = None, fmt: str = "text") -> bool: + result = run_analyzer(args) + output = (result.stdout or "") + (result.stderr or "") + if result.returncode != 0: + print(f" ❌ {label} failed (code {result.returncode})") + print(output) + return False + + required = required or [] + if fmt == "json": + try: + payload = json.loads(result.stdout or "") + except json.JSONDecodeError as exc: + print(f" ❌ {label} produced invalid JSON: {exc}") + print(result.stdout or "") + return False + if not isinstance(payload, dict): + print(f" ❌ {label} JSON root is not an object") + print(result.stdout or "") + return False + elif fmt == "sarif": + try: + payload = json.loads(result.stdout or "") + except json.JSONDecodeError as exc: + print(f" ❌ {label} produced invalid SARIF JSON: {exc}") + print(result.stdout or "") + return False + if payload.get("version") != "2.1.0": + print(f" ❌ {label} produced unexpected SARIF version") + print(result.stdout or "") + return False + + for needle in required: + if needle not in output: + print(f" ❌ {label} missing expected output: {needle}") + print(output) + return False + + print(f" ✅ {label} OK") + return True - # Missing-argument cases + # Missing-argument cases (all options requiring a value). missing_arg_cases = [ ("--only-file", "Missing argument for --only-file"), ("--only-dir", "Missing argument for --only-dir"), ("--exclude-dir", "Missing argument for --exclude-dir"), ("--only-func", "Missing argument for --only-func"), ("--only-function", "Missing argument for --only-function"), + ("--stack-limit", "Missing argument for --stack-limit"), + ("--dump-ir", "Missing argument for --dump-ir"), + ("--compile-arg", "Missing argument for --compile-arg"), + ("--analysis-profile", "Missing argument for --analysis-profile"), ("--jobs", "Missing argument for --jobs"), + ("--resource-model", "Missing argument for --resource-model"), + ("--escape-model", "Missing argument for --escape-model"), + ("--resource-summary-cache-dir", "Missing argument for --resource-summary-cache-dir"), + ("--compile-commands", "Missing argument for --compile-commands"), + ("--compdb", "Missing argument for --compdb"), + ("--base-dir", "Missing argument for --base-dir"), ("-I", "Missing argument for -I"), ("-D", "Missing argument for -D"), ] @@ -992,94 +1065,141 @@ def check_cli_parsing_and_filters() -> bool: else: print(f" ✅ {flag} missing-arg OK") - # Unknown option + # Unknown option and invalid values. result = subprocess.run([str(RUN_CONFIG.analyzer), "--unknown-option"], capture_output=True, text=True) output = (result.stdout or "") + (result.stderr or "") if "Unknown option: --unknown-option" not in output: print(" ❌ unknown option handling") print(output) ok = False + elif "Did you mean" in output: + print(" ❌ unknown option unexpectedly suggested a flag") + print(output) + ok = False else: print(" ✅ unknown option OK") - # jobs value parsing - for bad_value in ["0", "x", "-1"]: - result = subprocess.run( - [str(RUN_CONFIG.analyzer), f"--jobs={bad_value}", str(sample)], capture_output=True, text=True - ) - output = (result.stdout or "") + (result.stderr or "") - if result.returncode == 0 or "Invalid --jobs value:" not in output: - print(f" ❌ --jobs invalid value handling failed: {bad_value}") - print(output) - ok = False - else: - print(f" ✅ --jobs invalid value OK: {bad_value}") - - # only-function variants - only_function_cases = [ - ["--only-function=transition"], - ["--only-function=transition,does_not_exist"], - ["--only-function=transition, InitState::handle"], - ["--only-function", "transition"], - ["--only-func=transition"], - ["--only-func", "transition"], + unknown_suggestion_cases = [ + ("--only-fil", "Did you mean '--only-file'?"), + ("--format=sraif", "Did you mean '--format=sarif'?"), + ("--mdoe=abi", "Did you mean '--mode=abi'?"), ] - for opt in only_function_cases: - cmd = [str(RUN_CONFIG.analyzer), str(sample)] + opt - result = subprocess.run(cmd, capture_output=True, text=True) + for bad_opt, expected_hint in unknown_suggestion_cases: + result = subprocess.run([str(RUN_CONFIG.analyzer), bad_opt], capture_output=True, text=True) output = (result.stdout or "") + (result.stderr or "") - if result.returncode != 0 or "Function:" not in output: - print(f" ❌ only-function case failed: {opt}") + if result.returncode == 0 or expected_hint not in output: + print(f" ❌ suggestion handling failed: {bad_opt}") print(output) ok = False else: - print(f" ✅ only-function case OK: {opt}") - - # only-file / only-dir with space - only_file_dir_cases = [ - ["--only-file", str(sample)], - ["--only-dir", str(sample.parent)], + print(f" ✅ suggestion handling OK: {bad_opt}") + + invalid_value_cases = [ + (["--jobs=0", str(sample)], "Invalid --jobs value:"), + (["--jobs=x", str(sample)], "Invalid --jobs value:"), + (["--jobs=-1", str(sample)], "Invalid --jobs value:"), + (["--analysis-profile=unknown", str(sample)], "Invalid --analysis-profile value:"), + (["--stack-limit=oops", str(sample)], "Invalid --stack-limit value:"), + (["--mode=unknown", str(sample)], "Unknown mode: unknown (expected 'ir' or 'abi')"), ] - for opt in only_file_dir_cases: - cmd = [str(RUN_CONFIG.analyzer), str(sample)] + opt + ["--only-function=transition"] - result = subprocess.run(cmd, capture_output=True, text=True) + for args, needle in invalid_value_cases: + result = run_analyzer(args) output = (result.stdout or "") + (result.stderr or "") - if result.returncode != 0 or "Function:" not in output: - print(f" ❌ only-file/dir case failed: {' '.join(opt)}") + if result.returncode == 0 or needle not in output: + print(f" ❌ invalid-value handling failed: {' '.join(args)}") print(output) ok = False else: - print(f" ✅ only-file/dir case OK: {' '.join(opt)}") - - # -D variants - macro_cases = [ - ["-DHELLO"], - ["-D", "HELLO"], - ["-DVALUE=42"], - ["-D", "VALUE=42"], - ] - for opt in macro_cases: - cmd = [str(RUN_CONFIG.analyzer), str(sample)] + opt + ["--only-function=transition"] - result = subprocess.run(cmd, capture_output=True, text=True) - output = (result.stdout or "") + (result.stderr or "") - if result.returncode != 0 or "Function:" not in output: - print(f" ❌ macro case failed: {' '.join(opt)}") - print(output) + print(f" ✅ invalid-value handling OK: {' '.join(args)}") + + with tempfile.TemporaryDirectory(prefix="ct_cli_option_matrix_") as tmp: + tmpdir = Path(tmp) + dump_ir_space = tmpdir / "dump-space.ll" + dump_ir_eq = tmpdir / "dump-eq.ll" + resource_cache = tmpdir / "resource-cache" + compdb = tmpdir / "compile_commands.json" + + entries = [ + { + "directory": str(sample.resolve().parent), + "file": str(sample.resolve()), + "arguments": ["clang", "-c", str(sample.resolve())], + } + ] + compdb.write_text(json.dumps(entries), encoding="utf-8") + + success_cases = [ + ("--demangle", [str(sample), "--demangle", "--only-function=transition"], ["Function:"], "text"), + ("--quiet", [str(sample), "--quiet"], [], "text"), + ("--verbose", [str(sample), "--verbose", "--only-function=transition"], ["Function:"], "text"), + ("--STL", [str(sample), "--STL", "--only-function=transition"], ["Function:"], "text"), + ("--stl", [str(sample), "--stl", "--only-function=transition"], ["Function:"], "text"), + ("--only-file space", [str(sample), "--only-file", str(sample), "--only-function=transition"], ["Function:"], "text"), + ("--only-file equals", [str(sample), f"--only-file={sample}", "--only-function=transition"], ["Function:"], "text"), + ("--only-dir space", [str(sample), "--only-dir", str(sample.parent), "--only-function=transition"], ["Function:"], "text"), + ("--only-dir equals", [str(sample), f"--only-dir={sample.parent}", "--only-function=transition"], ["Function:"], "text"), + ("--exclude-dir space", [str(sample), "--exclude-dir", "never-match-dir", "--only-function=transition"], ["Function:"], "text"), + ("--exclude-dir equals", [str(sample), "--exclude-dir=never-match-dir", "--only-function=transition"], ["Function:"], "text"), + ("--only-function equals", [str(sample), "--only-function=transition"], ["Function:"], "text"), + ("--only-function space", [str(sample), "--only-function", "transition"], ["Function:"], "text"), + ("--only-func equals", [str(sample), "--only-func=transition"], ["Function:"], "text"), + ("--only-func space", [str(sample), "--only-func", "transition"], ["Function:"], "text"), + ("--stack-limit space", [str(sample_c), "--stack-limit", "8MiB"], ["Function:"], "text"), + ("--stack-limit equals", [str(sample_c), "--stack-limit=8MiB"], ["Function:"], "text"), + ("--dump-filter", [str(sample), "--dump-filter", "--only-function=transition"], ["Function:"], "text"), + ("--dump-ir space", [str(sample_c), "--dump-ir", str(dump_ir_space)], ["Function:"], "text"), + ("--dump-ir equals", [str(sample_c), f"--dump-ir={dump_ir_eq}"], ["Function:"], "text"), + ("-I", [str(sample), f"-I{sample.parent}", "--only-function=transition"], ["Function:"], "text"), + ("-I ", [str(sample), "-I", str(sample.parent), "--only-function=transition"], ["Function:"], "text"), + ("-D", [str(sample), "-DHELLO", "--only-function=transition"], ["Function:"], "text"), + ("-D ", [str(sample), "-D", "HELLO", "--only-function=transition"], ["Function:"], "text"), + ("--compile-arg", [str(sample), "--compile-arg=-I.", "--only-function=transition"], ["Function:"], "text"), + ("--compdb-fast", [str(sample), "--compdb-fast", "--only-function=transition"], ["Function:"], "text"), + ("--analysis-profile space", [str(sample), "--analysis-profile", "fast", "--only-function=transition"], ["Function:"], "text"), + ("--analysis-profile equals", [str(sample), "--analysis-profile=full", "--only-function=transition"], ["Function:"], "text"), + ("--jobs space", [str(sample), "--jobs", "2", "--only-function=transition"], ["Function:"], "text"), + ("--jobs equals", [str(sample), "--jobs=2", "--only-function=transition"], ["Function:"], "text"), + ("--timing", [str(sample), "--timing", "--only-function=transition"], ["Function:"], "text"), + ("--resource-model space", [str(sample), "--resource-model", str(resource_model), "--only-function=transition"], ["Function:"], "text"), + ("--resource-model equals", [str(sample), f"--resource-model={resource_model}", "--only-function=transition"], ["Function:"], "text"), + ("--escape-model space", [str(sample), "--escape-model", str(escape_model), "--only-function=transition"], ["Function:"], "text"), + ("--escape-model equals", [str(sample), f"--escape-model={escape_model}", "--only-function=transition"], ["Function:"], "text"), + ("--resource-cross-tu", [str(sample), "--resource-cross-tu", "--only-function=transition"], ["Function:"], "text"), + ("--no-resource-cross-tu", [str(sample), "--no-resource-cross-tu", "--only-function=transition"], ["Function:"], "text"), + ("--uninitialized-cross-tu", [str(sample), "--uninitialized-cross-tu", "--only-function=transition"], ["Function:"], "text"), + ("--no-uninitialized-cross-tu", [str(sample), "--no-uninitialized-cross-tu", "--only-function=transition"], ["Function:"], "text"), + ("--resource-summary-cache-dir space", [str(sample), "--resource-summary-cache-dir", str(resource_cache), "--only-function=transition"], ["Function:"], "text"), + ("--resource-summary-cache-dir equals", [str(sample), f"--resource-summary-cache-dir={resource_cache}", "--only-function=transition"], ["Function:"], "text"), + ("--resource-summary-cache-memory-only", [str(sample), "--resource-summary-cache-memory-only", "--only-function=transition"], ["Function:"], "text"), + ("--warnings-only", [str(sample), "--warnings-only", "--only-function=transition"], ["Function:"], "text"), + ("--format=json", [str(sample), "--format=json"], [], "json"), + ("--format=sarif", [str(sample), "--format=sarif"], [], "sarif"), + ("--format=human", [str(sample), "--format=human", "--only-function=transition"], ["Function:"], "text"), + ("--base-dir space", [str(sample), "--format=sarif", "--base-dir", str(sample.parent)], [], "sarif"), + ("--base-dir equals", [str(sample), "--format=sarif", f"--base-dir={sample.parent}"], [], "sarif"), + ("--mode=ir", [str(sample), "--mode=ir", "--only-function=transition"], ["Function:"], "text"), + ("--mode=abi", [str(sample), "--mode=abi", "--only-function=transition"], ["Function:"], "text"), + ("--compile-commands space", [str(sample), "--compile-commands", str(compdb), "--only-function=transition"], ["Function:"], "text"), + ("--compile-commands equals", [str(sample), f"--compile-commands={compdb}", "--only-function=transition"], ["Function:"], "text"), + ("--compdb space", [str(sample), "--compdb", str(compdb), "--only-function=transition"], ["Function:"], "text"), + ("--compdb equals", [str(sample), f"--compdb={compdb}", "--only-function=transition"], ["Function:"], "text"), + ("--include-compdb-deps", [f"--compile-commands={compdb}", "--include-compdb-deps", "--warnings-only"], [], "text"), + ] + + for label, args, required, fmt in success_cases: + if not run_success_case(label, args, required, fmt): + ok = False + + if not dump_ir_space.exists(): + print(f" ❌ --dump-ir space did not create output file: {dump_ir_space}") ok = False else: - print(f" ✅ macro case OK: {' '.join(opt)}") - - # STL toggle - for opt in [["--STL"], ["--stl"]]: - cmd = [str(RUN_CONFIG.analyzer), str(sample)] + opt + ["--only-function=transition"] - result = subprocess.run(cmd, capture_output=True, text=True) - output = (result.stdout or "") + (result.stderr or "") - if result.returncode != 0 or "Function:" not in output: - print(f" ❌ STL flag case failed: {' '.join(opt)}") - print(output) + print(" ✅ --dump-ir space created file") + if not dump_ir_eq.exists(): + print(f" ❌ --dump-ir equals did not create output file: {dump_ir_eq}") ok = False else: - print(f" ✅ STL flag case OK: {' '.join(opt)}") + print(" ✅ --dump-ir equals created file") print() return ok @@ -1711,6 +1831,151 @@ def check_multi_tu_folder_analysis() -> bool: return True +def check_diagnostic_rule_coverage_regression() -> bool: + """ + Ensure representative rules are still emitted after analyzer refactors. + """ + print("=== Testing diagnostic rule coverage regression ===") + ok = True + + cases = [ + ( + "StackBufferOverflow", + ["test/bound-storage/bound-storage.c", "--format=json"], + {"StackBufferOverflow"}, + ), + ( + "VLAUsage", + ["test/vla/vla-unknown-stack.c", "--format=json"], + {"VLAUsage"}, + ), + ( + "AllocaTooLarge", + ["test/alloca/oversized-constant.c", "--format=json"], + {"AllocaTooLarge"}, + ), + ( + "SizeMinusOneWrite", + ["test/size-arg/strncpy-size-minus-1.c", "--format=json"], + {"SizeMinusOneWrite"}, + ), + ( + "MultipleStoresToStackBuffer", + ["test/multiple-storage/same-storage.c", "--format=json"], + {"MultipleStoresToStackBuffer"}, + ), + ( + "DuplicateIfCondition", + ["test/diagnostics/duplicate-else-if-basic.c", "--format=json"], + {"DuplicateIfCondition"}, + ), + ( + "UninitializedLocalRead", + ["test/uninitialized-variable/uninitialized-local-basic.c", "--format=json"], + {"UninitializedLocalRead"}, + ), + ( + "InvalidBaseReconstruction", + ["test/offset_of-container_of/container_of_wrong_member_offset_error.c", "--format=json"], + {"InvalidBaseReconstruction"}, + ), + ( + "StackPointerEscape", + ["test/escape-stack/return-buf.c", "--format=json"], + {"StackPointerEscape"}, + ), + ( + "ConstParameterNotModified", + ["test/pointer_reference-const_correctness/readonly-pointer.c", "--format=json"], + {"ConstParameterNotModified.Pointer", "ConstParameterNotModified.PointerConstOnly"}, + ), + ( + "ResourceLifetime.MissingRelease", + [ + "test/resource-lifetime/malloc-missing-release.c", + "--format=json", + "--resource-model=models/resource-lifetime/generic.txt", + ], + {"ResourceLifetime.MissingRelease"}, + ), + ] + + for label, args, expected in cases: + result = run_analyzer(args) + output = (result.stdout or "") + (result.stderr or "") + if result.returncode != 0: + print(f" ❌ {label} run failed (code {result.returncode})") + print(output) + ok = False + continue + + try: + payload = json.loads(result.stdout or "") + except json.JSONDecodeError as exc: + print(f" ❌ {label} invalid JSON output: {exc}") + print(result.stdout or "") + ok = False + continue + + diagnostics = payload.get("diagnostics", []) + rule_ids = {diag.get("ruleId", "") for diag in diagnostics} + if not any(rule in rule_ids for rule in expected): + print(f" ❌ {label} missing expected rule") + print(f" expected one of: {sorted(expected)}") + print(f" got: {sorted(rule_ids)}") + ok = False + continue + + has_loc = False + for diag in diagnostics: + location = diag.get("location", {}) + if int(location.get("startLine", 0) or 0) > 0: + has_loc = True + break + if not has_loc: + print(f" ❌ {label} has no diagnostic with source location") + print(result.stdout or "") + ok = False + continue + + print(f" ✅ {label} rule coverage OK") + + print() + return ok + + +def check_analyzer_module_unit_tests() -> bool: + """ + Run fine-grained C++ unit tests for analyzer modules. + """ + print("=== Testing analyzer module unit tests ===") + unit_test_bin = RUN_CONFIG.analyzer.parent / "stack_usage_analyzer_unit_tests" + if not unit_test_bin.exists(): + print(" [info] unit test binary not found, skipping") + print(f" expected: {unit_test_bin}") + print(" enable with: cmake -S . -B build -DBUILD_ANALYZER_UNIT_TESTS=ON") + print(" then build: cmake --build build --target stack_usage_analyzer_unit_tests") + print() + return True + + repo_root = Path(__file__).resolve().parent + result = subprocess.run( + [str(unit_test_bin), str(repo_root.resolve())], capture_output=True, text=True + ) + output = (result.stdout or "") + (result.stderr or "") + if result.returncode != 0: + print(f" ❌ analyzer module unit tests failed (code {result.returncode})") + print(output) + print() + return False + + print(" ✅ analyzer module unit tests OK") + if output.strip(): + print(output.rstrip()) + print() + return True + + def check_file(c_path: Path): """ Check that, for this file, all expectations are present in the analyzer output. @@ -1789,6 +2054,8 @@ def record_ok(ok: bool): return ok global_ok = record_ok(check_help_flags()) + if not record_ok(check_analyzer_module_unit_tests()): + global_ok = False if not record_ok(check_multi_file_json()): global_ok = False if not record_ok(check_multi_file_total_summary()): @@ -1817,8 +2084,10 @@ def record_ok(ok: bool): global_ok = False if not record_ok(check_human_vs_json_parity()): global_ok = False + if not record_ok(check_diagnostic_rule_coverage_regression()): + global_ok = False - c_files = sorted(list(RUN_CONFIG.test_dir.glob("**/*.c")) + list(RUN_CONFIG.test_dir.glob("**/*.cpp"))) + c_files = collect_fixture_sources() if not c_files: print(f"No .c/.cpp files found under {RUN_CONFIG.test_dir}") return 0 if global_ok else 1 From 70010c77867e0a304ce0e8b35b5b9ddc2ed86465 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:54:25 +0900 Subject: [PATCH 10/19] test(unit): add analyzer module unit tests for location, reachability, and preparation --- test/unit/analyzer_module_unit_tests.cpp | 270 +++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 test/unit/analyzer_module_unit_tests.cpp diff --git a/test/unit/analyzer_module_unit_tests.cpp b/test/unit/analyzer_module_unit_tests.cpp new file mode 100644 index 0000000..df68c34 --- /dev/null +++ b/test/unit/analyzer_module_unit_tests.cpp @@ -0,0 +1,270 @@ +#include "StackUsageAnalyzer.hpp" +#include "analysis/InputPipeline.hpp" +#include "analysis/Reachability.hpp" +#include "analysis/StackBufferAnalysis.hpp" +#include "analyzer/LocationResolver.hpp" +#include "analyzer/ModulePreparationService.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + struct LoadedModule + { + llvm::LLVMContext context; + std::unique_ptr module; + }; + + struct TestReport + { + int failures = 0; + + void expect(bool condition, const std::string& message) + { + if (!condition) + { + ++failures; + std::cerr << "[FAIL] " << message << "\n"; + } + else + { + std::cout << "[PASS] " << message << "\n"; + } + } + }; + + bool loadModuleFromSource(const std::filesystem::path& sourceFile, + const ctrace::stack::AnalysisConfig& config, LoadedModule& out, + std::string& errorOut) + { + llvm::SMDiagnostic err; + ctrace::stack::analysis::ModuleLoadResult load = + ctrace::stack::analysis::loadModuleForAnalysis(sourceFile.string(), config, out.context, + err); + if (!load.module) + { + errorOut = load.error; + if (err.getLineNo() != 0 || !err.getFilename().empty()) + { + std::string diagText; + llvm::raw_string_ostream os(diagText); + err.print("stack_usage_analyzer_unit_tests", os); + os.flush(); + errorOut += diagText; + } + return false; + } + + out.module = std::move(load.module); + return true; + } + + bool testLocationResolver(const std::filesystem::path& repoRoot, TestReport& report) + { + const ctrace::stack::AnalysisConfig config; + LoadedModule loaded; + std::string loadError; + const std::filesystem::path source = + repoRoot / "test/alloca/oversized-constant.c"; + if (!loadModuleFromSource(source, config, loaded, loadError)) + { + report.expect(false, "LocationResolver setup: failed to load module: " + loadError); + return false; + } + + const llvm::Instruction* instructionWithDebug = nullptr; + const llvm::AllocaInst* firstAlloca = nullptr; + + for (llvm::Function& F : *loaded.module) + { + for (llvm::BasicBlock& BB : F) + { + for (llvm::Instruction& I : BB) + { + if (instructionWithDebug == nullptr && I.getDebugLoc()) + instructionWithDebug = &I; + if (firstAlloca == nullptr) + firstAlloca = llvm::dyn_cast(&I); + } + } + } + + const ctrace::stack::analyzer::ResolvedLocation nullLoc = + ctrace::stack::analyzer::resolveFromInstruction(nullptr, true); + report.expect(!nullLoc.hasLocation, + "LocationResolver: null instruction returns no location"); + + report.expect(instructionWithDebug != nullptr, + "LocationResolver: found an instruction with debug info"); + if (instructionWithDebug != nullptr) + { + const ctrace::stack::analyzer::ResolvedLocation loc = + ctrace::stack::analyzer::resolveFromInstruction(instructionWithDebug, true); + report.expect(loc.hasLocation, "LocationResolver: resolveFromInstruction has location"); + report.expect(loc.line > 0, "LocationResolver: resolved line > 0"); + report.expect(loc.column > 0, "LocationResolver: resolved column > 0"); + report.expect(loc.startLine == loc.line, + "LocationResolver: startLine matches line for single instruction"); + report.expect(loc.endLine == loc.line, + "LocationResolver: endLine matches line for single instruction"); + } + + report.expect(firstAlloca != nullptr, "LocationResolver: found alloca instruction"); + if (firstAlloca != nullptr) + { + unsigned line = 0; + unsigned column = 0; + const bool ok = + ctrace::stack::analyzer::resolveAllocaSourceLocation(firstAlloca, line, column); + report.expect(ok, "LocationResolver: resolveAllocaSourceLocation succeeded"); + report.expect(line > 0, "LocationResolver: alloca source line > 0"); + report.expect(column > 0, "LocationResolver: alloca source column > 0"); + } + + return true; + } + + bool testReachabilityService(const std::filesystem::path& repoRoot, TestReport& report) + { + const ctrace::stack::AnalysisConfig config; + + auto verifyFixture = [&](const std::filesystem::path& sourcePath, bool expectUnreachable, + const std::string& fixtureLabel) + { + LoadedModule loaded; + std::string loadError; + if (!loadModuleFromSource(sourcePath, config, loaded, loadError)) + { + report.expect(false, "Reachability setup: failed to load module: " + loadError); + return; + } + + std::function shouldAnalyze = + [](const llvm::Function&) + { return true; }; + const auto issues = ctrace::stack::analysis::analyzeStackBufferOverflows( + *loaded.module, shouldAnalyze, config); + report.expect(!issues.empty(), fixtureLabel + " produced at least one buffer issue"); + + bool foundExpectedClassification = false; + for (const auto& issue : issues) + { + const bool isUnreachable = + ctrace::stack::analysis::isStaticallyUnreachableStackAccess(issue); + if (isUnreachable == expectUnreachable) + { + foundExpectedClassification = true; + break; + } + } + + if (expectUnreachable) + { + report.expect(foundExpectedClassification, + fixtureLabel + + " detects statically unreachable stack access in fixture"); + } + else + { + report.expect( + foundExpectedClassification, + fixtureLabel + " keeps non-unreachable stack accesses as reachable"); + } + }; + + verifyFixture(repoRoot / "test/bound-storage/unreachable-validation.c", true, + "Reachability: unreachable fixture"); + verifyFixture(repoRoot / "test/bound-storage/bound-storage.c", false, + "Reachability: baseline fixture"); + + return true; + } + + bool testModulePreparationService(const std::filesystem::path& repoRoot, TestReport& report) + { + const ctrace::stack::AnalysisConfig config; + LoadedModule loaded; + std::string loadError; + const std::filesystem::path source = repoRoot / "test/no-error/basic-main.c"; + if (!loadModuleFromSource(source, config, loaded, loadError)) + { + report.expect(false, + "ModulePreparationService setup: failed to load module: " + loadError); + return false; + } + + ctrace::stack::analyzer::ModulePreparationService service; + ctrace::stack::analyzer::PreparedModule prepared = service.prepare(*loaded.module, config); + + report.expect(!prepared.ctx.allDefinedFunctions.empty(), + "ModulePreparationService: has defined functions"); + report.expect(!prepared.ctx.functions.empty(), + "ModulePreparationService: has analyzable functions"); + report.expect(prepared.localStack.size() == prepared.ctx.allDefinedFunctions.size(), + "ModulePreparationService: localStack covers all defined functions"); + + bool graphCoversAll = true; + for (llvm::Function* F : prepared.ctx.allDefinedFunctions) + { + if (prepared.callGraph.find(F) == prepared.callGraph.end()) + { + graphCoversAll = false; + break; + } + } + report.expect(graphCoversAll, "ModulePreparationService: call graph covers all functions"); + + const llvm::Function* mainFn = loaded.module->getFunction("main"); + report.expect(mainFn != nullptr, "ModulePreparationService: main function exists"); + if (mainFn != nullptr) + { + report.expect(prepared.ctx.isDefined(*mainFn), + "ModulePreparationService: main is in defined set"); + report.expect(prepared.ctx.shouldAnalyze(*mainFn), + "ModulePreparationService: main is analyzable"); + } + + report.expect(prepared.recursionState.InfiniteRecursionFuncs.empty(), + "ModulePreparationService: baseline fixture has no infinite recursion"); + + return true; + } +} // namespace + +int main(int argc, char** argv) +{ + if (argc != 2) + { + std::cerr << "Usage: stack_usage_analyzer_unit_tests \n"; + return 2; + } + + const std::filesystem::path repoRoot = std::filesystem::path(argv[1]); + TestReport report; + + (void)testLocationResolver(repoRoot, report); + (void)testReachabilityService(repoRoot, report); + (void)testModulePreparationService(repoRoot, report); + + if (report.failures == 0) + { + std::cout << "All analyzer module unit tests passed.\n"; + return 0; + } + + std::cerr << report.failures << " analyzer module unit test(s) failed.\n"; + return 1; +} From 702fb8e7c99e833864604a5127a5e6615646ea53 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:54:33 +0900 Subject: [PATCH 11/19] docs(architecture): document analyzer module responsibilities and patterns --- docs/architecture/analyzer-modules.md | 94 +++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/architecture/analyzer-modules.md diff --git a/docs/architecture/analyzer-modules.md b/docs/architecture/analyzer-modules.md new file mode 100644 index 0000000..baa3b28 --- /dev/null +++ b/docs/architecture/analyzer-modules.md @@ -0,0 +1,94 @@ +# Analyzer Modules Architecture + +This document describes the module split introduced around `StackUsageAnalyzer` to reduce coupling, improve testability, and keep `main.cpp` and the public API focused on orchestration. + +## Goals + +- Keep analysis orchestration separated from LLVM parsing and diagnostic formatting details. +- Make core services independently testable with focused unit tests. +- Preserve existing integration behavior while enabling smaller regression checks. + +## Modules + +### `src/analyzer/AnalysisPipeline.cpp` + +Role: +- Entry point for module-level analysis execution. +- Coordinates preparation, analysis passes, and diagnostic emission. + +Pattern: +- `Facade` over lower-level analysis services. + +Why: +- A single coordinator makes control flow explicit while avoiding a very large `StackUsageAnalyzer.cpp`. + +### `src/analyzer/ModulePreparationService.cpp` + +Role: +- Builds `ModuleAnalysisContext`. +- Computes local stack sizes, filtered call graph, and recursion metadata. + +Pattern: +- `Application Service` with a small `Builder-like` output (`PreparedModule`). + +Why: +- Preparation logic is pure module state derivation and should be reusable without triggering diagnostic side effects. + +### `src/analyzer/LocationResolver.cpp` + +Role: +- Converts LLVM debug locations into normalized source coordinates. +- Resolves source location for allocas using debug intrinsics fallbacks. + +Pattern: +- `Domain Service` (stateless policy logic). + +Why: +- Location derivation has multiple LLVM-specific fallbacks; isolating it keeps diagnostics code simpler and easier to test. + +### `src/analyzer/DiagnosticEmitter.cpp` + +Role: +- Converts analysis findings into final diagnostics outputs. +- Central place for rule IDs, severities, and source location mapping. + +Pattern: +- `Adapter` between analysis model objects and output/report models. + +Why: +- Separates "what was found" from "how it is reported". + +### `src/analysis/Reachability.cpp` + +Role: +- Contains static reachability heuristics for stack access findings. + +Pattern: +- `Policy` function isolated from pass execution. + +Why: +- Reachability criteria can evolve independently and be regression-tested as a focused unit. + +## Data Flow + +1. Input pipeline loads/normalizes LLVM module. +2. `ModulePreparationService` creates `PreparedModule`. +3. Analysis passes compute findings. +4. `Reachability` filters/annotates specific findings. +5. `LocationResolver` provides precise locations. +6. `DiagnosticEmitter` produces diagnostics consumed by CLI/lib callers. + +## Test Strategy + +Fine-grained unit tests live in: +- `test/unit/analyzer_module_unit_tests.cpp` + +Covered modules: +- `LocationResolver` +- `Reachability` +- `ModulePreparationService` + +Execution: +- Built as `stack_usage_analyzer_unit_tests` (standalone project builds only). +- Wired into `run_test.py` via `check_analyzer_module_unit_tests()`. +- Also registered in CTest as `analyzer_module_unit_tests`. From a399eee56384ccccfb7e67b608263e1b1e97b713 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:54:58 +0900 Subject: [PATCH 12/19] docs(readme): add library arg forwarding guidance and architecture reference --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 1326a3b..dcde490 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ python3 scripts/ci/run_code_analysis.py \ GitHub Actions consumer example is available at: - `docs/ci/github-actions-consumer.yml` - `docs/ci/github-actions-module-consumer.yml` (consume this repo directly via `uses:`) +- Analyzer architecture notes: `docs/architecture/analyzer-modules.md` ### Reusable GitHub Action module (for other repositories) @@ -274,6 +275,32 @@ Examples: When inputs are auto-discovered from `compile_commands.json` and multiple files are analyzed, the CLI auto-selects `fast` unless you explicitly pass `--analysis-profile=full`. +### Library mode: forward analyzer args from another CLI + +If you embed the analyzer as a library and still want to reuse analyzer-style +arguments (`--mode=...`, `--jobs=...`, etc.), use the CLI parser bridge: + +- `ctrace::stack::cli::parseArguments(const std::vector&)` +- `ctrace::stack::cli::parseCommandLine(const std::string&)` + +Example: + +```cpp +#include "cli/ArgParser.hpp" + +auto parsed = ctrace::stack::cli::parseCommandLine( + "--mode=abi --analysis-profile=fast --warnings-only --jobs=4" +); +if (parsed.status == ctrace::stack::cli::ParseStatus::Error) { + // handle parsed.error +} + +ctrace::stack::AnalysisConfig cfg = parsed.parsed.config; +``` + +This keeps one single source of truth for option semantics between CLI and +library consumers. + When `--compile-commands` is provided and no input file is passed on the CLI, the analyzer automatically uses `compile_commands.json` as the source of truth: - it analyzes supported entries (`.c`, `.cc`, `.cpp`, `.cxx`, `.ll`) From 4de3748bdd4014803f4b7e73df4b485a69b776da Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:55:33 +0900 Subject: [PATCH 13/19] feat(extern-project): support local source fallback and forwarded analyzer args --- extern-project/CMakeLists.txt | 22 ++++++++--- extern-project/src/main.cpp | 71 +++++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/extern-project/CMakeLists.txt b/extern-project/CMakeLists.txt index 38b15f4..f81fbf9 100644 --- a/extern-project/CMakeLists.txt +++ b/extern-project/CMakeLists.txt @@ -6,11 +6,23 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FetchContent) -FetchContent_Declare( - stack_analyzer - GIT_REPOSITORY https://github.com/CoreTrace/coretrace-stack-analyzer.git - GIT_TAG main -) +# Prefer the local parent checkout when this example is built from the +# repository tree; fallback to GitHub when used standalone. +set(STACK_ANALYZER_LOCAL_SOURCE "${CMAKE_CURRENT_LIST_DIR}/..") +if(EXISTS "${STACK_ANALYZER_LOCAL_SOURCE}/CMakeLists.txt") + message(STATUS "Using local stack analyzer source: ${STACK_ANALYZER_LOCAL_SOURCE}") + FetchContent_Declare( + stack_analyzer + SOURCE_DIR "${STACK_ANALYZER_LOCAL_SOURCE}" + ) +else() + message(STATUS "Using remote stack analyzer source from GitHub") + FetchContent_Declare( + stack_analyzer + GIT_REPOSITORY https://github.com/CoreTrace/coretrace-stack-analyzer.git + GIT_TAG main + ) +endif() FetchContent_MakeAvailable(stack_analyzer) diff --git a/extern-project/src/main.cpp b/extern-project/src/main.cpp index 499968b..d922a7e 100644 --- a/extern-project/src/main.cpp +++ b/extern-project/src/main.cpp @@ -1,24 +1,60 @@ #include "StackUsageAnalyzer.hpp" +#include "cli/ArgParser.hpp" #include #include #include #include "analysis/CompileCommands.hpp" +#include +#include -int main(int argc, char **argv) +int main(int argc, char** argv) { - if (argc < 2) + if (argc < 3) { - std::cerr << "usage: sa_consumer \n"; + std::cerr + << "usage: sa_consumer [analyzer options...]\n"; + std::cerr << "example: sa_consumer test.c build/compile_commands.json " + "--mode=abi --analysis-profile=fast --warnings-only --jobs=4 --format=json\n"; return 1; } std::string filename = argv[1]; std::string compile_file = argv[2]; std::string dbLoadError; + std::vector analyzer_args; + analyzer_args.reserve(argc > 3 ? static_cast(argc - 3) : 0u); + for (int i = 3; i < argc; ++i) + analyzer_args.emplace_back(argv[i]); - std::cout << compile_file << std::endl; - auto db = ctrace::stack::analysis::CompilationDatabase::loadFromFile(compile_file, - dbLoadError); + if (!analyzer_args.empty() && analyzer_args.front() == "--") + analyzer_args.erase(analyzer_args.begin()); + + auto parsed = ctrace::stack::cli::parseArguments(analyzer_args); + if (parsed.status == ctrace::stack::cli::ParseStatus::Help) + { + std::cerr + << "Analyzer help requested. Run stack_usage_analyzer --help for full option list.\n"; + return 0; + } + if (parsed.status == ctrace::stack::cli::ParseStatus::Error) + { + std::cerr << "Invalid analyzer args: " << parsed.error << "\n"; + return 1; + } + if (!parsed.parsed.inputFilenames.empty()) + { + std::cerr + << "Do not pass input files in analyzer options; use the first positional argument.\n"; + return 1; + } + if (parsed.parsed.compileCommandsExplicit) + { + std::cerr << "Do not pass --compile-commands/--compdb in analyzer options; " + "use the second positional argument.\n"; + return 1; + } + + auto db = ctrace::stack::analysis::CompilationDatabase::loadFromFile(compile_file, dbLoadError); if (!db) { std::cerr << "Failed to load compilation database: " << dbLoadError << std::endl; @@ -27,16 +63,27 @@ int main(int argc, char **argv) llvm::LLVMContext ctx; llvm::SMDiagnostic diag; - ctrace::stack::AnalysisConfig cfg; - cfg.mode = ctrace::stack::AnalysisMode::IR; - cfg.stackLimit = 8 * 1024 * 1024; + ctrace::stack::AnalysisConfig cfg = std::move(parsed.parsed.config); cfg.compilationDatabase = std::move(db); - // std::call_once(ctrace::stack::initializeLLVM, ctrace::stack::initializeLLVMFlag); auto res = ctrace::stack::analyzeFile(filename, cfg, ctx, diag); - // Example: SARIF output to stdout - std::cout << ctrace::stack::toSarif(res, filename) << std::endl; + switch (parsed.parsed.outputFormat) + { + case ctrace::stack::cli::OutputFormat::Json: + std::cout << ctrace::stack::toJson(res, filename) << "\n"; + break; + case ctrace::stack::cli::OutputFormat::Sarif: + std::cout << ctrace::stack::toSarif(res, filename, "coretrace-stack-analyzer", "0.1.0", + parsed.parsed.sarifBaseDir) + << "\n"; + break; + case ctrace::stack::cli::OutputFormat::Human: + std::cerr << "Human output is CLI-specific; falling back to JSON output in this library " + "example.\n"; + std::cout << ctrace::stack::toJson(res, filename) << "\n"; + break; + } return 0; } From 69f446948b2c17a750265e873efd2fcb1dbad651 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:55:42 +0900 Subject: [PATCH 14/19] docs(extern-project): add consumer build and run instructions --- extern-project/README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 extern-project/README.md diff --git a/extern-project/README.md b/extern-project/README.md new file mode 100644 index 0000000..403f99d --- /dev/null +++ b/extern-project/README.md @@ -0,0 +1,32 @@ +# extern-project (library consumer example) + +This folder demonstrates how to consume `coretrace::stack_usage_analyzer_lib` +from another project and forward analyzer options from your own CLI. + +## Build + +```bash +cmake -S extern-project -B extern-project/build +cmake --build extern-project/build -j +``` + +## Run + +```bash +./extern-project/build/sa_consumer \ + test/alloca/oversized-constant.c \ + build/compile_commands.json \ + --mode=abi \ + --analysis-profile=fast \ + --warnings-only \ + --jobs=4 \ + --format=sarif +``` + +Notes: +- Input file is the first positional argument. +- `compile_commands.json` path is the second positional argument. +- All remaining arguments are parsed with the analyzer's CLI parser bridge + (`ctrace::stack::cli::parseArguments(...)`). +- Do not pass `--compile-commands` / `--compdb` in forwarded args here; use + the second positional argument. From 1eaf2eb282883b4273e93479d7539aa8d7f88133 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:55:48 +0900 Subject: [PATCH 15/19] ci(integration): add consumer fixture analysis job --- .github/workflows/test-ci-integration.yml | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/.github/workflows/test-ci-integration.yml b/.github/workflows/test-ci-integration.yml index cc47aaf..bcf7e9e 100644 --- a/.github/workflows/test-ci-integration.yml +++ b/.github/workflows/test-ci-integration.yml @@ -7,6 +7,7 @@ on: - "Dockerfile" - "action.yml" - "scripts/ci/**" + - "fixtures/ci-consumer-project/**" - ".github/workflows/test-ci-integration.yml" pull_request: branches: [main] @@ -169,3 +170,80 @@ jobs: test -f docker-ci-results.sarif || { echo "CI SARIF missing!"; exit 1; } test -f docker-ci-results.json || { echo "CI JSON missing!"; exit 1; } echo "CI script Docker test passed!" + + # ================================================================= + # Job 3: Test analyzer on a minimal consumer project in CI + # ================================================================= + test-consumer-project: + name: Test Consumer Project + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure consumer fixture (compile_commands.json) + run: | + cmake -S fixtures/ci-consumer-project -B fixtures/ci-consumer-project/build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Run CoreTrace Stack Analyzer (consumer fixture) + uses: ./ + id: consumer-analysis + with: + compile-commands: fixtures/ci-consumer-project/build/compile_commands.json + fail-on: none + base-dir: ${{ github.workspace }} + analysis-profile: fast + resource-model: default + resource-cache-memory-only: "true" + warnings-only: "false" + sarif-file: artifacts/consumer/action-results.sarif + json-file: artifacts/consumer/action-results.json + upload-sarif: false + + - name: Validate consumer JSON and SARIF + run: | + python3 - <<'PY' + import json + from pathlib import Path + + json_path = Path("artifacts/consumer/action-results.json") + sarif_path = Path("artifacts/consumer/action-results.sarif") + assert json_path.is_file(), f"Missing JSON output: {json_path}" + assert sarif_path.is_file(), f"Missing SARIF output: {sarif_path}" + + payload = json.loads(json_path.read_text(encoding="utf-8")) + diagnostics = payload.get("diagnostics", []) + assert diagnostics, "Expected at least one diagnostic in JSON output" + print(f"JSON diagnostics: {len(diagnostics)}") + + sarif = json.loads(sarif_path.read_text(encoding="utf-8")) + assert sarif.get("version") == "2.1.0", "Invalid SARIF version" + runs = sarif.get("runs", []) + assert runs, "SARIF has no runs" + results = runs[0].get("results", []) + assert results, "Expected at least one SARIF result" + + for result in results: + for loc in result.get("locations", []): + physical = loc.get("physicalLocation", {}) + artifact = physical.get("artifactLocation", {}) + uri = artifact.get("uri", "") + assert not uri.startswith("/"), f"URI must be relative: {uri}" + region = physical.get("region", {}) + col = int(region.get("startColumn", 1)) + assert col >= 1, f"Invalid SARIF startColumn: {col}" + print(f"SARIF results: {len(results)}") + PY + + - name: Upload consumer fixture artifacts + uses: actions/upload-artifact@v4 + with: + name: ci-consumer-analysis + path: | + artifacts/consumer/action-results.json + artifacts/consumer/action-results.sarif From ffda4f2ee35826df73796292d41192126285b924 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:56:00 +0900 Subject: [PATCH 16/19] test(duplicate-if): add expectations for nested duplicate else-if warnings --- test/diagnostics/duplicate-nested-if_2.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/diagnostics/duplicate-nested-if_2.c b/test/diagnostics/duplicate-nested-if_2.c index 5bf8dde..4d97a3b 100644 --- a/test/diagnostics/duplicate-nested-if_2.c +++ b/test/diagnostics/duplicate-nested-if_2.c @@ -23,4 +23,12 @@ int main(int argc, char* argv[]) } return 2; -} \ No newline at end of file +} + +// at line 13, column 26 +// [ !!Warn ] unreachable else-if branch: condition is equivalent to a previous 'if' condition +// ↳ else branch implies previous condition is false + +// at line 20, column 18 +// [ !!Warn ] unreachable else-if branch: condition is equivalent to a previous 'if' condition +// ↳ else branch implies previous condition is false From 3b06ec63022340c578a26e25e14a75a2e6aae776 Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 25 Feb 2026 20:56:07 +0900 Subject: [PATCH 17/19] docs(test): clarify expected cross-tu uninitialized warning behavior --- .../cross-tu-uninitialized-wrapper-use.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/uninitialized-variable/cross-tu-uninitialized-wrapper-use.c b/test/uninitialized-variable/cross-tu-uninitialized-wrapper-use.c index 73a7c55..c007217 100644 --- a/test/uninitialized-variable/cross-tu-uninitialized-wrapper-use.c +++ b/test/uninitialized-variable/cross-tu-uninitialized-wrapper-use.c @@ -5,6 +5,11 @@ typedef struct IntOutProps extern void fill_wrapper_cross_tu(const IntOutProps* props); +// Expected behavior: +// - with cross-TU uninitialized summaries enabled and +// cross-tu-uninitialized-wrapper-def.c analyzed in the same run, +// this file should not emit an uninitialized warning. +// - if analyzed alone, a local-TU warning on 'value' is expected. int cross_tu_read_after_wrapper(void) { int value; From 733ba3cb021b79fddfac6cde1b0b63398149adf5 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 26 Feb 2026 14:43:33 +0900 Subject: [PATCH 18/19] test(fixtures): add ci consumer CMake fixture for compile_commands integration --- fixtures/ci-consumer-project/CMakeLists.txt | 9 +++++++++ fixtures/ci-consumer-project/src/main.c | 6 ++++++ 2 files changed, 15 insertions(+) create mode 100644 fixtures/ci-consumer-project/CMakeLists.txt create mode 100644 fixtures/ci-consumer-project/src/main.c diff --git a/fixtures/ci-consumer-project/CMakeLists.txt b/fixtures/ci-consumer-project/CMakeLists.txt new file mode 100644 index 0000000..b7e8fc9 --- /dev/null +++ b/fixtures/ci-consumer-project/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) +project(ci_consumer_project C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +add_executable(ci_consumer + src/main.c +) diff --git a/fixtures/ci-consumer-project/src/main.c b/fixtures/ci-consumer-project/src/main.c new file mode 100644 index 0000000..4096514 --- /dev/null +++ b/fixtures/ci-consumer-project/src/main.c @@ -0,0 +1,6 @@ +int main(void) +{ + char buf[4]; + buf[5] = 'x'; + return 0; +} From d4cf72cfcff340e465ffb776775a8b58187e7927 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 26 Feb 2026 14:46:22 +0900 Subject: [PATCH 19/19] chore(style): format code with clang-format --- include/analyzer/DiagnosticEmitter.hpp | 28 +++--- src/analysis/FunctionFilter.cpp | 3 +- src/analysis/ResourceLifetimeAnalysis.cpp | 21 +++-- src/analysis/StackPointerEscape.cpp | 9 +- src/analyzer/AnalysisPipeline.cpp | 97 ++++++++------------- src/analyzer/DiagnosticEmitter.cpp | 101 ++++++++++------------ src/app/AnalyzerApp.cpp | 24 ++--- src/cli/ArgParser.cpp | 8 +- test/unit/analyzer_module_unit_tests.cpp | 11 +-- 9 files changed, 136 insertions(+), 166 deletions(-) diff --git a/include/analyzer/DiagnosticEmitter.hpp b/include/analyzer/DiagnosticEmitter.hpp index 39b7798..3b3c4b3 100644 --- a/include/analyzer/DiagnosticEmitter.hpp +++ b/include/analyzer/DiagnosticEmitter.hpp @@ -49,32 +49,27 @@ namespace ctrace::stack::analyzer AnalysisResult& result, const std::vector& bufferIssues); - void appendDynamicAllocaDiagnostics( - AnalysisResult& result, - const std::vector& issues); + void appendDynamicAllocaDiagnostics(AnalysisResult& result, + const std::vector& issues); void appendAllocaUsageDiagnostics(AnalysisResult& result, const AnalysisConfig& config, StackSize allocaLargeThreshold, const std::vector& issues); - void appendMemIntrinsicDiagnostics( - AnalysisResult& result, - const std::vector& issues); + void appendMemIntrinsicDiagnostics(AnalysisResult& result, + const std::vector& issues); - void appendSizeMinusKDiagnostics( - AnalysisResult& result, - const std::vector& issues); + void appendSizeMinusKDiagnostics(AnalysisResult& result, + const std::vector& issues); - void appendMultipleStoreDiagnostics( - AnalysisResult& result, - const std::vector& issues); + void appendMultipleStoreDiagnostics(AnalysisResult& result, + const std::vector& issues); void appendDuplicateIfConditionDiagnostics( AnalysisResult& result, const std::vector& issues); void appendUninitializedLocalReadDiagnostics( - AnalysisResult& result, - const std::vector& issues); + AnalysisResult& result, const std::vector& issues); void appendInvalidBaseReconstructionDiagnostics( AnalysisResult& result, @@ -86,7 +81,8 @@ namespace ctrace::stack::analyzer void appendConstParamDiagnostics(AnalysisResult& result, const std::vector& issues); - void appendResourceLifetimeDiagnostics( - AnalysisResult& result, const std::vector& issues); + void + appendResourceLifetimeDiagnostics(AnalysisResult& result, + const std::vector& issues); } // namespace ctrace::stack::analyzer diff --git a/src/analysis/FunctionFilter.cpp b/src/analysis/FunctionFilter.cpp index 4ba6eac..966d04f 100644 --- a/src/analysis/FunctionFilter.cpp +++ b/src/analysis/FunctionFilter.cpp @@ -196,8 +196,7 @@ namespace ctrace::stack::analysis name.starts_with("__tsan_") || name.starts_with("__msan_"); } - static void logFilterDecision(const llvm::Function& F, const std::string& file, - bool keep) + static void logFilterDecision(const llvm::Function& F, const std::string& file, bool keep) { coretrace::log(coretrace::Level::Info, "[filter] func={} file={} keep={}\n", F.getName().str(), file, keep ? "yes" : "no"); diff --git a/src/analysis/ResourceLifetimeAnalysis.cpp b/src/analysis/ResourceLifetimeAnalysis.cpp index 4782b66..34b44d2 100644 --- a/src/analysis/ResourceLifetimeAnalysis.cpp +++ b/src/analysis/ResourceLifetimeAnalysis.cpp @@ -2193,9 +2193,10 @@ namespace ctrace::stack::analysis { if (interprocUncertaintyReported.insert(stateKey).second) { - coretrace::log(coretrace::Level::Info, - "[DEBUG-INTERPROC] PATH=fromSummary func={} handle={}\n", - F.getName().str(), storage.displayName); + coretrace::log( + coretrace::Level::Info, + "[DEBUG-INTERPROC] PATH=fromSummary func={} handle={}\n", + F.getName().str(), storage.displayName); ResourceLifetimeIssue issue; issue.funcName = F.getName().str(); issue.resourceKind = resourceKind; @@ -2215,9 +2216,10 @@ namespace ctrace::stack::analysis { if (interprocUncertaintyReported.insert(stateKey).second) { - coretrace::log(coretrace::Level::Info, - "[DEBUG-INTERPROC] PATH=externalStore func={} handle={}\n", - F.getName().str(), storage.displayName); + coretrace::log( + coretrace::Level::Info, + "[DEBUG-INTERPROC] PATH=externalStore func={} handle={}\n", + F.getName().str(), storage.displayName); ResourceLifetimeIssue issue; issue.funcName = F.getName().str(); issue.resourceKind = resourceKind; @@ -2253,9 +2255,10 @@ namespace ctrace::stack::analysis { if (interprocUncertaintyReported.insert(stateKey).second) { - coretrace::log(coretrace::Level::Info, - "[DEBUG-INTERPROC] PATH=escapeUnmodeled func={} handle={}\n", - F.getName().str(), storage.displayName); + coretrace::log( + coretrace::Level::Info, + "[DEBUG-INTERPROC] PATH=escapeUnmodeled func={} handle={}\n", + F.getName().str(), storage.displayName); ResourceLifetimeIssue issue; issue.funcName = F.getName().str(); issue.resourceKind = resourceKind; diff --git a/src/analysis/StackPointerEscape.cpp b/src/analysis/StackPointerEscape.cpp index 35ad3dd..6483565 100644 --- a/src/analysis/StackPointerEscape.cpp +++ b/src/analysis/StackPointerEscape.cpp @@ -651,10 +651,11 @@ namespace ctrace::stack::analysis if (changed) { - coretrace::log(coretrace::Level::Warn, - "Stack escape inter-procedural analysis: reached fixed-point " - "iteration cap ({}); summary may be non-converged and conservative\n", - kEscapeSummaryMaxIterations); + coretrace::log( + coretrace::Level::Warn, + "Stack escape inter-procedural analysis: reached fixed-point " + "iteration cap ({}); summary may be non-converged and conservative\n", + kEscapeSummaryMaxIterations); } return summaries; diff --git a/src/analyzer/AnalysisPipeline.cpp b/src/analyzer/AnalysisPipeline.cpp index a6dbc3a..8fcbce3 100644 --- a/src/analyzer/AnalysisPipeline.cpp +++ b/src/analyzer/AnalysisPipeline.cpp @@ -51,9 +51,7 @@ namespace ctrace::stack::analyzer }; } // namespace - AnalysisPipeline::AnalysisPipeline(const AnalysisConfig& config) : config_(config) - { - } + AnalysisPipeline::AnalysisPipeline(const AnalysisConfig& config) : config_(config) {} AnalysisResult AnalysisPipeline::run(llvm::Module& mod) const { @@ -66,44 +64,34 @@ namespace ctrace::stack::analyzer if (!config_.timing) return; const auto end = Clock::now(); - const auto ms = std::chrono::duration_cast(end - start).count(); + const auto ms = + std::chrono::duration_cast(end - start).count(); std::cerr << label << " done in " << ms << " ms\n"; }; std::vector steps; - steps.push_back({"Function attrs pass", - [](PipelineData& state) - { - runFunctionAttrsPass(state.mod); - }}); + steps.push_back( + {"Function attrs pass", [](PipelineData& state) { runFunctionAttrsPass(state.mod); }}); - steps.push_back({"Prepare module", - [](PipelineData& state) + steps.push_back({"Prepare module", [](PipelineData& state) { state.prepared = std::make_unique( state.preparation.prepare(state.mod, state.config)); }}); - steps.push_back({"Build results", - [](PipelineData& state) - { - state.result = buildResults(*state.prepared, state.aux); - }}); + steps.push_back({"Build results", [](PipelineData& state) + { state.result = buildResults(*state.prepared, state.aux); }}); - steps.push_back({"Emit summary diagnostics", - [](PipelineData& state) - { - emitSummaryDiagnostics(state.result, *state.prepared, state.aux); - }}); + steps.push_back({"Emit summary diagnostics", [](PipelineData& state) + { emitSummaryDiagnostics(state.result, *state.prepared, state.aux); }}); - steps.push_back({"Compute alloca threshold", - [](PipelineData& state) + steps.push_back({"Compute alloca threshold", [](PipelineData& state) { - state.allocaLargeThreshold = analysis::computeAllocaLargeThreshold(state.config); + state.allocaLargeThreshold = + analysis::computeAllocaLargeThreshold(state.config); }}); - steps.push_back({"Stack buffer overflows", - [](PipelineData& state) + steps.push_back({"Stack buffer overflows", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -113,8 +101,7 @@ namespace ctrace::stack::analyzer appendStackBufferDiagnostics(state.result, issues); }}); - steps.push_back({"Dynamic allocas", - [](PipelineData& state) + steps.push_back({"Dynamic allocas", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -123,23 +110,21 @@ namespace ctrace::stack::analyzer appendDynamicAllocaDiagnostics(state.result, issues); }}); - steps.push_back({"Alloca usage", - [](PipelineData& state) - { - auto shouldAnalyze = [&](const llvm::Function& F) -> bool - { return state.prepared->ctx.shouldAnalyze(F); }; - const llvm::DataLayout& dataLayout = *state.prepared->ctx.dataLayout; - const std::vector issues = - analysis::analyzeAllocaUsage( - state.mod, dataLayout, state.prepared->recursionState.RecursiveFuncs, - state.prepared->recursionState.InfiniteRecursionFuncs, - shouldAnalyze); - appendAllocaUsageDiagnostics(state.result, state.config, - state.allocaLargeThreshold, issues); - }}); + steps.push_back( + {"Alloca usage", [](PipelineData& state) + { + auto shouldAnalyze = [&](const llvm::Function& F) -> bool + { return state.prepared->ctx.shouldAnalyze(F); }; + const llvm::DataLayout& dataLayout = *state.prepared->ctx.dataLayout; + const std::vector issues = + analysis::analyzeAllocaUsage( + state.mod, dataLayout, state.prepared->recursionState.RecursiveFuncs, + state.prepared->recursionState.InfiniteRecursionFuncs, shouldAnalyze); + appendAllocaUsageDiagnostics(state.result, state.config, + state.allocaLargeThreshold, issues); + }}); - steps.push_back({"Mem intrinsic overflows", - [](PipelineData& state) + steps.push_back({"Mem intrinsic overflows", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -150,8 +135,7 @@ namespace ctrace::stack::analyzer appendMemIntrinsicDiagnostics(state.result, issues); }}); - steps.push_back({"Size-minus-k writes", - [](PipelineData& state) + steps.push_back({"Size-minus-k writes", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -162,8 +146,7 @@ namespace ctrace::stack::analyzer appendSizeMinusKDiagnostics(state.result, issues); }}); - steps.push_back({"Multiple stores", - [](PipelineData& state) + steps.push_back({"Multiple stores", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -173,8 +156,7 @@ namespace ctrace::stack::analyzer appendMultipleStoreDiagnostics(state.result, issues); }}); - steps.push_back({"Duplicate if conditions", - [](PipelineData& state) + steps.push_back({"Duplicate if conditions", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -183,8 +165,7 @@ namespace ctrace::stack::analyzer appendDuplicateIfConditionDiagnostics(state.result, issues); }}); - steps.push_back({"Uninitialized local reads", - [](PipelineData& state) + steps.push_back({"Uninitialized local reads", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -195,8 +176,7 @@ namespace ctrace::stack::analyzer appendUninitializedLocalReadDiagnostics(state.result, issues); }}); - steps.push_back({"Invalid base reconstructions", - [](PipelineData& state) + steps.push_back({"Invalid base reconstructions", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -207,8 +187,7 @@ namespace ctrace::stack::analyzer appendInvalidBaseReconstructionDiagnostics(state.result, issues); }}); - steps.push_back({"Stack pointer escapes", - [](PipelineData& state) + steps.push_back({"Stack pointer escapes", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -218,8 +197,7 @@ namespace ctrace::stack::analyzer appendStackPointerEscapeDiagnostics(state.result, issues); }}); - steps.push_back({"Const params", - [](PipelineData& state) + steps.push_back({"Const params", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; @@ -228,8 +206,7 @@ namespace ctrace::stack::analyzer appendConstParamDiagnostics(state.result, issues); }}); - steps.push_back({"Resource lifetime", - [](PipelineData& state) + steps.push_back({"Resource lifetime", [](PipelineData& state) { auto shouldAnalyze = [&](const llvm::Function& F) -> bool { return state.prepared->ctx.shouldAnalyze(F); }; diff --git a/src/analyzer/DiagnosticEmitter.cpp b/src/analyzer/DiagnosticEmitter.cpp index 1ed4556..83319e3 100644 --- a/src/analyzer/DiagnosticEmitter.cpp +++ b/src/analyzer/DiagnosticEmitter.cpp @@ -23,7 +23,8 @@ namespace ctrace::stack::analyzer constexpr std::string_view kErrorPrefix = "[!!!Error]"; constexpr std::string_view kDiagIndentArrow = "\t\t ↳ "; - constexpr std::string_view prefixForSeverity(ctrace::stack::DiagnosticSeverity severity) noexcept + constexpr std::string_view + prefixForSeverity(ctrace::stack::DiagnosticSeverity severity) noexcept { switch (severity) { @@ -171,8 +172,8 @@ namespace ctrace::stack::analyzer functionResult.isRecursive = prepared.recursionState.RecursiveFuncs.count(fn) != 0; functionResult.hasInfiniteSelfRecursion = prepared.recursionState.InfiniteRecursionFuncs.count(fn) != 0; - functionResult.exceedsLimit = - (!functionResult.maxStackUnknown && totalInfo.bytes > prepared.ctx.config.stackLimit); + functionResult.exceedsLimit = (!functionResult.maxStackUnknown && + totalInfo.bytes > prepared.ctx.config.stackLimit); unsigned line = 0; unsigned column = 0; @@ -181,8 +182,8 @@ namespace ctrace::stack::analyzer if (!functionResult.isRecursive && totalInfo.bytes > localInfo.bytes) { - std::string path = - analysis::buildMaxStackCallPath(fn, prepared.callGraph, prepared.recursionState); + std::string path = analysis::buildMaxStackCallPath(fn, prepared.callGraph, + prepared.recursionState); if (!path.empty()) aux.callPaths[fn] = path; } @@ -289,12 +290,11 @@ namespace ctrace::stack::analyzer else if (!itLocals->second.empty()) { localsDetails += "\t\t ↳ locals: " + std::to_string(itLocals->second.size()) + - " variables (total " + - std::to_string(functionResult.localStack) + " bytes)\n"; + " variables (total " + + std::to_string(functionResult.localStack) + " bytes)\n"; std::vector> named = itLocals->second; - named.erase(std::remove_if(named.begin(), named.end(), - [](const auto& value) + named.erase(std::remove_if(named.begin(), named.end(), [](const auto& value) { return value.first == ""; }), named.end()); std::sort(named.begin(), named.end(), @@ -316,7 +316,8 @@ namespace ctrace::stack::analyzer { if (i > 0) listLine += ", "; - listLine += named[i].first + "(" + std::to_string(named[i].second) + ")"; + listLine += + named[i].first + "(" + std::to_string(named[i].second) + ")"; } localsDetails += listLine + "\n"; } @@ -333,9 +334,9 @@ namespace ctrace::stack::analyzer suffix += "\t\t ↳ path: " + itPath->second + "\n"; } - const std::string mainLine = - " potential stack overflow: exceeds limit of " + - std::to_string(prepared.ctx.config.stackLimit) + " bytes\n"; + const std::string mainLine = " potential stack overflow: exceeds limit of " + + std::to_string(prepared.ctx.config.stackLimit) + + " bytes\n"; message = "\t" + std::string(prefixForSeverity(DiagnosticSeverity::Error)) + mainLine + message + suffix; @@ -349,8 +350,7 @@ namespace ctrace::stack::analyzer } void appendStackBufferDiagnostics( - AnalysisResult& result, - const std::vector& bufferIssues) + AnalysisResult& result, const std::vector& bufferIssues) { for (const auto& issue : bufferIssues) { @@ -380,8 +380,8 @@ namespace ctrace::stack::analyzer if (issue.indexIsConstant) { body << "\t\t ↳ constant index " << issue.indexOrUpperBound - << " is out of bounds (0.." - << (issue.arraySize ? issue.arraySize - 1 : 0) << ")\n"; + << " is out of bounds (0.." << (issue.arraySize ? issue.arraySize - 1 : 0) + << ")\n"; } else { @@ -412,18 +412,17 @@ namespace ctrace::stack::analyzer } } - void appendDynamicAllocaDiagnostics( - AnalysisResult& result, - const std::vector& issues) + void appendDynamicAllocaDiagnostics(AnalysisResult& result, + const std::vector& issues) { for (const auto& issue : issues) { - const ResolvedLocation loc = resolveFromInstruction( - static_cast(issue.allocaInst)); + const ResolvedLocation loc = + resolveFromInstruction(static_cast(issue.allocaInst)); std::ostringstream body; - body << "\t[ !!Warn ] dynamic stack allocation detected for variable '" - << issue.varName << "'\n"; + body << "\t[ !!Warn ] dynamic stack allocation detected for variable '" << issue.varName + << "'\n"; body << "\t\t ↳ allocated type: " << issue.typeName << "\n"; body << "\t\t ↳ size of this allocation is not compile-time constant " "(VLA / variable alloca) and may lead to unbounded stack usage\n"; @@ -445,8 +444,8 @@ namespace ctrace::stack::analyzer { for (const auto& issue : issues) { - const ResolvedLocation loc = resolveFromInstruction( - static_cast(issue.allocaInst)); + const ResolvedLocation loc = + resolveFromInstruction(static_cast(issue.allocaInst)); bool isOversized = false; if (issue.sizeIsConst && issue.sizeBytes >= allocaLargeThreshold) @@ -531,9 +530,8 @@ namespace ctrace::stack::analyzer } } - void appendMemIntrinsicDiagnostics( - AnalysisResult& result, - const std::vector& issues) + void appendMemIntrinsicDiagnostics(AnalysisResult& result, + const std::vector& issues) { for (const auto& issue : issues) { @@ -544,8 +542,7 @@ namespace ctrace::stack::analyzer << " potential stack buffer overflow in " << issue.intrinsicName << " on variable '" << issue.varName << "'\n"; body << "\t\t ↳ destination stack buffer size: " << issue.destSizeBytes << " bytes\n"; - body << "\t\t ↳ requested " << issue.lengthBytes - << " bytes to be copied/initialized\n"; + body << "\t\t ↳ requested " << issue.lengthBytes << " bytes to be copied/initialized\n"; DiagnosticBuilder builder; builder.function(issue.funcName) @@ -556,9 +553,8 @@ namespace ctrace::stack::analyzer } } - void appendSizeMinusKDiagnostics( - AnalysisResult& result, - const std::vector& issues) + void appendSizeMinusKDiagnostics(AnalysisResult& result, + const std::vector& issues) { for (const auto& issue : issues) { @@ -593,9 +589,8 @@ namespace ctrace::stack::analyzer } } - void appendMultipleStoreDiagnostics( - AnalysisResult& result, - const std::vector& issues) + void appendMultipleStoreDiagnostics(AnalysisResult& result, + const std::vector& issues) { for (const auto& issue : issues) { @@ -605,8 +600,8 @@ namespace ctrace::stack::analyzer std::ostringstream body; body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) - << " multiple stores to stack buffer '" << issue.varName - << "' in this function (" << issue.storeCount << " store instruction(s)"; + << " multiple stores to stack buffer '" << issue.varName << "' in this function (" + << issue.storeCount << " store instruction(s)"; if (issue.distinctIndexCount > 0) body << ", " << issue.distinctIndexCount << " distinct index expression(s)"; body << ")\n"; @@ -660,8 +655,7 @@ namespace ctrace::stack::analyzer } void appendUninitializedLocalReadDiagnostics( - AnalysisResult& result, - const std::vector& issues) + AnalysisResult& result, const std::vector& issues) { for (const auto& issue : issues) { @@ -712,11 +706,13 @@ namespace ctrace::stack::analyzer if (haveLoc) builder.lineColumn(line, column); - builder.ruleId((issue.kind == analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInit || - issue.kind == - analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInitViaCall) - ? "UninitializedLocalRead" - : "UninitializedLocalVariable") + builder + .ruleId( + (issue.kind == analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInit || + issue.kind == + analysis::UninitializedLocalIssueKind::ReadBeforeDefiniteInitViaCall) + ? "UninitializedLocalRead" + : "UninitializedLocalVariable") .confidence((issue.kind == analysis::UninitializedLocalIssueKind::NeverInitialized) ? 0.75 : 0.90) @@ -727,8 +723,7 @@ namespace ctrace::stack::analyzer } void appendInvalidBaseReconstructionDiagnostics( - AnalysisResult& result, - const std::vector& issues) + AnalysisResult& result, const std::vector& issues) { for (const auto& issue : issues) { @@ -863,9 +858,8 @@ namespace ctrace::stack::analyzer { body << "\t" << prefixForSeverity(DiagnosticSeverity::Info) << " ConstParameterNotModified." << subLabel << ": parameter '" - << issue.paramName << "' in function '" << displayFuncName - << "' is declared '" << issue.currentType - << "' but the pointed object is never modified\n"; + << issue.paramName << "' in function '" << displayFuncName << "' is declared '" + << issue.currentType << "' but the pointed object is never modified\n"; body << kDiagIndentArrow << "consider '" << issue.suggestedType << "' for API const-correctness\n"; } @@ -895,8 +889,9 @@ namespace ctrace::stack::analyzer } } - void appendResourceLifetimeDiagnostics( - AnalysisResult& result, const std::vector& issues) + void + appendResourceLifetimeDiagnostics(AnalysisResult& result, + const std::vector& issues) { for (const auto& issue : issues) { diff --git a/src/app/AnalyzerApp.cpp b/src/app/AnalyzerApp.cpp index ed3be99..3d4a313 100644 --- a/src/app/AnalyzerApp.cpp +++ b/src/app/AnalyzerApp.cpp @@ -1865,20 +1865,20 @@ class AnalyzerApp namespace ctrace::stack::app { -RunResult runAnalyzerApp(cli::ParsedArguments parsedArgs, llvm::LLVMContext& context) -{ - AnalyzerApp app; - AppResult runResult = app.run(std::move(parsedArgs), context); - - RunResult result; - if (!runResult.isOk()) + RunResult runAnalyzerApp(cli::ParsedArguments parsedArgs, llvm::LLVMContext& context) { - result.error = std::move(runResult.error); + AnalyzerApp app; + AppResult runResult = app.run(std::move(parsedArgs), context); + + RunResult result; + if (!runResult.isOk()) + { + result.error = std::move(runResult.error); + return result; + } + + result.exitCode = *runResult.value; return result; } - result.exitCode = *runResult.value; - return result; -} - } // namespace ctrace::stack::app diff --git a/src/cli/ArgParser.cpp b/src/cli/ArgParser.cpp index a53e778..a8eeb55 100644 --- a/src/cli/ArgParser.cpp +++ b/src/cli/ArgParser.cpp @@ -92,7 +92,8 @@ namespace ctrace::stack::cli bool valid = false; }; - static std::optional suggestFixedValueOption(std::string_view unknownOption) + static std::optional + suggestFixedValueOption(std::string_view unknownOption) { return suggestByMatcher(unknownOption, true); } @@ -105,7 +106,7 @@ namespace ctrace::stack::cli } static std::optional suggestByMatcher(std::string_view query, - bool onlyFixedValueCandidates) + bool onlyFixedValueCandidates) { CandidateScore best = findBestCandidate(query, onlyFixedValueCandidates); if (!best.valid) @@ -166,7 +167,8 @@ namespace ctrace::stack::cli lowered.reserve(input.size()); for (char c : input) { - lowered.push_back(static_cast(std::tolower(static_cast(c)))); + lowered.push_back( + static_cast(std::tolower(static_cast(c)))); } return lowered; } diff --git a/test/unit/analyzer_module_unit_tests.cpp b/test/unit/analyzer_module_unit_tests.cpp index df68c34..1e6c8c6 100644 --- a/test/unit/analyzer_module_unit_tests.cpp +++ b/test/unit/analyzer_module_unit_tests.cpp @@ -77,8 +77,7 @@ namespace const ctrace::stack::AnalysisConfig config; LoadedModule loaded; std::string loadError; - const std::filesystem::path source = - repoRoot / "test/alloca/oversized-constant.c"; + const std::filesystem::path source = repoRoot / "test/alloca/oversized-constant.c"; if (!loadModuleFromSource(source, config, loaded, loadError)) { report.expect(false, "LocationResolver setup: failed to load module: " + loadError); @@ -152,8 +151,7 @@ namespace return; } - std::function shouldAnalyze = - [](const llvm::Function&) + std::function shouldAnalyze = [](const llvm::Function&) { return true; }; const auto issues = ctrace::stack::analysis::analyzeStackBufferOverflows( *loaded.module, shouldAnalyze, config); @@ -179,9 +177,8 @@ namespace } else { - report.expect( - foundExpectedClassification, - fixtureLabel + " keeps non-unreachable stack accesses as reachable"); + report.expect(foundExpectedClassification, + fixtureLabel + " keeps non-unreachable stack accesses as reachable"); } };