Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dart test # run all tests
dart test test/model/eagle_eye_config_item_test.dart # single file
dart test -n "returns null" # single test by name
dart run eagle_eye:main # run the CLI against the current project
dart run eagle_eye:main --verbose # trace every check performed
```

CI (`pr.yml`) runs `dart test` then `dart analyze` on PRs to `main`; both must
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased

- Add a `--verbose` (`-v`) flag that prints every configuration, file, import, and pattern check performed by EagleEye. Add `--help` (`-h`) for usage information.

## 2.0.5

- Fix Exclusive Dependencies Rule to depend only on certain files. An import is allowed when it matches **any** of the listed patterns, and a file that imports nothing is also allowed.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ dev_dependencies:
dart run eagle_eye:main
```

### Verbose mode

Pass `--verbose` (or `-v`) to print every configuration, file, import, and
pattern check performed by Eagle Eye:

```sh
dart run eagle_eye:main --verbose
```

Verbose lines are prefixed with `[verbose]` on stdout, so regular output stays
unchanged when the flag is omitted. Use `--help` to see all available options.

### Auto-generate config with a prompt

You can use [this prompt](./SETUP_PROMPT.md) with an AI assistant to automatically detect your architecture and generate an `eagle_eye_config.json` with the right rules.
Expand Down
4 changes: 2 additions & 2 deletions bin/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:eagle_eye/eagle_eye_launcher.dart';

void main() {
EagleEyeLauncher().launchEagleEye();
void main(List<String> args) {
EagleEyeLauncher().launchEagleEye(args: args);
}
9 changes: 9 additions & 0 deletions doc/1-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,15 @@ dart run eagle_eye:main
- **Exit code 0** with a success message: setup is correct.
- **Exit code 1** with violations listed: the rules are working — review the violations to confirm they are real issues or adjust your config.

To see every configuration, file, import, and pattern check performed, run:

```bash
dart run eagle_eye:main --verbose
```

Verbose lines are prefixed with `[verbose]`. Use `--help` (or `-h`) to list the
available options.

---

## 6. Optional — integrate into CI/CD
Expand Down
12 changes: 10 additions & 2 deletions lib/analyzer/checker/exclusive_dependencies_rule_checker.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:eagle_eye/analyzer/regex_helper.dart';
import 'package:eagle_eye/util/check_reporter.dart';

/// Validates whether a file imports only allowed dependencies.
///
Expand All @@ -9,9 +10,12 @@ class ExclusiveDependenciesRuleChecker {
/// Helper used for regex-based pattern matching.
RegexHelper regexHelper;

/// Optional callback used to report each pattern checked.
final CheckReporter? onCheck;

/// Creates a new [ExclusiveDependenciesRuleChecker] with the given
/// [regexHelper].
ExclusiveDependenciesRuleChecker(this.regexHelper);
/// [regexHelper] and optional [onCheck] reporter.
ExclusiveDependenciesRuleChecker(this.regexHelper, {this.onCheck});

/// Checks if [importDirective] conforms to the specified
/// [justWithPatterns] for the given [filePath].
Expand All @@ -28,6 +32,10 @@ class ExclusiveDependenciesRuleChecker {
importDirective,
justWithItem,
);
onCheck?.call(
' allowed pattern "$justWithItem": '
'${matches ? 'match -> allowed' : 'no match'}',
);
if (matches == true) {
return null;
}
Expand Down
12 changes: 10 additions & 2 deletions lib/analyzer/checker/forbidden_dependencies_rule_checker.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:eagle_eye/analyzer/regex_helper.dart';
import 'package:eagle_eye/util/check_reporter.dart';

/// Validates whether a file imports disallowed dependencies.
///
Expand All @@ -8,9 +9,12 @@ class ForbiddenDependenciesRuleChecker {
/// Helper used for regex-based pattern matching.
RegexHelper regexHelper;

/// Optional callback used to report each pattern checked.
final CheckReporter? onCheck;

/// Creates a new [ForbiddenDependenciesRuleChecker] with the given
/// [regexHelper].
ForbiddenDependenciesRuleChecker(this.regexHelper);
/// [regexHelper] and optional [onCheck] reporter.
ForbiddenDependenciesRuleChecker(this.regexHelper, {this.onCheck});

/// Checks if [importDirective] violates any of [noDepsWithPatterns]
/// for the given [filePath].
Expand All @@ -27,6 +31,10 @@ class ForbiddenDependenciesRuleChecker {
importDirective,
noDepsWithItem,
);
onCheck?.call(
' forbidden pattern "$noDepsWithItem": '
'${matches ? 'match -> violation' : 'no match'}',
);
if (matches == true) {
return '$filePath should not depends on $importDirective';
}
Expand Down
16 changes: 14 additions & 2 deletions lib/analyzer/eagle_eye_matcher.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:eagle_eye/analyzer/regex_helper.dart';
import 'package:eagle_eye/model/eagle_eye_config.dart';
import 'package:eagle_eye/model/eagle_eye_config_item.dart';
import 'package:eagle_eye/util/check_reporter.dart';

/// A matcher finds the configuration item that applies to a given file path.
///
Expand All @@ -15,11 +16,15 @@ class EagleEyeMatcher {
/// Helper class used to perform regex-based pattern matching.
final RegexHelper regexHelper;

/// Creates a new [EagleEyeMatcher] instance with the provided [config]
/// and [regexHelper].
/// Optional callback used to report each file pattern checked.
final CheckReporter? onCheck;

/// Creates a new [EagleEyeMatcher] instance with the provided [config],
/// [regexHelper], and optional [onCheck] reporter.
EagleEyeMatcher({
required this.config,
required this.regexHelper,
this.onCheck,
});

/// Finds the first [EagleEyeConfigItem] whose [filePattern]
Expand All @@ -35,7 +40,14 @@ class EagleEyeMatcher {
filePath,
configurationItem.filePattern,
);
onCheck?.call(
' filePattern "${configurationItem.filePattern}" vs "$filePath": '
'${configItemFounded ? 'match' : 'no match'}',
);
if (configItemFounded) {
final ruleLabel =
configurationItem.name ?? configurationItem.filePattern;
onCheck?.call(' rule "$ruleLabel" selected');
return configurationItem;
}
}
Expand Down
131 changes: 78 additions & 53 deletions lib/analyzer/eagle_eye_visitor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:eagle_eye/analyzer/checker/forbidden_dependencies_rule_checker.d
import 'package:eagle_eye/analyzer/regex_helper.dart';
import 'package:eagle_eye/model/eagle_eye_config_item.dart';
import 'package:eagle_eye/model/violation.dart';
import 'package:eagle_eye/util/check_reporter.dart';

/// A visitor that analyzes import directives to validate architectural rules
/// defined in an [EagleEyeConfigItem].
Expand Down Expand Up @@ -39,87 +40,111 @@ class EagleEyeVisitor extends RecursiveAstVisitor<void> {
/// The parsed compilation unit, used to resolve line/column numbers.
final CompilationUnit unit;

/// Optional callback used to report each import and pattern checked.
final CheckReporter? onCheck;

/// Creates a new [EagleEyeVisitor] with the given configuration,
/// file path, callback, and helpers.
/// file path, callback, helpers, and optional [onCheck] reporter.
EagleEyeVisitor({
required this.configItem,
required this.filePath,
required this.errorCallback,
required this.regexHelper,
required this.applicationName,
required this.unit,
this.onCheck,
});

@override
void visitImportDirective(ImportDirective node) {
super.visitImportDirective(node);

final importDirective = node.uri.stringValue;
final location = unit.lineInfo.getLocation(node.offset);
final lineNumber = location.lineNumber;
final columnNumber = location.columnNumber;

if (importDirective == null) {
onCheck?.call(' skipping import without URI (line $lineNumber)');
return;
}

if (!importDirective.contains(applicationName)) {
onCheck?.call(
' skipping external import "$importDirective" (line $lineNumber)',
);
return;
}

if (importDirective?.contains(applicationName) == true) {
final location = unit.lineInfo.getLocation(node.offset);
final lineNumber = location.lineNumber;
final columnNumber = location.columnNumber;
onCheck?.call(
' internal import "$importDirective" (line $lineNumber)',
);

if (configItem.dependenciesAllowed == false) {
if (configItem.dependenciesAllowed == false) {
onCheck?.call(' dependenciesAllowed is false -> violation');
errorCallback(
Violation(
ruleType: RuleType.noImportsAllowed,
filePath: filePath,
lineNumber: lineNumber,
columnNumber: columnNumber,
importUri: importDirective,
ruleName: configItem.name,
description: '$filePath should not contains any import.',
),
);
} else if (configItem.forbiddenDependencies != null) {
onCheck?.call(' rule type: forbiddenDependencies');
final checker = ForbiddenDependenciesRuleChecker(
regexHelper,
onCheck: onCheck,
);
String? errorDescription = checker.check(
noDepsWithPatterns: configItem.forbiddenDependencies!,
importDirective: importDirective,
filePath: filePath,
);

if (errorDescription != null) {
errorCallback(
Violation(
ruleType: RuleType.noImportsAllowed,
ruleType: RuleType.forbiddenDependency,
filePath: filePath,
lineNumber: lineNumber,
columnNumber: columnNumber,
importUri: importDirective,
ruleName: configItem.name,
description: '$filePath should not contains any import.',
description: errorDescription,
),
);
} else if (configItem.forbiddenDependencies != null) {
if (importDirective != null) {
final checker = ForbiddenDependenciesRuleChecker(regexHelper);
String? errorDescription = checker.check(
noDepsWithPatterns: configItem.forbiddenDependencies!,
importDirective: importDirective,
filePath: filePath,
);

if (errorDescription != null) {
errorCallback(
Violation(
ruleType: RuleType.forbiddenDependency,
filePath: filePath,
lineNumber: lineNumber,
columnNumber: columnNumber,
importUri: importDirective,
ruleName: configItem.name,
description: errorDescription,
),
);
}
}
} else if (configItem.exclusiveDependencies != null) {
if (importDirective != null) {
final checker = ExclusiveDependenciesRuleChecker(regexHelper);
String? errorDescription = checker.check(
justWithPatterns: configItem.exclusiveDependencies!,
importDirective: importDirective,
}
} else if (configItem.exclusiveDependencies != null) {
onCheck?.call(' rule type: exclusiveDependencies');
final checker = ExclusiveDependenciesRuleChecker(
regexHelper,
onCheck: onCheck,
);
String? errorDescription = checker.check(
justWithPatterns: configItem.exclusiveDependencies!,
importDirective: importDirective,
filePath: filePath,
);

if (errorDescription != null) {
errorCallback(
Violation(
ruleType: RuleType.exclusiveDependency,
filePath: filePath,
);

if (errorDescription != null) {
errorCallback(
Violation(
ruleType: RuleType.exclusiveDependency,
filePath: filePath,
lineNumber: lineNumber,
columnNumber: columnNumber,
importUri: importDirective,
ruleName: configItem.name,
description: errorDescription,
),
);
}
}
lineNumber: lineNumber,
columnNumber: columnNumber,
importUri: importDirective,
ruleName: configItem.name,
description: errorDescription,
),
);
}
} else {
onCheck?.call(' no dependency constraint configured -> allowed');
}
}
}
Loading
Loading