PLC Lint // static analysis

Real findings, dissected

5 bugs the compiler doesn't catch

A CODESYS project builds with zero errors — and ships with an uncalled timer, a truncated setpoint, and a divide by a variable that's sometimes zero. The syntax is valid; the behavior isn't. Here's what static analysis finds where the compiler stays silent.

8real boiler-house exports
62–86score out of 100
~10 secfrom upload to report
0lines stored on disk

You inherited the project

Someone else wrote it, the documentation is a folder of PDFs, and the person who knew why that IF is there has left the company. You sign the acceptance act anyway.

  • What the code actually does — POUs, tasks, devices and I/O bindings on one page
  • Where it will bite: uncalled timers, unreachable branches, division by a variable that is sometimes zero
  • What is dead weight — POUs nothing calls, copy-pasted loops, leftovers from the previous site

No NDA argument to have with yourself here: the code isn't yours, nothing is written to disk, and the report takes ten seconds. Checking your own project before handover works exactly the same.

Findings — by severity

uncalled-timer high · critical

Uncalled timer

VAR
    Pause_pump : TOF;
END_VAR

// .Q is used in the pump-start logic…
IF NOT Pause_pump.Q THEN Start_Pump(); END_IF
// …but the block is never called — the timer never ticks,
// Pause_pump.Q stays at its default forever.
Compiler: builds PLC Lint: caught

The block is declared and read, but never called — the inter-start pause simply doesn't work. Found on a real boiler-house project.

real-to-time-mul medium · warning

Setpoint truncated by REAL_TO_TIME

tPulse := REAL_TO_TIME(rSetpoint) * 10;
// REAL_TO_TIME treats the number as milliseconds and
// truncates BEFORE the multiply — the setpoint is gone.
Compiler: builds PLC Lint: caught

The classic “converting seconds to ms”. The multiply can't save it — the fraction is lost during the conversion.

div-by-zero high · critical

Divide by a zero variable

rAvg := rSum / iCount;
// iCount is 0 on an empty sample → runtime exception
// on site, not on the bench.
Compiler: builds PLC Lint: caught

The linter distinguishes a literal 0 divisor, a variable that's never assigned nonzero, or one assigned 0 with no later <> 0 guard. Type-aware: plain REAL division is not flagged — false positives were killed on purpose.

unreachable-branch high · critical

Unreachable IF/ELSIF branch

IF x >= 100 THEN
    Alarm_Low();
ELSIF x >= 120 THEN   // unreachable: >=120 already caught above
    Alarm_High();
END_IF
Compiler: builds PLC Lint: caught

The thresholds are in the wrong order — the high alarm never fires. The rule compares the full right-hand operand, so it won't confuse this with real hysteresis (T + 0.02).

watchdog-disabled medium · warning

Task watchdog disabled

<TaskSettings KindOfTask="Cyclic" Interval="t#10ms">
    <Watchdog Enabled="false" />   <!-- on EVERY task -->
</TaskSettings>
Compiler: builds PLC Lint: caught

Not a bug per se — but for a critical process a stalled scan won't get restarted. Better to see it in the summary than discover it on site.

The actual report — not a mockup

Real output on a synthetic project with planted bugs. It's the same report you get for your own export — score, severity, per-rule breakdown, and every finding with a concrete fix.

PLC Lint report Open the full live report →

New: the other side of the handover

The person who signs the acceptance act cannot read the code

A quality report is written for the engineer who wrote the code. But the money and the real fear sit with the party accepting the site: they sign for something they cannot read, and “uncalled timer at Main:214” tells them nothing. So the same analysis has a second face — a list of questions to ask the contractor, in plain language, with no code and no line numbers. Print it, attach it to the act, get the answers in writing.

Grouped by what it costs you: answer before signing / clarify during acceptance / notes for maintenance. Built from the same report — nothing is stored on the server.

Open a live acceptance protocol →

Also new

Boiler-house / heat-point profileAn opt-in rule for analog values that are never range-checked anywhere in the project: a broken Pt1000 reads as a valid 0 °C and the valve opens fully. One rule, not six — the other five were dropped after measuring them on nine real exports.
Industry benchmark“48 out of 100” means nothing; “48 against a median of 62” means everything. Published only once the sample is large enough — an average over a dozen files is not an average.
Second opinion before commissioningThe automatic report plus a written verdict from an engineer on which findings are actually dangerous on your site. Answer within 24 hours.

What PLC Lint checks

30 checks across code, structure and config. A sample of what it flags:

That's a sample — all 30 rules, with fixes, are on GitHub.

Run your own export — see your score

Upload a .xml from CODESYS (Project → Export PLCopenXML), TwinCAT3, WAGO or ОВЕН. Report in ~10 seconds. Free, no signup for the first analysis, nothing stored.

…or see a live sample report →