Skip to content

Commit 8df11ee

Browse files
committed
Implement check-useless-excludes meta hook
1 parent 88c676a commit 8df11ee

File tree

4 files changed

+118
-8
lines changed

4 files changed

+118
-8
lines changed

pre_commit/meta_hooks/__init__.py

Whitespace-only changes.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import re
2+
import sys
3+
4+
import pre_commit.constants as C
5+
from pre_commit.clientlib import load_config
6+
from pre_commit.git import get_all_files
7+
8+
9+
def exclude_matches_any(filenames, include, exclude):
10+
include_re, exclude_re = re.compile(include), re.compile(exclude)
11+
for filename in filenames:
12+
if include_re.search(filename) and exclude_re.search(filename):
13+
return True
14+
return False
15+
16+
17+
def check_useless_excludes(config_file=None):
18+
config = load_config(config_file or C.CONFIG_FILE)
19+
files = get_all_files()
20+
useless_excludes = False
21+
22+
exclude = config.get('exclude')
23+
if exclude != '^$' and not exclude_matches_any(files, '', exclude):
24+
print('The global exclude pattern does not match any files')
25+
useless_excludes = True
26+
27+
for repo in config['repos']:
28+
for hook in repo['hooks']:
29+
include, exclude = hook.get('files', ''), hook.get('exclude')
30+
if exclude and not exclude_matches_any(files, include, exclude):
31+
print(
32+
'The exclude pattern for {} does not match any files'
33+
.format(hook['id'])
34+
)
35+
useless_excludes = True
36+
37+
return useless_excludes
38+
39+
40+
if __name__ == '__main__':
41+
sys.exit(check_useless_excludes())

pre_commit/repository.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
import logging
66
import os
7+
import pipes
78
import shutil
89
import sys
910
from collections import defaultdict
@@ -247,13 +248,16 @@ def _venvs(self):
247248

248249

249250
class MetaRepository(LocalRepository):
251+
# Note: the hook `entry` is passed through `shlex.split()` by the command
252+
# runner, so to prevent issues with spaces and backslashes (on Windows) it
253+
# must be quoted here.
250254
meta_hooks = {
251-
'test-hook': {
252-
'name': 'Test Hook',
253-
'files': '',
255+
'check-useless-excludes': {
256+
'name': 'Check for useless excludes',
257+
'files': '.pre-commit-config.yaml',
254258
'language': 'system',
255-
'entry': 'echo "Hello World!"',
256-
'always_run': True,
259+
'entry': pipes.quote(sys.executable),
260+
'args': ['-m', 'pre_commit.meta_hooks.check_useless_excludes'],
257261
},
258262
}
259263

tests/commands/run_test.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ def test_meta_hook_passes(
653653
(
654654
'hooks', (
655655
OrderedDict((
656-
('id', 'test-hook'),
656+
('id', 'check-useless-excludes'),
657657
)),
658658
),
659659
),
@@ -663,13 +663,78 @@ def test_meta_hook_passes(
663663
_test_run(
664664
cap_out,
665665
repo_with_passing_hook,
666-
opts={'verbose': True},
667-
expected_outputs=[b'Hello World!'],
666+
opts={},
667+
expected_outputs=[b'Check for useless excludes'],
668668
expected_ret=0,
669669
stage=False,
670670
)
671671

672672

673+
def test_useless_exclude_global(
674+
cap_out, repo_with_passing_hook, mock_out_store_directory,
675+
):
676+
config = OrderedDict((
677+
('exclude', 'foo'),
678+
(
679+
'repos', [
680+
OrderedDict((
681+
('repo', 'meta'),
682+
(
683+
'hooks', (
684+
OrderedDict((
685+
('id', 'check-useless-excludes'),
686+
)),
687+
),
688+
),
689+
)),
690+
],
691+
),
692+
))
693+
add_config_to_repo(repo_with_passing_hook, config)
694+
695+
_test_run(
696+
cap_out,
697+
repo_with_passing_hook,
698+
opts={'all_files': True},
699+
expected_outputs=[
700+
b'Check for useless excludes',
701+
b'The global exclude pattern does not match any files',
702+
],
703+
expected_ret=1,
704+
stage=False,
705+
)
706+
707+
708+
def test_useless_exclude_for_hook(
709+
cap_out, repo_with_passing_hook, mock_out_store_directory,
710+
):
711+
config = OrderedDict((
712+
('repo', 'meta'),
713+
(
714+
'hooks', (
715+
OrderedDict((
716+
('id', 'check-useless-excludes'),
717+
('exclude', 'foo'),
718+
)),
719+
),
720+
),
721+
))
722+
add_config_to_repo(repo_with_passing_hook, config)
723+
724+
_test_run(
725+
cap_out,
726+
repo_with_passing_hook,
727+
opts={'all_files': True},
728+
expected_outputs=[
729+
b'Check for useless excludes',
730+
b'The exclude pattern for check-useless-excludes '
731+
b'does not match any files',
732+
],
733+
expected_ret=1,
734+
stage=False,
735+
)
736+
737+
673738
@pytest.yield_fixture
674739
def modified_config_repo(repo_with_passing_hook):
675740
with modify_config(repo_with_passing_hook, commit=False) as config:

0 commit comments

Comments
 (0)