Chapter 278: Static Code Analysis Tools

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand what static code analysis is and its importance in embedded software development.
  • Learn about the clang-tidy tool and its integration with ESP-IDF.
  • Run static analysis on your ESP-IDF project to find potential bugs and style issues.
  • Interpret the reports generated by the analysis tool.
  • Customize the analysis checks to fit your project’s needs.
  • Integrate static analysis into your Continuous Integration (CI) pipeline.

Introduction

As we’ve discussed, writing correct and reliable code is the cornerstone of professional embedded development. While unit testing (Chapter 276) is excellent for verifying the logic of your code, and Continuous Integration (Chapter 277) automates the build process, another layer of quality assurance is needed to catch common programming errors, potential bugs, and stylistic inconsistencies before the code is even compiled.

This is the role of Static Code Analysis. These are automated tools that “read” your source code without executing it (hence, “static”) and check it against a vast database of rules and patterns for common mistakes. Think of it as an expert programmer reviewing your code, pointing out subtle issues like potential null pointer dereferences, resource leaks, and violations of coding standards.

Integrating static analysis into your workflow is a best practice that elevates code quality, reduces debugging time, and enhances long-term maintainability. ESP-IDF provides built-in support for one of the most powerful C/C++ analysis tools available: clang-tidy.

Theory

What is Static Code Analysis?

Static code analysis is the automated inspection of source code without actually running it. This distinguishes it from dynamic analysis, which involves observing the program’s behavior during execution (e.g., debugging or profiling).

The analysis tool, often called a “linter” or “static analyzer,” parses the code just like a compiler does. However, instead of generating machine code, it checks the code’s structure, syntax, and data flow against a predefined set of rules.

graph TD
    subgraph "Development Environment"
        A[<b>Source Code</b><br>main.c, component.h, etc.]
    end

    subgraph "ESP-IDF Build System"
        B{{"<b>idf.py clang-check</b>"}}
    end

    subgraph "Analysis Engine"
        C["<b>Clang-Tidy Tool</b><br>Parses code & applies rules"]
    end

    subgraph "Output"
        D[<b>Analysis Report</b><br>List of warnings, errors,<br>and style suggestions]
    end

    A --> B
    B --> C
    C --> D

    %% Styling
    classDef start fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF
    classDef endo fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46

    class A start
    class B process
    class C process
    class D endo

These tools can detect a wide range of issues that are often missed by human reviewers and may not be flagged by a standard compiler build:

  • Potential Bugs: Dereferencing a pointer that could be null, using a variable before it’s initialized, array out-of-bounds access.
  • Resource Leaks: Forgetting to free() memory allocated with malloc(), or failing to close a file handle.
  • Security Vulnerabilities: Using unsafe functions like strcpy(), integer overflows.
  • Style Violations: Inconsistent naming conventions, magic numbers, overly complex functions.
  • Best Practice Adherence: Suggesting the use of modern C++ features or more efficient constructs.

Clang-Tidy in ESP-IDF

The ESP-IDF build system integrates clang-tidy, a powerful and highly configurable linter that is part of the LLVM/Clang compiler toolchain. Because clang-tidy uses the same frontend as the Clang compiler, it has a deep understanding of C, C++, and Objective-C code, which allows it to perform very sophisticated checks and provide highly accurate feedback.

You can run clang-tidy on your project with a simple idf.py command. It will analyze your source files and print a report of any issues it finds directly to your console.

Configuring Checks with .clang-tidy

One of the greatest strengths of clang-tidy is its configurability. You can control exactly which checks are enabled or disabled by creating a configuration file named .clang-tidy in the root directory of your project. This file uses YAML syntax to specify which rules to apply.

graph TD
    subgraph "Project Root Directory"
        A["<b>Project Files</b><br>(main.c, components, etc.)"]
        B{"<b>.clang-tidy file</b><br><i>(YAML format)</i>"}
    end

    subgraph "Execution"
        C{{"<b>idf.py clang-check</b>"}}
    end

    subgraph "Clang-Tidy Engine"
        D{"Reads .clang-tidy?"}
        E["Applies checks defined in file<br>(e.g., bugprone-*, -readability-*)"]
        F["Applies default ESP-IDF checks"]
    end

    subgraph "Result"
        G[<b>Filtered Analysis Report</b>]
    end

    A --> C
    B --> D
    C --> D

    D -- "Yes, file exists" --> E
    D -- "No, file missing" --> F
    E --> G
    F --> G

    %% Styling
    classDef start fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF
    classDef endo fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46

    class A,B start
    class C process
    class D decision
    class E,F process
    class G endo

This allows you to tailor the analysis to your team’s specific coding standards and priorities. You can start with a general set of checks and gradually enable more as your project matures.

Practical Example: Finding Bugs with clang-tidy

Let’s deliberately introduce some common errors into a new component and see how clang-tidy helps us find and fix them.

Step 1: Create a Component with Flawed Code

Create a new component named buggy_component with the following structure and files.

components/buggy_component/include/buggy_component.h

C
#ifndef BUGGY_COMPONENT_H
#define BUGGY_COMPONENT_H

#include <stddef.h>

void process_data(int *data, size_t size);

#endif // BUGGY_COMPONENT_H

components/buggy_component/buggy_component.c

C
#include "buggy_component.h"
#include <stdlib.h>
#include <stdio.h>

// This function has several potential issues.
void process_data(int *data, size_t size)
{
    int* local_buffer = malloc(size * sizeof(int));

    // Bug 1: Potential null pointer dereference if malloc fails.
    for (size_t i = 0; i < size; ++i) {
        local_buffer[i] = data[i] * 2;
    }

    // Bug 2: An unused variable.
    int a_variable_that_is_not_used = 10;

    printf("Processing complete.\n");

    // Bug 3: Memory leak! local_buffer is never freed.
}

components/buggy_component/CMakeLists.txt

Plaintext
idf_component_register(SRCS "buggy_component.c"
                       INCLUDE_DIRS "include")

Finally, make sure to add buggy_component to the COMPONENTS list in your project’s root CMakeLists.txt file.

Step 2: Run the Static Analyzer

Now, run the static analysis command from your project’s root directory.

  1. Open a VS Code terminal sourced for ESP-IDF.
  2. Run the clang-check command:idf.py clang-check

Step 3: Observe and Interpret the Output

The tool will analyze the files and produce a report. The output will look something like this (exact wording may vary slightly):

Plaintext
...
Running clang-tidy on 1 files
/path/to/my_project/components/buggy_component/buggy_component.c:9:5: warning: The result of 'malloc' is not checked for nullness [clang-analyzer-core.uninitialized.Assign]
    local_buffer[i] = data[i] * 2;
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/path/to/my_project/components/buggy_component/buggy_component.c:14:9: warning: Value stored to 'a_variable_that_is_not_used' is never read [clang-analyzer-deadcode.DeadStores]
    int a_variable_that_is_not_used = 10;
        ^
/path/to/my_project/components/buggy_component/buggy_component.c:6:24: warning: Potential leak of memory pointed to by 'local_buffer' [clang-analyzer-memory.MemLeak]
    int* local_buffer = malloc(size * sizeof(int));
                       ^
Checked 1 files
Clang-tidy found warnings or errors

Let’s break down this report:

  1. warning: The result of 'malloc' is not checked for nullness: This is clang-tidy telling us that malloc can return NULL if memory allocation fails, and our code uses local_buffer without first checking if it’s NULL. This is a critical bug that would cause a crash.
  2. warning: Value stored to 'a_variable_that_is_not_used' is never read: This points out dead code. While not a functional bug, it indicates unclean code and could be a symptom of an unfinished or incorrect implementation.
  3. warning: Potential leak of memory pointed to by 'local_buffer': This is another critical bug. The function allocates memory on the heap but never calls free() before exiting, leading to a memory leak.

Step 4: Fix the Code

Based on the report, let’s fix the code.

components/buggy_component/buggy_component.c (Corrected)

C
#include "buggy_component.h"
#include <stdlib.h>
#include <stdio.h>

void process_data(int *data, size_t size)
{
    // Allocate memory
    int* local_buffer = malloc(size * sizeof(int));

    // FIX 1: Check if malloc succeeded before using the pointer.
    if (local_buffer == NULL) {
        printf("Error: Failed to allocate memory!\n");
        return; // Exit early if allocation fails.
    }

    for (size_t i = 0; i < size; ++i) {
        local_buffer[i] = data[i] * 2;
    }

    // FIX 2: Remove the unused variable.
    // int a_variable_that_is_not_used = 10;

    printf("Processing complete.\n");

    // FIX 3: Free the allocated memory to prevent a leak.
    free(local_buffer);
}

Now, if you run idf.py clang-check again, it will complete without any warnings, confirming that the issues have been resolved.

Step 5: Customize Checks with .clang-tidy

You can create a .clang-tidy file in your project root to control the checks. For example, to use a modern, performance-focused set of checks while disabling noisy ones, your file might look like this:

.clang-tidy

Plaintext
# See https://clang.llvm.org/extra/clang-tidy/checks/list.html for all checks.
Checks: >
  -*,
  bugprone-*,
  cert-*,
  clang-analyzer-*,
  modernize-*,
  performance-*,
  portability-*,
  readability-*,
  -modernize-use-trailing-return-type
  • -*: This first line disables all checks by default.
  • The following lines with a * (e.g., bugprone-*) enable all checks within that category.
  • A line with a - at the start (e.g., -modernize-use-trailing-return-type) explicitly disables a specific check.

Tip: Start with a broad set of checks and fine-tune your .clang-tidy file over time. It’s better to have a few high-value checks that everyone follows than a thousand noisy warnings that get ignored.

Variant Notes

Static code analysis is a hardware-independent process. The tools analyze the source code itself, focusing on the C language standards, potential data flow problems, and stylistic rules. The analysis happens on the build machine before the code is compiled for any specific target.

Therefore, the clang-tidy functionality and the issues it finds are identical across all ESP32 variants (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, C6, H2, etc.). A potential memory leak or null pointer dereference is a bug regardless of the underlying chip architecture. This makes static analysis a universally beneficial practice for all your ESP-IDF projects.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Analysis Paralysis Running idf.py clang-check on an existing project produces hundreds of warnings, making it hard to know where to start. Start Small: Create a .clang-tidy file that disables all checks, then enables only the most critical ones.
1. Start with Checks: '-*,clang-analyzer-*'
2. Fix all reported issues.
3. Gradually add more checks like bugprone-* or cert-*.
False Positive Warning Clang-tidy flags a line of code that you have manually verified and know is correct and safe. The warning is just noise. Suppress Inline: Add a // NOLINT or // NOLINTNEXTLINE comment to the end of the specific line.
Example:
my_register = 0xDEADBEEF; // NOLINT(cppcoreguidelines-pro-type-union-access)
Use sparingly and add a comment explaining why it’s a false positive.
Misconfigured .clang-tidy The idf.py clang-check command fails with a YAML parsing error, or your custom check configuration seems to be ignored. Check Syntax: YAML is sensitive to indentation (use spaces, not tabs).
1. Use an online YAML linter to validate your file’s syntax.
2. Ensure the file is named exactly .clang-tidy and is in the project root.
Cryptic Warning Message The report shows a warning like [cert-err34-c] and you don’t understand the risk or how to fix it. Consult the Docs: The check name is your key to understanding the problem.
1. Copy the check name (e.g., cert-err34-c).
2. Search for “clang-tidy cert-err34-c” online.
3. The official LLVM/Clang documentation provides detailed explanations and compliant/non-compliant code examples for every check.

Exercises

  1. Integrate Static Analysis into CI: Modify the GitHub Actions workflow (.github/workflows/ci.yml) from the previous chapter. Add a new job or step that runs idf.py clang-check. Make the CI pipeline fail if clang-tidy finds any issues. This ensures that no code with static analysis warnings can be merged into your main branch.
  2. Find a Real Bug: Browse the ESP-IDF source code or a public ESP32 project on GitHub. Clone the repository, run idf.py clang-check on it, and see if you can find any legitimate, pre-existing bugs reported by the tool.
  3. Experiment with Checks: Add the cppcoreguidelines-* checks to your .clang-tidy file. Rerun the analysis on your project. Do you see any new warnings? Research one of the new warnings to understand what rule from the C++ Core Guidelines it relates to.

Summary

  • Static Code Analysis inspects source code without executing it to find potential bugs, leaks, and style issues.
  • ESP-IDF integrates clang-tidy, a powerful static analyzer for C/C++ code.
  • The idf.py clang-check command runs the analysis on your project.
  • You can precisely control which checks are performed by creating and configuring a .clang-tidy file in your project’s root directory.
  • Static analysis is hardware-independent and provides equal value across all ESP32 variants.
  • Integrating static analysis into your regular development and CI workflow is a key practice for writing high-quality, robust embedded firmware.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top