From 74186fbafeb2408611bbe251cff26b7013a727ab Mon Sep 17 00:00:00 2001 From: Wolfvin Date: Tue, 14 Jul 2026 15:09:54 +0700 Subject: [PATCH] fix(ignore): fallback matcher matches non-anchored dir patterns at any depth (refs #271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _FnmatchMatcher (used when pathspec is absent) only matched non-anchored dir patterns (target/, build/, dist/) at the workspace root, so nested build dirs like src/target/debug were not ignored. Match non-anchored dir patterns against every sub-path at segment boundaries — whole-segment, so build/ still does not match build-tools/. Fixes test_actual_target_dir_is_ignored. Co-Authored-By: Claude Opus 4.8 --- scripts/codelensignore.py | 43 ++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/scripts/codelensignore.py b/scripts/codelensignore.py index 41bc34a6..3db506cc 100644 --- a/scripts/codelensignore.py +++ b/scripts/codelensignore.py @@ -208,12 +208,7 @@ def is_ignored(self, rel_path: str) -> bool: result = False for is_neg, rx, anchored, dir_only in self._rules: if dir_only: - # Match if rel == pat or rel.startswith(pat + '/') - # We achieve this by matching the pattern OR pattern + '/*' - # Use the regex against the path and any prefix path that - # ends at a separator. - # Simpler: check the rule against every prefix of rel. - matched = self._match_dir_prefix(rx, rel) + matched = self._match_dir_prefix(rx, rel, anchored) else: matched = bool(rx.match(rel)) if matched: @@ -221,17 +216,37 @@ def is_ignored(self, rel_path: str) -> bool: return result @staticmethod - def _match_dir_prefix(rx: 're.Pattern', rel: str) -> bool: - """True if *rel* OR any ancestor directory matches *rx*.""" - # Check the full path first + def _match_dir_prefix(rx: 're.Pattern', rel: str, anchored: bool = True) -> bool: + """True if *rel* is inside a directory matched by *rx*. + + For an *anchored* pattern (``/target/``) only root-relative ancestor + directories count. For a *non-anchored* pattern (``target/`` — the + gitignore default) the directory may sit at ANY depth, so a whole path + segment matching the pattern is enough. Segment matching (not substring) + keeps ``build/`` from matching ``build-tools/`` (issue #271 / gitignore + backward-compat): ``src/target/debug/x`` is ignored by ``target/`` but + ``build-tools/config`` is not ignored by ``build/``. + """ + # Check the full path first (handles patterns with wildcards/subpaths). if rx.match(rel): return True - # Then check every ancestor directory parts = rel.split('/') - for i in range(1, len(parts)): - prefix = '/'.join(parts[:i]) - if rx.match(prefix): - return True + if anchored: + # Root-anchored: only ancestor paths measured from the root. + for i in range(1, len(parts)): + if rx.match('/'.join(parts[:i])): + return True + else: + # Non-anchored: the pattern (single- or multi-segment) may sit at + # any depth → test every sub-path that both starts and ends on a + # segment boundary. This matches `target/` against `src/target/x` + # and `build/keep/` against `build/keep/x`, while whole-segment + # boundaries keep `build/` from matching `build-tools/`. + n = len(parts) + for i in range(n): + for j in range(i + 1, n + 1): + if rx.match('/'.join(parts[i:j])): + return True return False