Show HN: PLC Lint – static analysis for CODESYS/PLCopenXML

caspershes1 pts0 comments

Report — DemoBoiler · PLC Lint

← check another file<br>Report: DemoBoiler

demo.xml · version n/a ·<br>POUs: 7 · globals: 49

👋<br>This is a demo report on a synthetic project — to show what you'll get. No need to upload your own code. Want to check yours?

🔎<br>1 POU in graphical languages (CFC 1) — line-based checks were NOT applied to them; names are partially covered (occurrences in blocks/expressions). This is a tool limitation, not a sign these POUs are clean.

Quality score: 0/100

Save the report to your profile — you'll build a verified quality track. The code is not stored, only the aggregate.

Analyze your export

Acceptance protocol →

10

Critical

10

Warning

13

Minor

Severity distribution

Critical 10<br>Warning 10<br>Minor 13

Top rules

dead-pou

naming

div-by-zero

implicit-check-disabled

family-gap

copy-paste

Every rule has a noise? button — click it if the rule gives irrelevant hits on your code. The signal helps us tune the checks: lower the severity, narrow the condition or disable extras. Your code is not sent.

Critical 10 findings

CFC: division by a literal zero<br>1 · cfc-div-zero<br>noise?

In graphical code (CFC/FBD) the divisor of a DIV/MOD block is wired to a literal constant 0 — a runtime exception when the block executes. Checked against the connection graph.

Suggested fixes

Wire a variable/setpoint into the divisor input instead of the literal 0 — usually a debug stub left behind.<br>If the divisor can go to zero at runtime — pick a safe value before DIV/MOD (a SEL on a compare-with-0).

CfcDiv<br>— the divisor of the DIV block is wired to a literal 0 → a guaranteed division by zero

Implicit check does not protect<br>1 · broken-implicit-check<br>noise?

An implicit check function (Check…) exists, but its protective logic is gutted: an empty body, or the check (divisor=0 / bounds) removed. The runtime calls it, but it catches nothing — a false sense of safety, more dangerous than its absence.

Suggested fixes

Restore the protective logic: CheckDivReal must test IF divisor = 0 THEN … := 1;<br>CheckBounds/CheckRange… must clamp the value to lower..upper. Compare with the default CODESYS implementation.

CheckBounds<br>— no lower/upper bounds comparison — the value is not clamped — the check exists but doesn't work (a false sense of safety)

Division by a literal zero<br>2 · div-by-zero<br>noise?

Division or MOD by a literal zero — a guaranteed runtime exception (division by zero) whenever this line is reached.

Suggested fixes

Remove the division by 0 — probably a typo in the divisor.<br>If the divisor can be zero dynamically — guard it: IF d <> 0 THEN r := x / d; END_IF

DivZeroVar:1<br>— division by znever — the variable is 0 (init 0/empty) and never assigned → a guaranteed division by zero

qa := 5 / znever;

Main:2<br>— division/MOD by a literal zero

d := d / 0;

Global name typo (gvl.*)<br>1 · undeclared-global<br>noise?

A gvl. reference has no declaration in the GVL — almost always a typo; the variable is silently created or the code won't compile.

Suggested fixes

Check the spelling against the GVL declaration (case, underscores, digits).<br>If the variable is really needed — add it to the GVL with a type and a comment.

Main<br>— gvl.missing — not in the GVL (likely a typo)

Array index out of bounds<br>1 · array-bounds<br>noise?

A constant index falls outside the declared ARRAY[lo..hi] bounds — reaching this line reads/writes memory that isn't yours. Without active implicit checks the runtime won't intercept it. Checked for one-dimensional arrays with numeric bounds and a literal index.

Suggested fixes

Bring the index into the declared range or widen the array bounds.<br>Check before access: IF idx >= LO AND idx

Main:1<br>— arr[5] out of bounds [0..3]

arr[5] := 1;

Infinite loop with no exit<br>1 · infinite-loop<br>noise?

WHILE TRUE / REPEAT … UNTIL FALSE with no EXIT or RETURN inside — the scan never finishes: the task blocks and the watchdog fires (and if the watchdog is off, the controller stalls). In a PLC, long processing is spread across scans, not spun in a loop.

Suggested fixes

Add an exit condition and EXIT, or bound it with an iteration counter.<br>Split heavy processing across scans — an infinite loop trips the task watchdog.

Main:11<br>— WHILE TRUE without EXIT/RETURN

Channel never written — gap in a channel family<br>2 · family-gap<br>noise?

A field is declared for every channel in a family (boilers 1–4, pumps 1–3) and used by all of them but exactly one, where it is never written. The usual cause: the block was copied from a neighbouring channel and left unfinished. The compiler stays silent — the variable exists and the type is right, nobody just writes to it. On site this shows up as “boiler 4 never reports alarms” while the other three work, and it never reproduces on the bench. The rule relies on declarations: if the field is not declared for that channel, it is different hardware rather than a gap, and nothing is reported.

Suggested fixes

Check that channel's block: an assignment was most likely copied from the...

check zero division noise literal divisor

Related Articles