<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aiepco</title>
    <description>The latest articles on DEV Community by Aiepco (@aiepco).</description>
    <link>https://dev.to/aiepco</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4146656%2Ff6369a66-03fd-4a36-bd00-fcd305b6b0fe.png</url>
      <title>DEV Community: Aiepco</title>
      <link>https://dev.to/aiepco</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aiepco"/>
    <language>en</language>
    <item>
      <title>A linter that silently passes is worse than no linter at all</title>
      <dc:creator>Aiepco</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:43:36 +0000</pubDate>
      <link>https://dev.to/aiepco/a-linter-that-silently-passes-is-worse-than-no-linter-at-all-22o6</link>
      <guid>https://dev.to/aiepco/a-linter-that-silently-passes-is-worse-than-no-linter-at-all-22o6</guid>
      <description>&lt;p&gt;Our static analyzer had been reporting &lt;code&gt;no issues found&lt;/code&gt; on our PLC codebase for months.&lt;/p&gt;

&lt;p&gt;Then we built a fixture stuffed with deliberate bugs — and found that &lt;strong&gt;two of its rules had never fired on a single line of code.&lt;/strong&gt; Not once, since the day they were written.&lt;/p&gt;

&lt;p&gt;This is a post-mortem of our own verification toolchain failing, and the eight checks we now run before we trust any checker.&lt;/p&gt;




&lt;h2&gt;
  
  
  Context: what we verify, and why it matters
&lt;/h2&gt;

&lt;p&gt;We develop and verify Siemens S7-1200/1500 PLC programs (SCL, IEC 61131-3). The code runs production lines. A defect is not a bad user experience — it's a stopped line, scrapped product, or a safety incident.&lt;/p&gt;

&lt;p&gt;So we built a verification chain: static analysis rules, a self-test suite, and mutation testing. This article is about that toolchain failing at its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding #1 — a rule that could never fire
&lt;/h2&gt;

&lt;p&gt;Here is what a normal run looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[motor_control_ref.scl]
  LOC=86 SLOC=61 comment_rate=27% McCabe≈9 nesting=3 magic_numbers=0
  POU: FUNCTION_BLOCK FB200_MotorControl (line 11)
  No static issues found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One green line. Looks like good news.&lt;/p&gt;

&lt;p&gt;Meanwhile we were writing a fixture — a file full of deliberately planted defects — to check whether the rules actually work. The result was unsettling: the fixture contained &lt;code&gt;#m_rB&lt;/code&gt; used as a divisor, and unguarded array access like &lt;code&gt;#m_Buffer[#i_Index]&lt;/code&gt;. The rules for &lt;em&gt;division-by-zero risk&lt;/em&gt; and &lt;em&gt;array-out-of-bounds risk&lt;/em&gt; reported nothing.&lt;/p&gt;

&lt;p&gt;The cause was in the regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/(?![/\\*])\s*([A-Za-z_][A-Za-z0-9_]*)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks for a variable name following a slash. But &lt;strong&gt;SCL local variables carry a &lt;code&gt;#&lt;/code&gt; prefix&lt;/strong&gt; — the real code is &lt;code&gt;#m_rA / #m_rB&lt;/code&gt;. After the slash comes a space, then &lt;code&gt;#&lt;/code&gt;, then the name. The &lt;code&gt;#&lt;/code&gt; broke the match.&lt;/p&gt;

&lt;p&gt;Two rules, written months earlier, had never matched anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding #2 — the scary part is its siblings
&lt;/h2&gt;

&lt;p&gt;Fixing the regex took five minutes. The whole next day went into a different question: &lt;strong&gt;how many other rules are like this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We ran every rule against the fixture. Three more problems surfaced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Nature&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;VAR CONSTANT&lt;/code&gt; treated as a static variable&lt;/td&gt;
&lt;td&gt;All 6 constants flagged with "non-standard naming prefix"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;False positive&lt;/strong&gt; — rule too broad&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Safety-comment check too aggressive&lt;/td&gt;
&lt;td&gt;One function block produced &lt;strong&gt;17 warnings&lt;/strong&gt; of the same kind&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Noise flood&lt;/strong&gt; — real issues get buried&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Call-graph parsed &lt;code&gt;ELSIF (...)&lt;/code&gt; as an FB call&lt;/td&gt;
&lt;td&gt;Call-depth analysis distorted&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Misparse&lt;/strong&gt; — parser boundary error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three rules, three different diseases: &lt;strong&gt;too broad, too noisy, misparsed.&lt;/strong&gt; Together with the two that never fired, that's five failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The common property: none of them raised an error.&lt;/strong&gt; The tool didn't crash, didn't throw, didn't warn. It just quietly produced a wrong conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding #3 — "passes" is not one thing
&lt;/h2&gt;

&lt;p&gt;We then ran mutation testing: inject a defect into clean code and see whether the chain catches it. Twelve mutation classes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Syntax / structural&lt;/strong&gt; (missing &lt;code&gt;END_IF&lt;/code&gt;, missing semicolon, unbalanced parentheses, naming violations, magic numbers, stripped comments) → &lt;strong&gt;caught 6/6 by static analysis.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic&lt;/strong&gt; (&lt;code&gt;AND&lt;/code&gt; → &lt;code&gt;OR&lt;/code&gt;, deleted &lt;code&gt;NOT&lt;/code&gt;, inverted comparison, altered initial value, removed interlock) → &lt;strong&gt;caught 0/6.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most instructive number: we flipped the &lt;code&gt;AND&lt;/code&gt; in a start condition to &lt;code&gt;OR&lt;/code&gt; — the logic is now exactly backwards — and compared the text against the reference implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLEU-4 cumulative : 0.9981
Line-level match  : 97.6%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nearly a perfect score. &lt;strong&gt;Semantic mutations can make code look &lt;em&gt;more&lt;/em&gt; like the reference, not less.&lt;/strong&gt; No text- or structure-based tool can see them.&lt;/p&gt;

&lt;p&gt;So we put that limitation &lt;em&gt;into the report&lt;/em&gt; rather than tuning the numbers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Static analysis passing ≠ compiling ≠ being logically correct.&lt;br&gt;
Semantic correctness is the job of dynamic verification (simulation, on-machine trace comparison) and human review.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Finding #4 — the docs referenced scripts that didn't exist
&lt;/h2&gt;

&lt;p&gt;One more, and it is embarrassing rather than interesting: our framework documentation listed a set of tooling scripts. At that moment, &lt;strong&gt;none of them existed yet.&lt;/strong&gt; The documentation was complete; the implementation was zero.&lt;/p&gt;

&lt;p&gt;Same disease: interface declarations divorced from implementations. We added an assertion that scans the docs and fails if any referenced script or template is missing. Dead links don't get a second chance.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed
&lt;/h2&gt;

&lt;p&gt;Three rules, all cheap:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every rule needs a fixture that must trigger it.&lt;/strong&gt; Write the rule, immediately build a sample containing that defect, assert it is reported. Freeze the sample as a regression test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every rule needs a counter-fixture that must &lt;em&gt;not&lt;/em&gt; trigger it.&lt;/strong&gt; Run it against compliant code, assert zero warnings. Without this you get the worse failure: the team learns to ignore all warnings, which is identical to having no checker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The toolchain must prove itself first.&lt;/strong&gt; Ours is 45 self-test assertions, a few seconds to run:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;selftest_tools.py
&lt;span class="go"&gt;assertions 45, failures 0
result: OK — toolchain behaves as declared
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule is blunt: &lt;strong&gt;if the toolchain self-test isn't fully green, you may not use it to judge code.&lt;/strong&gt; An unverified tool's "pass" is not evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight checks we now apply to any checker
&lt;/h2&gt;

&lt;p&gt;Abstracted, this is a checklist you can run against your own linters, analyzers or CI gates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does every rule have a sample that must trigger it?&lt;/strong&gt; (No → you don't know whether it runs at all.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does every rule have a counter-sample that must not trigger it?&lt;/strong&gt; (No → it may be killing valid code.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the warning count sane?&lt;/strong&gt; 17 identical warnings in one file means the rule is noise, and the team will ignore it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When the tool fails, does it error or does it go silent?&lt;/strong&gt; Silent failure is the dangerous kind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is there a meta-test guarding the tool itself?&lt;/strong&gt; Changing A and breaking B is the norm, not the exception.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have you written down what the tool deliberately does &lt;em&gt;not&lt;/em&gt; do?&lt;/strong&gt; Unstated boundaries are read as omniscience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does every file referenced in the documentation actually exist?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Before the word "passed", is there a line saying &lt;em&gt;which layer&lt;/em&gt; passed?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why publish this
&lt;/h2&gt;

&lt;p&gt;There's no growth chart or ROI curve here. It's a record of our own toolchain failing and being fixed.&lt;/p&gt;

&lt;p&gt;We publish it because of a fairly plain belief: &lt;strong&gt;if a supplier won't tell you whether their own checkers have been verified, you should trust what they hand you even less.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We prefer to say three separate sentences — static checks passed / it compiles / the logic is correct — rather than one vague "quality assured". Keeping them separate is more useful to the person paying for the work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is a real internal post-mortem. All figures (14 static rules, 45 self-test assertions, 6/6 vs 0/6 mutation detection, BLEU 0.9981) come from actual tool output, and contain no client information.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We develop and verify Siemens S7-1200/1500 PLC programs — and we build the tooling that proves they're correct. How we work: &lt;a href="https://plc.aiepco.com" rel="noopener noreferrer"&gt;plc.aiepco.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>programming</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
