loam.dev
v0.1.4 · preview DE

Rules

loam.dev ships six analysis capabilities, all behind one stable Rule interface. Adding a new rule never changes the pipeline — only the rule list. One rule is live today; five are planned and will ship as post-MVP iterations. Status is always honest.

Live

unused-public-exports live

Unused public exports

Finds public API members — classes, methods, getters/setters, fields, enums, typedefs — that have no references anywhere in the project. Analysis runs on the resolved element model, not regex, so generated-code inputs (Drift, freezed, Riverpod, json_serializable) are automatically excluded without any configuration.

slop (bad)
// public but never used outside this library
class OldHelper {
  static String format(String s) => s.trim();
}

// also unreferenced — reported by loam.dev
enum LegacyStatus { active, archived }
clean
// either remove it, make it private, or add
// a loam-ignore directive with a reason:
// loam-ignore: unused-public-exports – re-exported via barrel

// kept and used:
class ActiveHelper {
  static String format(String s) => s.trim();
}

Going deeper

The Developer Guide covers the full CLI reference, configuration options (rule toggles, path suppression, inline // loam-ignore: directives), and the complete baseline onboarding sequence — including how to integrate loam gate into GitHub Actions.

Automatic suppression for code-generator inputs is described in detail under "Automatic codegen-input suppression" in the guide — the three detection signals (base-type registry, annotation registry, structural fallback) work without any loam.yaml configuration.

Planned

These five rules are on the post-MVP roadmap. Each is a separate plugin behind the same Rule interface — the pipeline will not change when they ship.

circular-dependencies planned

Circular dependencies

Detects import cycles across your Dart libraries. Cycles make compilation order ambiguous, prevent tree-shaking, and are a common sign of unclear layer responsibilities.

slop (bad)
// lib/a.dart
import 'b.dart';

// lib/b.dart
import 'a.dart'; // ← cycle: a → b → a
clean
// Extract shared types into a third library
// lib/shared.dart  (no imports from a or b)
// lib/a.dart  imports shared.dart
// lib/b.dart  imports shared.dart
code-duplicates planned

Code duplicates

Finds structurally similar code blocks using AST-normalised token hashing. Flags helpers that were copy-pasted rather than extracted — a classic AI-agent side effect.

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

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

// both features import the shared helper
complexity-hotspots planned

Complexity hotspots

Measures cyclomatic and cognitive complexity per function/method and aggregates them into a project health score. Flags functions that are too complex to review or test reliably.

slop (bad)
// cognitive complexity ≫ threshold
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) { /* swallowed */ }
      }
    }
  }
}
clean
// split into focused, testable 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 { … }
architecture-boundaries planned

Architecture boundaries

Auto-detects your layer layout (core/, features/, services/, …) and flags imports that cross forbidden boundaries. Zero-config: the derived rule set is written out transparently and can be frozen or refined.

slop (bad)
// features/profile/profile_page.dart
// ↓ features layer reaching into services internals
import '../../services/db/internal_schema.dart';
clean
// services exposes a public API instead:
// services/user_repository.dart (public interface)
// features/profile/profile_page.dart
import '../../services/user_repository.dart';
anti-ai-slop planned

Anti-AI-slop

Detects AI-agent quality shortcuts: empty catch blocks, narrative filler comments, groundless // ignore: directives, dead guard clauses, and hallucinated abstractions. Deterministic AST patterns run first; an optional LLM layer handles intent-level slop via a verdict cache — the same code always produces the same verdict, zero token cost on repeats.

slop (bad)
// empty catch — classic slop
try {
  await fetchData();
} catch (e) {} // ← swallows all errors

// narrative filler comment
/// This method processes the data by iterating
/// over all items and applying the transformation.
List<String> process(List<String> items) =>
    items.map((i) => i.toUpperCase()).toList();
clean
// handle or rethrow with context
try {
  await fetchData();
} catch (e, st) {
  log.error('fetchData failed', e, st);
  rethrow;
}

// doc comment adds value, not noise
/// Returns [items] uppercased.
List<String> process(List<String> items) =>
    items.map((i) => i.toUpperCase()).toList();

Going deeper

The Developer Guide covers the full CLI reference, output formats, configuration, and worked examples — including how to integrate loam gate into GitHub Actions.