Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,10 @@ ext/pssparser.h
ext/pssparser_api.h

python/core.cpp
python/pssast.cpp
python/pssast.pyx
python/pssast.h
python/pssast_api.h
python/ast.cpp
python/ast.pyx
python/ast.h
python/ast_api.h
python/pssparser/pssast.pxd
python/pssparser/pssast_decl.pxd
python/PyBaseVisitor.*
Expand Down
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,31 @@
This project provides a ANTLR4-based parser for the Accellera PSS language.
It also provides an AST (data model) for processing the result of the parser.

[![Build Status](https://dev.azure.com/mballance/psstools/_apis/build/status/PSSTools.pssparser?branchName=master)](https://dev.azure.com/mballance/psstools/_build/latest?definitionId=15&branchName=master)
[![Build Status](https://dev.azure.com/mballance/psstools/_apis/build/status/PSSTools.pssparser?branchName=master)](https://dev.azure.com/mballance/psstools/_build/latest?definitionId=15&branchName=master)

## Checker Plug-ins

`pssparser` supports a plug-in system for custom Python-based checkers that
run after a successful parse and link. Plug-ins can be contributed by any
installed Python package via the `pssparser.checkers` `entry_points` group,
or loaded on the fly with `--load-checker MODULE:CLASS`.

```bash
# List all registered checkers
pssparser --list-checkers

# List all marker IDs (built-in + plug-in)
pssparser --list-markers

# Describe a specific marker
pssparser --describe PSS001

# Run only a specific checker
pssparser --checker naming-convention model.pss

# Load and run a local checker (no install needed)
pssparser --load-checker myproject.rules:StyleChecker model.pss
```

See [docs/checker_plugin_guide.rst](docs/checker_plugin_guide.rst) for a
full guide to writing and registering checker plug-ins.
289 changes: 289 additions & 0 deletions docs/checker_plugin_guide.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
####################
Checker Plug-in Guide
####################

.. contents::
:local:
:depth: 2

Concept
=======

``pssparser`` ships with built-in syntax and semantic checks implemented in
C++. The **checker plug-in system** lets you — or any third-party package —
add custom Python checks that run as a third phase, after a successful parse
and link.

Each checker is a Python class that inherits from
:class:`pssparser.checkers.CheckerBase`. Checkers receive a
:class:`pssparser.checkers.CheckContext` object that provides read access to
the linked AST and an API to emit structured diagnostics.

The built-in ``CoreChecker`` (name ``"core"``) is always registered and
documents every marker the C++ parser and linker can produce. It cannot be
disabled.

Writing a Checker
=================

Below is a complete, real-world example — a naming-convention checker that
warns when ``action`` or ``struct`` type names do not start with an uppercase
letter. The full source lives in ``examples/pss_naming_checker/``.

.. code-block:: python

# pss_naming/checker.py
from __future__ import annotations
from typing import TYPE_CHECKING

import pssparser.ast as pss_ast
from pssparser.checkers import CheckerBase, MarkerDef

if TYPE_CHECKING:
from pssparser.checkers import CheckContext


class NamingConventionChecker(CheckerBase):
name = "naming-convention"
description = "Warn when action or struct type names do not start with uppercase"

marker_defs = [
MarkerDef(
id="PSC001",
severity="warning",
summary="Action type name does not start with an uppercase letter",
detail=(
"PSS convention uses PascalCase for action type names. "
"Rename the action so that its first letter is uppercase, "
"e.g. rename ``write_data`` to ``WriteData``."
),
),
MarkerDef(
id="PSC002",
severity="warning",
summary="Struct type name does not start with an uppercase letter",
detail=(
"PSS convention uses PascalCase for struct type names. "
"Rename the struct so that its first letter is uppercase, "
"e.g. rename ``my_packet`` to ``MyPacket``."
),
),
]

# Name-checking only needs the parse tree; no linked AST required.
runs_without_link = True

def check(self, context: "CheckContext") -> None:
for global_scope in context.global_scopes:
# context.file_map maps GlobalScope.getFileid() → source path.
# GlobalScope.getFilename() is not reliably set by the parser.
filename = context.file_map.get(global_scope.getFileid(), "")
self._walk(context, global_scope, filename)

def _walk(self, context, scope, filename: str) -> None:
"""Recursively walk *scope* and emit markers for naming violations."""
for child in scope.children():
if isinstance(child, pss_ast.Action):
self._check_name(context, child, filename, "PSC001", "Action")
elif isinstance(child, pss_ast.Struct):
self._check_name(context, child, filename, "PSC002", "Struct")
# Recurse into any child scope (components, packages, …)
if isinstance(child, pss_ast.Scope):
self._walk(context, child, filename)

@staticmethod
def _check_name(context, node, filename, code, kind):
name_expr = node.getName()
name = name_expr.getId()
if not name or name[0].isupper():
return
loc = name_expr.getLocation()
context.add_marker(
code=code,
file=filename,
line=loc.lineno,
col=loc.linepos,
message=f"{kind} '{name}' should start with an uppercase letter",
)

Step-by-step walkthrough:

1. **Subclass** :class:`~pssparser.checkers.CheckerBase`.
2. **Set** ``name`` — a short unique slug used on the command line.
3. **Set** ``description`` — shown by ``--list-checkers``.
4. **Declare** ``marker_defs`` — one :class:`~pssparser.checkers.MarkerDef`
per diagnostic code your checker can emit.
5. **Set** ``runs_without_link = True`` when your checker only needs the raw
parse tree (faster; works even when ``--syntax-only`` is active).
6. **Implement** ``check(context)`` — walk the AST and call
:meth:`~pssparser.checkers.CheckContext.add_marker` to emit diagnostics.

.. tip::

``context.file_map`` is a ``dict[int, str]`` mapping
``GlobalScope.getFileid()`` to the source file path. Use it instead of
``GlobalScope.getFilename()``, which may be empty.

``context.global_scopes`` contains only the user-supplied source files;
the built-in PSS library scopes are filtered out automatically.

Declaring Markers
=================

Every diagnostic your checker may emit must be declared as a
:class:`~pssparser.checkers.MarkerDef` in the class-level ``marker_defs``
list:

.. code-block:: python

from pssparser.checkers import MarkerDef

MarkerDef(
id="PSC001", # globally unique ID
severity="warning", # "error", "warning", "info", or "hint"
summary="Short description for --list-markers",
detail="Longer explanation shown by --describe PSC001",
)

**ID naming convention**: use a three-letter prefix (e.g. ``PSC`` for your
checker package) followed by a zero-padded three-digit number (``PSC001``).
The ``PSS`` prefix is reserved for the built-in ``CoreChecker``. Choose a
unique prefix and register it in your project's documentation to avoid future
collisions.

The :class:`~pssparser.checkers.CheckerManager` enforces globally unique IDs
across all registered checkers at discovery time.

Registering via entry_points
=============================

The standard way to make your checker available to all ``pssparser``
invocations is to declare it as an ``entry_points`` contribution in your
package's ``setup.cfg`` or ``pyproject.toml``:

``setup.cfg``:

.. code-block:: ini

[options.entry_points]
pssparser.checkers =
naming-convention = mypkg.pss_rules:NamingConventionChecker
unused-imports = mypkg.pss_rules:UnusedImportChecker

``pyproject.toml``:

.. code-block:: toml

[project.entry-points."pssparser.checkers"]
naming-convention = "mypkg.pss_rules:NamingConventionChecker"
unused-imports = "mypkg.pss_rules:UnusedImportChecker"

The left-hand side (e.g. ``naming-convention``) becomes the registered
*name* of the checker and is used on the command line with ``--checker`` and
``--no-checker``.

After installation (``pip install .``), your checker is auto-discovered every
time ``pssparser`` runs.

Ad-hoc Loading
==============

For development or one-off use you can load a checker without installing it:

.. code-block:: bash

pssparser --load-checker mypkg.pss_rules:NamingConventionChecker model.pss

The ``--load-checker`` flag may be repeated to load multiple checkers. The
loaded checker participates in all ``--checker`` / ``--no-checker`` filtering
using its ``name`` attribute.

Combine with ``--list-checkers`` to inspect what will run before doing an
actual parse:

.. code-block:: bash

pssparser --load-checker mypkg.pss_rules:NamingConventionChecker \
--list-checkers

Checker Selection
=================

By default, all registered (non-builtin) checkers run. Use these flags to
change that:

``--checker NAME``
Run **only** the named checker(s). May be repeated. ``NAME`` must match
a registered checker name (from ``entry_points``) or a checker previously
loaded with ``--load-checker``. Specifying an unknown name is an error.

.. code-block:: bash

pssparser --checker naming-convention model.pss

``--no-checker NAME``
**Exclude** the named checker. May be repeated. Ignored when
``--checker`` is also specified (explicit selection takes precedence).
Unknown names are silently ignored.

.. code-block:: bash

pssparser --no-checker deprecated-syntax model.pss

Precedence rules:

1. Start with all registered + ``--load-checker`` checkers.
2. If ``--checker`` is present, keep *only* those names.
3. Otherwise, remove any names listed in ``--no-checker``.

Querying the Registry
=====================

``--list-checkers``
Print a table of all registered checkers and their declared marker IDs,
then exit with code 0. No source files are required.

.. code-block:: bash

pssparser --list-checkers

``--list-markers``
Print a table of every declared :class:`~pssparser.checkers.MarkerDef`
across all checkers (including the built-in core), then exit with code 0.

.. code-block:: bash

pssparser --list-markers

``--describe ID``
Print the full :class:`~pssparser.checkers.MarkerDef` record (summary,
severity, detail text, and owning checker) for a single marker ID, then
exit with code 0. Exits with code 2 if the ID is not found.

.. code-block:: bash

pssparser --describe PSS002

Accessing the AST
=================

Inside ``check(context)``, the linked AST is available as
``context.root`` (a ``RootSymbolScope``) and the per-file ``GlobalScope``
nodes are in ``context.global_scopes``. See :doc:`ast_usage_guide` for a
full guide to navigating the AST.

To resolve a ``GlobalScope`` to its source path, use ``context.file_map``::

filename = context.file_map.get(gs.getFileid(), "")

``context.file_map`` is a ``dict[int, str]`` (fileid → path).
``GlobalScope.getFilename()`` is not reliably populated by the parser, so
always prefer ``file_map``.

``context.global_scopes`` contains only the user-supplied source files;
the built-in PSS library scopes are filtered out automatically.

If your checker only needs the *parse tree* (not the linked AST), set
``runs_without_link = True`` on the class. The checker will then run even
when ``--syntax-only`` is passed. When ``runs_without_link = False``
(the default), the checker is skipped in ``--syntax-only`` mode.
Loading
Loading