Linter
catnip lint analyse le CST sans exécuter le programme. Il combine syntaxe, formatage et règles sémantiques ; l'analyse
de noms et le CFG inter-branches restent explicitement opt-in.
Le linter ne décide pas si le programme mérite de tourner. Il établit seulement la liste des raisons connues pour lesquelles il pourrait le regretter.
CLI
# Analyse standard : syntaxe + style + sémantique
catnip lint script.cat
catnip lint src/
# Une famille seulement
catnip lint --level syntax script.cat
catnip lint --level style script.cat
catnip lint --level semantic script.cat
# Analyses opt-in
catnip lint --check-names script.cat # E200
catnip lint --deep script.cat # CFG : W310 à W313
# stdin
echo 'x = y + 1' | catnip lint --check-names --stdin
Les dossiers sont parcourus récursivement pour les fichiers .cat. --check-names est désactivé par défaut parce
qu'une application hôte peut injecter des noms que le fichier ne déclare pas.
Métriques
catnip lint --max-depth 8 script.cat
catnip lint --max-complexity 15 script.cat
catnip lint --max-length 50 script.cat
catnip lint --max-params 8 script.cat
La valeur 0 désactive la métrique correspondante. Les valeurs par défaut sont respectivement 5, 10, 30 statements et 6
paramètres.
Désactiver un diagnostic
# noqa agit sur une ligne :
x = compute() # noqa
y = external_name # noqa: E200
--disable agit sur l'analyse entière ; --enable retire un code de l'ensemble désactivé :
catnip lint --disable W401,I200 script.cat
catnip lint --enable W401 script.cat
La configuration persistante utilise la même liste :
[lint]
disable = ["W401", "I200"]
L'ensemble effectif vaut (configuration ∪ --disable) \ --enable. Un code inconnu est accepté afin qu'une configuration
puisse rester commune à plusieurs versions du linter.
Code de sortie
Les erreurs produisent toujours un statut non nul. Les warnings sont consultatifs sauf avec --strict; les diagnostics
Info et Hint restent consultatifs :
catnip lint --check-names --deep --strict src/
Diagnostics
Syntaxe et style
| Code | Sévérité | Condition |
|---|---|---|
E100 |
Error | erreur de parsing |
W100 |
Warning | ligne différente de la sortie du formatteur |
W101 |
Warning | espaces en fin de ligne |
W102 |
Info | nombre de lignes différent après formatage |
Une erreur de syntaxe arrête les analyses qui nécessitent un CST valide.
Noms
| Code | Sévérité | Condition |
|---|---|---|
E200 |
Error | nom non défini, avec --check-names |
E205 |
Error | écriture prouvée vers une capture dans un callback ND parallèle |
W200 |
Warning | variable locale définie mais jamais lue |
W201 |
Warning | paramètre jamais lu |
W202 |
Warning | affectation du retour None d'un import wild |
W203 |
Warning | mot-clé utilisé comme nom de variable |
W204 |
Warning | variable locale masquant un scope extérieur |
W205 |
Warning | mutation possible d'une capture de type indéterminable (callback ND) |
Les noms globaux ne déclenchent pas W200, car ils peuvent former l'API du module. Les noms préfixés par _ et le
paramètre self sont également ignorés. Une affectation dans un bloc de la même fonction écrit dans la liaison
existante ; W204 vise la frontière d'une fonction ou closure.
E205 interdit l'écriture prouvée vers une capture dans les callbacks ~~/~> : réaffectation (total = total + x),
affectation par indice ou attribut (bag[i] = x, b.v = x), et appel d'une méthode mutatrice en place quand le type du
receveur est prouvé — définition visible par littéral de collection ([...], {...}, list(...), dict(...),
set(...)), ou instance d'une struct du fichier dont la méthode résolue (chaîne extends locale comprise) assigne
self. Une lambda nommée passée à l'opérateur est analysée comme une lambda littérale. Le résultat ne doit pas dépendre
du mode sequential, thread ou process; une réduction explicite remplace l'accumulation partagée.
W205 couvre le seul cas restant : une méthode mutatrice (append, add, update, ...) appelée sur une capture dont
le type ne peut pas être déterminé. Le nom seul ne condamne pas — une méthode de struct locale qui n'écrit pas self ne
déclenche rien, même nommée add.
Le linter distingue la preuve du soupçon. La preuve arrête le programme ; le soupçon remplit son dossier.
Contrôle de flux
| Code | Sévérité | Condition |
|---|---|---|
W300 |
Warning | code après return, raise, break ou continue |
W301 |
Warning | branche rendue morte par un booléen littéral |
W302 |
Warning | while True sans sortie détectable |
W303 |
Info | variable de condition jamais modifiée dans la boucle |
W304 |
Warning | accès dict à clé littérale sous ?? |
W310 |
Warning | variable définie sur une partie des chemins, avec --deep |
W311 |
Warning | code après des branches toutes terminales, avec --deep |
W312 |
Warning | valeur écrasée avant lecture, avec --deep |
W313 |
Hint | else/elif redondant après une branche terminale, avec --deep |
W300 compte aussi le code qu'un ; place sur la ligne du terminateur : break; x = 1 signale x = 1, aussi
inatteignable qu'à la ligne suivante.
W304 rappelle que ?? ne capture pas KeyError : d['k'] ?? fallback échoue si la clé manque, tandis que
d.get('k') ?? fallback couvre clé absente et valeur None. Les parenthèses sont transparentes :
(d['k']) ?? fallback porte le même risque et le même diagnostic. Un indice multiple dont tous les éléments sont des
littéraux string (m['a', 'b']) est un accès dict à clé tuple et porte le même risque ; dès qu'un élément n'en est pas
un (m[0, j]), la forme est lue comme de l'indexation ND et reste hors périmètre.
L'analyse --deep construit un CFG par scope. Elle calcule l'initialisation définie en avant pour W310 et la liveness
en arrière pour W312; elle ne suit pas les valeurs runtime.
Broadcast, types et pipeline
| Code | Sévérité | Condition |
|---|---|---|
W401 |
Warning | builtin impur dans un broadcast sous ND.thread ou ND.process |
E300 |
Error | incompatibilité de type prouvable |
E400 |
Error | fichier refusé pendant transformation ou analyse sémantique |
W401 vise une liste fermée de builtins à effet de bord (print, input, open, breakpoint) lorsque leur ordre
d'exécution n'est pas garanti.
E300 couvre les incompatibilités démontrables sur valeurs littérales ou types inférés : défauts de paramètres et
champs, retours, appels monomorphes et constructeurs de struct. Un type inconnu ne produit pas de diagnostic : ce
contrôle n'est pas un système de types complet.
E400 rapporte le verdict du pipeline avant exécution, par exemple un return hors fonction, une déclaration de struct
invalide ou un pragma inconnu. Le masquer avec noqa tait le rapport, pas le refus du runtime.
Suggestions et métriques
| Code | Sévérité | Condition |
|---|---|---|
I100 |
Hint | appel récursif hors position terminale |
I101 |
Hint | comparaison redondante avec un booléen |
I102 |
Hint | auto-affectation |
I103 |
Hint | match non exhaustif |
I200 |
Hint | profondeur d'imbrication au-dessus du seuil |
I201 |
Hint | complexité cyclomatique au-dessus du seuil |
I202 |
Hint | fonction au-dessus du seuil de statements |
I203 |
Hint | fonction au-dessus du seuil de paramètres |
Pour enum, union taggée et booléen, I103 énumère les variantes manquantes lorsque le type du scrutinee est connu.
Sinon, il demande un catch-all (_ ou variable nue sans guard). Voir
UNIONS.
API Python
from pathlib import Path
from catnip.tools import lint_code, lint_file
result = lint_code(
source,
check_names=True,
check_ir=True,
max_cyclomatic_complexity=15,
)
for diagnostic in result.diagnostics:
print(diagnostic.code, diagnostic.line, diagnostic.message)
file_result = lint_file(Path('script.cat'))
LintResult expose diagnostics, errors, warnings, has_errors et summary(). Chaque diagnostic contient code,
sévérité, ligne, colonne, message, source éventuel et suggestion éventuelle.
Limites
- les modules chargés dynamiquement ne sont pas analysés ;
- le CFG suit les définitions et lectures, pas les valeurs ;
- la distinction entre shadowing et mutation d'une capture utilise la lecture du nom dans le membre droit ;
- l'inférence de types reste volontairement partielle.
Ces limites réduisent les faux positifs sur les programmes dont le contexte réel n'existe qu'au moment de l'exécution.