Skip to content

Commit

Permalink
Add linter to command line
Browse files Browse the repository at this point in the history
  • Loading branch information
Argmaster committed Jan 31, 2025
1 parent 4808ba2 commit c23252a
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 18 deletions.
13 changes: 13 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,20 @@
"AXBY",
"AYBX",
"Backdrilling",
"cftp",
"chgt",
"clbd",
"clbn",
"cmfr",
"cmnp",
"CMNP",
"cmnt",
"CMPN",
"cpgd",
"cpgn",
"crot",
"csup",
"cval",
"Cxxx",
"DONUTVAR",
"dpmm",
Expand Down
57 changes: 56 additions & 1 deletion src/pygerber/console/gerber.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import json
from pathlib import Path
from typing import Any, Callable, Literal, Optional
from typing import Any, Callable, Iterable, Literal, Optional

import click

Expand Down Expand Up @@ -514,3 +514,58 @@ def _project(files: str, output: str, dpmm: int) -> None:
Layers are merged from first to last, thus last layer will be on top.
"""
raise NotImplementedError


@gerber.command("lint")
@click.argument("files", nargs=-1)
@click.option(
"-r",
"--rules",
type=str,
multiple=True,
help=(
"Linting rules to be applied. Option can be used multiple times and accepts "
"comma separated list of rules, eg. `-r DEP001,DEP002,DEP003 -r DEP004`. "
"If not specified, all available rules will be applied."
),
)
def lint(files: str, rules: list[str]) -> None:
"""Lint Gerber files with specified rules."""
if len(files) == 0:
msg = "At least one file must be specified."
raise click.UsageError(msg)

def _parse_rules(rules: list[str]) -> Iterable[str]:
for rule in rules:
yield from rule.split(",")

from pygerber.gerber.linter import RULE_REGISTRY, Linter
from pygerber.gerber.parser import parse

if len(rules) != 0:
rule_objects = [RULE_REGISTRY[rule_id]() for rule_id in _parse_rules(rules)]
else:
rule_objects = [r() for r in RULE_REGISTRY.values()]

linter = Linter(rule_objects)

for file in files:
path = Path(file).expanduser().resolve()

ast = parse(path.read_text())
violations = linter.lint(ast)

for violation in violations:
click.echo(
f"{path.as_posix()}:{violation.line}:{violation.column} "
f"{violation.rule_id}: {violation.title}"
)


@gerber.command("list-lint-rules")
def list_lint_rules() -> None:
"""List available linting rules."""
from pygerber.gerber.linter.rules import RULE_REGISTRY

for rule_id in RULE_REGISTRY:
click.echo(rule_id)
17 changes: 17 additions & 0 deletions src/pygerber/gerber/linter/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
"""Code diagnostic logic."""

from __future__ import annotations

from pygerber.gerber.linter.event_ast_visitor import EventAstVisitor
from pygerber.gerber.linter.linter import Linter
from pygerber.gerber.linter.rule_violation import RuleViolation
from pygerber.gerber.linter.rules import DEP001, RULE_REGISTRY, Rule, StaticRule

__all__ = [
"DEP001",
"RULE_REGISTRY",
"EventAstVisitor",
"Linter",
"Rule",
"RuleViolation",
"StaticRule",
]
2 changes: 2 additions & 0 deletions src/pygerber/gerber/linter/rule_violation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ class RuleViolation(BaseModel):
description: str
start_offset: int
end_offset: int
line: int
column: int
2 changes: 1 addition & 1 deletion src/pygerber/gerber/linter/rules/DEP001.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class DEP001(StaticRule):
"""Rule DEP001 class implements a specific linting rule."""

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

from __future__ import annotations

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


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

rule_id = "DEP002"
title = "Use of deprecated G55 code."
description = (
"This historic code optionally precedes D03 code. It has no effect. "
"Deprecated in 2012."
)
trigger_nodes = (G55,)
9 changes: 9 additions & 0 deletions src/pygerber/gerber/linter/rules/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
"""`rules` package contains all the linting rules for Gerber files."""

from __future__ import annotations

from pygerber.gerber.linter.rules.DEP001 import DEP001
from pygerber.gerber.linter.rules.DEP002 import DEP002
from pygerber.gerber.linter.rules.rule import RULE_REGISTRY, Rule
from pygerber.gerber.linter.rules.static_rule import StaticRule

__all__ = ["DEP001", "DEP002", "RULE_REGISTRY", "Rule", "StaticRule"]
14 changes: 10 additions & 4 deletions src/pygerber/gerber/linter/rules/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from abc import ABC, abstractmethod
from typing import ClassVar, Optional, TypeVar

from pygerber.gerber.ast.nodes import Node
from pygerber.gerber.ast.nodes import Node, SourceInfo
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
Expand Down Expand Up @@ -38,15 +38,21 @@ def bind_rule_to_violation_collector(self, collector: ViolationCollector) -> Non
"""Bind the rule to the violation collector."""
self.collector = collector

def report_violation(self, start_offset: int, end_offset: int) -> None:
def report_violation(self, source_info: Optional[SourceInfo]) -> 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,
start_offset=(source_info.location if source_info is not None else 0),
end_offset=(
source_info.location + source_info.length
if source_info is not None
else 0
),
line=(source_info.line if source_info is not None else 0),
column=(source_info.column if source_info is not None else 0),
)
self.collector.add_violation(violation)

Expand Down
15 changes: 3 additions & 12 deletions src/pygerber/gerber/linter/rules/static_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class StaticRule(Rule):
"""

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

def get_violation_title(self) -> str:
Expand All @@ -21,21 +21,12 @@ def get_violation_title(self) -> str:

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

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
),
)
self.report_violation(node.source_info)
Empty file.
27 changes: 27 additions & 0 deletions test/unit/test_gerber/test_linter/test_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from __future__ import annotations

from pygerber.gerber.ast.nodes import G54, G55, File
from pygerber.gerber.linter.linter import Linter
from pygerber.gerber.linter.rules import DEP001, DEP002


def test_DEP001() -> None:
rule = DEP001()
linter = Linter([rule])

ast = File(nodes=[G54()])

violations = linter.lint(ast)

assert len(list(violations)) == 1


def test_DEP002() -> None:
rule = DEP002()
linter = Linter([rule])

ast = File(nodes=[G55()])

violations = linter.lint(ast)

assert len(list(violations)) == 1

0 comments on commit c23252a

Please sign in to comment.