diff --git a/src/git_commit_guard/__init__.py b/src/git_commit_guard/__init__.py index 99c048f..ba4f2e4 100644 --- a/src/git_commit_guard/__init__.py +++ b/src/git_commit_guard/__init__.py @@ -34,6 +34,15 @@ ) _NON_IMPERATIVE_SUFFIX_RE = re.compile(r"(?:ing|ed)$") +_VERB_FORMING_PREFIXES = frozenset( + { + "re", + "pre", + "auto", + "co", + "under", + } +) _TRAILER_RE = re.compile(r"^[\w-]+:\s+\S") _GITHUB_REMOTE_RE = re.compile( r"github\.com[:/](?P[^/]+)/(?P[^/\s]+?)(?:\.git)?$" @@ -235,6 +244,13 @@ def check_imperative(desc, result): if tagged[1][1] != "VB": if wordnet.morphy(first, wordnet.VERB) == first: return + if "-" in first: + hyphen_prefix, hyphen_base = first.split("-", 1) + if ( + hyphen_prefix in _VERB_FORMING_PREFIXES + and wordnet.morphy(hyphen_base, wordnet.VERB) == hyphen_base + ): + return result.error( f"expected imperative verb, got '{tagged[1][0]}' (POS={tagged[1][1]})", check=Check.IMPERATIVE, diff --git a/tests/test_git_commit_guard.py b/tests/test_git_commit_guard.py index 73f7c16..7656167 100644 --- a/tests/test_git_commit_guard.py +++ b/tests/test_git_commit_guard.py @@ -573,6 +573,39 @@ def test_pos_fallback_unknown_word_fails(self): assert not r.ok assert "POS=NN" in r.errors[0][2] + def test_hyphen_re_prefix_verb_passes(self): + r = Result() + check_imperative("re-enable signature check", r) + assert r.ok + + def test_hyphen_auto_prefix_verb_passes(self): + r = Result() + check_imperative("auto-detect format from header", r) + assert r.ok + + def test_hyphen_pre_prefix_verb_passes(self): + r = Result() + check_imperative("pre-process input lines", r) + assert r.ok + + def test_hyphen_unknown_prefix_fails(self): + # 'high' is not in the verb-forming prefix allowlist + r = Result() + check_imperative("high-level overview of foo", r) + assert not r.ok + + def test_hyphen_non_verb_base_fails(self): + # 'in' is not a verb, so even though split shape matches, base check rejects + r = Result() + check_imperative("built-in helper function", r) + assert not r.ok + + def test_hyphen_inflected_suffix_still_fails(self): + # Suffix check runs before hyphen escape — 're-running' caught by ing$ + r = Result() + check_imperative("re-running the tests", r) + assert not r.ok + class TestDownloadIfMissing: def test_skips_download_when_present(self):