Linter

Analyse statique du code Catnip : syntaxe, style et sémantique.

Vue d'ensemble

Le linter catnip lint effectue une analyse en trois phases pour détecter les problèmes avant exécution :

  1. Syntaxe : Le code respecte-t-il la grammaire ?
  2. Style : Le formatage suit-il les conventions ?
  3. Sémantique : Les variables sont-elles définies ? Les types cohérents ?

Chaque phase peut être exécutée indépendamment ou combinée.

Le linter ne se contente pas de valider que le code peut s'exécuter. Il vérifie qu'il mérite de s'exécuter.

Utilisation CLI

Analyse complète

# Toutes les vérifications (syntaxe + style + sémantique)
catnip lint script.cat

# Multiple fichiers
catnip lint *.cat
catnip lint src/*.cat tests/*.cat

Niveaux de vérification

# Syntaxe seulement (rapide)
catnip lint -l syntax script.cat

# Style seulement
catnip lint -l style script.cat

# Sémantique seulement
catnip lint -l semantic script.cat

# Tout (défaut)
catnip lint -l all script.cat

Depuis stdin

# Pipe
echo 'x = y + 1' | catnip lint --stdin

# Fichier via cat
cat script.cat | catnip lint --stdin

Mode verbose

# Affiche "OK" pour les fichiers sans problème
catnip -v lint script.cat

Codes de diagnostic

Les diagnostics suivent une convention de nommage :

  • Exxx : Erreurs (empêchent l'exécution)
  • Wxxx : Warnings (problèmes potentiels)

Syntaxe (E1xx)

Code Description
E100 Erreur de syntaxe générique
E101 Token inattendu
E102 Bracket/parenthèse non fermé
E103 String non terminée
$ echo 'x = 1 +' | catnip lint --
<stdin>:1:1: error [E100]: Unexpected end-of-input

Style (W2xx)

Code Description
W200 Ligne diffère du format attendu
W201 Espaces en fin de ligne
W202 Newline manquante en fin de fichier
W203 Indentation incohérente
$ echo 'x=1+2  ' | catnip lint -l style --
<stdin>:1:1: warning [W200]: Line differs from formatted version
<stdin>:1:6: warning [W201]: Trailing whitespace

Sémantique - Noms (E3xx/W3xx)

Code Description
E300 Nom non défini
E301 Fonction non définie
W310 Variable définie mais non utilisée
W311 Variable masquée (shadowing)
W312 Redéfinition d'un builtin
$ echo 'y = x * 2' | catnip lint --
<stdin>:1:1: error [E300]: Name 'x' is not defined
$ echo 'x = 42' | catnip lint --
<stdin>:1:1: warning [W310]: Variable 'x' is defined but never used

Sémantique - Types (W4xx)

Code Description
W400 Incompatibilité de type potentielle
W401 Code inatteignable
W402 Condition toujours vraie
W403 Condition toujours fausse

Sémantique - Patterns (E5xx/W5xx)

Code Description
E500 Match non exhaustif
W501 Pattern inatteignable
W502 Guard redondant

Exemples pratiques

Code avec erreur de syntaxe

$ cat broken.cat
factorial = (n) => {
    if n <= 1 { 1 }
    else { n * factorial(n - 1)  # missing }
}

$ catnip lint broken.cat
broken.cat:4:1: error [E101]: Unexpected token: '}'

Code avec problèmes sémantiques

$ cat issues.cat
x = 42
y = z + 1
result = x * 2

$ catnip lint issues.cat
issues.cat:1:1: error [E300]: Name 'z' is not defined
issues.cat:1:1: warning [W310]: Variable 'y' is defined but never used

Code propre

$ cat clean.cat
factorial = (n) => {
    if n <= 1 { 1 }
    else { n * factorial(n - 1) }
}
print(factorial(5))

$ catnip -v lint clean.cat
clean.cat: OK

No issues found

Intégration CI/CD

GitHub Actions

name: Lint
on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install catnip
      - run: catnip lint src/*.cat

GitLab CI

lint:
  image: python:3.12
  script:
    - pip install catnip
    - catnip lint **/*.cat

Pre-commit hook

.git/hooks/pre-commit :

#!/bin/bash
for file in $(git diff --cached --name-only --diff-filter=ACM | grep '\.cat$'); do
    if ! catnip lint -l syntax "$file"; then
        echo "Lint failed for $file"
        exit 1
    fi
done

Utilisation programmatique

API simple

from catnip.tools import lint_code, lint_file

# Depuis une string
result = lint_code('x = y + 1')
print(result.has_errors)  # True
print(result.summary())   # "1 error"

for diag in result.diagnostics:
    print(f"{diag.line}:{diag.column} [{diag.code.value}] {diag.message}")

# Depuis un fichier
from pathlib import Path
result = lint_file(Path('script.cat'))

Configuration fine

from catnip.tools import Linter

# Syntaxe seulement
linter = Linter(check_syntax=True, check_style=False, check_semantic=False)

# Style seulement
linter = Linter(check_syntax=False, check_style=True, check_semantic=False)

# Tout
linter = Linter()  # défaut: tout activé

result = linter.lint(source_code)

Accès aux diagnostics

from catnip.tools.linter import Severity, DiagnosticCode

result = lint_code(source)

# Filtrer par sévérité
errors = result.errors      # Severity.ERROR
warnings = result.warnings  # Severity.WARNING

# Filtrer par code
undefined = [d for d in result.diagnostics
             if d.code == DiagnosticCode.E300_UNDEFINED_NAME]

# Formater avec contexte
for diag in result.diagnostics:
    print(diag.format_rich(source.splitlines()))

Architecture interne

Le linter opère en trois phases séquentielles :

Phase 1 : Analyse syntaxique

Source → Tree-sitter Parser → Parse Tree (ou erreur)

Utilise le parser Tree-sitter avec la grammaire compilée. Les nœuds ERROR dans l'arbre sont convertis en diagnostics E1xx.

Si cette phase échoue (erreurs critiques), les phases suivantes sont ignorées.

Phase 2 : Analyse stylistique

Source → Formatter → Formatted Source
Source vs Formatted → Différences ligne par ligne

Compare le code source avec sa version formatée par catnip format. Les différences génèrent des warnings W2xx.

Détection additionnelle :

  • Trailing whitespace (regex sur chaque ligne)
  • Newlines en fin de fichier

Phase 3 : Analyse sémantique

Parse Tree → Transformer → IR
IR → Visitor → Diagnostics

Parcourt l'IR (Intermediate Representation) avec un visiteur qui :

  • Trace les scopes (push/pop sur pile)
  • Enregistre les définitions de variables
  • Détecte les références à des noms non définis
  • Collecte les variables non utilisées

L'analyseur sémantique connaît les builtins Python (print, len, range, etc.) et les builtins Catnip (pragma). Ils ne déclenchent pas d'erreur E300.

Différences avec l'exécution

Aspect Linter Runtime
Variables non définies Détecté statiquement CatnipNameError à l'exécution
Variables non utilisées Warning W310 Ignoré
Types Analyse limitée Erreur si incompatible
Pattern matching Analyse exhaustivité CatnipPatternError si aucun match
Performance Rapide (pas d'exécution) Dépend du code

Le linter détecte les problèmes certains (variables non définies) et les problèmes probables (variables non utilisées). Il ne peut pas détecter les erreurs qui dépendent des valeurs runtime.

Limitations

  1. Positions approximatives : Les positions ligne/colonne pour les erreurs sémantiques sont parfois imprécises (limitation du transformer IR)

  2. Analyse de flux limitée : Le linter ne suit pas les branches conditionnelles pour déterminer si une variable est toujours définie

  3. Pas d'inférence de types : L'analyse de types est minimale, pas de système de types complet

  4. Modules externes : Les modules chargés via pragma('feature', 'numpy') ne sont pas analysés

Ces limitations reflètent un choix : mieux vaut un linter rapide avec quelques faux négatifs qu'un analyseur complet qui prend 10 secondes sur chaque fichier.