From cb21b7ffbe12c7e225035eafca2e0e868743305d Mon Sep 17 00:00:00 2001 From: heumsi Date: Tue, 31 Mar 2026 11:50:57 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20rule=20name=20validat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validate rule names against [a-zA-Z0-9_-]+ pattern during config loading. This ensures rule names are compatible with the upcoming inline ignore comment syntax (# pdl: ignore[rule-name]). Co-Authored-By: Claude Opus 4.6 (1M context) --- python_dependency_linter/config.py | 11 +++++- tests/test_config.py | 54 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/python_dependency_linter/config.py b/python_dependency_linter/config.py index 548a775..92e7253 100644 --- a/python_dependency_linter/config.py +++ b/python_dependency_linter/config.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from dataclasses import dataclass from pathlib import Path @@ -38,12 +39,20 @@ def _parse_allow_deny(data: dict | None) -> AllowDeny | None: ) +_RULE_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$") + + def _parse_rules(rules_data: list[dict]) -> list[Rule]: rules = [] for r in rules_data: + name = r["name"] + if not _RULE_NAME_PATTERN.match(name): + raise ValueError( + f"Invalid rule name '{name}'. Rule names must match [a-zA-Z0-9_-]+" + ) rules.append( Rule( - name=r["name"], + name=name, modules=r["modules"], allow=_parse_allow_deny(r.get("allow")), deny=_parse_allow_deny(r.get("deny")), diff --git a/tests/test_config.py b/tests/test_config.py index 980159c..bad4faf 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,7 @@ from pathlib import Path +import pytest + from python_dependency_linter.config import find_config, load_config FIXTURES = Path(__file__).parent / "fixtures" @@ -127,3 +129,55 @@ def test_find_config_skips_pyproject_without_section(tmp_path, monkeypatch): (tmp_path / "pyproject.toml").write_text("[tool.other]\nfoo = 1\n") monkeypatch.chdir(tmp_path) assert find_config() is None + + +def test_valid_rule_names(tmp_path): + config_content = """\ +rules: + - name: attribute-matches-type + modules: src.* + - name: bool_method + modules: src.* + - name: rule1 + modules: src.* +""" + config_file = tmp_path / "config.yaml" + config_file.write_text(config_content) + config = load_config(config_file) + assert len(config.rules) == 3 + + +def test_invalid_rule_name_with_space(tmp_path): + config_content = """\ +rules: + - name: "my rule" + modules: src.* +""" + config_file = tmp_path / "config.yaml" + config_file.write_text(config_content) + with pytest.raises(ValueError, match=r"Invalid rule name 'my rule'"): + load_config(config_file) + + +def test_invalid_rule_name_with_special_char(tmp_path): + config_content = """\ +rules: + - name: "rule!name" + modules: src.* +""" + config_file = tmp_path / "config.yaml" + config_file.write_text(config_content) + with pytest.raises(ValueError, match=r"Invalid rule name 'rule!name'"): + load_config(config_file) + + +def test_invalid_rule_name_with_mixed(tmp_path): + config_content = """\ +rules: + - name: "rule name 123" + modules: src.* +""" + config_file = tmp_path / "config.yaml" + config_file.write_text(config_content) + with pytest.raises(ValueError, match=r"Invalid rule name 'rule name 123'"): + load_config(config_file)