Skip to content

Commit cbb5684

Browse files
ptr727claude
andcommitted
Refuse a Path That Does Not Exist, Rather Than Scanning the Caller's Directory
`discover` reads any argument that is neither a file nor a directory as `.`, so a typo scanned the caller's directory while the rule set anchored on the missing path's parent. The run then described one tree and judged it by another, which is this PR's own subject in a different guise. A missing path is now refused, naming each one. Two existing cases passed fake paths as a mocking convenience and were refused by the new check before reaching the guard they exist to test. They use real directories now, which preserves what they assert rather than loosening the check to accommodate them. Two cases added: a missing path is refused, and an existing one is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f2037e7 commit cbb5684

2 files changed

Lines changed: 36 additions & 2 deletions

File tree

‎scripts/prose_lint.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,14 @@ def main(argv: list[str] | None = None) -> int:
11101110
# Only a repository declares a model, so two of them refuse and anything else resolves to one anchor.
11111111
# TestScanRootDecidesTheRuleSet carries the cases and the reason each one exists.
11121112
scan_paths=a.pathsor ['.']
1113+
# A path that does not exist is refused rather than absorbed.
1114+
# `discover` reads a non-file, non-directory argument as `.`, so a typo scanned the caller's directory while the rule set anchored on the missing path's parent.
1115+
absent= [pforpinscan_pathsifnotPath(p).exists()]
1116+
ifabsent:
1117+
print(f"error: requested path(s) do not exist: {', '.join(sorted(absent))}. Refusing "
1118+
'rather than falling back to the current directory, which would scan one tree and '
1119+
'choose the rule set from another.', file=sys.stderr)
1120+
return2
11131121
git_roots= {foundforfoundin (repo_root(Path(p)) forpinscan_paths) iffound}
11141122
iflen(git_roots) >1:
11151123
print('error: the requested paths span more than one repository ('+

‎scripts/test_prose_lint.py‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,20 +1528,26 @@ def test_a_path_under_no_repository_is_refused_too(self) -> None:
15281528
repository-relative ones, so the scope drops every file and the run exits 0. Testing for
15291529
a *different* root missed this, because there is no root to differ from.
15301530
"""
1531+
# A real directory, since a path that does not exist is refused earlier for another reason.
1532+
# That would pass this assertion without ever exercising the diff guard.
1533+
loose=self.tmp/'loose'
1534+
loose.mkdir()
15311535
withmock.patch.object(prose_lint, 'repo_root',
15321536
side_effect=lambdap: '/hub'ifstr(p) =='.'else''), \
15331537
contextlib.redirect_stderr(io.StringIO()) aserr:
1534-
self.assertEqual(2, prose_lint.main(['--diff', 'HEAD', '/tmp/loose']))
1538+
self.assertEqual(2, prose_lint.main(['--diff', 'HEAD', str(loose)]))
15351539
self.assertIn('no git repository', err.getvalue())
15361540

15371541
deftest_list_files_still_reports_scope_across_repositories(self) ->None:
15381542
"""It reports the scan scope and never consults the diff, so the guard must not stop it."""
15391543
clean=self.tmp/'clean.md'
15401544
clean.write_text('fine\n', encoding='utf-8')
1545+
other=self.tmp/'other'
1546+
other.mkdir()
15411547
withmock.patch.object(prose_lint, 'repo_root',
15421548
side_effect=lambdap: '/hub'ifstr(p) =='.'else'/other'), \
15431549
mock.patch.object(prose_lint, 'discover', return_value=[clean]):
1544-
self.assertEqual(0, prose_lint.main(['--list-files', '--diff', 'HEAD', '/other/tree']))
1550+
self.assertEqual(0, prose_lint.main(['--list-files', '--diff', 'HEAD', str(other)]))
15451551

15461552
deftest_a_matching_repository_is_not_refused(self) ->None:
15471553
"""The guard must not reject the ordinary case it sits in front of."""
@@ -1851,6 +1857,26 @@ def test_paths_spanning_two_repositories_refuse_rather_than_pick_one(self) -> No
18511857
[str(self.release), str(self.operational), '--check', 'home-path']))
18521858
self.assertIn('more than one repository', self.err.getvalue())
18531859

1860+
deftest_a_path_that_does_not_exist_is_refused_rather_than_absorbed(self) ->None:
1861+
"""`discover` reads a non-file, non-directory argument as `.`.
1862+
1863+
A typo therefore scanned the caller's directory while the rule set anchored on the missing
1864+
path's parent, so the run described one tree and judged it by another.
1865+
"""
1866+
withmock.patch.object(prose_lint, 'repo_root', return_value=''):
1867+
self.assertEqual(2, prose_lint.main(
1868+
[str(self.tmp/'no-such-dir'), '--check', 'home-path']))
1869+
self.assertIn('do not exist', self.err.getvalue())
1870+
1871+
deftest_an_existing_path_is_not_refused_by_that_check(self) ->None:
1872+
"""The guard must not reject the ordinary case it sits in front of."""
1873+
good=self.tmp/'good.md'
1874+
good.write_text('Nothing here breaks a rule.\n', encoding='utf-8')
1875+
withmock.patch.object(prose_lint, 'repo_root', return_value=''), \
1876+
mock.patch.object(prose_lint, 'discover', return_value=[good]):
1877+
self.assertEqual(0, prose_lint.main([str(good), '--check', 'home-path']))
1878+
self.assertNotIn('do not exist', self.err.getvalue())
1879+
18541880
deftest_several_paths_under_no_repository_are_not_a_conflict(self) ->None:
18551881
"""Only a repository declares a model, so two loose paths are not ambiguous.
18561882

0 commit comments

Comments
 (0)