Formatteur de code
Sommaire
- Vue d'ensemble
- Utilisation CLI
- Formater un fichier
- Options
- Formater depuis stdin
- Règles de formatage
- Espaces
- Indentation
- Commentaires
- F-strings et b-strings
- Newlines
- Jointure de lignes
- Coupure de lignes longues
- Exemples
- Code mal formaté
- Code formaté
- Pattern matching
- Broadcasting et listes
- Alignement en colonne
- Symboles alignés
- Groupement
- Configuration
- Architecture interne
- 1. Conversion CST → Doc
- 2. Layout greedy
- 3. Préservation d'intention source
- 4. Alignement en colonne (post-processing)
- 5. Normalisation finale
- Références
- Utilisation programmatique
- Formatteur avec options
- Intégration éditeurs
- VSCode
- Vim/Neovim
- Pre-commit hook
- Limitations connues
Outil de formatage automatique du code Catnip avec un style opinionated.
Vue d'ensemble
Le formatteur catnip format applique un style cohérent sur tout le code Catnip. Il utilise un pretty-printer
Wadler-Leijen qui reconstruit le code depuis l'arbre syntaxique (tree-sitter CST) en préservant :
- Les commentaires (inline et standalone)
- Les newlines intentionnelles (pas de reformatage destructif)
- La structure du code (seuls les espaces et l'indentation sont ajustés)
Le style appliqué s'inspire de Black (Python) : un seul style, pas de configuration, zéro débat.
Code invalide : le formatteur opère sur l'arbre syntaxique. Si le code ne parse pas correctement (noeuds ERROR dans tree-sitter), le texte source est préservé tel quel sans tentative de normalisation partielle.
Utilisation CLI
Formater un fichier
# Formater et afficher sur stdout
catnip format script.cat
# Formater en place
catnip format -i script.cat
# Formater un dossier (récursif, tous les .cat)
catnip format -i src/
# Si aucun .cat trouvé : affiche "No .cat files found"
# Vérifier le formatage (CI)
catnip format --check src/
# Afficher le diff
catnip format --diff script.cat
Options
catnip format -l 80 script.cat # Line length (défaut: 120)
catnip format --indent-size 2 src/ # Taille d'indentation (défaut: 4)
catnip format --align src/ # Forcer --align (activé par défaut)
Formater depuis stdin
echo 'x=1+2*3' | catnip format --stdin
cat script.cat | catnip format --stdin
Règles de formatage
Espaces
Opérateurs binaires : espace avant et après
# Avant
x=1+2*3
y==42
# Après
x = 1 + 2 * 3
y == 42
Opérateurs unaires : pas d'espace entre l'opérateur et l'opérande
# Avant
x = - 1
y = a + - b
z = + x
w = ~ a
# Après
x = -1
y = a + -b
z = +x
w = ~a
not keyword : espace avant (sauf début de ligne) et espace après
# Avant
a and notb
notx
# Après
a and not b
not x
Assignation et lambda
# Avant
x=42
f=(n)=>{n*2}
# Après
x = 42
f = (n) => { n * 2 }
Keyword arguments : pas d'espace autour de = dans les appels
# Avant
dict(name = "catnip", version = "0.1")
f(timeout = 5.0)
# Après
dict(name="catnip", version="0.1")
f(timeout=5.0)
L'assignation normale (x = 1) conserve ses espaces. La distinction est automatique : = entre parenthèses = keyword
argument.
Méthodes et attributs après DOT : pas d'espace
# Les keywords utilisés comme noms de méthode ne sont pas espacés
re.match(pattern, text) # pas: re. match (pattern, text)
obj.return_value # pas: obj. return_value
Virgules : espace après, pas avant
# Avant
list(1,2,3)
func(a,b,c)
# Après
list(1, 2, 3)
func(a, b, c)
Keywords en position valeur : pas d'espace avant , ou ;
# (op, a, b) → préservé tel quel (pas "op , a, b")
# struct S { op; x; } → pas "op ; x ;"
Point-virgules : espace après, pas avant
# Avant
x = 1;y = 2;z = 3
# Après
x = 1; y = 2; z = 3
Parenthèses : pas d'espace intérieur
# Avant
func( x , y )
( a + b )
# Après
func(x, y)
(a + b)
Broadcasting : pas d'espace après .[, espace entre opérateur et opérande
# Avant
numbers.[ * 2 ]
data.[ if > 10 ]
data.[~>abs]
# Après
numbers.[* 2]
data.[if > 10]
data.[~> abs]
Fullslice : pas d'espace autour des :
# Avant
data.[ 1 : 3 ]
data.[-2 : ]
# Après
data.[1:3]
data.[-2:]
Indentation
Blocs : 4 espaces par niveau
# Avant
if x{
y=1
}
# Après
if x {
y = 1
}
Instanciation de structs : pas d'espace avant {
# Les blocs de contrôle gardent l'espace
if x { ... }
while y { ... }
struct Point { x; y; }
# Les instanciations de structs n'ont pas d'espace
Point{1, 2}
f(Point{x, y}, other)
Structures imbriquées
# Avant
match x{
1=>{print("one")}
_=>{
if y{print("other")}
}
}
# Après
match x {
1 => {print("one")}
_ => {
if y {print("other")}
}
}
Commentaires
Commentaires inline : exactement 2 espaces avant #
# Avant
x = 42# important
y = 1,# trailing
# Après
x = 42 # important
y = 1, # trailing
Commentaires standalone : indentation normale
# Avant
if x {
# case one
y = 1
}
# Après
if x {
# case one
y = 1
}
F-strings et b-strings
Les f-strings et b-strings sont preservees intactes (pas de reformatage du contenu) :
x = f"hello {name}" # préservé
y = b'raw bytes' # préservé
Newlines
Lignes vides preservees : les sauts de ligne intentionnels du codeur sont respectes.
Maximum 2 newlines consecutives : les sequences de 3+ newlines sont reduites a 2.
Pas de newlines en début de fichier : les lignes vides initiales sont supprimées.
Une seule newline en fin de fichier.
Décorateurs isolés sur leur propre ligne : chaque @decorator est placé seul sur sa ligne, comme en Python.
# Avant
@jit f = (n) => { n * 2 }
@jit @pure g = (x) => { x + 1 }
# Après
@jit
f = (n) => { n * 2 }
@jit
@pure
g = (x) => { x + 1 }
Cette règle s'applique aux décorateurs d'assignments (@jit, @pure, @cached) et aux décorateurs de méthodes dans
les structs/traits (@abstract, @static).
Jointure de lignes
Les lignes de continuation qui tiennent dans line_length sont jointes automatiquement :
# Avant (coupure manuelle inutile si line_length=120)
f(aaaa,
bbbb,
cccc)
# Après
f(aaaa, bbbb, cccc)
La jointure détecte les continuations par :
- Fin de ligne :
,,(,[,+,-,*,/,and,or,=> - Début de ligne suivante :
.,+,-,),],and,or
Les lignes vides (séparateurs de sections) ne sont jamais jointes.
Magic trailing comma
Une virgule terminale avant ) ou ] force le maintien de la disposition multilignes, même si tout tiendrait sur une
seule ligne.
# Virgule terminale -> multilignes préservé
data = dict(
a=1,
b=2,
)
# Fonctionne aussi avec des commentaires trailing
points = list(
Point(0, 0),
Point(1, 1),
Point(2, 2), # doublon
)
Ce comportement s'inspire de Black (Python) et Prettier (JS) : la virgule terminale est un signal explicite du développeur pour conserver la disposition verticale.
Disposition source-aware (sans trailing comma)
Sans virgule terminale, le formatteur respecte le choix du développeur :
- Premier argument sur la même ligne que
(→ le formatteur garde le layout inline. Les coupures se font à l'intérieur des groupes imbriqués si nécessaire. - Premier argument sur la ligne suivante → le formatteur force le multiline (chaque argument sur sa propre ligne).
# Inline -> reste inline (breaks internes si nécessaire)
report = SalesReport(list(
Item("a", 10),
Item("b", 20)
))
# Multiline -> reste multiline
data = dict(
a=1,
b=2
)
# Inline court -> reste inline
f(a, b)
Ce mécanisme s'applique aux appels de fonction, list(), tuple(), set() et dict().
Concaténation de strings multilignes
Les concaténations de strings explicitement réparties sur plusieurs lignes sont préservées. Quand les deux opérandes du
+ sont des littéraux string, le formatteur considère que la coupure est intentionnelle (lisibilité, SQL, HTML...) :
# Préservé tel quel
query = "SELECT * FROM users " +
"WHERE active = 1 " +
"ORDER BY name"
# Les concaténations non-string sont toujours jointes si elles tiennent
x = a +
b
# → x = a + b
Fermetures empilées
Les délimiteurs fermants empilés à des niveaux d'indentation différents sont préservés sur des lignes séparées :
# Préservé (pas de fusion en "))") :
users = list(
dict(
name="Alice",
),
)
Coupure de lignes longues
Les lignes qui dépassent line_length sont coupées automatiquement aux meilleurs points :
# Avant (line_length=40)
f(aaaa, bbbb, cccc, dddd, eeee)
# Après
f(aaaa, bbbb, cccc, dddd,
eeee)
Points de coupure par ordre de priorité :
- Après
,dans un contexte parenthèse/crochet - Avant opérateur binaire (
+,-,and,or...) - Avant
=> - Après
(/[d'ouverture
La ligne de continuation est indentée d'un niveau supplémentaire.
Exemples
Code mal formaté
# Factorial
factorial =( n ) => {
if n<=1{return 1}
else{return n*factorial(n-1)} # recursive
}
x=factorial( 5 )
print(x)
Code formaté
# Factorial
factorial = (n) => {
if n <= 1 { return 1}
else { return n * factorial(n - 1)} # recursive
}
x = factorial(5)
print(x)
Pattern matching
# Avant
check=(x)=>{
match x{
1|2|3=>{print("small")}
n if n>10=>{print("big")} # guard
_=>{print("other")}
}
}
# Après
check = (x) => {
match x {
1 | 2 | 3 => {print("small")}
n if n > 10 => {print("big")} # guard
_ => {print("other")}
}
}
Broadcasting et listes
# Avant
numbers=list(1,2,3,4,5)
doubled=numbers.[*2]
filtered=numbers.[if>3]
# Après
numbers = list(1, 2, 3, 4, 5)
doubled = numbers.[* 2]
filtered = numbers.[if > 3]
Alignement en colonne
Le formatteur aligne les symboles => (match arms) et # (commentaires trailing) sur les groupes de lignes
consécutives. L'alignement des assignations = est désactivé : il casse dès qu'on renomme une variable et crée du bruit
dans les diffs git.
Symboles alignés
Match arms (=>) — alignement des flèches dans les bras de match
match x {
1 => { "one" }
22 => { "twenty-two" }
other => { "other" }
}
Commentaires trailing (#) — alignement des # inline (les commentaires standalone sont ignorés)
x = 1 # first
longer = 2 # second
Groupement
Un groupe est une suite de lignes consécutives non-vides, au même niveau d'indentation, contenant toutes le symbole cible. Une ligne vide, un changement d'indentation, ou une ligne à l'intérieur d'un string literal multilignes casse le groupe. Minimum 2 lignes pour déclencher l'alignement.
Configuration
Activé par défaut. Désactivable via TOML :
[format]
align = false
Architecture interne
Le formatteur est un pretty-printer algébrique Wadler-Leijen, implémenté en Rust dans catnip_tools/src/pretty/. Le
pipeline a 4 étapes :
Tree-sitter CST
│
▼
convert.rs (dispatch récursif, ~50 types de nœuds)
│
▼
Doc (arena-allocated document algebra)
│
▼
layout.rs (Leijen greedy best-fit, O(n))
│
▼
(String, Vec<usize>) ─── texte + line_map
│
▼
align.rs (post-processing optionnel)
│
▼
String finale
1. Conversion CST → Doc
Le convertisseur parcourt récursivement l'arbre Tree-sitter et produit un document algébrique. Chaque type de nœud a un traitement dédié :
- Expressions (
convert_expr.rs) : binaires (opérateur en fin de ligne en mode break), unaires, appels, chaînes d'accès, collections, kwargs - Statements (
convert_stmt.rs) : if/elif/else, while, for, match/patterns, pragma - Déclarations (
convert_decl.rs) : lambda, struct, trait avec champs/méthodes/extends/implements - Commentaires : rattachés au nœud précédent (trailing) ou émis en standalone selon la position source
L'algebra documentaire (doc.rs) fournit les primitives : Text, Line, SoftLine, HardLine, Nest, Group,
Concat, Verbatim, SourceLine. Les combinateurs dérivés (combinators.rs) : bracket, surround, intersperse,
comma_line.
2. Layout greedy
L'algorithme de Leijen (stack-based, itératif) décide pour chaque Group : flat ou break.
- Mesure la largeur flat via
flat_width()avec short-circuit - Si flat tient dans la largeur restante :
Line→ espace,SoftLine→ rien - Sinon break :
Line/SoftLine→\n+ indentation courante HardLineforce toujours un saut de ligneVerbatimest émis tel quel (strings multilignes)
Le line_map est construit naturellement pendant le layout via les annotations SourceLine.
3. Préservation d'intention source
Le formatteur respecte les choix explicites du programmeur :
- Trailing comma magic : une virgule avant
)force le mode multiline - Disposition source-aware : le premier argument inline → inline préservé ; premier argument à la ligne → multiline préservé (appels, collections, dicts)
- If/elif/else :
} else {sur la même ligne ou}\nelse {sur des lignes séparées -- le choix source est préservé - Method chains :
obj.method(...)sur une ligne reste sur une ligne ; les breaks source entre membres sont préservés avec indentation - Struct body : les champs/méthodes respectent le layout source (inline
x; y;, multiline, blank lines entre champs et méthodes) - Blocs multilignes : un bloc
{ }qui occupe plusieurs lignes dans la source reste multiline - String concat multiline :
"a" +\n "b"où les deux opérandes sont des strings reste sur plusieurs lignes - Blank lines : un gap ≥ 2 lignes entre statements produit une ligne vide
- Semicolons struct : les
;sur les champs sont préservés si présents dans la source, pas ajoutés sinon
4. Alignement en colonne (post-processing)
Compare le texte formaté au source original via le line_map. Aligne => (match arms) et # (commentaires trailing)
sur les groupes de lignes consécutives au même indent. L'alignement des = est désactivé.
5. Normalisation finale
- Suppression des newlines en début de fichier
- Maximum 2 newlines consécutives
- Suppression du trailing whitespace sur les lignes vides
- Exactement une newline en fin de fichier
Références
- Wadler, "A prettier printer" (1998)
- Lindig, "Strictly Pretty" (2000)
- Leijen, "PPrint, a prettier printer" (2001)
Utilisation programmatique
from catnip.tools import format_code
from catnip._rs import FormatConfig
source = """
x=1+2*3 # no spaces
y = 42 # too many
"""
formatted = format_code(source)
print(formatted)
# Output:
# x = 1 + 2 * 3 # no spaces
# y = 42 # too many
Formatteur avec options
config = FormatConfig(indent_size=2, line_length=80)
formatted = format_code(source, config)
# Avec alignement en colonne
config = FormatConfig(align=True)
formatted = format_code(source, config)
Intégration éditeurs
VSCode
Créer .vscode/settings.json :
{
"[catnip]": {
"editor.defaultFormatter": "catnip-formatter",
"editor.formatOnSave": true
}
}
Vim/Neovim
" Format on save
autocmd BufWritePre *.cat !catnip format % > %.tmp && mv %.tmp %
" Format selection
vnoremap <leader>f :!catnip format --<CR>
Pre-commit hook
.git/hooks/pre-commit :
#!/bin/bash
for file in $(git diff --cached --name-only --diff-filter=ACM | grep '\.cat$'); do
catnip format "$file" > "$file.tmp" && mv "$file.tmp" "$file"
git add "$file"
done
Limitations connues
- Broadcasting edge cases :
.[peut avoir une espace superflue dans certains cas complexes - Jointure non-idempotente : formatter deux fois un code avec des lignes jointes puis recoupees peut donner un resultat different (les coupures de ligne ne reproduisent pas forcement l'indentation originale)
Le formatteur ajuste les espaces et l'indentation, pas la structure logique du code.