C#
C is Holzmann's original "Power of Ten" target language. Several clauses that every other language adapts away apply literally to C, so it gains a dedicated set of new language-specific rules: the five C-family rules (the "homecoming", shared with C++) express rules 1, 3, 8, and 9 directly. New in v2.7.0.
File extensions#
.c and .h.
.h ownership: .h headers are linted as C. A C++ project that uses .h headers gets them linted as C - content sniffing is out of scope, so a header shared between C and C++ is treated as C. C++ keeps the C++-only header extensions (.hpp, .hxx, .hh). This is a documented limitation.
Quick start#
pip install 'safelint[c]' # adds .c, .h
# or
uv add 'safelint[c]'
safelint check src/ # lint a directory
The tree-sitter-c grammar ships as the opt-in [c] extra; the base install bundles no grammars.
Suppression directives#
C uses line-comment directives (// form): // nosafe (all rules on the line), // nosafe: SAFE106 (a specific rule), and the file-scope // safelint: ignore. Block-comment (/* */) directives parse but are not recognised, matching every other language's line-directive-only convention.
Rules that fire on C#
21 rules apply: the 16 cross-language rules plus the 5 new C-family rules (shared with C++).
Cross-language rules#
| Code | Rule | C notes |
|---|---|---|
| SAFE101 | function_length | Source lines on function_definition. |
| SAFE102 | nesting_depth | if / for / while / do / switch. |
| SAFE103 | max_arguments | parameter_declaration count; void is zero args. |
| SAFE104 | complexity | if/loops/case/ternary + && / ||. |
| SAFE105 | no_recursion | Direct self-recursion. |
| SAFE302 | global_mutation | File-scope mutable declaration; const / typedef / extern / prototypes exempt; static counts. |
| SAFE303 | side_effects_hidden | Pure-named function doing C I/O (io_functions_c). |
| SAFE304 | side_effects | Any non-I/O-named function doing C I/O. |
| SAFE309 | dynamic_code_execution | dlopen / dlsym. Disabled by default. |
| SAFE501 | unbounded_loops | while (1) / while (true) / for (;;); a goto out of the loop counts as an exit. |
| SAFE601 | missing_assertions | The assert(...) macro. Disabled by default. |
| SAFE603 | blanket_suppression | clang-tidy bare // NOLINT. Disabled by default. |
| SAFE701 | test_existence | <stem>_test.c / test_<stem>.c. Disabled by default. |
| SAFE702 | test_coupling | Same convention. Disabled by default. |
| SAFE801 | tainted_sink | argv / getenv / ... into system / strcpy / ... Disabled by default. |
| SAFE802 | return_value_ignored | Discarded fclose / fwrite / ...; (void)f() exempt. Disabled by default. |
C-family rules (the Power-of-Ten homecoming)#
| Code | Rule | Holzmann rule | Default | Behaviour |
|---|---|---|---|---|
| SAFE106 | nonlocal_jumps | 1 | enabled (warning) | Every goto and every setjmp / longjmp / sigsetjmp / siglongjmp call. Idiomatic goto err surfaces without blocking; annotate with // nosafe: SAFE106. |
| SAFE310 | dynamic_allocation | 3 | disabled | Calls to the malloc family (malloc / calloc / realloc / aligned_alloc / free / strdup). |
| SAFE311 | complex_macro | 8 | disabled | Function-like macros using ## or __VA_ARGS__, and object-like macros whose replacement is not a balanced syntactic unit. |
| SAFE312 | conditional_compilation | 8 | disabled | Every #if / #ifdef / #ifndef beyond the include-guard idiom. |
| SAFE313 | restricted_pointers | 9 | disabled | Declarators with more than one pointer level (int **p) and function-pointer declarators. |
Rules not registered for C#
| Code | Rule | Why |
|---|---|---|
| SAFE201 / SAFE202 / SAFE203 | bare_except / empty_except / logging_on_error | C has no try/catch; error discipline is SAFE802's return-code checking. |
| SAFE301 | global_state | No global keyword; file-scope state is the SAFE302 port. |
| SAFE305 | wide_scope_declaration | No var hoisting distinction. |
| SAFE401 | resource_lifecycle | C cleanup (goto err, explicit fclose / free) needs flow analysis; allocation discipline is SAFE310's job. Documented gap. |
| SAFE803 | null_dereference | Chained deref off a call is rare C style; nullable-return tracking without flow analysis would be noise. Documented gap. |
Key C adaptations#
function_definitionis the only function node (no methods / closures). The name nests underdeclarator.declarator.(void)f()(the explicit-discard cast) wraps the call in acast_expression, so SAFE802 does not fire on it.argventers taint analysis via function-parameter seeding;argv[1]keeps the taint.- SAFE313 fires on
char **argv(a two-level pointer) - intentional for the strict, opt-in rule. - SAFE313 is a syntactic declarator check: a pointer level hidden behind a
typedef(typedef int *intp; intp *pp;) or a macro is not counted - the paper's no-hidden-dereference clause needs type resolution and is a documented gap.
Configuration#
Every C-specific config key is set in either form. [tool.safelint.rules.<name>] for pyproject.toml, [rules.<name>] for standalone safelint.toml.
Enable the opt-in C rules#
# pyproject.toml
[tool.safelint.rules.dynamic_allocation]
enabled = true
[tool.safelint.rules.restricted_pointers]
enabled = true
# safelint.toml
[rules.dynamic_allocation]
enabled = true
[rules.restricted_pointers]
enabled = true
SAFE106 non-local jump calls#
# pyproject.toml
[tool.safelint.rules.nonlocal_jumps]
nonlocal_jump_calls_c = ["setjmp", "longjmp", "sigsetjmp", "siglongjmp"]
# safelint.toml
[rules.nonlocal_jumps]
nonlocal_jump_calls_c = ["setjmp", "longjmp", "sigsetjmp", "siglongjmp"]
SAFE310 allocation calls#
# pyproject.toml
[tool.safelint.rules.dynamic_allocation]
enabled = true
allocation_calls_c = ["malloc", "calloc", "realloc", "aligned_alloc", "free", "strdup", "xmalloc"]
# safelint.toml
[rules.dynamic_allocation]
enabled = true
allocation_calls_c = ["malloc", "calloc", "realloc", "aligned_alloc", "free", "strdup", "xmalloc"]