Skip to content

Latest commit

 

History

History
123 lines (95 loc) · 5.12 KB

File metadata and controls

123 lines (95 loc) · 5.12 KB

Writing a plugin

Two extension points exist: analyzers (produce evidence) and reporters (render results). Both are discovered via standard Python entry points — no changes to this package are needed to add one, and a plugin is just an ordinary pip-installable package.

Writing an analyzer

An analyzer is any class satisfying this shape (crie/analyzers/base.py):

from collections.abc import Iterable
from typing import Any, ClassVar

from crie.core.models import Category, Domain, Evidence, Language
from crie.core.repository import AnalysisContext


class MyAnalyzer:
    name: ClassVar[str] = "my_analyzer"
    supported_languages: ClassVar[frozenset[Language]] = frozenset()  # empty = language-agnostic

    def __init__(self, options: dict[str, Any] | None = None) -> None:
        self.options = options or {}

    def analyze(self, context: AnalysisContext) -> Iterable[Evidence]:
        for file in context.files:
            yield Evidence(
                analyzer=self.name,
                file_path=file.relative_path,
                category=Category.COMPLEXITY,  # feeds the risk dimensions; pick one that fits
                domain=Domain.MAINTAINABILITY,  # feeds software evaluation; optional, set either/both/neither
                metric="my_metric",
                raw_value=42.0,       # HIGHER MUST MEAN RISKIER (see below)
                unit="widgets",
                explanation="42 widgets found",
            )

Notes:

  • No subclassing is required — Analyzer is a structural Protocol. Any object with a name, supported_languages, and analyze() satisfies it.
  • raw_value must be defined so that a higher number means higher risk, within its own metric's distribution. The normalizer and both aggregators treat every metric identically based on this convention; a metric that inverts it (e.g. "test coverage percentage" instead of "coverage gap") will silently score backwards. If a metric genuinely has no consistent risk direction (see architecture.py's instability, which is deliberately informational), leave both category and domain unset — it still gets normalized and shown, just never averaged into a score.
  • context.files is the already-filtered, already-language-detected file list; context.repository.git() gives you the shared, cached GitRepository if you need git history — reuse it rather than shelling out independently.
  • Raise nothing you don't want to abort the whole run over: an analyzer that raises is caught by the pipeline and recorded in metadata.analyzer_errors, but the file it was analyzing gets none of its evidence. Prefer catching your own expected failure modes (bad syntax, missing files) and simply yielding no evidence for that file.
  • Pick an existing Category if one fits (see scoring-model.md) and/or an existing Domain if one fits (see software-evaluation.md); introducing a new one means it won't be included in any built-in weight preset until you also add config overrides for it.
  • context.profile (a ProjectProfile) is available if a finding should be suppressed or reframed for certain project types — see debuggability.py's print_debugging check for a worked example.

Registering it

In your plugin package's pyproject.toml:

[project.entry-points."crie.analyzers"]
my_analyzer = "my_package.analyzers:MyAnalyzer"

Once your package is installed in the same environment as crie, it shows up automatically in crie list-analyzers and can be enabled/configured like any built-in analyzer via crie.config.yaml's analyzers.my_analyzer block.

A plugin that fails to import is skipped with a logged warning (crie/analyzers/registry.py::discover_analyzers) — one broken plugin doesn't take down analysis for everyone else.

Writing a reporter

Same pattern, simpler protocol (crie/reporters/base.py):

from typing import ClassVar

from crie.core.models import AnalysisResult
from crie.reporters.base import Reporter


class MyReporter(Reporter):  # subclassing gets you the default write() for free
    name: ClassVar[str] = "my_format"
    file_extension: ClassVar[str] = "txt"

    def render(self, result: AnalysisResult, top_n: int = 25) -> str:
        return f"{result.repository.name}: {len(result.files)} files analyzed\n"

Subclassing Reporter is optional but gets you write() (creates the output directory, writes report.<file_extension>) for free — you only need to implement render(). Register it the same way:

[project.entry-points."crie.reporters"]
my_format = "my_package.reporters:MyReporter"

It then becomes available via --format my_format on crie analyze and crie report.

Testing a plugin against this project's fixtures

If you're developing against a local checkout of this repo, tests/support/repo_builder.py has a small GitRepoBuilder you can reuse to build a throwaway git repository with controlled, backdated commit history in a test — see tests/conftest.py's crafted_repo fixture for a worked example.