Skip to content

Repository files navigation

C Linter

License CI Coverage Doc Coverage Typing

c-linter is a specialized command-line tool and Python SDK designed to strictly enforce rigid C coding standards using the libclang AST (Abstract Syntax Tree).

Rather than relying on fragile regex matching, this tool actually parses the code to guarantee 100% accurate structural enforcement. It is ideal for usage in strict CI environments or as a pre-commit hook.

For a deep dive into how the tool is built and structured, please see the Architecture Documentation.

Why an AST Linter?

Traditional linters often rely on Regular Expressions (Regex) to grep for banned patterns. This approach is fundamentally flawed for C because it ignores scope, macros, and comments. A regex looking for malloc might trigger on a variable named malloc_counter or fail if the allocation is hidden behind a #define.

By hooking directly into libclang, c-linter "sees" the code exactly as the compiler does. It resolves types, follows preprocessor macros, and understands the syntax tree natively, ensuring zero false positives from string matching.


Enforced Standards

1. Strict C89 Compliance

The linter enforces -std=c89 -pedantic. Features like mixing declarations and code, // comments, or for (int i...) will immediately fail the lint process.

  • Bad:
    int i = 0;
    // This is a C99 comment
    for (int j = 0; j < 10; j++) { ... } /* C99 inline declaration */
  • Good:
    int i = 0;
    int j;
    /* This is a C89 compliant comment */
    for (j = 0; j < 10; j++) { ... }

2. Return Type Strictness

Functions cannot return arbitrary structs or pointers. All user-defined function returns must evaluate to int, an enum, void, or a fundamental math type (float, double). This ensures that complex state passes via pointer arguments, and failure states map cleanly to integral statuses.

  • Bad:
    struct Buffer create_buffer(void); /* Error: Returning a complex struct */
  • Good:
    int create_buffer(struct Buffer* out_buffer); /* Pass by pointer, return status */

3. Mandatory Allocation Checking

The AST is searched for allocations (malloc, calloc, realloc). Any resulting pointer must be explicitly checked against NULL (or !p) within the same lexical scope before it is used or returned.

  • Bad:
    char* data = (char*)malloc(10);
    data[0] = 'a'; /* Error: Potential failure from allocation is not checked */
  • Good:
    char* data = (char*)malloc(10);
    if (data == NULL) return -1;
    data[0] = 'a';

4. Nodiscard Enforcement

Any call to a function that evaluates to an int must have its return value evaluated or assigned. It cannot be used in a discarded expression statement, unless explicitly cast to (void).

  • Bad:
    init_system(); /* Error: Call to int-returning function is discarded */
  • Good:
    int status = init_system();
    if (status != 0) return status;
    
    /* Or intentionally discard: */
    (void)init_system();

5. Safe CRT Enforcement

Usage of unsafe standard C library functions (like fopen, strcpy, sprintf) are banned. The linter suggests the _s alternative (e.g. fopen_s).

  • Exemption: You can safely fall back to the unsafe function if it is guarded behind an #else block mapping to #ifdef __STDC_WANT_LIB_EXT1__ or standard Windows macros.

  • Bad:

    FILE* f = fopen("file.txt", "w"); /* Error: Use safe CRT alternative */
  • Good:

    FILE* f;
    fopen_s(&f, "file.txt", "w");

6. Windows Format Literals

Using size and pointer format specifiers like %zu, %I64d, or %Iu are flagged unless the line is strictly wrapped in an #ifdef block for Windows (e.g., WIN32, _MSC_VER, __CYGWIN__, __MINGW64__).

  • Bad:
    printf("Size: %I64d\n", size); /* Error: Format specifier used without Windows guard */
  • Good:
    #ifdef _WIN32
        printf("Size: %I64d\n", size);
    #endif

Build System Integration

To provide 100% accurate analysis, c-linter can read from your build system's compile_commands.json to extract precise include paths, compiler flags, and #define macros used to build your project.

You can specify the path using -p / --build-dir, but by default, c-linter will auto-discover the file if it exists in ./build, ./out, or the project root.

Header-Only Strategy: Linting standalone .h files often fails because they rely on types defined in a .c file that includes them. c-linter uses a "header-only strategy" to automatically inject standard types (like size_t or uint32_t) when linting headers, preventing cascading "unknown type name" errors.


Installation

This project is built using Hatch. It depends on libclang natively, which is automatically bundled when installing the Python package via PyPI, ensuring a seamless cross-platform experience.

# Install the linter
pip install c-linter

(Note: For local development, use pip install -e . from the repository root).

Integration

You can natively enforce these rules across your organization by pulling c-linter directly into your CI pipelines. No manual system dependencies are required.

Pre-commit Hook

To use this linter as a pre-commit hook, add the following to your .pre-commit-config.yaml:

repos:
  - repo: https://github.com/SamuelMarks/c-linter
    rev: v0.1.0 # Or use a specific commit hash
    hooks:
      - id: c-linter
        # Optional: override default flags
        # args: ["--no-windows", "--no-safe-crt"]

GitHub Action

You can easily consume this repository as a native GitHub Action in your workflows. It will automatically scan all .c files in your repository by default.

Create .github/workflows/lint-c.yml:

name: Lint C Code

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run C Linter
        uses: SamuelMarks/c-linter@v0.1.0
        with:
          # Optional configurations (these are the defaults):
          # files: '.'
          # no-windows: 'false'
          # no-safe-crt: 'false'

You can run the linter directly against one or more files or directories:

c-linter src/ include/

CLI Flags

Rule Configuration

  • --std STD: C standard version (e.g., c89, c99, c11).
  • --no-windows: Disables the Windows format literal guard checks.
  • --no-safe-crt: Disables Safe CRT function replacement checks.
  • --strict-safe-crt: Enable strict Safe CRT enforcement (flags strncpy and strncat).
  • --no-discarded-returns: Disable the discarded return value check globally.
  • --no-tolerate-c99: Do not tolerate C99 type extensions (like _Bool or long long) in C89 mode.
  • --no-test-relaxations: Disable relaxed rules for test files.
  • --freestanding: Enforce a freestanding environment (disables built-in headers).

Build & Environment

  • -I INCLUDE, --include INCLUDE: Add directory to include search path. (Note: The linter automatically detects and includes include/ and src/ directories if they exist in the target path).
  • -p BUILD_DIR, --build-dir BUILD_DIR: Path to build directory containing compile_commands.json (auto-discovered if omitted).
  • --no-header-strategy: Disable auto-injection of standard headers when linting standalone .h files.

Exclusions & Suppressions

  • --exclude EXCLUDE: Glob pattern to exclude files/directories.
  • --safe-crt-exclude: Glob pattern to exclude files/directories from Safe CRT checks.
  • --ignore-returns IGNORE_RETURNS: Comma-separated list of functions or macros to ignore discarded returns for.
  • --ignore-missing-includes: Suppress 'file not found' diagnostics.
  • --no-pedantic: Suppress standard compiler pedantic warnings like 'no newline at end of file'.
  • --ignore-formatting: Alias for --no-pedantic.

Output & Actions

  • --max-issues-per-file: Maximum number of compiler diagnostics to report per file (default: 50). Set to 0 to disable.
  • --fix: Automatically fix trivial warnings (e.g., missing newlines at EOF).

Configuration Files

c-linter supports reading its configuration from pyproject.toml or a dedicated .c-linter.toml file.

.c-linter.toml example:

std = "c89"
exclude = ["build/", "vendor/"]
ignore_returns = ["printf", "fprintf"]
include = ["include"]
max_issues_per_file = 100

pyproject.toml example:

[tool.c-linter]
std = "c99"
no_safe_crt = true

Inline Ignore Comments

You can selectively bypass warnings in your source files on a case-by-case basis using special comments:

  • // NOLINT or /* c-linter-disable */: Ignores all linting rules on the current line.
  • // NOLINTNEXTLINE: Ignores all linting rules on the following line.
  • // NOLINTFILE or // c-linter-disable-file: Ignores all linting rules for the entire file.

Scoped Suppressions

You can also suppress specific rules by providing a comma-separated list of scopes:

  • // NOLINT(safe-crt)
  • // NOLINT(discarded-return, return-type)
  • // NOLINTNEXTLINE(windows-format)
  • // NOLINT(unchecked-allocation, compiler-diagnostic)

Python SDK

The project provides a fully documented, strictly typed Python SDK so you can integrate the AST rules directly into other Python tooling or test suites.

from c_linter import lint_code, lint_file, Issue

# Linting from a string in memory
code = """
int do_something(void) { return 1; }
int main(void) {
    do_something(); /* Error: discarded int */
    return 0;
}
"""

issues = lint_code(code)
for issue in issues:
    print(f"[{issue.line}:{issue.column}] {issue.message}")

# Linting directly from disk (supports the same flags as the CLI)
file_issues = lint_file("src/main.c", check_windows=True, check_safe_crt=False)

Development

The project maintains 100% Test Coverage, 100% Documentation Coverage, and 100% Strict Type Annotations.

To run the development suite:

# Run tests and assert 100% coverage
pytest --cov=c_linter --cov-report=term-missing

# Run strict type checking
mypy src --strict

# Run docstring coverage
interrogate -v src

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

C linting (C89, MSVC, safe CRT, etc.) for GitHub Actions, precommit hooks, and more

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages