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.