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
2 changes: 1 addition & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ Use the `exclude` field to skip directories during compilation. This improves pe
- `coverage/**` - Matches "coverage" and all subdirectories
- `projects/**/apm/**` - Complex nested matching with `**`

**Default exclusions** (always applied):
**Default exclusions** (always applied, matched on exact path components):
- `node_modules`, `__pycache__`, `.git`, `dist`, `build`, `apm_modules`
- Hidden directories (starting with `.`)

Expand Down
2 changes: 1 addition & 1 deletion docs/compilation.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ Use the `exclude` field to skip directories during compilation, improving perfor
- Prevent duplicate instruction discovery

**Default Exclusions:**
APM always excludes these directories (no configuration needed):
APM always excludes directories whose path contains an exact component matching one of these names (no configuration needed). A directory named `rebuild/` is **not** excluded just because it contains `build` as a substring.
- `node_modules`
- `__pycache__`
- `.git`
Expand Down
4 changes: 2 additions & 2 deletions src/apm_cli/compilation/context_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,8 @@ def _analyze_project_structure(self) -> None:
if any(part.startswith('.') for part in current_path.parts[len(self.base_dir.parts):]):
continue

# Default hardcoded exclusions for backwards compatibility
if any(ignore in str(current_path) for ignore in DEFAULT_EXCLUDED_DIRNAMES):
# Default hardcoded exclusions — match on exact path components
if any(part in DEFAULT_EXCLUDED_DIRNAMES for part in relative_path.parts):
continue

# Apply configurable exclusion patterns
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/compilation/test_context_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,60 @@ def test_default_exclusions_still_work(self):
assert base_path / "custom_exclude" not in cached_dirs # Custom exclusion


class TestSubstringExclusionFalsePositives:
"""Test that directory exclusions use component matching, not substring matching (Fixes #158)."""

def test_directory_containing_exclusion_token_not_excluded(self):
"""Directories like 'rebuild/' must NOT be excluded just because 'build' is a substring."""
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir).resolve()
for name in ["rebuild", "apm_modules_guide", "redistribution", "node_modules_compat"]:
d = base / "src" / name
d.mkdir(parents=True, exist_ok=True)
(d / "file.py").touch()

optimizer = ContextOptimizer(base_dir=str(base))
optimizer._analyze_project_structure()

cached = set(optimizer._directory_cache.keys())
for name in ["rebuild", "apm_modules_guide", "redistribution", "node_modules_compat"]:
assert base / "src" / name in cached, f"'{name}' was incorrectly excluded"

def test_exact_exclusion_names_still_excluded(self):
"""Directories exactly named 'build', 'dist', etc. must still be excluded."""
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir).resolve()
for name in ["build", "dist", "node_modules", "__pycache__"]:
d = base / name
d.mkdir(parents=True, exist_ok=True)
(d / "file.py").touch()
(base / "src").mkdir(exist_ok=True)
(base / "src" / "app.py").touch()

optimizer = ContextOptimizer(base_dir=str(base))
optimizer._analyze_project_structure()

cached = set(optimizer._directory_cache.keys())
assert base / "src" in cached
for name in ["build", "dist", "node_modules", "__pycache__"]:
assert base / name not in cached, f"'{name}' should be excluded"

def test_nested_exclusion_name_still_excluded(self):
"""A directory named 'build' nested under a non-excluded parent must still be excluded."""
with tempfile.TemporaryDirectory() as tmpdir:
base = Path(tmpdir).resolve()
(base / "src" / "build").mkdir(parents=True)
(base / "src" / "build" / "output.js").touch()
(base / "src" / "app.py").touch()

optimizer = ContextOptimizer(base_dir=str(base))
optimizer._analyze_project_structure()

cached = set(optimizer._directory_cache.keys())
assert base / "src" in cached
assert base / "src" / "build" not in cached


class TestExpandGlobPattern:
"""Test _expand_glob_pattern brace expansion."""

Expand Down