👋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.
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 zero1 · 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
Wire a variable/setpoint into the divisor input instead of the literal 0 — usually a debug stub left behind.
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 protect1 · 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
Restore the protective logic: CheckDivReal must test IF divisor = 0 THEN … := 1;
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 zero2 · div-by-zero
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.
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
Check the spelling against the GVL declaration (case, underscores, digits).
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 bounds1 · 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
Bring the index into the declared range or widen the array bounds.
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 exit1 · 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
Add an exit condition and EXIT, or bound it with an iteration counter.
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 family2 · 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
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 := ....
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 channel1 · 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
Fix the index in the value: bPump2_Fault := gvl.alarm_nasos_CO2.
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 zero1 · div-by-zero
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.
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
Cast to REAL: r := INT_TO_REAL(x) / N.0;
Or divide by a real constant: x / 10.0 instead of x / 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
Set a non-zero delay: tmr(IN := cond, PT := T#5S);
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 itself1 · 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
The right-hand side probably should be another variable (typo) — check the intent.
If the line is redundant — remove it.
Main:6
— assignment to itself — no effect
x := x;
Empty IF/loop branch1 · 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
Fill the branch with logic or remove the empty IF/loop.
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
Add an ELSE branch with default handling (signal/log/safe state).
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 active3 · 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
Regenerate the POUs for Implicit Checks via the standard CODESYS dialog — that gives the check its CheckFunction registration so the runtime calls it.
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 group1 · 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
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.
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 called6 · 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
Not needed — delete the POU.
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 name4 · 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
Give it a meaningful name by purpose (e.g. iRetryCount instead of n).
CheckBounds
— x — single-character name, give it a meaningful one
Main
— d — single-character name, give it a meaningful one
Main
— x — single-character name, give it a meaningful one
Main
— c — 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
Create one FUNCTION_BLOCK with input parameters (setpoints, loop tags) and instantiate it per loop.
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 code1 · 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
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.
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.