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
48 changes: 47 additions & 1 deletion code2logic/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ class ProjectAnalyzer:
".mypy_cache",
".ruff_cache",
".cache",
".subactor",
".worktrees",
".planfile",
".benchmarks",
"managed_components",
"workshop",
}

# Files to ignore
Expand Down Expand Up @@ -132,6 +138,7 @@ def __init__(
include_private: bool = False,
enable_similarity: bool = True,
respect_gitignore: bool = True,
exclude: Optional[List[str]] = None,
):
"""
Initialize the project analyzer.
Expand All @@ -142,12 +149,14 @@ def __init__(
verbose: Whether to print status messages
include_private: Whether to include private functions/classes
enable_similarity: Whether to enable similarity detection
exclude: Optional list of exclusion glob patterns or directories
"""
self.root_path = Path(root_path).resolve()
self.verbose = verbose
self.include_private = include_private
self.enable_similarity = enable_similarity
self.respect_gitignore = respect_gitignore
self.exclude_patterns: List[str] = list(exclude or [])
self.modules: List[ModuleInfo] = []
self.languages: Dict[str, int] = defaultdict(int)

Expand All @@ -164,6 +173,30 @@ def __init__(
if verbose:
self._print_status()

def _is_excluded(self, path: Path | str) -> bool:
"""Check if a path matches any exclude patterns."""
if not self.exclude_patterns:
return False
import fnmatch

try:
rel = str(Path(path).relative_to(self.root_path)).replace("\\", "/")
except Exception:
rel = str(path).replace("\\", "/")

parts = set(rel.split("/"))
for pattern in self.exclude_patterns:
clean = pattern.rstrip("/").replace("\\", "/")
if clean in parts:
return True
if fnmatch.fnmatch(rel, pattern) or fnmatch.fnmatch(rel, clean):
return True
if pattern.endswith("/**") and (
rel == pattern[:-3] or rel.startswith(pattern[:-2])
):
return True
return False

def _print_status(self):
"""Print library availability status."""
parts = []
Expand Down Expand Up @@ -275,6 +308,12 @@ def _scan_files(self):
if filename in self.IGNORE_FILES:
continue

if any(part in self.IGNORE_DIRS for part in fp.parts):
continue

if self._is_excluded(fp):
continue

ext = fp.suffix.lower()
language = self.LANGUAGE_EXTENSIONS.get(ext)
if language is None and ext == "":
Expand Down Expand Up @@ -342,14 +381,21 @@ def _scan_files(self):

else:
for root, dirnames, filenames in os.walk(self.root_path):
dirnames[:] = [d for d in dirnames if d not in self.IGNORE_DIRS]
dirnames[:] = [
d
for d in dirnames
if d not in self.IGNORE_DIRS and not self._is_excluded(Path(root) / d)
]
for filename in filenames:
files_seen += 1
fp = Path(root) / filename

if filename in self.IGNORE_FILES:
continue

if self._is_excluded(fp):
continue

ext = fp.suffix.lower()
language = self.LANGUAGE_EXTENSIONS.get(ext)
if language is None and ext == "":
Expand Down
8 changes: 8 additions & 0 deletions code2logic/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,13 @@ def main(argv=None):
action="store_true",
help="Do not respect .gitignore (scan all files under path)",
)
parser.add_argument(
"--exclude",
"-e",
nargs="+",
default=[],
help="Patterns or directories to exclude from analysis",
)
parser.add_argument(
"--no-similarity",
action="store_true",
Expand Down Expand Up @@ -957,6 +964,7 @@ def main(argv=None):
verbose=args.verbose or args.debug,
enable_similarity=not args.no_similarity,
respect_gitignore=not args.no_gitignore,
exclude=args.exclude,
)
project = analyzer.analyze()
analyze_time = time.time() - analyze_start
Expand Down
1 change: 1 addition & 0 deletions project/TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
| **ticket-003** | [`README.md`](./ticket-003/README.md) | - | - | - | - | - |
| **ticket-004** | [`README.md`](./ticket-004/README.md) | - | - | - | - | - |
| **ticket-005** | [`README.md`](./ticket-005/README.md) | - | - | - | - | - |
| **ticket-006** | [`README.md`](./ticket-006/README.md) | - | - | - | - | - |
<!-- AUTO:TICKET_INDEX:END -->
23 changes: 23 additions & 0 deletions project/ticket-007/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Ticket 007: Support exclude patterns and directory pruning in code2logic

- **ID**: ticket-007
- **Owner**: agent:antigravity
- **Status**: IN_PROGRESS
- **Workflow state**: EDIT
- **Created**: 2026-10-13

## Goal and scope

Support custom `--exclude` patterns in CLI and `ProjectAnalyzer`, expand default `IGNORE_DIRS` to prune `.subactor`, `.worktrees`, `.planfile`, `.benchmarks`, `managed_components`, and `workshop`, and filter ignored directories and excluded paths during both git file listing and directory walking.

## Acceptance criteria

- [x] AC-01: Add `.subactor`, `.worktrees`, `.planfile`, `.benchmarks`, `managed_components`, and `workshop` to default `IGNORE_DIRS`.
- [x] AC-02: Support `--exclude` / `-e` CLI option in `code2logic` and `exclude` parameter in `ProjectAnalyzer`.
- [x] AC-03: Filter excluded paths and ignored directories in `_scan_files`.
- [x] AC-04: Full test suite passes.

## Tracking boundary

This directory contains the minimal reviewed intent. Optional participant prose
and raw command logs are not required delivery output.
25 changes: 25 additions & 0 deletions project/ticket-007/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-007",
"summary": "Support exclude patterns and directory pruning in code2logic",
"workstream": "application",
"classification": {
"kind": "SERVICE",
"priority": "P2",
"origin": "health"
},
"allowedPaths": [
"project/ticket-007/**",
"TODO.md",
"project/TICKETS.md",
"code2logic/**",
"tests/**"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null
}
54 changes: 54 additions & 0 deletions tests/test_excludes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Tests for exclude patterns and directory pruning in ProjectAnalyzer."""

import tempfile
from pathlib import Path
from code2logic.analyzer import ProjectAnalyzer


def test_project_analyzer_exclude_directories():
"""Verify that custom exclude patterns exclude directories and files."""
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "src").mkdir()
(root / "src" / "app.py").write_text("def run(): pass\n")
(root / "vendor").mkdir()
(root / "vendor" / "lib.py").write_text("def vendored(): pass\n")
(root / "custom_ignored").mkdir()
(root / "custom_ignored" / "foo.py").write_text("def foo(): pass\n")

analyzer = ProjectAnalyzer(
str(root),
use_treesitter=False,
respect_gitignore=False,
exclude=["custom_ignored", "custom_ignored/**"],
)
project = analyzer.analyze()

module_paths = [m.path for m in project.modules]
assert any("app.py" in p for p in module_paths)
assert not any("foo.py" in p for p in module_paths)
# vendor is in default IGNORE_DIRS
assert not any("lib.py" in p for p in module_paths)


def test_default_ignore_dirs():
"""Verify default IGNORE_DIRS includes .subactor, .worktrees, etc."""
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "main.py").write_text("def main(): pass\n")
(root / ".subactor").mkdir()
(root / ".subactor" / "hidden.py").write_text("def sub(): pass\n")
(root / ".worktrees").mkdir()
(root / ".worktrees" / "wt.py").write_text("def wt(): pass\n")

analyzer = ProjectAnalyzer(
str(root),
use_treesitter=False,
respect_gitignore=False,
)
project = analyzer.analyze()

module_paths = [m.path for m in project.modules]
assert any("main.py" in p for p in module_paths)
assert not any("hidden.py" in p for p in module_paths)
assert not any("wt.py" in p for p in module_paths)
Loading