-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Implement custom GlobMatcher and verify against glob package #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -69,11 +69,21 @@ about the evaluation logic. | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ## Target pattern | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| A target pattern is a glob path pattern used to determine which files an import | ||||||||||||||||||||||||||
| rule applies to. A path pattern must be relative to the project root, and can | ||||||||||||||||||||||||||
| contain wildcards to match multiple files. See the documentation of | ||||||||||||||||||||||||||
| [glob](https://pub.dev/packages/glob#syntax) package for more details about the | ||||||||||||||||||||||||||
| wildcards. | ||||||||||||||||||||||||||
| A target pattern is a glob-like path pattern used to determine which files an | ||||||||||||||||||||||||||
| import rule applies to. A path pattern must be relative to the project root, and | ||||||||||||||||||||||||||
| can contain wildcards to match multiple files. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ### Wildcards | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| - `*` - Matches zero or more characters except `/` (single directory level) | ||||||||||||||||||||||||||
| - Example: `lib/*.dart` matches `lib/app.dart` but not `lib/src/app.dart` | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| - `**` - Matches one or more path segments including `/` (recursive) | ||||||||||||||||||||||||||
| - Example: `lib/**` matches `lib/app.dart` and `lib/src/app.dart` | ||||||||||||||||||||||||||
| - Example: `**/src/**` matches `lib/src/app.dart` and | ||||||||||||||||||||||||||
| `features/auth/src/user.dart` | ||||||||||||||||||||||||||
|
Comment on lines
+81
to
+84
|
||||||||||||||||||||||||||
| - `**` - Matches one or more path segments including `/` (recursive) | |
| - Example: `lib/**` matches `lib/app.dart` and `lib/src/app.dart` | |
| - Example: `**/src/**` matches `lib/src/app.dart` and | |
| `features/auth/src/user.dart` | |
| - `**` - Recursive wildcard. When used on its own, it matches zero or more path | |
| segments (including the empty string). When adjacent to `/`, it matches one or | |
| more path segments. | |
| - Example: `lib/**` matches `lib/app.dart` and `lib/src/app.dart` | |
| - Example: `**/src/**` matches `lib/src/app.dart` and | |
| `features/auth/src/user.dart` | |
| - Example: `target: "**"` matches every file in the project, and `foo**` | |
| matches `foo`, `foo/bar.dart`, and so on. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| /// A simple glob pattern matcher supporting * and ** wildcards. | ||
| /// | ||
| /// Wildcards: | ||
| /// - `*` matches zero or more characters except `/` (single directory level) | ||
| /// - `**` matches zero or more characters including `/` (recursive) | ||
| /// | ||
| /// Key behaviors: | ||
| /// - `foo*` matches `foo` (star can match empty string) | ||
| /// - `foo**` matches `foo` (double-star can match empty string) | ||
| /// - `lib/**` matches `lib/x` but NOT `lib/` (requires at least one segment after /) | ||
| /// - `**/lib` matches `x/lib` but NOT `lib` (requires at least one segment before /) | ||
| /// | ||
|
Comment on lines
+4
to
+12
|
||
| /// Examples: | ||
| /// - `lib/*.dart` matches `lib/app.dart` but not `lib/src/app.dart` | ||
| /// - `lib/**` matches any file under lib/ at any depth (but not `lib/` itself) | ||
| /// - `lib/**.g.dart` matches any .g.dart file under lib/ at any depth | ||
| /// - `**/src/**` matches any file under any src/ directory | ||
| /// | ||
| /// Intentional differences from glob package: | ||
| /// - Does not support `?` (single character wildcard) | ||
| /// - Does not support `[...]` (character classes) | ||
| /// - Does not support `{a,b}` (alternatives) | ||
| /// - Does not normalize paths (no dot-dot resolution) | ||
| /// - Does not support case-insensitive matching | ||
| /// - Does not support platform-specific path contexts | ||
| class GlobMatcher { | ||
| /// Creates a glob matcher with the given pattern. | ||
| /// | ||
| /// The pattern is compiled to a regex at construction time for performance. | ||
| GlobMatcher(String pattern) | ||
| : _pattern = pattern, | ||
| _regex = _compileToRegex(pattern); | ||
|
|
||
| final String _pattern; | ||
| final RegExp _regex; | ||
|
|
||
| /// Checks if [path] matches this glob pattern. | ||
| bool matches(String path) => _regex.hasMatch(path); | ||
|
|
||
| /// Converts a glob pattern to a regular expression. | ||
| /// | ||
| /// Algorithm: | ||
| /// 1. Escape regex special characters (except *) | ||
| /// 2. Replace ** patterns with placeholders (before single * replacement) | ||
| /// 3. Replace single * with [^\/]* (zero or more non-slash chars) | ||
| /// 4. Replace placeholders with actual regex patterns | ||
| /// 5. Anchor with ^ and $ | ||
| static RegExp _compileToRegex(String pattern) { | ||
| var regex = pattern; | ||
|
|
||
| // Step 1: Escape regex special characters (except *) | ||
| // Characters to escape: . + ? ^ $ { } [ ] ( ) | \ | ||
| regex = regex.replaceAllMapped( | ||
| RegExp(r'[.+?^${}()\[\]\\|]'), | ||
| (match) => '\\${match[0]}', | ||
| ); | ||
|
|
||
| // Step 2: Replace ** patterns with placeholders | ||
| // We use placeholders to avoid later * replacements affecting these patterns | ||
| // Important: ** behavior depends on context: | ||
| // - /**/ → matches /.+/ (at least one char between slashes) | ||
| // - /** at end → matches /.+ (at least one char after slash) | ||
| // - **/ at start → matches .+/ (at least one char before slash) | ||
| // - ** standalone (no adjacent /) → matches .* (zero or more chars) | ||
|
|
||
| // Use unique placeholders that won't conflict with actual patterns | ||
| const slashStarStarSlash = '\x00SLASH_STAR_STAR_SLASH\x00'; | ||
| const slashStarStar = '\x00SLASH_STAR_STAR\x00'; | ||
| const starStarSlash = '\x00STAR_STAR_SLASH\x00'; | ||
| const starStar = '\x00STAR_STAR\x00'; | ||
|
|
||
| regex = regex.replaceAll('/**/', slashStarStarSlash); | ||
| regex = regex.replaceAll('/**', slashStarStar); | ||
| regex = regex.replaceAll('**/', starStarSlash); | ||
| regex = regex.replaceAll('**', starStar); | ||
|
|
||
| // Step 3: Replace single * with [^/]* (zero or more non-slash chars) | ||
| regex = regex.replaceAll('*', r'[^/]*'); | ||
|
|
||
| // Step 4: Replace placeholders with actual regex patterns | ||
| regex = regex.replaceAll(slashStarStarSlash, '/.+/'); | ||
| regex = regex.replaceAll(slashStarStar, '/.+'); | ||
| regex = regex.replaceAll(starStarSlash, '.+/'); | ||
| regex = regex.replaceAll(starStar, '.*'); // Zero or more for standalone ** | ||
|
|
||
| // Step 5: Anchor the pattern | ||
| regex = '^$regex\$'; | ||
|
|
||
| return RegExp(regex); | ||
| } | ||
|
|
||
| @override | ||
| String toString() => 'GlobMatcher($_pattern)'; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| import 'package:glob/glob.dart'; | ||
| import 'package:meta/meta.dart'; | ||
| import 'package:path/path.dart' as p; | ||
|
|
||
| import 'glob_matcher.dart'; | ||
|
|
||
| /// Severity level for import rule violations. | ||
| enum Severity { error, warning, info } | ||
|
|
||
|
|
@@ -27,7 +28,7 @@ class TargetPattern { | |
|
|
||
| /// Checks if the given file path matches this target pattern. | ||
| bool matches(String file) { | ||
| final glob = Glob(pattern); | ||
| final glob = GlobMatcher(pattern); | ||
| return glob.matches(file); | ||
| } | ||
|
Comment on lines
29
to
33
|
||
|
|
||
|
|
@@ -54,7 +55,7 @@ class DisallowPattern { | |
| /// The [dirValue] parameter is used to substitute $TARGET_DIR placeholders in the pattern. | ||
| bool matches(String importUri, String dirValue) { | ||
| final substitutedPattern = pattern.replaceAll(r'$TARGET_DIR', dirValue); | ||
| final glob = Glob(substitutedPattern); | ||
| final glob = GlobMatcher(substitutedPattern); | ||
| return glob.matches(importUri); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This section states that
**matches “one or more path segments”, but the implementation/tests also rely on**matching empty in some contexts (e.g.,foo**matchingfoo). Please adjust the wording here to match the actual semantics (standalone**can be empty;/**and**/require at least one segment).