Skip to content

Commit

Permalink
Introduce linter infrastructure
Browse files Browse the repository at this point in the history
  • Loading branch information
Argmaster committed Jan 31, 2025
1 parent defddf9 commit 4808ba2
Show file tree
Hide file tree
Showing 9 changed files with 819 additions and 0 deletions.
613 changes: 613 additions & 0 deletions src/pygerber/gerber/linter/event_ast_visitor.py

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions src/pygerber/gerber/linter/linter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""`linter` module contains Linter class implementation."""

from __future__ import annotations

from typing import Iterable

from pygerber.gerber.ast.nodes.file import File
from pygerber.gerber.linter.event_ast_visitor import EventAstVisitor
from pygerber.gerber.linter.rule_violation import RuleViolation
from pygerber.gerber.linter.rules.rule import Rule
from pygerber.gerber.linter.violation_collector import ViolationCollector


class Linter:
"""Linter class implements high level linting API for Gerber files."""

def __init__(self, rules: list[Rule]) -> None:
"""Initialize the Linter object."""
self.rules = rules

def _register_rule(self, rule: Rule) -> None:
"""Register a rule with the linter."""
rule.bind_rule_to_ast_visitor(self.event_ast_visitor)
rule.bind_rule_to_violation_collector(self.violation_collector)

def lint(self, ast: File) -> Iterable[RuleViolation]:
"""Lint the AST and return a object containing all violations."""
self.violation_collector = ViolationCollector()
self.event_ast_visitor = EventAstVisitor()

for rule in self.rules:
self._register_rule(rule)

ast.visit(self.event_ast_visitor)
return self.violation_collector.violations
15 changes: 15 additions & 0 deletions src/pygerber/gerber/linter/rule_violation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""`rule_violation` contains the RuleViolation class."""

from __future__ import annotations

from pydantic import BaseModel


class RuleViolation(BaseModel):
"""Represents a rule violation detected by the linter."""

rule_id: str
title: str
description: str
start_offset: int
end_offset: int
21 changes: 21 additions & 0 deletions src/pygerber/gerber/linter/rules/DEP001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""`DEP001` module contains linter rule DEP001 implementation.""" # noqa: N999

from __future__ import annotations

from pygerber.gerber.ast.nodes import G54
from pygerber.gerber.linter.rules.rule import register_rule
from pygerber.gerber.linter.rules.static_rule import StaticRule


@register_rule
class DEP001(StaticRule):
"""Rule DEP001 class implements a specific linting rule."""

rule_id = "DEP001"
message = "Use of deprecated G54 code."
description = (
"This historic code optionally precedes an aperture "
"selection Dnn command. It has no effect. "
"Sometimes used. Deprecated in 2012."
)
trigger_nodes = (G54,)
9 changes: 9 additions & 0 deletions src/pygerber/gerber/linter/rules/GRB001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""`GRB001` module contains linter rule GRB001 implementation.""" # noqa: N999

from __future__ import annotations


class GRB001:
"""Rule GRB001 class implements a specific linting rule."""

rule_id = "GRB001"
1 change: 1 addition & 0 deletions src/pygerber/gerber/linter/rules/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""`rules` package contains all the linting rules for Gerber files."""
67 changes: 67 additions & 0 deletions src/pygerber/gerber/linter/rules/rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""`rule` module contains the Rule class."""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import ClassVar, Optional, TypeVar

from pygerber.gerber.ast.nodes import Node
from pygerber.gerber.linter.event_ast_visitor import EventAstVisitor
from pygerber.gerber.linter.rule_violation import RuleViolation
from pygerber.gerber.linter.violation_collector import ViolationCollector


class Rule(ABC):
"""Base class for Gerber linter rules."""

rule_id: ClassVar[str]
collector: Optional[ViolationCollector] = None

@abstractmethod
def get_violation_title(self) -> str:
"""Return a title of message that describes the rule violation."""

@abstractmethod
def get_violation_description(self) -> str:
"""Return a description of the rule violation."""

@abstractmethod
def get_trigger_nodes(self) -> list[type[Node]]:
"""Return a list of node names that trigger the rule."""

def bind_rule_to_ast_visitor(self, visitor: EventAstVisitor) -> None:
"""Bind the rule to the visitor."""
for node_type in self.get_trigger_nodes():
visitor.register_listener(node_type, self.node_callback)

def bind_rule_to_violation_collector(self, collector: ViolationCollector) -> None:
"""Bind the rule to the violation collector."""
self.collector = collector

def report_violation(self, start_offset: int, end_offset: int) -> None:
"""Report a violation."""
if self.collector is not None:
violation = RuleViolation(
rule_id=self.rule_id,
title=self.get_violation_title(),
description=self.get_violation_description(),
start_offset=start_offset,
end_offset=end_offset,
)
self.collector.add_violation(violation)

@abstractmethod
def node_callback(self, node: Node) -> None:
"""Check the node for violations."""


RULE_REGISTRY: dict[str, type[Rule]] = {}

T = TypeVar("T", bound="Rule")


def register_rule(rule: type[T]) -> type[T]:
"""Register a rule with the linter."""
assert rule.rule_id not in RULE_REGISTRY, f"Rule {rule.rule_id} already registered."
RULE_REGISTRY[rule.rule_id] = rule
return rule
41 changes: 41 additions & 0 deletions src/pygerber/gerber/linter/rules/static_rule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""`static_rule` module contains definition of StaticRule class."""

from __future__ import annotations

from pygerber.gerber.ast.nodes import Node
from pygerber.gerber.linter.rules.rule import Rule


class StaticRule(Rule):
"""StaticRule class is a base class for simple rules requiring no dynamic messages
and no logic except boolean triggered/not triggered check.
"""

title: str
message: str
trigger_nodes: tuple[type[Node]]

def get_violation_title(self) -> str:
"""Return a title of message that describes the rule violation."""
return self.title

def get_violation_description(self) -> str:
"""Return a description of the rule violation."""
return self.message

def get_trigger_nodes(self) -> list[type[Node]]:
"""Return a list of node names that trigger the rule."""
return list(self.trigger_nodes)

def node_callback(self, node: Node) -> None:
"""Check the node for violations."""
self.report_violation(
start_offset=(
node.source_info.location if node.source_info is not None else 0
),
end_offset=(
node.source_info.location + node.source_info.length
if node.source_info is not None
else 0
),
)
17 changes: 17 additions & 0 deletions src/pygerber/gerber/linter/violation_collector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""`violation_collector` module contains the ViolationCollector class."""

from __future__ import annotations

from pygerber.gerber.linter.rule_violation import RuleViolation


class ViolationCollector:
"""ViolationCollector class is a container for RuleViolations."""

def __init__(self) -> None:
"""Initialize the ViolationCollector object."""
self.violations: list[RuleViolation] = []

def add_violation(self, violation: RuleViolation) -> None:
"""Add a violation to the collector."""
self.violations.append(violation)

0 comments on commit 4808ba2

Please sign in to comment.