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
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.
// 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 } // 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
Detects import cycles across your Dart libraries. Cycles make compilation order ambiguous, prevent tree-shaking, and are a common sign of unclear layer responsibilities.
// lib/a.dart
import 'b.dart';
// lib/b.dart
import 'a.dart'; // ← cycle: a → b → a // 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
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.
// 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}'; // lib/shared/date_format.dart
String formatDate(DateTime d) =>
'${d.day}.${d.month}.${d.year}';
// both features import the shared helper 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.
// 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 */ }
}
}
}
} // 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
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.
// features/profile/profile_page.dart
// ↓ features layer reaching into services internals
import '../../services/db/internal_schema.dart'; // 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
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.
// 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(); // 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.