Checker Plug-in Guide¶
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
pssparser.checkers.CheckerBase. Checkers receive a
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/.
# 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:
Subclass
CheckerBase.Set
name— a short unique slug used on the command line.Set
description— shown by--list-checkers.Declare
marker_defs— oneMarkerDefper diagnostic code your checker can emit.Set
runs_without_link = Truewhen your checker only needs the raw parse tree (faster; works even when--syntax-onlyis active).Implement
check(context)— walk the AST and calladd_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
MarkerDef in the class-level marker_defs
list:
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 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:
[options.entry_points]
pssparser.checkers =
naming-convention = mypkg.pss_rules:NamingConventionChecker
unused-imports = mypkg.pss_rules:UnusedImportChecker
pyproject.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:
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:
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 NAMERun only the named checker(s). May be repeated.
NAMEmust match a registered checker name (fromentry_points) or a checker previously loaded with--load-checker. Specifying an unknown name is an error.pssparser --checker naming-convention model.pss
--no-checker NAMEExclude the named checker. May be repeated. Ignored when
--checkeris also specified (explicit selection takes precedence). Unknown names are silently ignored.pssparser --no-checker deprecated-syntax model.pss
Precedence rules:
Start with all registered +
--load-checkercheckers.If
--checkeris present, keep only those names.Otherwise, remove any names listed in
--no-checker.
Querying the Registry¶
--list-checkersPrint a table of all registered checkers and their declared marker IDs, then exit with code 0. No source files are required.
pssparser --list-checkers--list-markersPrint a table of every declared
MarkerDefacross all checkers (including the built-in core), then exit with code 0.pssparser --list-markers--describe IDPrint the full
MarkerDefrecord (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.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 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.