Skip to content

Commit 07cb739

Browse files
committed
feat(cli): --entrypoint-rules for user rule packs (#27)
1 parent ac476bd commit 07cb739

2 files changed

Lines changed: 45 additions & 1 deletion

File tree

codeanalyzer/__main__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import sys
33
from importlib.metadata import version as _pkg_version, PackageNotFoundError
44
from pathlib import Path
5-
from typing import Optional, Annotated
5+
from typing import List, Optional, Annotated
66

77
import typer
88

@@ -299,6 +299,14 @@ def main(
299299
min=-1,
300300
),
301301
] = 50,
302+
entrypoint_rules: Annotated[
303+
Optional[List[Path]],
304+
typer.Option(
305+
"--entrypoint-rules",
306+
help="Extra entrypoint rules file (YAML). Repeatable; merges with "
307+
"the shipped rules. A malformed file is an error.",
308+
),
309+
] = None,
302310
):
303311
# Determinism: pin the interpreter hash seed before any analysis (no-op
304312
# when PYTHONHASHSEED is already set; --version exits before this).
@@ -385,6 +393,7 @@ def main(
385393
pycg_shard_timeout=pycg_shard_timeout,
386394
pycg_shard_strategy=pycg_shard_strategy,
387395
pycg_max_iter=pycg_max_iter,
396+
entrypoint_rules=tuple(entrypoint_rules or ()),
388397
)
389398

390399
_set_log_level(options.verbosity)

test/test_entrypoint_rules.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,38 @@ def test_well_formed_disable_list_removes_the_shipped_rule(tmp_path):
3939
rs = load_rules([user])
4040
flask = rs.frameworks["flask"]
4141
assert all(r.id != "flask.route" for r in flask.decorators)
42+
43+
44+
def test_user_rules_merge_additively_with_shipped(tmp_path):
45+
extra = tmp_path / "mine.yml"
46+
extra.write_text(
47+
"version: 1\n"
48+
"frameworks:\n"
49+
" inhouse:\n"
50+
" detect: [inhouse]\n"
51+
" decorators:\n"
52+
" - id: inhouse.handler\n"
53+
" match: 'inhouse.app.handler'\n"
54+
)
55+
rs = load_rules([extra])
56+
assert "flask" in rs.frameworks # shipped survives
57+
assert "inhouse" in rs.frameworks # user added
58+
assert rs.rulesets == ["shipped", f"user:{extra}"]
59+
60+
61+
def test_user_file_can_disable_a_shipped_rule(tmp_path):
62+
off = tmp_path / "off.yml"
63+
off.write_text("version: 1\ndisable: [flask.route]\n")
64+
rs = load_rules([off])
65+
assert not any(r.id == "flask.route" for r in rs.frameworks["flask"].decorators)
66+
assert any(r.id == "flask.bp-verb" for r in rs.frameworks["flask"].decorators)
67+
68+
69+
def test_bad_confidence_value_is_rejected(tmp_path):
70+
bad = tmp_path / "c.yml"
71+
bad.write_text(
72+
"version: 1\nframeworks:\n x:\n decorators:\n"
73+
" - id: x.y\n match: 'x.y'\n confidence: probably\n"
74+
)
75+
with pytest.raises(RulesError):
76+
load_rules([bad])

0 commit comments

Comments
 (0)