heimdall_test 0.3.0
heimdall_test: ^0.3.0 copied to clipboard
Architecture rules for Dart source trees inspired by ArchUnit.
Heimdall Test #
Development status: Heimdall Test is still under active development. The public API may change between minor versions, and breaking changes can happen while the package is being shaped.
Heimdall Test is a Dart architecture testing package inspired by ArchUnit. It
imports Dart source trees with the analyzer, keeps the real AST nodes, and
lets tests describe executable rules about files, type declarations, members,
dependencies, layers, and feature slices.
Features #
- Import Dart projects or subtrees with
HeimdallFileImporter. - Filter imported files with options such as
ExcludeTestsImportOption,IncludeLibraryImportOption, andExcludeGeneratedDartImportOption. - Run fluent rules for classes, files, fields, methods, constructors, and code
units through
Heimdall. - Check naming, paths, annotations, modifiers, parse errors, imports, source snippets, public class/file conventions, and basic member usage.
- Query dependencies between declarations using local imports, exports, parts, and barrel files.
- Model layer rules with allowed access constraints.
- Model slices by path capture and detect cycles between them.
- Freeze existing violations so only new findings fail a test.
- Bundle reusable checks with plugins and run them through
HeimdallRunner.
Quick Start #
import 'package:heimdall_test/heimdall_test.dart';
void main() {
final project = const HeimdallFileImporter().importPath();
Heimdall.classes()
.that()
.resideInPath('src/data')
.should()
.haveNameEndingWith('Repository')
.check(project)
.assertNoFindings();
}
Importing Source #
HeimdallFileImporter recursively reads .dart files, parses them with the
analyzer, and returns a HeimdallProject.
final project = const HeimdallFileImporter(
importOptions: [ExcludeTestsImportOption()],
).importPath('.');
The imported model exposes:
HeimdallProject.filesHeimdallProject.declarationsHeimdallProject.typeDeclarationsHeimdallProject.dependenciesHeimdallSourceFile.parseErrors- analyzer nodes enriched by Heimdall extensions such as
name,line,relativePath,targetUri, andtargetFile
Fluent Rules #
Class and type declaration rules:
Heimdall.classes()
.that()
.arePublic()
.and()
.resideInPath('lib/src/domain')
.should()
.beFinal()
.check(project)
.assertNoFindings();
File rules:
Heimdall.files()
.that()
.resideInPath('lib/src')
.should()
.notImportUri('dart:mirrors')
.check(project)
.assertNoFindings();
Member rules:
Heimdall.fields()
.that()
.arePublic()
.should()
.beFinal()
.check(project)
.assertNoFindings();
Built-In Sights #
Heimdall.code() provides source hygiene rules:
Heimdall.code().shouldParse().check(project).assertNoFindings();
Heimdall.code().shouldNotImportDartMirrors().check(project).assertNoFindings();
Heimdall.code().publicSignaturesShouldNotUseDynamic().check(project).assertNoFindings();
Heimdall.dependencies() provides dependency rules:
Heimdall.dependencies()
.noClassesShouldDependOnUpperDirectories()
.check(project)
.assertNoFindings();
Layers #
Use layers when package areas may only depend on specific other areas.
final rule = Heimdall.layers()
.layer('Domain').definedBy(['lib/src/domain'])
.layer('Application').definedBy(['lib/src/application'])
.layer('Infrastructure').definedBy(['lib/src/data'])
.whereLayer('Domain').mayNotAccessAnyLayer()
.whereLayer('Application').mayOnlyAccessLayers(['Domain'])
.whereLayer('Infrastructure').mayOnlyAccessLayers(['Application', 'Domain'])
.asRule(description: 'onion architecture');
rule.check(project).assertNoFindings();
Slices #
Use slices for repeated architecture areas, such as features.
Heimdall.slices('lib/src/features/(*)')
.shouldBeFreeOfCycles()
.check(project)
.assertNoFindings();
(*) captures the slice name from the path. Heimdall can report cycles or any
cross-slice dependency.
Freezing Known Violations #
Freezing records the current findings in a JSON file and reports only new findings on later runs.
Heimdall.classes()
.should()
.beFinal()
.freeze(storePath: '.heimdall/final_classes.json')
.check(project)
.assertNoFindings();
Plugins #
Plugins group reusable rules.
final class ArchitecturePlugin implements HeimdallRulePlugin {
@override
String get name => 'architecture';
@override
Iterable<PluginRule> rules() => [
PluginRule(
'code parses',
(project) => Heimdall.code().shouldParse().check(project),
),
];
}
final reports = HeimdallRunner([ArchitecturePlugin()]).run(project);
for (final report in reports) {
report.assertNoFindings();
}
Path Patterns #
Path-based APIs use pathMatches semantics:
- Plain text matches a path segment, for example
data. - A plain path like
lib/src/datamatches only that exact path. - A path fragment like
data/repositorymatches that sequence of segments anywhere. - Use
lib/src/data/**to match every item underlib/src/data. - Use
**data/repository/**or**/data/repository/**to match a nested path anywhere in the source tree. ..service..matches a path segment namedservice;..data/service..matches that sequence of path segments anywhere...is a path-fragment wildcard. It can cross/separators and is useful for "contains this path".*is a glob wildcard inside one path segment. It does not cross/.**matches across path segments.- A glob pattern starting with
/, such as/data/**/*impl.dart, matches that segment sequence at any depth. (*)captures one path segment for slice and layer helpers.
Examples:
pathMatches('lib/src/data', 'lib/src/data'); // true
pathMatches('lib/src/data/user_repository.dart', 'lib/src/data'); // false
pathMatches('lib/src/features/user/data/repository/user_repository.dart', 'data/repository'); // true
pathMatches('lib/src/data/user_repository.dart', 'lib/src/data/**'); // true
pathMatches('lib/src/features/user/data/repository/user_repository.dart', '**data/repository/**'); // true
pathMatches('lib/src/features/user/data/service/user_service.dart', '..data/service..'); // true
pathMatches('lib/src/features/user/data/service/user_impl.dart', '/data/**/*impl.dart'); // true
Reports #
Every executable rule returns a HeimdallReport with a description, checked
item count, and HeimdallFinding list. In tests, call assertNoFindings() to
turn rule findings into a failing assertion.