Skip to content

Commit 6098bd9

Browse files
mkannwischerclaude
andcommitted
lint: Add ast-grep check for visibility-macro annotations
Add an ast-grep lint for the MLD_INTERNAL_API / MLD_EXTERNAL_API / MLD_API_QUALIFIER convention on externally-linked declarations under mldsa/src/ (functions for now; data definitions not yet covered). Rule lives in scripts/.ast-grep/mldsa-declarations.yml as a multi-document YAML with two rules (function definition + function prototype). Each rule matches on tree-sitter-c node kinds (function_definition / declaration), excludes the three accepted annotation macros, static functions, and assembly entry points (*_asm, empty_cu_*). scripts/lint gains a check-ast-grep step (placed right after the C clang-format step); nix/util.nix's linters package picks up the ast-grep binary. Invocation: ast-grep scan --rule scripts/.ast-grep/mldsa-declarations.yml . Rationale for ast-grep over alternatives considered: - CodeQL: powerful but requires a full build + database; overkill for a lint rule and pulls in a non-OSS CLI. - Semgrep: C parser chokes on unknown macros and lacks sibling- relationship matchers, forcing return-type enumeration and implicit reliance on parser quirks. - ast-grep: tree-sitter-c parses reliably; rule matches on AST node kinds and names required annotations literally via regex. Known limitations: - Unlike CodeQL (which only sees compiled code), ast-grep scans all source text, so declarations inside #if blocks (e.g. MLDSA_DEBUG, native backend headers) are also flagged. Either annotate them or scope the `files:` globs narrower in follow-up. - A `static MLD_INLINE ...` combination parses with `static` outside the function_definition AST node, so the rule falls back to a text-regex and a `follows:` sibling check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 55c9fb6 commit 6098bd9

3 files changed

Lines changed: 129 additions & 1 deletion

File tree

nix/util.nix

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ rec {
8888
shfmt
8989
shellcheck
9090
actionlint
91-
ruff;
91+
ruff
92+
ast-grep;
9293

9394
inherit (pkgs.python3Packages)
9495
mpmath sympy pyparsing pyyaml rich;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Copyright (c) The mldsa-native project authors
2+
# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT
3+
#
4+
# ast-grep rules enforcing visibility-macro annotations on every
5+
# externally-linked declaration under mldsa/src/. See mldsa/src/common.h
6+
# for the macro definitions.
7+
#
8+
# Invoked by scripts/lint via `ast-grep scan --rule`.
9+
10+
id: mldsa-function-def-missing-visibility
11+
language: c
12+
files:
13+
- 'mldsa/src/**/*.c'
14+
- 'mldsa/src/**/*.h'
15+
rule:
16+
all:
17+
- kind: function_definition
18+
- not:
19+
has:
20+
kind: type_identifier
21+
regex: '^(MLD_INTERNAL_API|MLD_EXTERNAL_API|MLD_API_QUALIFIER)$'
22+
stopBy: end
23+
- not:
24+
follows:
25+
stopBy: neighbor
26+
has:
27+
kind: identifier
28+
regex: '^(MLD_INTERNAL_API|MLD_EXTERNAL_API|MLD_API_QUALIFIER)$'
29+
stopBy: end
30+
- not:
31+
has:
32+
kind: storage_class_specifier
33+
regex: '^static$'
34+
stopBy: end
35+
- not:
36+
follows:
37+
stopBy: neighbor
38+
has:
39+
kind: storage_class_specifier
40+
regex: '^static$'
41+
stopBy: end
42+
- not:
43+
regex: '^\s*(static|MLD_STATIC_TESTABLE)\b'
44+
- not:
45+
has:
46+
kind: identifier
47+
regex: '^(empty_cu_|.*_asm$)'
48+
stopBy: end
49+
severity: error
50+
message: |-
51+
Function definition has external linkage but is not preceded by
52+
MLD_INTERNAL_API, MLD_EXTERNAL_API, or MLD_API_QUALIFIER
53+
(see mldsa/src/common.h).
54+
55+
---
56+
57+
id: mldsa-function-proto-missing-visibility
58+
language: c
59+
files:
60+
- 'mldsa/src/**/*.c'
61+
- 'mldsa/src/**/*.h'
62+
rule:
63+
all:
64+
- kind: declaration
65+
- has:
66+
kind: function_declarator
67+
stopBy: end
68+
- not:
69+
has:
70+
kind: type_identifier
71+
regex: '^(MLD_INTERNAL_API|MLD_EXTERNAL_API|MLD_API_QUALIFIER)$'
72+
stopBy: end
73+
- not:
74+
follows:
75+
stopBy: neighbor
76+
has:
77+
kind: identifier
78+
regex: '^(MLD_INTERNAL_API|MLD_EXTERNAL_API|MLD_API_QUALIFIER)$'
79+
stopBy: end
80+
- not:
81+
has:
82+
kind: storage_class_specifier
83+
regex: '^static$'
84+
stopBy: end
85+
- not:
86+
follows:
87+
stopBy: neighbor
88+
has:
89+
kind: storage_class_specifier
90+
regex: '^static$'
91+
stopBy: end
92+
- not:
93+
regex: '^\s*(static|MLD_STATIC_TESTABLE)\b'
94+
- not:
95+
has:
96+
kind: identifier
97+
regex: '^(empty_cu_|.*_asm$)'
98+
stopBy: end
99+
severity: error
100+
message: |-
101+
Function prototype has external linkage but is not preceded by
102+
MLD_INTERNAL_API, MLD_EXTERNAL_API, or MLD_API_QUALIFIER
103+
(see mldsa/src/common.h).

scripts/lint

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,30 @@ lint-c-files()
195195
checkerr "Lint C" "$(lint-c-files)"
196196
gh_group_end
197197

198+
check-ast-grep()
199+
{
200+
if ! command -v ast-grep >/dev/null; then
201+
gh_error_simple "ast-grep missing" "ast-grep is not installed"
202+
error "Check ast-grep"
203+
SUCCESS=false
204+
gh_summary_failure "Check ast-grep"
205+
return 0
206+
fi
207+
208+
if (cd "$ROOT" && ast-grep scan --rule scripts/.ast-grep/mldsa-declarations.yml .); then
209+
info "Check ast-grep"
210+
gh_summary_success "Check ast-grep"
211+
else
212+
error "Check ast-grep"
213+
gh_summary_failure "Check ast-grep"
214+
SUCCESS=false
215+
fi
216+
}
217+
218+
gh_group_start "Check ast-grep (visibility-macro annotations)"
219+
check-ast-grep
220+
gh_group_end
221+
198222
check-eol-dry-run()
199223
{
200224
for file in $(git ls-files -- ":/" ":/!:*.png"); do

0 commit comments

Comments
 (0)