From cbb8739b9b33f6c11ba6db761646e3839be00c19 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 23 Oct 2026 00:20:39 +0200 Subject: [PATCH 1/2] fix(engine): describe the rules of plugins given as instances A check gathered the rule descriptions of the plugins it loaded from plugin roots only. A plugin given to analyze_project as an instance reported its findings, but its rules reached the report by id alone. The descriptions now come from every plugin of the check, loaded ones first, then those given as instances, the first declaration of an id kept. --- src/coretrace_python/engine.py | 13 +++---- ...test_rule_descriptions_of_given_plugins.py | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 tests/test_rule_descriptions_of_given_plugins.py diff --git a/src/coretrace_python/engine.py b/src/coretrace_python/engine.py index e29a52c..63830e7 100644 --- a/src/coretrace_python/engine.py +++ b/src/coretrace_python/engine.py @@ -176,13 +176,14 @@ ) -def rule_descriptions(registry: PluginRegistry) -> Mapping[str, RuleDescription]: +def rule_descriptions(plugins: Iterable[Plugin]) -> Mapping[str, RuleDescription]: """What every rule of a check detects, for its report: the rules the engine reports - itself, and those each loaded plugin describes, the first declaration of an id kept.""" + itself, and those each of its plugins describes, loaded or given as an instance, the + first declaration of an id kept.""" described: dict[str, RuleDescription] = dict(ENGINE_RULES) - for loaded in registry: - for rule_id, description in loaded.plugin.rules.items(): + for plugin in plugins: + for rule_id, description in plugin.rules.items(): described.setdefault(rule_id, description) return MappingProxyType(described) @@ -395,7 +396,7 @@ def analyze_file(source: SourceFile, plugin_roots: Sequence[Path], root: Path | functions = len(analyzable_functions(manager.module)) coverage = Coverage((FileCoverage(str(source.source_id), "analysed", functions, len(supported)),)) kept, suppressed = partition(findings, lambda sid: source.text if sid == source.source_id else None) - return FileAnalysis(kept, coverage, suppressed, rule_descriptions(registry)) + return FileAnalysis(kept, coverage, suppressed, rule_descriptions(loaded.plugin for loaded in registry)) def resolve_dependencies(root: Path, sources: SourceManager) -> DependencyGraph: @@ -692,7 +693,7 @@ def analyze_project( suppressed, accepted, components, - rule_descriptions(registry), + rule_descriptions(all_plugins), ) diff --git a/tests/test_rule_descriptions_of_given_plugins.py b/tests/test_rule_descriptions_of_given_plugins.py new file mode 100644 index 0000000..68a6a90 --- /dev/null +++ b/tests/test_rule_descriptions_of_given_plugins.py @@ -0,0 +1,35 @@ +"""Acceptance tests for the review of #274: a plugin given to ``analyze_project`` as an +instance, rather than loaded from a plugin root, describes its rules in the report as a +loaded plugin does. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import ClassVar + +from coretrace_python import engine +from coretrace_python.findings import Finding, RuleDescription +from coretrace_python.plugins import Plugin, PluginContext + +QUIET = RuleDescription("Quiet rule", "A rule of a plugin given as an instance.") + + +class Quiet(Plugin): + name: ClassVar[str] = "quiet" + rules: ClassVar[Mapping[str, RuleDescription]] = { + "quiet-rule": QUIET, + } + + def analyze(self, ctx: PluginContext) -> Sequence[Finding]: + return () + + +def analysed(tmp_path: Path) -> engine.ProjectAnalysis: + (tmp_path / "app.py").write_text("x = 1\n", encoding="utf-8") + return engine.analyze_project(tmp_path, [engine.BUNDLED_PLUGINS], plugins=[Quiet()]) + + +def test_a_plugin_given_as_an_instance_describes_its_rules(tmp_path: Path) -> None: + assert analysed(tmp_path).rules["quiet-rule"] == QUIET From 40c0442ac765c2e1bf7c83785e6479b6557baf68 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 23 Oct 2026 00:20:40 +0200 Subject: [PATCH 2/2] test(rules): keep a loaded plugin's description of a rule an instance describes too A plugin given as an instance that describes command-injection again does not replace the description of the bundled plugin, loaded before it. --- tests/test_rule_descriptions_of_given_plugins.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_rule_descriptions_of_given_plugins.py b/tests/test_rule_descriptions_of_given_plugins.py index 68a6a90..f39d5f9 100644 --- a/tests/test_rule_descriptions_of_given_plugins.py +++ b/tests/test_rule_descriptions_of_given_plugins.py @@ -1,6 +1,6 @@ """Acceptance tests for the review of #274: a plugin given to ``analyze_project`` as an instance, rather than loaded from a plugin root, describes its rules in the report as a -loaded plugin does. +loaded plugin does, the first declaration of an id kept. """ from __future__ import annotations @@ -20,6 +20,7 @@ class Quiet(Plugin): name: ClassVar[str] = "quiet" rules: ClassVar[Mapping[str, RuleDescription]] = { "quiet-rule": QUIET, + "command-injection": RuleDescription("Another title", "Another description."), } def analyze(self, ctx: PluginContext) -> Sequence[Finding]: @@ -33,3 +34,7 @@ def analysed(tmp_path: Path) -> engine.ProjectAnalysis: def test_a_plugin_given_as_an_instance_describes_its_rules(tmp_path: Path) -> None: assert analysed(tmp_path).rules["quiet-rule"] == QUIET + + +def test_a_loaded_plugin_keeps_the_description_of_a_rule_an_instance_describes_too(tmp_path: Path) -> None: + assert analysed(tmp_path).rules["command-injection"].title == "Command injection"