loam.dev
v0.1.13 · previewEN

Rules

loam.dev bietet dreizehn Analyse-Fähigkeiten, alle hinter einem stabilenRule-Interface. Eine neue Rule hinzuzufügen ändert die Pipeline nicht — nur die Rule-Liste. Elf Rules sind heute live, zwei sind geplant und erscheinen als Post-MVP-Iterationen. Der Status ist immer ehrlich angegeben.

Live

unused-public-exportslive

Ungenutzte öffentliche Exports

Findet öffentliche API-Member — Klassen, Methoden, Getter/Setter, Felder, Enums, Typedefs — ohne Referenz im gesamten Projekt. Die Analyse läuft auf dem aufgelösten Element-Model, nicht per Regex. Code-Generator-Eingaben (Drift, freezed, Riverpod, json_serializable) werden automatisch ausgeschlossen, ohne jede Konfiguration.

Slop (schlecht)
// öffentlich, aber nirgendwo verwendet
class OldHelper {
  static String format(String s) => s.trim();
}

// ebenfalls unreferenziert — wird gemeldet
enum LegacyStatus { active, archived }
sauber
// entweder entfernen, privat machen oder
// mit Begründung unterdrücken:
// loam-ignore: unused-public-exports – re-exported via barrel

// genutzt und behalten:
class ActiveHelper {
  static String format(String s) => s.trim();
}
circular-dependencieslive

Zirkuläre Dependencies

Erkennt Import-Zyklen zwischen Dart-Bibliotheken. Zyklen machen die Kompilierungsreihenfolge mehrdeutig, verhindern Tree-Shaking und sind ein häufiges Zeichen unklarer Schichtverantwortlichkeiten.

Slop (schlecht)
// lib/a.dart
import 'b.dart';

// lib/b.dart
import 'a.dart'; // ← Zyklus: a → b → a
sauber
// Gemeinsame Typen in eine dritte Bibliothek auslagern:
// lib/shared.dart  (importiert weder a noch b)
// lib/a.dart  importiert shared.dart
// lib/b.dart  importiert shared.dart
code-duplicateslive

Code-Duplikate

loam.dev findet Duplikate im Code per AST-normalisiertem Token-Hashing — exakte Kopien (Typ-1) und strukturell identische Blöcke mit umbenannten Bezeichnern oder Literalen (Typ-2). Jedes Finding beschreibt einen vollständigen Cluster mit allen Fundstellen, sodass die Baseline die gesamte Gruppe als einen Eintrag verfolgt. Ein klassischer KI-Agent-Nebeneffekt: Hilfsfunktionen, die kopiert statt extrahiert wurden.

Slop (schlecht)
// feature_a/utils.dart
String formatDate(DateTime d) =>
    '${d.day}.${d.month}.${d.year}';

// feature_b/helpers.dart  ← Beinahe-Duplikat
String renderDate(DateTime d) =>
    '${d.day}.${d.month}.${d.year}';
sauber
// lib/shared/date_format.dart
String formatDate(DateTime d) =>
    '${d.day}.${d.month}.${d.year}';

// beide Features importieren den gemeinsamen Helper
// loam-ignore: code-duplicates – bewusste Symmetrie
complexity-hotspotslive

Komplexitäts-Hotspots

Misst zyklomatische und kognitive Komplexität je Funktion/Methode und aggregiert sie zu einem Projekt-Health-Score. Markiert Funktionen, die zu komplex zum Review oder Testen sind.

Slop (schlecht)
// kognitive Komplexität ≫ Schwellwert
Future<void> syncData(List<Item> items) async {
  for (final item in items) {
    if (item.isValid) {
      if (item.needsSync) {
        try {
          if (await remote.has(item.id)) {
            await remote.update(item);
          } else { await remote.create(item); }
        } catch (e) { /* geschluckt */ }
      }
    }
  }
}
sauber
// aufgeteilt in fokussierte, testbare Helpers
Future<void> syncData(List<Item> items) async {
  final candidates = items.where(_needsSync);
  await Future.wait(candidates.map(_syncOne));
}

Future<void> _syncOne(Item item) async { … }
a11y-form-field-labellive

Formularfeld ohne Label

Meldet Flutter-TextField- und TextFormField-Widgets, die weder ein decoration: InputDecoration(labelText: …) (noch hintText als Fallback) noch einen umschließenden Semantics(label: …)-Vorfahren haben. WCAG 1.3.1 (Info and Relationships), 3.3.2 (Labels or Instructions) und 4.1.2 (Name, Role, Value) verlangen, dass Formularfelder ein programmatisch bestimmbares Label haben. Die Analyse läuft auf dem aufgelösten Element-Model — lokale Klassen namens TextField werden nie gemeldet. Konservativ: ein decoration:-Wert, der keine direkte InputDecoration(…)-Erstellung ist, wird ignoriert. Rein AST-basiert: Laufzeitkontrast, Fokusreihenfolge, Touch-Target-Größen und dynamische Decoration-Werte sind außerhalb des Scope.

Slop (schlecht)
// keine Decoration — Screen Reader hat kein Label
TextField()

// InputDecoration vorhanden, aber ohne labelText oder hintText
TextField(
  decoration: InputDecoration(),
)
sauber
// labelText liefert das primäre zugängliche Label
TextField(
  decoration: InputDecoration(labelText: 'E-Mail-Adresse'),
)

// hintText als Fallback (z. B. Suchfelder)
TextField(
  decoration: InputDecoration(hintText: 'Suchen…'),
)

// oder mit Semantics umschließen
Semantics(
  label: 'E-Mail-Adresse',
  child: TextField(),
)
a11y-image-labellive

Bild ohne semantisches Label

Meldet Flutter-Image/Image.asset/Image.network/Image.file/Image.memory-Widgets ohne semanticLabel, die nicht per excludeFromSemantics: true aus dem Accessibility-Tree ausgeschlossen sind. WCAG 1.1.1 (Non-text Content) verlangt für jedes nicht-dekorative Bild eine Textalternative. Die Analyse läuft auf dem aufgelösten Element-Model — lokale Klassen namens Image werden nie gemeldet.

Slop (schlecht)
// kein semantisches Label — Screen Reader liest nichts
Image.asset('assets/hero.png')

// explizit leeres Label wird ebenfalls gemeldet
Image.network('https://example.com/photo.jpg',
  semanticLabel: '',  // ← leer = fehlend
sauber
// beschreibt, was das Bild vermittelt
Image.asset('assets/hero.png',
  semanticLabel: 'App-Hero-Illustration')

// rein dekorativ: explizit ausschließen
Image.asset('assets/divider.png',
  excludeFromSemantics: true)
a11y-icon-button-labellive

Icon-Button ohne zugänglichen Namen

Meldet Flutter-IconButton-Widgets ohne tooltip und ohne umschließendes Semantics(label: …), sowie GestureDetector/InkWell-Widgets, deren direktes child ein reines Icon-Widget ist und denen ebenfalls ein Semantics(label: …)-Vorfahre fehlt. WCAG 4.1.2 (Name, Role, Value) verlangt, dass jedes interaktive Steuerelement einen programmatisch bestimmbaren Namen hat. Die Analyse läuft auf dem aufgelösten Element-Model — lokale Klassen namens IconButton werden nie gemeldet. Rein AST-basiert: zusammengesetzte Icon-Kinder und dynamische tooltip-Werte sind außerhalb des Scope.

Slop (schlecht)
// kein tooltip — Screen Reader gibt nichts aus
IconButton(
  icon: Icon(Icons.close),
  onPressed: () => Navigator.pop(context),
)

// GestureDetector mit bloßem Icon-Kind — gleiches Problem
GestureDetector(
  child: Icon(Icons.delete),
  onTap: () => deleteItem(),
)
sauber
// tooltip liefert den zugänglichen Namen
IconButton(
  icon: Icon(Icons.close),
  tooltip: 'Schließen',
  onPressed: () => Navigator.pop(context),
)

// oder mit Semantics für GestureDetector/InkWell wrappen
Semantics(
  label: 'Element löschen',
  child: GestureDetector(
    child: Icon(Icons.delete),
    onTap: () => deleteItem(),
  ),
)
a11y-interactive-semanticslive

Interaktives Widget ohne Semantik

Meldet generische und benutzerdefinierte (nicht-Flutter-)Widgets, die einen onTap-, onPressed- oder onLongPress-Callback tragen, aber keinen umschließenden Semantics(label: …)-Vorfahren haben. WCAG 4.1.2 (Name, Role, Value) verlangt, dass jedes interaktive Steuerelement einen programmatisch bestimmbaren Namen hat. Die Analyse läuft auf dem aufgelösten Element-Model — Flutter-Built-in-Widgets, die bereits von Geschwister-Rules erfasst werden (IconButton, GestureDetector, InkWell, TextField, TextFormField), sind aus dieser Rule ausgeschlossen. Rein AST-basiert: außerhalb des Scope sind Widgets, die über vererbte Callbacks interaktiv werden, benutzerdefinierte Gesture-Recognizer, die die genannten Parameternamen umgehen, und Accessible-Name-Mechanismen außer Semantics(label: …).

Slop (schlecht)
// Custom-Karte ohne Semantics — Screen Reader hat keinen Namen
CustomCard(
  onTap: () => showDetails(item),
)

// Custom-Button mit onPressed — ebenfalls unbenannt
ActionChip(
  onPressed: () => doAction(),
)
sauber
// mit Semantics umschließen, um einen zugänglichen Namen zu geben
Semantics(
  label: 'Details anzeigen',
  button: true,
  child: CustomCard(
    onTap: () => showDetails(item),
  ),
)

// oder ein Widget wählen, das label nativ unterstützt
ActionChip(
  label: Text('Aktion ausführen'),
  onPressed: () => doAction(),
)
slop-empty-catchlive

Leerer oder nur-Kommentar-catch-Block

Meldet catch-Blöcke, deren Body leer ist ({}) oder nur Kommentare enthält — beide verschlucken Exceptions stillschweigend. Dies erweitert den eingebauten empty_catches-Lint (der nur den buchstäblich leeren Fall erkennt) um die Kommentar-only-Variante, die KI-Agenten häufig produzieren, wenn sie einen catch-Block mit einem Füll-Kommentar unterdrücken. Schweregrad: warning. Bodys mit einem rethrow, throw oder einem Logging-Aufruf werden nie gemeldet (konservative Allowlist). Generierte Dateien (.g.dart, .freezed.dart, …) werden automatisch übersprungen.

Slop (schlecht)
// buchstäblich leer — Exception verschwindet
try {
  await fetchData();
} catch (e) {}

// nur Kommentar — gleiches Problem, von empty_catches übersehen
try {
  await fetchData();
} catch (e) {
  // TODO: behandeln
}
sauber
// loggen und weiterwerfen — Kontext bleibt erhalten
try {
  await fetchData();
} catch (e, st) {
  log('fetchData fehlgeschlagen', error: e, stackTrace: st);
  rethrow;
}

// oder in einen Domain-Error einwickeln
try {
  await fetchData();
} catch (e) {
  throw DataLoadException(cause: e);
}
slop-unjustified-ignorelive

Unbegründete // ignore:-Direktive

Meldet // ignore:- und // ignore_for_file:-Direktiven ohne schriftliche Begründung — weder als Inline-Text nach den Lint-Namen noch als Kommentar in der vorherigen Zeile. Grundlose Unterdrückungsdirektiven sind ein häufiger KI-Agenten-Shortcut: der Agent bringt den Linter zum Schweigen, statt die eigentliche Ursache zu beheben. Schweregrad: info. Generierte Dateien (.g.dart, .freezed.dart, …) werden automatisch übersprungen.

Slop (schlecht)
// kein Grund angegeben — warum wird dieser Lint unterdrückt?
// ignore: avoid_print
print('debug output');

// Datei-weite Unterdrückung ohne Begründung
// ignore_for_file: prefer_const_constructors
sauber
// Inline-Begründung: erklärt Reviewern, warum die Unterdrückung beabsichtigt ist
// ignore: avoid_print – CLI-Ausgabepfad, kein Debug-Statement
print('done.');

// Begründung in der Zeile darüber wird ebenfalls akzeptiert
// Unterdrückung: Diese Datei stammt aus vor Null-Safety und ist für die Migration vorgesehen.
// ignore_for_file: avoid_print
slop-narrative-commentlive

Narrativer Füll-// Kommentar

Meldet // Kommentare unmittelbar vor einer Deklaration, deren Text den Namen wiederholt (z. B. // build vor void build()) oder einen festen narrativen Ausdruck verwendet (constructor, getter, setter, build method). Solche Füll-Kommentare fügen keine Information über den Bezeichner-Namen hinaus hinzu — ein typisches KI-Agenten-Ausgabemuster. Schweregrad: info. Generierte Dateien werden automatisch übersprungen.

Slop (schlecht)
// MyWidget
class MyWidget extends StatelessWidget {
  // build
  @override
  Widget build(BuildContext context) => const SizedBox();

  // constructor
  const MyWidget();
}
sauber
class MyWidget extends StatelessWidget {
  // Rendert als Platzhalter mit der Größe 0 während der Ladephase.
  @override
  Widget build(BuildContext context) => const SizedBox();

  const MyWidget();
}

Mehr in die Tiefe

DerDeveloper Guide(englisch) enthält die vollständige CLI-Referenz, Konfigurationsoptionen (Rule-Toggles, Pfad-Suppression, Inline-// loam-ignore:-Direktiven) und die vollständige Baseline-Onboarding-Sequenz — einschließlich der Integration von loam gate in GitHub Actions.

Die automatische Unterdrückung für Code-Generator-Eingaben ist im Guide unter „Automatic codegen-input suppression" beschrieben — die drei Erkennungssignale (Base-Type-Registry, Annotation-Registry, struktureller Fallback) funktionieren ohne loam.yaml-Konfiguration.

Geplant

Diese Rules stehen auf der Post-MVP-Roadmap. Jede ist ein eigenständiges Plugin hinter demselben Rule-Interface — die Pipeline ändert sich beim Erscheinen nicht.

architecture-boundariesgeplant

Architektur-Grenzen

Erkennt das Layer-Layout automatisch (core/, features/, services/, …) und meldet Imports, die verbotene Grenzen überschreiten. Zero-Config: das abgeleitete Regelset wird transparent herausgeschrieben und kann eingefroren oder verfeinert werden.

Slop (schlecht)
// features/profile/profile_page.dart
// ↓ Features-Schicht greift in Services-Interna
import '../../services/db/internal_schema.dart';
sauber
// services exponiert stattdessen ein öffentliches API:
// services/user_repository.dart (öffentliches Interface)
// features/profile/profile_page.dart
import '../../services/user_repository.dart';
anti-ai-slopgeplant

Anti-AI-Slop

Erkennt KI-typische Qualitätsmängel: tote Guard-Clauses und halluzinierte Abstraktionen. (Grundlose // ignore:-Direktiven sind bereits als slop-unjustified-ignore live; leere und kommentar-only catch-Blöcke sind bereits als slop-empty-catch live; narrative Füll-Kommentare sind bereits als slop-narrative-comment live.) Deterministische AST-Muster laufen zuerst; ein optionaler LLM-Layer erkennt Slop auf Intentionsebene — über einen Verdict-Cache deterministisch und kostenarm.

Slop (schlecht)
// narrativer Füll-Kommentar
/// Diese Methode verarbeitet die Daten, indem sie
/// über alle Elemente iteriert und die Transformation
/// anwendet.
List<String> process(List<String> items) =>
    items.map((i) => i.toUpperCase()).toList();
sauber
// Doku-Kommentar mit Mehrwert, nicht Rauschen
/// Gibt [items] in Großbuchstaben zurück.
List<String> process(List<String> items) =>
    items.map((i) => i.toUpperCase()).toList();

Mehr in die Tiefe

DerDeveloper Guide(englisch) enthält die vollständige CLI-Referenz, Output-Formate, Konfiguration und ausgearbeitete Beispiele — einschließlich der Integration von loam gate in GitHub Actions.