Hyena Dart
A Dart and Flutter codebase analyzer for finding unused declarations and measuring code complexity. Hyena uses the official Dart analyzer package for AST-based analysis.
Features
- Dead Code Detection - Find unused classes, constructors, functions, methods, enums, variables, fields, and typedefs
- Complexity Metrics - Cyclomatic complexity, lines of code, nesting levels, parameter count, maintainability index
- Multiple Output Formats - Console (colored), JSON, Markdown, HTML, and SARIF 2.1
- Configurable - Exclude patterns, thresholds, and analysis options via YAML config
- Workspace Aware - Analyze Dart workspaces package by package without double-counting files
- Framework Aware - Keep configured routes, registrations, callbacks, and annotated declarations reachable
- CI/CD Ready - Finding-based exit codes, baselines, source suppressions, and SARIF output
Installation
Add Hyena to each project as a dev dependency. This pins the analyzer version for local development and CI:
dart pub add dev:hyena_dart
dart run hyena_dart --help
dart run hyena_dart --version
dart run hyena_dart analyze .
For a Flutter project, use flutter pub add dev:hyena_dart and then the same
dart run commands. Hyena supports project-local execution only, keeping its
version in the project's dependency resolution and lockfile.
Quick Start
# Analyze current directory
dart run hyena_dart analyze .
# Analyze specific path
dart run hyena_dart analyze lib
# Dead code analysis only
dart run hyena_dart dead-code lib
# Complexity analysis only
dart run hyena_dart complexity lib
Dead-code analysis reports unused public and private declarations by default.
Reusable package authors can preserve exported public APIs with
--ignore-exports or hyena.dead_code.ignore_exports: true.
Migrating from v1.x
- Add Hyena as a project dev dependency with
dart pub add dev:hyena_dart(orflutter pub add dev:hyena_dart); v2 no longer maps global executables. - Run the CLI and MCP server with
dart run hyena_dartanddart run hyena_dart:hyena_mcp. - Expect unused public declarations in dead-code results. Set
ignore_exports: truefor reusable packages whose exported API must remain reachable without an in-repository caller. - Update consumers of console, JSON, Markdown, and HTML reports to expect project- or workspace-relative target and file paths. Existing SARIF output, MCP results, and baseline fingerprints were already relative and remain compatible.
Dart Workspaces and Monorepos
Point Hyena at a directory whose pubspec.yaml declares a Dart workspace to
analyze the root package and every workspace member:
dart pub get
dart run hyena_dart analyze .
Hyena supports explicit, nested, and glob workspace entries, subject to the
workspace syntax supported by the installed Dart SDK. As required by Dart,
each listed member must declare resolution: workspace. Every package is
reported independently and keeps its own configuration. Dead-code reachability
is joined across package boundaries, so a declaration used by another workspace
package stays live while same-named declarations and references from dead callers
do not. Descendant package directories are excluded from their parent package,
so source files are never counted twice or analyzed under the wrong package
boundary.
Configuration is discovered separately for each package. A package-local
hyena.yaml or analysis_options.yaml takes precedence; otherwise discovery
continues up to the workspace root. An explicit --config file applies to all
packages. Console, JSON, Markdown, and HTML reports contain package sections,
and every report path, SARIF location, and baseline fingerprint is relative to
the common workspace root.
AI Assistant Integration
Hyena includes a read-only MCP server. The source repository also provides an
agent skill. The MCP server exposes one tool, hyena_analyze, for dead-code
and complexity analysis with structured results.
After adding Hyena as a project dev dependency, configure an MCP client to
launch the server over standard input/output. -C selects the project before
resolving its local Hyena dependency, while the server root remains .:
{
"mcpServers": {
"hyena": {
"command": "dart",
"args": ["-C", "/absolute/path/to/dart-project", "run", "hyena_dart:hyena_mcp", "--root", "."]
}
}
}
The workspace root is a startup argument chosen by the user, not a tool
argument chosen by the model. Tool calls accept only a relative path and a
checks value of both, dead-code, or complexity.
Each request is limited to 10,000 Dart files and 50 MiB of Dart source, returns at most 200 findings with bounded text fields, and is terminated after two minutes. The server processes only one analysis at a time; narrow the target path if a result is truncated.
Per-request cancellation is not supported because dart_mcp 0.5.x does not
expose JSON-RPC request IDs to tool handlers. Disconnecting the client stops
the worker immediately; otherwise the two-minute limit still applies.
Security properties of the MCP interface:
- stdio transport only; it does not open a local or remote network server
- no shell execution, target-code execution, file writes, or baseline changes
- canonical workspace checks that reject traversal and existing symlink escapes
- bounded source size, file count, result count, runtime, and concurrency
- configuration discovery stops at the configured workspace root
The server still runs with the operating-system permissions of the MCP client. Dart analysis may read the installed SDK, package-resolution metadata, and resolved dependencies outside the workspace root, but it does not execute them. Because application-level checks are not an OS sandbox, another process that can mutate the workspace could race validation by replacing a checked path. For stronger isolation, launch the server in a sandbox or container with the workspace and dependency cache mounted read-only and networking disabled.
Contributors using an Agent Skills-compatible client can load the
analyzing-dart-code repository skill.
It prefers the constrained MCP tool and documents a JSON CLI fallback.
CLI Reference
Commands
analyze - Full Analysis
Run both dead code and complexity analysis.
dart run hyena_dart analyze <path> [options]
| Option | Short | Description | Default |
|---|---|---|---|
--format |
-f |
Output format: console, json, markdown, html, sarif |
console |
--output |
-o |
Output file path (prints to stdout if not specified) | - |
--config |
-c |
Path to configuration file | - |
--no-color |
- | Disable colored output | false |
--baseline |
- | Suppress findings recorded in a baseline file | - |
--write-baseline |
- | Write current findings to a baseline file | - |
--fail-on |
- | Exit 1 for dead-code, complexity, or both |
- |
--dead-code |
- | Include dead code analysis | true |
--complexity |
- | Include complexity analysis | true |
--ignore-exports |
- | Preserve exported public APIs from dead-code findings | false |
--ignore-private |
- | Ignore private entities | false |
Examples:
# Full analysis with HTML report
dart run hyena_dart analyze lib --format=html --output=report.html
# JSON output for CI/CD
dart run hyena_dart analyze lib --format=json --output=analysis.json
# Skip complexity analysis
dart run hyena_dart analyze lib --no-complexity
dead-code - Dead Code Analysis
Analyze codebase for unused code entities.
dart run hyena_dart dead-code <path> [options]
| Option | Short | Description | Default |
|---|---|---|---|
--format |
-f |
Output format | console |
--output |
-o |
Output file path | - |
--config |
-c |
Path to configuration file | - |
--baseline |
- | Suppress findings recorded in a baseline file | - |
--write-baseline |
- | Write current findings to a baseline file | - |
--fail-on |
- | Exit 1 when dead-code findings remain | - |
--ignore-exports |
- | Preserve exported public APIs from dead-code findings | false |
--ignore-private |
- | Ignore private entities | false |
Examples:
# Preserve a reusable package's exported public API
dart run hyena_dart dead-code lib --ignore-exports
# Markdown report
dart run hyena_dart dead-code lib --format=markdown --output=dead-code.md
complexity - Complexity Analysis
Analyze code complexity metrics.
dart run hyena_dart complexity <path> [options]
| Option | Short | Description | Default |
|---|---|---|---|
--format |
-f |
Output format | console |
--output |
-o |
Output file path | - |
--config |
-c |
Path to configuration file | - |
--baseline |
- | Suppress findings recorded in a baseline file | - |
--write-baseline |
- | Write current findings to a baseline file | - |
--fail-on |
- | Exit 1 when complexity findings remain | - |
--threshold |
-t |
Cyclomatic complexity threshold for warnings | 20 |
Examples:
# Set custom threshold
dart run hyena_dart complexity lib --threshold=15
# JSON output
dart run hyena_dart complexity lib --format=json
Configuration
Create a hyena.yaml file in your project root:
hyena:
# Glob patterns to exclude from analysis
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "**/*.mocks.dart"
- "**/generated/**"
# Complexity thresholds
complexity:
cyclomatic_threshold: 20
max_nesting: 5
max_parameters: 6
# Dead code options
dead_code:
ignore_main: true
ignore_exports: false
ignore_private: false
# Declarations retained as framework or generated-code roots
entry_points:
- AppRoutes
- ServiceRegistry.register
- generatedCallbacks
# Annotations whose declarations are retained as roots
entry_point_annotations:
- RoutePage
- injectable
- riverpod.Riverpod
Entry points use exact declaration names. A simple name such as register
matches declarations with that simple name; a qualified name such as
ServiceRegistry.register targets one member. Annotation names can omit a
leading @. Simple annotation names match the final component regardless of
an import prefix, while qualified names match the exact lexical prefix and
name.
Configured declarations become dead-code reachability roots, so declarations they call or reference are retained too. Configured classes and other type containers also retain their public members. The lists are empty by default; add only entry points actually used by a framework, generator, serializer, router, dependency-injection system, or plugin runtime.
You can also add the configuration to your existing analysis_options.yaml:
# Your existing linter rules...
linter:
rules:
- prefer_const_constructors
# Hyena configuration
hyena:
exclude:
- "**/*.g.dart"
complexity:
cyclomatic_threshold: 15
Output Formats
Console (Default)
Colored terminal output with summary and details.
JSON
Machine-readable format for CI/CD integration:
{
"targetPath": "lib",
"duration": "205ms",
"deadCode": {
"summary": {
"totalDeclarations": 170,
"unusedCount": 5,
"deadCodePercentage": "2.94"
},
"unusedEntities": [...]
},
"complexity": {
"summary": {
"totalFiles": 17,
"totalFunctions": 133,
"highComplexityFunctions": 2
},
"files": [...]
}
}
Markdown
GitHub-friendly format with tables and collapsible sections.
HTML
Visual report with styled cards, tables, color-coded metrics, and project-relative source paths.
SARIF
SARIF 2.1 output for code-scanning systems:
dart run hyena_dart analyze . --format=sarif --output=hyena.sarif
CI/CD Usage
Hyena exits with code 0 by default, even when it reports findings. Opt into a stable exit code 1 for selected categories:
dart run hyena_dart analyze . --fail-on=dead-code,complexity
To adopt Hyena without failing on existing findings, create and commit a baseline, then fail only on new findings:
dart run hyena_dart analyze . --write-baseline=hyena-baseline.json
dart run hyena_dart analyze . \
--baseline=hyena-baseline.json \
--fail-on=dead-code,complexity
Baseline fingerprints use the rule, package-relative path, symbol type, and full symbol name. Moving a declaration to another line does not invalidate its baseline entry.
Performance Benchmarks
The source repository includes deterministic single-package and workspace benchmark corpora. Run these commands from a source checkout. Fixture generation and warm-up runs happen outside measured samples. The harness records median and p95 wall time, files and lines per second, sampled process RSS, runtime metadata, and correctness signatures:
# Fast local regression suite
dart run benchmark/benchmark.dart \
--suite=quick \
--output=benchmark-results.json
# Larger 500/1,000-file and 20-package scenarios
dart run benchmark/benchmark.dart --suite=full
# Advisory comparison with the released v1.2.1 baseline
dart run benchmark/benchmark.dart \
--baseline=benchmark/results/v1.2.1.json
The Performance benchmarks GitHub Actions workflow runs either generated
suite on demand and uploads its JSON result. Comparisons are intentionally
advisory until repeated hosted-runner measurements establish stable regression
thresholds.
The open-source benchmark report records pinned Melos, Flame, and Flutter packages measurements, representative review leads, and known compatibility limits. Findings from these scans are triage candidates rather than automatic refactoring instructions.
For an explicitly reviewed production checkout, use the read-only external mode:
dart run benchmark/benchmark.dart \
--target=/path/to/repository \
--label=reviewed-production-repo \
--checks=both \
--output=production-benchmark.json
External mode does not run target code, shell hooks, or pub get, and it does
not write inside the target. Output paths inside the target, including paths
through symlink aliases, are rejected. The checkout should already contain the
package resolution metadata needed by Dart analysis. Hyena may read resolved
SDK and package dependencies just as it does during normal analysis. Benchmark
JSON uses the supplied label and aggregate corpus counts rather than recording
the target's absolute path.
Source Suppressions
Place an ignore comment immediately before a declaration when a finding is intentional:
// hyena:ignore dead-code
void retainedForReflection() {}
// hyena:ignore complexity
void generatedDispatcher() {
// All complexity threshold findings are suppressed.
}
// Rule-specific complexity suppressions:
// hyena:ignore cyclomatic-complexity
void stateMachine() {}
// hyena:ignore max-nesting
void nestedParser() {}
// hyena:ignore max-parameters
void frameworkCallback(int a, int b, int c, int d, int e, int f, int g) {}
Suppressed dead-code declarations remain reachability roots, so dependencies used by an intentionally retained declaration are not reported as cascading dead code.
Metrics Explained
Dead Code Detection
Detects the following unused entities:
- Classes (including abstract classes)
- Explicit unnamed, named, and private constructors
- Mixins and Extensions
- Enums and enum values
- Top-level and instance functions/methods
- Getters and setters
- Variables and fields
- Typedefs
Complexity Metrics
| Metric | Description |
|---|---|
| Cyclomatic Complexity | Number of linearly independent paths through code. Higher = more complex. |
| Lines of Code (LOC) | Non-blank lines in a function. |
| Max Nesting Level | Deepest level of nested control structures. |
| Parameter Count | Number of function parameters. |
| Maintainability Index | Composite score (0-100). Higher = more maintainable. |
Complexity Thresholds
| Cyclomatic Complexity | Risk Level |
|---|---|
| 1-10 | Low - Simple, easy to test |
| 11-20 | Moderate - More complex |
| 21-50 | High - Difficult to test |
| 50+ | Very High - Untestable, refactor recommended |
Programmatic Usage
You can also use Hyena as a library:
import 'package:hyena_dart/hyena_dart.dart';
void main() async {
// AnalysisRunner automatically handles single packages and Dart workspaces.
final workspaceResult = await const AnalysisRunner().analyze('.');
print('Packages: ${workspaceResult.packageAnalyses.length}');
final config = AnalyzerConfig(
cyclomaticThreshold: 15,
ignoreExports: false,
);
// Dead code analysis
final deadCodeAnalyzer = DeadCodeAnalyzer(config);
final deadCodeReport = await deadCodeAnalyzer.analyze('./lib');
print('Unused entities: ${deadCodeReport.unusedCount}');
// Complexity analysis
final complexityAnalyzer = ComplexityAnalyzer(config);
final complexityReport = await complexityAnalyzer.analyze('./lib');
print('High complexity functions: ${complexityReport.highComplexityFunctions.length}');
// Generate reports
final result = AnalysisResult(
deadCodeReport: deadCodeReport,
complexityReport: complexityReport,
targetPath: './lib',
duration: Duration(milliseconds: 100),
);
final reporter = JsonReporter();
final json = await reporter.generate(result);
print(json);
}
License
MIT