
I got tired of our PLC coding standards living in a PDF nobody re-reads, so I built a linter for Structured Text
Every company I've worked in has had a coding standard. Naming conventions, forbidden constructs, "always add an ELSE to a CASE on an enum", "don't change a SAFETY_ constant without sign-off". It was in a Word doc or a PDF, and realistically, few people opened it after their first week. Enforcement was whatever a reviewer happened to catch during a visual scan, which meant it was inconsistent and missed things.
So I built a tool that automatically checks ST. It's called plc-st-review. It's free, MIT licensed, and open source.
What it actually does: it parses your .st files into a real syntax tree (using a tree-sitter grammar for IEC 61131-3 ST) and runs semantic checks on the structure, not regex on the text. 56 checks in this release. A few concrete examples of what it flags:
- A
TON.PTthat changed fromT#2stoT#200msbetween commits (10x faster, easy to miss in a diff). - An FB instance whose
.Qyou read, but you never actually call the instance, so you're reading stale outputs. - A literal array index outside the declared bounds,
arr[15]whenarrisARRAY [0..9]. WHILE TRUEwith noEXITin the body.- A
VAR_GLOBAL CONSTANTnamedSAFETY_TIMEOUTwhose value changed (it raises the severity of safety-prefixed names). - A
CASEon an enum that gained a value but has no matching branch and noELSE. - Naming convention drift, configurable per declaration kind (prefix/suffix/regex), plus a
forbidden_symbolsblocklist you define.
The part that matters if your shop doesn't do pull requests (most don't): you don't need a PR, a server, or even Git. Install it and point the CLI at a folder:
npm install -g plc-st-review
plc-st-review --lint "src/**/*.st"
That runs every single revision check on every file and prints findings to the terminal. That's it. If you do run a GitHub or GitLab workflow, there's an Action and a CI image that posts the findings as inline review comments on the changed lines, but that's optional, not the entry point.
New in this version is a --metrics mode that measures a whole codebase instead of reviewing a diff. Point it at a folder, and it ranks your POUs by cyclomatic complexity and nesting depth, and tells you which function blocks nothing calls anymore:
plc-st-review --metrics src/
Top 5 by complexity:
FB_ConveyorState complexity: 10 nesting: 2 LOC: 42
FB_AxisRamp complexity: 5 nesting: 1 LOC: 27
...
Dead code:
FB_Legacy (0 callers)
FB_Watchdog (0 callers)
It can also emit the call graph as a Graphviz file or a JSON report for a dashboard. Again, no PR needed; it just reads the files.
There's no LLM in the path. Findings are deterministic; the same code produces the same findings every time. I did that on purpose. I want it to be something you can gate a pipeline on, not a slot machine.
Honest about what it is and isn't, because I know this crowd:
- It parses standard IEC 61131-3 Structured Text. It does not understand vendor dialect extensions (TwinCAT, CODESYS-specific stuff) yet. That's on the roadmap, and right now it stays portable on purpose.
- It reads
.sttext files. The one real prerequisite is that your ST exists as text at all. If your project lives only inside a proprietary binary project format and you never export it, there's nothing for the parser to read. Most vendor IDEs can export ST to source files; that's what you'd point it at. - It can't compile your project, and it doesn't try to. It checks structure and semantics from the parse tree, which is exactly why it works in CI, where the vendor compiler isn't available.
Repo (with a live demo PR where every check fires on real ST): https://github.com/HeytalePazguato/plc-st-review
I'd genuinely like to hear from people who program or review ST code for a living: what does your standard enforce that a tool like this should catch and currently doesn't? What's the dumbest bug you've seen slip through a visual review? That feedback is what tells me which checks to build next.