diff --git a/README.md b/README.md index 48c9682..f362e64 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ If working through CoreCoder was useful, here are a few other tools I've built a ## Contributing / License -Before you send anything, run `pytest tests/ -q` (86 tests), `ruff check`, and `compileall`, and make sure they're green. MIT licensed: fork it, learn from it, ship something better. A mention of this project is appreciated. +Before you send anything, run `pytest tests/ -q` (87 tests), `ruff check`, and `compileall`, and make sure they're green. MIT licensed: fork it, learn from it, ship something better. A mention of this project is appreciated. --- diff --git a/README_CN.md b/README_CN.md index 328e6ab..8f5408b 100644 --- a/README_CN.md +++ b/README_CN.md @@ -200,7 +200,7 @@ quit / exit 退出(Ctrl+C 取消当前回合) ## 贡献 / License -动手之前先跑一遍 `pytest tests/ -q`(86 个测试)、`ruff check` 和 `compileall`,绿了再提。MIT License,欢迎 fork 拿去造更好的东西,能在 README 里留一句出处就更好。 +动手之前先跑一遍 `pytest tests/ -q`(87 个测试)、`ruff check` 和 `compileall`,绿了再提。MIT License,欢迎 fork 拿去造更好的东西,能在 README 里留一句出处就更好。 --- diff --git a/corecoder/tools/grep.py b/corecoder/tools/grep.py index 60f4165..7ddf319 100644 --- a/corecoder/tools/grep.py +++ b/corecoder/tools/grep.py @@ -45,9 +45,11 @@ def execute(self, pattern: str, path: str = ".", include: str | None = None) -> if base.is_file(): files = [base] + scan_truncated = False else: - files = self._walk(base, include) + files, scan_truncated = self._walk(base, include) + scan_limit_msg = "... (5000 file scan limit reached; results may be incomplete)" matches = [] for fp in files: try: @@ -59,14 +61,23 @@ def execute(self, pattern: str, path: str = ".", include: str | None = None) -> matches.append(f"{fp}:{lineno}: {line.rstrip()}") if len(matches) >= 200: matches.append("... (200 match limit reached)") + if scan_truncated: + matches.append(scan_limit_msg) return "\n".join(matches) - return "\n".join(matches) if matches else "No matches found." + if matches: + if scan_truncated: + matches.append(scan_limit_msg) + return "\n".join(matches) + if scan_truncated: + return f"No matches found in scanned files.\n{scan_limit_msg}" + return "No matches found." @staticmethod - def _walk(root: Path, include: str | None) -> list[Path]: + def _walk(root: Path, include: str | None) -> tuple[list[Path], bool]: """Walk dir tree, skipping junk dirs.""" results = [] + truncated = False for item in root.rglob(include or "*"): # skip junk dirs *inside* the search root - matching item.parts would # also catch an ancestor named e.g. "build" and hide the whole tree @@ -75,5 +86,6 @@ def _walk(root: Path, include: str | None) -> list[Path]: if item.is_file(): results.append(item) if len(results) >= 5000: + truncated = True break - return results + return results, truncated diff --git a/tests/test_tools.py b/tests/test_tools.py index 2fc4454..c63a0c1 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -284,6 +284,18 @@ def test_grep_searches_under_skip_named_ancestor(tmp_path): assert "needle" in r +def test_grep_reports_truncated_file_scan(monkeypatch, tmp_path): + """A truncated file scan must be reported as an incomplete result.""" + grep = get_tool("grep") + def fake_walk(root, include): + return [], True + monkeypatch.setattr(type(grep), "_walk", staticmethod(fake_walk)) + r = grep.execute(pattern="needle", path=str(tmp_path)) + assert "No matches found in scanned files." in r + assert "5000 file scan limit reached" in r + assert "results may be incomplete" in r + + def test_grep_skips_junk_dirs_inside_root(tmp_path): """Junk dirs *inside* the search root are still skipped.""" (tmp_path / "node_modules").mkdir()