Skip to content

Adding a New Check

Hugo edited this page Feb 26, 2026 · 1 revision

Adding a New Check

This guide walks through adding a new diagnostic check to the analyzer.


Step 1: Define the Error Code

Add a new entry to DescriptiveErrorCode in include/StackUsageAnalyzer.hpp:

enum class DescriptiveErrorCode
{
    // ... existing codes ...
    YourNewCheck = 17  // next available number
};

Update the EnumTraits<DescriptiveErrorCode> specialization in the same file:

template <> struct EnumTraits<DescriptiveErrorCode>
{
    static constexpr std::array<std::string_view, 18> names = {
        // ... existing names ...
        "YourNewCheck"
    };
};

Step 2: Create the Analysis Module

Header

Create include/analysis/YourNewCheck.hpp:

#pragma once

#include <llvm/IR/Module.h>
#include <vector>

namespace ctrace::stack::analysis
{

struct YourNewCheckFinding
{
    const llvm::Function* function = nullptr;
    const llvm::Instruction* instruction = nullptr;
    // ... specific finding fields ...
};

std::vector<YourNewCheckFinding> runYourNewCheck(
    const llvm::Module& module,
    const llvm::Function& function);

} // namespace ctrace::stack::analysis

Implementation

Create src/analysis/YourNewCheck.cpp:

#include "analysis/YourNewCheck.hpp"

#include <llvm/IR/Instructions.h>

namespace ctrace::stack::analysis
{

std::vector<YourNewCheckFinding> runYourNewCheck(
    const llvm::Module& module,
    const llvm::Function& function)
{
    std::vector<YourNewCheckFinding> findings;

    for (const auto& BB : function)
    {
        for (const auto& I : BB)
        {
            // Your analysis logic here
            // Iterate over instructions, check conditions, etc.
        }
    }

    return findings;
}

} // namespace ctrace::stack::analysis

Step 3: Register in CMakeLists.txt

Add the new source file to STACK_ANALYZER_SOURCES:

set(STACK_ANALYZER_SOURCES
    # ... existing sources ...
    src/analysis/YourNewCheck.cpp
)

Step 4: Integrate into the Pipeline

Wire into AnalysisPipeline

In src/analyzer/AnalysisPipeline.cpp, add a call to your analysis module within the per-function analysis loop:

#include "analysis/YourNewCheck.hpp"

// Inside the analysis loop:
auto yourFindings = analysis::runYourNewCheck(module, function);
// Process findings...

Emit Diagnostics

In src/analyzer/DiagnosticEmitter.cpp, add a case to convert your findings to Diagnostic structs:

for (const auto& finding : yourFindings)
{
    Diagnostic diag;
    diag.severity = DiagnosticSeverity::Warning;
    diag.errCode = DescriptiveErrorCode::YourNewCheck;
    diag.ruleId = "YourNewCheck";
    diag.message = "description of the issue";
    // Set location from finding.instruction debug info
    // via LocationResolver
    result.diagnostics.push_back(std::move(diag));
}

Step 5: Add Tests

Regression Test Fixture

Create a test file test/your-new-check/basic-case.c:

// Expected diagnostic annotations:
// at line X, column Y
// [ !!Warn ] your diagnostic message here

void test_function(void)
{
    // Code that triggers your check
}

The test runner (run_test.py) parses comment annotations to validate expected diagnostics. The format is:

// at line <N>, column <N>
// [ !!Warn ] <message prefix>

or for info-level:

// [!Info] <message prefix>

Run Tests

python3 test/run_test.py --analyzer ./build/stack_usage_analyzer test/your-new-check/

Unit Tests (optional)

If your module has isolated logic worth testing, add tests to test/unit/analyzer_module_unit_tests.cpp:

// Build with: -DBUILD_ANALYZER_UNIT_TESTS=ON

Step 6: Update Models (if applicable)

If your check uses external model files (like resource-lifetime or escape analysis), define the model format and add entries to the bundled models in models/.


Checklist

  • New DescriptiveErrorCode entry + EnumTraits update
  • Analysis module header (include/analysis/)
  • Analysis module implementation (src/analysis/)
  • Source file registered in CMakeLists.txt
  • Wired into AnalysisPipeline.cpp
  • Diagnostic emission in DiagnosticEmitter.cpp
  • Regression test fixture in test/
  • Tests pass with run_test.py
  • Code formatted with clang-format

Tips

  • Study existing analysis modules for patterns. DuplicateIfCondition.cpp is a good simple example. ResourceLifetimeAnalysis.cpp is a complex example with cross-TU support.
  • Use IRValueUtils.hpp for common LLVM IR traversal utilities.
  • Use LocationResolver to convert llvm::Instruction* to source locations.
  • The Reachability module can filter your findings if they occur in dead code.
  • Keep analysis modules stateless when possible -- take the module/function as input, return findings as output.

Clone this wiki locally