Formatteur de code
catnip format reconstruit le code depuis le CST tree-sitter avec un pretty-printer Wadler-Leijen. Il normalise les
espaces et l'indentation tout en conservant commentaires, annotations de type et dispositions multilignes explicites. La
récupération d'erreur de Tree-sitter permet de formater certains fichiers incomplets, mais ne garantit ni un rendu
identique ni la correction d'une syntaxe invalide.
CLI
# Afficher le résultat ou modifier en place
catnip format script.cat
catnip format -i script.cat
catnip format -i src/
# Contrôle CI et diff
catnip format --check src/
catnip format --diff script.cat
# stdin et configuration ponctuelle
echo 'x=1+2*3' | catnip format --stdin
catnip format --line-length 80 --indent-size 2 script.cat
Les dossiers sont parcourus récursivement pour les fichiers .cat. La configuration persistante vit dans catnip.toml
:
[format]
line_length = 120
indent_size = 4
align = true
Règles
Espaces et délimiteurs
| Construction | Règle |
|---|---|
opérateur binaire, affectation, => |
une espace de chaque côté |
| opérateur unaire | aucune espace avant l'opérande |
| arguments nommés | aucune espace autour de = |
| virgule et point-virgule | aucune espace avant, une après |
| parenthèses et index | aucune espace intérieure |
attribut ou méthode après . |
aucune espace |
| slice | aucune espace autour de : |
| commentaire trailing | deux espaces avant # |
# Avant
f=(x,y)=>{obj. method (x= 1);data.[ *2 ]}
# Après
f = (x, y) => {obj.method(x=1); data.[* 2]}
Les blocs de contrôle et les déclarations gardent une espace avant {; les patterns de struct n'en ont pas :
if ready { run() }
struct Point { x; y; }
match p {
Point{x, y} => { x + y }
}
Indentation, commentaires et décorateurs
L'indentation vaut quatre espaces par défaut. Les commentaires standalone suivent le niveau du statement associé. Chaque décorateur occupe sa propre ligne :
@jit
@pure
compute = (x) => {
# Le commentaire reste dans le bloc.
x * 2
}
Un commentaire est admis par la grammaire entre n'importe quels tokens, et le formateur le préserve dans toutes ces
positions : au fil d'une expression multiligne, entre deux bras de match, entre deux clauses except, dans une chaîne
de méthodes, entre les paramètres d'une lambda, sur sa propre ligne au milieu d'une liste d'arguments ou d'une
collection. Un commentaire en fin de ligne reste en fin de ligne ; un commentaire seul garde sa ligne. Un seul cas est
déplacé : entre la condition d'un if/while et son bloc, le commentaire remonte au-dessus du statement — la position
d'origine n'existe pas dans la forme normalisée.
Le formateur relit sa propre sortie pour vivre. Tout ce qu'il perdrait le serait deux fois.
Les f-strings, b-strings et strings multilignes sont émises sans reformater leur contenu.
Lignes
- les lignes vides significatives sont conservées, avec au plus deux sauts consécutifs ;
- les lignes vides initiales sont supprimées ; après un shebang, une ligne vide laissée par l'auteur est conservée (une au plus) ;
- le fichier se termine par un seul saut de ligne ;
- une continuation est réunie si le groupe tient dans
line_length; - une virgule terminale avant
)ou]maintient le groupe en disposition verticale ; - sans virgule terminale, un premier argument placé à la ligne conserve la disposition multilignes ;
- ces deux règles valent aussi pour une liste de paramètres, celle d'une lambda comme celle d'une méthode ;
- un dict
{}en disposition verticale garde sa première entrée sur la ligne de l'accolade — une accolade suivie d'un retour à la ligne se relirait comme un début de bloc ; - les concaténations de littéraux string déjà multilignes et les délimiteurs fermants empilés restent séparés ;
line_lengthse mesure en colonnes d'affichage (UAX #11), pas en octets ni en caractères : un accent vaut une colonne, un idéogramme ou un emoji en vaut deux.
# La virgule terminale conserve ce layout.
points = list(
Point(0, 0),
Point(1, 1),
)
# Sans virgule, cette forme reste inline si elle tient.
points = list(Point(0, 0), Point(1, 1))
Quand une ligne dépasse la limite, le formatteur coupe en priorité après une virgule, avant un opérateur binaire ou
=>, puis après un délimiteur ouvrant. La continuation reçoit un niveau d'indentation supplémentaire.
Pour une signature, la largeur mesurée comprend ce qui suit les paramètres — ), le type de retour, => — et pas
seulement les paramètres : la coupure survient donc plus tôt qu'une lecture des seuls paramètres le laisserait croire.
Seule l'accolade ouvrante du corps reste hors de la mesure, ce qui autorise un dépassement d'une colonne quand la
signature tombe exactement sur la limite.
Alignement en colonne
Avec align = true, un alignement déjà présent sur les deux premières lignes d'un groupe peut être conservé pour les
=> des bras de match et les commentaires trailing. Les deux premières lignes valent pour le groupe entier : une fois
l'intention détectée, l'alignement s'applique à toutes ses lignes, y compris celles qui ne l'étaient pas — un bras
ajouté plus long réaligne l'ensemble. En CLI, --align et --no-align forcent le comportement dans un sens ou dans
l'autre ; sans l'un ni l'autre, la valeur vient de la configuration. L'option ne crée pas un alignement pour tous les
groupes :
match value {
1 => { "one" }
22 => { "twenty-two" }
other => { "other" }
}
x = 1 # first
longer = 2 # second
Une ligne vide, un changement d'indentation ou une string multilignes termine le groupe. Les affectations = ne sont
pas alignées afin qu'un renommage ne modifie pas les lignes voisines.
Les colonnes se comptent en largeur d'affichage, ni en octets ni en caractères : un accent occupe une colonne, un idéogramme ou un emoji en occupe deux (UAX #11). Deux motifs qui s'affichent sur la même largeur reçoivent donc le même remplissage, quel que soit leur nombre de caractères.
match sonde {
'température' => { -50.0 }
'pression' => { 950.0 }
}
match etiquette {
'日本' => { 1 }
'abcd' => { 2 }
'x' => { 3 }
}
API Python
from catnip._rs import FormatConfig
from catnip.tools import format_code
formatted = format_code('x=1+2*3')
compact = format_code(source, FormatConfig(indent_size=2, line_length=80))
Pipeline
Le convertisseur catnip_tools/src/pretty/ transforme le CST en algèbre de documents (Text, Line, Nest, Group,
Verbatim). Le layout choisit pour chaque groupe la forme plate ou cassée, puis un post-traitement aligne => et #.
Les annotations de ligne relient le résultat au source sans reconstruire une seconde représentation syntaxique.
Cette architecture suit les travaux de Philip Wadler, Christian Lindig et Daan Leijen sur les pretty-printers algébriques.
Fichier que le formateur ne sait pas lire
Un source dont la syntaxe est refusée n'est pas reformaté : la commande affiche l'erreur et sa position, sort en code 1,
et -i laisse le fichier tel quel. Un arbre de syntaxe incomplet ne décrit pas ce que le fichier contient, et imprimer
depuis cet arbre modifie le texte au lieu de le mettre en forme.
Le formateur travaille donc sur des fichiers entiers et syntaxiquement complets, pas sur des fragments : une liste d'arguments ou une parenthèse isolée n'est pas un programme.
Limitations
- certaines combinaisons complexes autour de
.[peuvent conserver une espace superflue ; - une jointure suivie d'une nouvelle coupure peut empêcher l'idempotence sur quelques dispositions source-aware.
Le formatteur ajuste le texte sans négocier avec la structure logique. La structure n'a pas ouvert de guichet.