free quality report for PLCopenXML exports
← check another file

Report: DemoBoiler

demo.xml · version n/a · POUs: 7 · globals: 49
👋 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?
🔎 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.
0
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 Warning 10 Minor 13

Top rules

dead-pou 6
naming 4
div-by-zero 3
implicit-check-disabled 3
family-gap 2
copy-paste 2

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 1 · cfc-div-zero
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
  1. Wire a variable/setpoint into the divisor input instead of the literal 0 — usually a debug stub left behind.
  2. If the divisor can go to zero at runtime — pick a safe value before DIV/MOD (a SEL on a compare-with-0).
  • CfcDiv — the divisor of the DIV block is wired to a literal 0 → a guaranteed division by zero
Implicit check does not protect 1 · broken-implicit-check
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
  1. Restore the protective logic: CheckDivReal must test IF divisor = 0 THEN … := 1;
  2. CheckBounds/CheckRange… must clamp the value to lower..upper. Compare with the default CODESYS implementation.
  • CheckBounds — 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 2 · div-by-zero
Division or MOD by a literal zero — a guaranteed runtime exception (division by zero) whenever this line is reached.
Suggested fixes
  1. Remove the division by 0 — probably a typo in the divisor.
  2. If the divisor can be zero dynamically — guard it: IF d <> 0 THEN r := x / d; END_IF
  • DivZeroVar:1 — division by znever — the variable is 0 (init 0/empty) and never assigned → a guaranteed division by zero
    qa := 5 / znever;
  • Main:2 — division/MOD by a literal zero
    d := d / 0;
Global name typo (gvl.*) 1 · undeclared-global
A gvl.<name> reference has no declaration in the GVL — almost always a typo; the variable is silently created or the code won't compile.
Suggested fixes
  1. Check the spelling against the GVL declaration (case, underscores, digits).
  2. If the variable is really needed — add it to the GVL with a type and a comment.
  • Main — gvl.missing — not in the GVL (likely a typo)
Array index out of bounds 1 · array-bounds
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
  1. Bring the index into the declared range or widen the array bounds.
  2. Check before access: IF idx >= LO AND idx <= HI THEN arr[idx] … END_IF
  • Main:1 — arr[5] out of bounds [0..3]
    arr[5] := 1;
Infinite loop with no exit 1 · infinite-loop
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
  1. Add an exit condition and EXIT, or bound it with an iteration counter.
  2. Split heavy processing across scans — an infinite loop trips the task watchdog.
  • Main:11 — WHILE TRUE without EXIT/RETURN
Channel never written — gap in a channel family 2 · family-gap
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
  1. Check that channel's block: an assignment was most likely copied from the neighbour and kept its indexes — gvl.kotel_3_alarms3 := ... instead of gvl.kotel_4_alarms3 := ....
  2. If the field genuinely does not apply to that channel (no such hardware), drop its declaration — the rule goes quiet and the GVL loses a dead variable.
  • ktr_errors — field kotel_4_alarms3 is declared but never written for channel 4, while channels 1, 2, 3 do write it — looks like the block was copied from a neighbouring channel and left unfinished
  • ktr_errors — field kotel_4_code_error_3 is declared but never written for channel 4, while channels 1, 2, 3 do write it — looks like the block was copied from a neighbouring channel and left unfinished
Channel parameter fed from another channel 1 · param-channel-mismatch
In a function block call the parameter name refers to one channel while the value comes from another: `bPump2_Fault := gvl.alarm_nasos_CO1`. The rule fires only when channel numbering is proven within the same call — consistent pairs exist for at least two channels. The consequence is quiet and dangerous: a faulty pump is treated as healthy and picked for duty, the interlock never fires, and the log looks perfectly normal.
Suggested fixes
  1. Fix the index in the value: bPump2_Fault := gvl.alarm_nasos_CO2.
  2. Repeated per-channel calls are better folded into a loop over an array (FOR i := 1 TO 3 DO fb[i](...)) — then there is nothing left to renumber.
  • work_nasos_CO:2 — channel 2 is fed from channel 1: in call fbSeize the parameter name says channel 2, but the signal comes from 1
    bPump2_Fault := gvl.alarm_nasos_CO1

Warning 10 findings

Division by a literal zero 1 · div-by-zero
Division or MOD by a literal zero — a guaranteed runtime exception (division by zero) whenever this line is reached.
Suggested fixes
  1. Remove the division by 0 — probably a typo in the divisor.
  2. If the divisor can be zero dynamically — guard it: IF d <> 0 THEN r := x / d; END_IF
  • DivZeroVar:3 — division by zcond — the variable is assigned 0 on some branch and there's no zcond <> 0 guard → a possible division by zero
    qb := 5 / zcond;
Integer division (lost fraction?) 1 · int-division
Dividing an integer by an integer constant drops the fraction. Use /N.0 or *_TO_REAL if precision matters.
Suggested fixes
  1. Cast to REAL: r := INT_TO_REAL(x) / N.0;
  2. Or divide by a real constant: x / 10.0 instead of x / 10.
  • Main:3 — Integer division (lost fraction?)
    iq := iSum / 10;
Timer with zero delay (PT := T#0) 1 · timer-zero-pt
PT is set to zero (T#0S) — the timer fires in the same scan, effectively no delay. Usually a forgotten or wrong time setpoint.
Suggested fixes
  1. Set a non-zero delay: tmr(IN := cond, PT := T#5S);
  2. If no delay is needed — remove the timer and act on the condition directly.
  • Main:4 — PT := T#0 — the timer fires instantly (no delay)
    t1(IN := c, PT := T#0S);
Assignment of a variable to itself 1 · self-assignment
A line like `x := x;` — assigning a variable to itself does nothing. Usually a typo on the right-hand side (another variable was meant) or a leftover from deleted logic.
Suggested fixes
  1. The right-hand side probably should be another variable (typo) — check the intent.
  2. If the line is redundant — remove it.
  • Main:6 — assignment to itself — no effect
    x := x;
Empty IF/loop branch 1 · empty-branch
An IF/ELSIF/ELSE branch or a loop body is empty (`THEN END_IF`, `DO END_FOR`) — the logic was either never written or cut out during an edit and the construct left behind.
Suggested fixes
  1. Fill the branch with logic or remove the empty IF/loop.
  2. If empty on purpose — leave an explanatory comment.
  • Main:7 — empty IF … THEN … END_IF
CASE without an ELSE branch (no default) 1 · case-without-else
CASE without an ELSE branch: if the selector takes a value not among the labels, nothing runs — a silent skip (a common state-machine bug).
Suggested fixes
  1. Add an ELSE branch with default handling (signal/log/safe state).
  2. If all selector values are truly covered — keep ELSE with an explanatory comment.
  • Main:8 — CASE without ELSE — unhandled selector values are silently ignored
Check present but not active 3 · implicit-check-disabled
A check function exists in the project but is not active: either not registered (no CheckFunction tag — the runtime never calls it), or an entire category (bounds/division/range/pointers) has no active check.
Suggested fixes
  1. Regenerate the POUs for Implicit Checks via the standard CODESYS dialog — that gives the check its CheckFunction registration so the runtime calls it.
  2. Add the missing category (bounds/division/range/pointer) via Add Object → POUs for Implicit Checks.
  • project — the runtime doesn't check 'division by zero' — no active check function for this category
  • project — the runtime doesn't check 'range overflow' — no active check function for this category
  • project — the runtime doesn't check 'pointer dereference' — no active check function for this category
Line indexes differently from its group 1 · index-mismatch
Across a group of similar lines the relation between index positions holds nearly everywhere but is broken in one line: eleven lines write to their own channel, the twelfth writes to someone else's. That is what a copied neighbouring branch with partial renumbering looks like. The rule does not compare whole lines: numbers from different dimensions (boiler number and alarm block number) coincide routinely, and that case is accounted for. This is a heuristic — before changing anything, check whether the line is a deliberate exception.
Suggested fixes
  1. Compare the line with its neighbours in the group: usually the destination was not renumbered — w_cascadK4 := r_cascadK4 instead of w_cascadK3 := r_cascadK4.
  2. If the line is a deliberate exception, put a comment next to it — that saves the next reader (and you in six months) an investigation.
  • reindex_cascad:14 — positions 1 and 2 match in 11 of 12 lines in this group but differ here (3 vs 4) — the line was likely copied and renumbered only partially
    IF gvl.stop_K3 THEN gvl.w_cascadK4 := gvl.r_cascadK4; END_IF

Minor 13 findings

Dead code — POU is never called 6 · dead-pou
The POU is never called and never used as a type — dead code or a forgotten name change (check against the spec before deleting).
Suggested fixes
  1. Not needed — delete the POU.
  2. A forgotten version — check the call name against the spec, possibly a typo in the name.
  • CfcDiv — CfcDiv — never called and not used as a type
  • DivZeroVar — DivZeroVar — never called and not used as a type
  • Main — Main — never called and not used as a type
  • ktr_errors — ktr_errors — never called and not used as a type
  • reindex_cascad — reindex_cascad — never called and not used as a type
  • work_nasos_CO — work_nasos_CO — never called and not used as a type
Cryptic variable name 4 · naming
Single-character names (other than counters i/j/k/n) are unreadable during maintenance. I/O tags and FB pins are left alone.
Suggested fixes
  1. Give it a meaningful name by purpose (e.g. iRetryCount instead of n).
  • CheckBoundsx — single-character name, give it a meaningful one
  • Maind — single-character name, give it a meaningful one
  • Mainx — single-character name, give it a meaningful one
  • Mainc — single-character name, give it a meaningful one
Duplicated logic block (copy-paste) 2 · copy-paste
Structurally identical blocks (even with renamed tags) are a source of bugs: a fix in one place is forgotten in the others, and copy-pasting someone else's loop drags along its gating flags. A candidate for a shared parameterised FB.
Suggested fixes
  1. Create one FUNCTION_BLOCK with input parameters (setpoints, loop tags) and instantiate it per loop.
  2. For per-channel repeats — a FOR loop over an array of structures.
  • ktr_errors — inside ktr_errors: 1 duplicated block(s), repeated up to 5× (~13 lines) — extract into a loop/FB
  • reindex_cascad — inside reindex_cascad: 1 duplicated block(s), repeated up to 5× (~13 lines) — extract into a loop/FB
Magic number — raw constant in code 1 · magic-number
A large/fractional raw constant without a name — easy to miss when changing a setpoint and hard to understand. Move it to a VAR CONSTANT. Array indices, FOR bounds and small integers (<16) don't count; equal constants are grouped by value.
Suggested fixes
  1. Declare a named constant and use it: VAR CONSTANT SEC_PER_HOUR : UDINT := 3600; END_VAR
  • ktr_errors:18 — constant 50 — extract into a named VAR CONSTANT
    gvl.nasos_1_freq := 50;
This is a synthetic project. Real ones usually have more.

Exporting from CODESYS takes half a minute, the report takes another. Your file is processed in memory and never stored. No project at hand? Download this one and run it yourself.

How to export PLCopenXML from CODESYS →

What the linter does NOT see (needs an eye and a cross-check with the spec): signal polarity, swapped hysteresis (< set+hyst / > set−hyst), copy-paste of someone else's loop. The report is a reason to re-check, not a verdict.
Processing happens in memory — the uploaded code is not stored. The tool advises; code changes are up to the engineer (see what the linter can't see). What happens to your code → · Your code & privacy · Industry benchmark · Feedback · GitHub