Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
73969cc
fix(rules): revive use-pipe-transform-interface via file-level PipeTr…
ksom Aug 9, 2026
5b64694
fix(rules): revive contextual-lifecycle with export-aware patterns
ksom Aug 9, 2026
0a319c5
fix(rules): make no-component-suffix decorator-aware
ksom Aug 9, 2026
210c523
fix(rules): make no-directive-suffix decorator-aware
ksom Aug 9, 2026
0a82737
fix(rules): make no-service-suffix decorator-aware
ksom Aug 9, 2026
5b4912e
fix(rules): make prefer-inject-function decorator-aware
ksom Aug 9, 2026
b463c0e
fix(rules): prefer-standalone only flags explicit standalone: false
ksom Aug 9, 2026
d5d006e
feat(rules)!: remove sort-ngmodule-metadata-arrays
ksom Aug 9, 2026
7eab9ef
fix(rules): no-empty-lifecycle-method matches : void and ngOnChanges(…
ksom Aug 9, 2026
050d713
fix(rules): no-output-on-prefix matches typed and definite-assignment…
ksom Aug 9, 2026
7bad0d8
fix(rules): prefer-model-signal requires a paired @Input()
ksom Aug 9, 2026
771c8a1
fix(cli)!: emit valid Biome config; drop fictional severity mapping
ksom Aug 9, 2026
60f2bf7
fix(pkg): ship rules-to-migrate.json and docs, declare biome peer dep…
ksom Aug 9, 2026
cd207c2
test(integration): fail loudly when biome output is unusable
ksom Aug 9, 2026
4b288f5
test(integration): add packed-tarball consumer smoke test
ksom Aug 9, 2026
4ccdb51
ci: run integration tests, track latest Biome, wire up semantic-release
ksom Aug 9, 2026
daf034f
test(unit): align reference-spec layer with shipped rule semantics
ksom Aug 9, 2026
ea35ce6
docs: describe actual consumption path, severities, and engine limita…
ksom Aug 9, 2026
85cdddd
fix(rules): use-lifecycle-interface checks interfaces at file level
ksom Aug 9, 2026
099def6
test(integration): assert diagnostic messages and add real-world fixt…
ksom Aug 9, 2026
2014514
docs(rules): align all rule docs with shipped behavior
ksom Aug 9, 2026
136e870
chore: upgrade to Biome 2.5.5
ksom Aug 9, 2026
befed80
fix(rules): match every class and member shape with native AST nodes
ksom Sep 4, 2026
7100714
feat(rules): add a safe fix to no-empty-lifecycle-method
ksom Sep 4, 2026
a551583
feat(rules): add directive-selector
ksom Sep 4, 2026
d27f61a
feat(rules): add no-output-native
ksom Sep 4, 2026
cad5109
feat(rules): add no-conflicting-lifecycle
ksom Sep 4, 2026
e7861c3
feat(rules): add no-lifecycle-call
ksom Sep 4, 2026
b917477
feat(rules): add no-async-lifecycle-method
ksom Sep 4, 2026
8ef2542
feat(rules): add no-pipe-impure
ksom Sep 4, 2026
2060c6a
feat(rules): add no-attribute-decorator
ksom Sep 4, 2026
c7b8673
feat(presets): make the recommended preset Angular-safe
ksom Sep 4, 2026
829ab22
chore: require Biome 2.5, test on 2.5.12 and guard the minimum in CI
ksom Sep 4, 2026
85f64e9
docs: describe the Angular-safe preset, suppressions, fixes and patte…
ksom Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ jobs:
- name: Typecheck
run: npm run typecheck

- name: Run tests
- name: Unit tests (reference-spec layer)
run: npm run test:coverage

- name: Integration tests (real biome binary)
run: npm run test:integration

- name: Upload coverage to Codecov
if: matrix.node-version == '22'
uses: codecov/codecov-action@v5
Expand All @@ -47,6 +50,61 @@ jobs:
files: ./coverage/lcov.info
fail_ci_if_error: false

biome-latest:
name: Integration tests against latest Biome
runs-on: ubuntu-latest
# Early warning when a new Biome release changes plugin behavior.
# Non-blocking: a breaking upstream release should not fail unrelated PRs.
continue-on-error: true

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Install latest Biome
run: |
npm install --no-save @biomejs/biome@latest
npx biome --version

- name: Integration tests
run: npm run test:integration

biome-min:
name: Integration tests against minimum supported Biome (2.5.0)
runs-on: ubuntu-latest
# Guards the peerDependencies range: native AST node patterns and plugin
# rewrites both require Biome 2.5.0.

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Install Biome 2.5.0
run: |
npm install --no-save @biomejs/biome@2.5.0
npx biome --version

- name: Integration tests
run: npm run test:integration

lint:
name: Lint
runs-on: ubuntu-latest
Expand Down
36 changes: 16 additions & 20 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@ name: Release

on:
push:
tags:
- 'v*'
branches: [main]

permissions:
contents: write
contents: read

jobs:
release:
name: Publish to npm
name: semantic-release
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
id-token: write

steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@v5
Expand All @@ -27,22 +34,11 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Run all tests
run: npm run test:all

- name: Create GitHub Release
uses: actions/create-release@v1
- name: Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref_name }}
release_name: ${{ github.ref_name }}
body: |
See [CHANGELOG.md](https://github.com/ksom/biome-plugin-angular/blob/main/CHANGELOG.md) for details.
draft: false
prerelease: false
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
87 changes: 57 additions & 30 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,76 @@
# biome-plugin-angular

## Projet
Plugin Biome pour Angular 17-21. Règles TypeScript en GritQL pour Biome 2.x.
Plugin Biome pour Angular 17-22. Règles TypeScript en GritQL pour Biome 2.5+.
Port des règles @angular-eslint + nouvelles règles signal-based (Angular moderne).

## Commandes
- npm test : lance tous les tests (Vitest)
- npm test : tests unitaires (Vitest) — couche "reference spec" (pattern-matcher ts-morph)
- npm run test:integration : tests d'intégration avec le binaire biome réel (source de vérité)
- npm run test:all : les deux suites
- npm run lint : lint du projet

## Conventions
- Un commit par règle : "feat(rules): add {rule-name}"
- Chaque règle a : un .grit, un test, un doc .md, et une entrée dans les presets
- Un commit par règle : "feat(rules): add {rule-name}" / "fix(rules): …"
- Chaque règle a : un .grit, un test d'intégration, un doc .md, une entrée dans rules-to-migrate.json
- Utiliser les règles existantes comme template pour les nouvelles
- Toute nouvelle règle DOIT être vérifiée contre le binaire biome réel (test d'intégration), pas seulement le pattern-matcher

## Structure des règles
- rules/high-priority/ — règles critiques (prefer-standalone, no-empty-lifecycle-method)
- rules/conventions/ — conventions Angular (no-input-rename, component-selector, etc.)
- rules/quality/ — qualité de code (use-lifecycle-interface, contextual-lifecycle, etc.)
- rules/modern/ — APIs signal-based Angular 17+ (prefer-signal-inputs, prefer-inject-function, etc.)

## Commandes de test
- npm test : tests unitaires (Vitest, 217 tests)
- npm run test:integration : tests d'intégration avec le binaire biome réel (68 tests, 5 skipped)
- npm run test:all : les deux suites

## État actuel
- 20 règles dans biome-manifest.jsonc (format Biome 2.4.8+)
- 4 règles skippées dans rules-to-migrate.json (obsolètes ou inversées)
- Tests unitaires : 21 fichiers, 217 tests, tous verts
- Tests intégration : 20 fichiers, 68 tests verts, 5 skipped (limites GritQL)
- Catégorie "modern" : prefer-signal-inputs, prefer-output-function, prefer-signal-queries,
prefer-model-signal, prefer-host-property, prefer-inject-function

## Limitations GritQL (Biome 2.4.8)
GritQL ne supporte pas le pattern `@Decorator\nclass` comme une unité. Conséquences :

**Règles heuristiques** (fonctionnent mais sans vérification du décorateur) :
- no-component-suffix : flag toute classe `*Component` (pas seulement @Component)
- no-directive-suffix : flag toute classe `*Directive` (pas seulement @Directive)
- no-service-suffix : flag toute classe `*Service` (pas seulement @Injectable)
- prefer-inject-function : flag tout constructor avec access modifiers (pas seulement Angular)

**Règle limitée** :
- sort-ngmodule-metadata-arrays : détecte la présence d'arrays, pas leur ordre de tri
- 26 règles dans biome-manifest.jsonc, toutes fonctionnelles et décorateur-aware
- Angular 17-22, Biome >= 2.5.0 (peerDep) — dev sur 2.5.12
- sort-ngmodule-metadata-arrays supprimée (GritQL ne peut pas comparer l'ordre d'une liste)
- prefer-standalone ne signale que standalone: false (défaut standalone depuis Angular 19)
- contextual-lifecycle aligné sur upstream : @Injectable/@Pipe n'autorisent que ngOnDestroy ;
@Component ET @Directive autorisent tous les hooks
- prefer-inject-function signale TOUT paramètre de constructeur d'une classe décorée
(aligné sur @angular-eslint/prefer-inject), pas seulement ceux avec modificateur
- Tests d'intégration : source de vérité, incluent un smoke test consommateur (npm pack → install → lint)
et tests/integration/native-shapes.test.ts (implements, readonly, export default, --write)
- Consommation : "extends": ["biome-plugin-angular/presets/recommended.json"] (recommandé)
Le preset = plugin scoped **/*.ts (hors *.spec.ts) + unsafeParameterDecoratorsEnabled
+ style/useImportType off. Sans ces deux réglages Biome : parse error sur @Inject(),
et `biome lint --write` casse le build Angular (import type → NG2003)
- Sévérités fixées dans les .grit (register_diagnostic) — linter.rules.plugins N'EXISTE PAS dans Biome
- Messages préfixés "[rule-name] …" (les métavariables ne sont PAS interpolées dans message) ;
suppression : // biome-ignore lint/plugin/<rule-name>: raison
- Auto-fix : seul no-empty-lifecycle-method, scindé en 2 fichiers de même stem
(rules/high-priority/no-empty-lifecycle-method.grit = classe implémentant l'interface, pas de
fix ; rules/high-priority/fixes/no-empty-lifecycle-method.grit = fix safe). Biome apparie
rewrites et diagnostics par ordre d'apparition → impossible de mélanger branches fixables et
non fixables dans un même pattern. Bug Biome : un diagnostic supprimé (biome-ignore) est
quand même réécrit par --write si un autre match du même fichier est fixé.
Les autres migrations pointent vers les schematics officiels (ng generate @angular/core:…)

**Règles non-fonctionnelles** (compilent mais ne détectent pas les violations) :
- contextual-lifecycle : nécessite le type de décorateur pour le contexte
- use-pipe-transform-interface : nécessite @Pipe + class comme unité
## GritQL (Biome 2.5+) — règles d'écriture de patterns (vérifiées empiriquement)
- TOUJOURS matcher classes et membres via les nœuds AST natifs Biome (PascalCase, champs du
js.ungram) : JsClassDeclaration(decorators, id, implements_clause, members),
JsExport(decorators, export_clause = JsClassDeclaration(...)),
JsExportDefaultDeclarationClause(declaration = JsClassExportDefaultDeclaration(...)),
JsPropertyClassMember(modifiers, name, value), JsMethodClassMember(name, body),
JsConstructorClassMember(parameters), JsFunctionBody(statements), JsCallExpression(callee)
→ couvre implements, extends, export default, readonly, override, async…
Les décorateurs placés avant `export` sont sur JsExport, d'où 3 branches par règle
- `$x <: within JsClassDeclaration(implements_clause = $impl)` remonte à la classe englobante
- Champ optionnel absent (implements_clause) : bindé quand même, `not contains` passe
- Snippets : n'utiliser que pour décorateurs/appels simples ; un snippet méthode ne matche pas
les modificateurs `override`/`protected` ; `async $hook() { $_ }` OK avec $hook en nom
- Regex = full match obligatoire (`r"^on[A-Z].*"`), `(?s)` pour multi-lignes ; groupes non
capturants `(?:)` sauf capture avec variable `r"(.*)Change$"($base)`
- `pattern foo($body) {…}` : NE PAS utiliser — `$body <: contains …` échoue silencieusement
depuis l'appelant. Inliner le `or`
- message = chaîne statique (pas d'interpolation de $var)
- Rewrite : `$m => .` supprime ; fix_kind = "safe"|"unsafe" dans register_diagnostic ;
écrire le rewrite AVANT register_diagnostic quand le pattern utilise `within` ;
supprimer un membre d'objet laisse une virgule orpheline (syntax error)
- Suppression : le commentaire biome-ignore doit être sur la ligne juste au-dessus du nœud
signalé (entre le décorateur et `export class` pour un diagnostic sur le nom de classe)
- Diagnostics plugin : category "plugin" dans le JSON, nom du plugin = stem du .grit
(suppression lint/plugin/<stem>)
- Bench rapide : dossier temp avec biome.json { plugins: [./x.grit] } + `biome lint --reporter=json`
59 changes: 51 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ tests/
rules/ # One unit test per rule
integration/ # One integration test per rule (real biome binary)
docs/rules/ # One .md file per rule
presets/ # recommended.json, strict.json
biome-manifest.jsonc # Biome 2.4.8+ plugin manifest
presets/ # recommended.json (plugin + Angular-safe Biome settings)
biome-manifest.jsonc # Biome 2.5+ plugin manifest
cli.js # CLI entrypoint
```

Expand All @@ -66,14 +66,57 @@ cli.js # CLI entrypoint
}
```

**GritQL constraints** (Biome 2.4.8):
**Match classes and members through Biome's native AST nodes**, not code
snippets. Snippets only match the exact shape they spell out (a class snippet
misses `implements`, a method snippet misses `override`), while native nodes
expose every field:

```grit
or {
JsClassDeclaration(decorators = $decorators, id = $name, members = $members),
JsExport(decorators = $decorators, export_clause = JsClassDeclaration(id = $name, members = $members)),
JsExport(decorators = $decorators, export_clause = JsExportDefaultDeclarationClause(declaration = JsClassExportDefaultDeclaration(id = $name, members = $members)))
} where {
$decorators <: contains `@Component($args)`,
...
}
```

Decorators written before `export` belong to the JsExport node, hence the
three branches. Field names come from Biome's grammar
(`xtask/codegen/js.ungram`): `JsPropertyClassMember(modifiers, name, value)`,
`JsMethodClassMember(name, body)`, `JsConstructorClassMember(parameters)`,
`implements_clause`, `JsFunctionBody(statements)`…

**GritQL constraints** (Biome 2.5, verified against the binary):
- No `/* */` block comments (causes compilation failure)
- No `//` inline comments inside `or {}` / `where {}` blocks
- Use non-capturing regex groups `(?:...)` instead of `(...)`
- Regex must match full node text (e.g. `r"^on[A-Z].*"` not `r"^on[A-Z]"`)
- `@Decorator\nclass` patterns do not match at runtime

3. **Register the rule** in `biome-manifest.jsonc`.
- Regex must match the full node text (e.g. `r"^on[A-Z].*"` not `r"^on[A-Z]"`);
use `(?s)` to span lines. Unused capture groups must be non-capturing `(?:...)`;
a capturing group needs a variable: `r"(.*)Change$"($base)`
- Diagnostic messages are static: metavariables are NOT interpolated. Start
every message with the rule name in brackets, `[rule-name] …`, so users can
write `biome-ignore lint/plugin/rule-name` (the plugin name is the .grit file stem)
- `pattern name($x) { … }` definitions are unreliable: a list bound inside a
definition cannot be searched with `contains` from the caller. Inline the `or`
- Snippets with a call need one variant per argument shape: `output()`,
`output($a)`, `output<$t>()`, `output<$t>($a)`… or match `JsCallExpression(callee = $c)`
- An absent optional field (`implements_clause` on a class without one) still
binds, and `not contains` on it succeeds
- Rewrites: `$node => .` deletes the node; declare `fix_kind = "safe"` in
`register_diagnostic` for `--write` to apply it, and write the rewrite BEFORE
`register_diagnostic` when the pattern uses `within`. Deleting an object
member leaves a dangling comma (syntax error) — do not rewrite object literals
- Biome pairs rewrites with diagnostics by order of appearance, so one pattern
cannot mix fixable and non-fixable branches. Split such a rule in two files
with the SAME stem (`rules/<cat>/<rule>.grit` + `rules/<cat>/fixes/<rule>.grit`):
the plugin name, hence the suppression name, comes from the stem
- Parameter decorators (`@Inject(X) x`) are a parse error unless
`javascript.parser.unsafeParameterDecoratorsEnabled` is on (the preset and
the test runner set it)

3. **Register the rule** in `biome-manifest.jsonc`, `index.js` and
`tests/helpers/biome-runner.ts` (`RULE_PATHS`).

4. **Add the detection function** in `tests/helpers/pattern-matcher.ts`
using `ts-morph` to mirror the GritQL pattern for unit testing.
Expand Down
Loading
Loading