Formatteur de code

Outil de formatage automatique du code Catnip avec style opinionated.

Vue d'ensemble

Le formatteur catnip format applique un style de code cohérent et uniforme sur l'ensemble du code Catnip. Contrairement à un pretty-printer qui reconstruit le code depuis l'AST, le formatteur utilise une approche token-based qui préserve :

  • 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.

Utilisation CLI

Formater un fichier

# Formater et afficher sur stdout
catnip format script.cat

# Formater et sauvegarder (redirection)
catnip format script.cat > script_formatted.cat

# Formater en place (écrase le fichier)
catnip format script.cat > /tmp/temp.cat && mv /tmp/temp.cat script.cat

Formater depuis stdin

# Lecture stdin avec --
echo 'x=1+2*3' | catnip format --

# Pipe
cat script.cat | catnip format --

# Formater multiple fichiers
for f in *.cat; do catnip format "$f" > "${f}.tmp" && mv "${f}.tmp" "$f"; done

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

# Avant
- x
not  y

# Après
-x
not y

Assignation et lambda

# Avant
x=42
f=(n)=>{n*2}

# Après
x = 42
f = (n) => {n * 2}

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)

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 .[

# Avant
numbers.[ * 2 ]
data.[ if > 10 ]

# Après
numbers.[* 2]
data.[if > 10]

Indentation

Blocs : 4 espaces par niveau

# Avant
if x{
y=1
}

# Après
if x {
    y = 1
}

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 : minimum 2 espaces avant

# Avant
x = 42# important

# Après
x = 42  # important

Commentaires standalone : indentation normale

# Avant
if x {
# case one
y = 1
}

# Après
if x {
    # case one
    y = 1
}

Newlines

Maximum 2 newlines consécutives

# Avant
x = 1

y = 2

# Après
x = 1

y = 2

Une seule newline en fin de fichier

# Avant
x = 1

# Après
x = 1

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]

Architecture interne

Le formatteur utilise une approche token-based en 3 phases :

1. Tokenisation avec préservation des commentaires

from catnip._rs import TreeSitterParser

# Parser Rust Tree-sitter
parser = TreeSitterParser()
tokens = parser.tokenize(source)

# Chaque token contient : type, value, line, column, end_line, end_column
# Les commentaires sont extraits automatiquement depuis l'arbre de syntaxe

Le tokenizer Rust extrait directement les tokens depuis l'arbre Tree-sitter, préservant naturellement les commentaires comme nœuds de l'arbre syntaxique.

2. Application des règles de formatage

Le formatteur parcourt les tokens et applique les règles localement :

for token in tokens:
    # Indentation en début de ligne
    if at_line_start:
        result.append(' ' * (indent_level * 4))

    # Espace avant opérateur binaire ?
    if needs_space_before(token, prev_token):
        result.append(' ')

    # Token lui-même
    result.append(token.value)

    # Espace après virgule, opérateur, etc. ?
    if needs_space_after(token, next_token):
        result.append(' ')

Règles principales :

  • _needs_space_before() : opérateurs binaires, keywords
  • _needs_space_after() : virgules, =>, keywords
  • Gestion de l'indentation via LBRACE/RBRACE
  • Préservation des commentaires avec espacement minimal

3. Normalisation finale

# Max 2 newlines consécutives
formatted = re.sub(r'\n{3,}', '\n\n', formatted)

# Une seule newline en fin
formatted = formatted.rstrip('\n') + '\n'

Différences avec un pretty-printer AST

Critère Pretty-printer AST Formatteur token-based
Commentaires ✗ Perdus (ignorés par parser) ✓ Préservés
Newlines ✗ Reconstruites arbitrairement ✓ Respectées
Structure ✗ Reformatée complètement ✓ Ajustée localement
Fidélité ✗ Code reconstruit ✓ Code original préservé
Performance Plus lent (parse + transform) Plus rapide (lex only)

Le formatteur token-based est un formatter respectueux comme Black, pas un uglifier qui détruit le code.

Utilisation programmatique

from catnip.formatter import format_code

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

from catnip.formatter import TokenBasedFormatter

formatter = TokenBasedFormatter(indent_size=2)  # 2 espaces au lieu de 4
formatted = formatter.format(source)

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

  1. Pas de mode "check" : le formatteur reformate toujours, pas de vérification sans modification
  2. Pas d'option --in-place : nécessite redirection manuelle
  3. Broadcasting edge cases : .[ peut avoir un espace superflu dans certains cas complexes
  4. Blocs single-line : pas d'optimisation pour { return 1 } vs {\n return 1\n}

Ces limitations reflètent le principe : un seul style, pas de config, comportement prévisible.