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.
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.
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++) { ... }
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 */
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';
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();
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
#elseblock 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");
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
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.
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).
You can natively enforce these rules across your organization by pulling c-linter directly into your CI pipelines. No manual system dependencies are required.
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"]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/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_Boolorlong 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 includesinclude/andsrc/directories if they exist in the target path).-p BUILD_DIR, --build-dir BUILD_DIR: Path to build directory containingcompile_commands.json(auto-discovered if omitted).--no-header-strategy: Disable auto-injection of standard headers when linting standalone.hfiles.
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).
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 = 100pyproject.toml example:
[tool.c-linter]
std = "c99"
no_safe_crt = trueYou can selectively bypass warnings in your source files on a case-by-case basis using special comments:
// NOLINTor/* c-linter-disable */: Ignores all linting rules on the current line.// NOLINTNEXTLINE: Ignores all linting rules on the following line.// NOLINTFILEor// c-linter-disable-file: Ignores all linting rules for the entire file.
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)
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)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 srcLicensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
at your option.
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.