iglint
A fast linter for Ignition 8.3 projects
iglint reads an Ignition project the way the gateway stores it on disk and reports the errors that otherwise only show up on the gateway: a script Jython refuses to compile, a binding that points at a property that does not exist, a library function called with the wrong number of arguments. It runs in your editor, in pre-commit and in CI, in milliseconds rather than minutes.
Version 0.1 · MIT or Apache-2.0 · Ignition 8.3
$ iglint check
- Line/Overview/view.json:20:32BIND003 `if` does not accept 2 arguments (parameters: condition, trueReturn, falseReturn)
- Line/Overview/view.json:20:86BIND005 `view.custom.lineSped`: custom property `lineSped` is not defined
- Line/Overview/view.json:33:30SCR103 Local variable `rate` is assigned to but never used
- Line/Overview/view.json:33:37REF003 `line.util.formatRate` requires 2 arguments, the call passes 1
- Line/Overview/view.json:33:131REF001 View `Line/Detail` does not exist
- shift-counters/handleTimerEvent.py:6:5SCR104 Redefinition of unused `handleTimerEvent` from line 1
- shift-counters/handleTimerEvent.py:6:5SCR307 `handleTimerEvent` is redefined here with parameters that cannot take no arguments: Ignition runs this last definition, not `def handleTimerEvent():`, and every call fails with a TypeError
Found 5 errors and 2 warnings.
Ignition tells you late
The Designer saves a view or a script without complaint. Whether it works is decided later, on the gateway, often in front of an operator.
Errors surface on the gateway
Jython compiles a script when the gateway loads it, so a script that is too large ("method too large") or does not compile only fails there. A binding to a misspelled property quietly evaluates to null with bad quality. A popup that opens a renamed view fails when someone clicks the button.
Review cannot see inside the JSON
A Perspective view is a large JSON file with scripts and expressions embedded as escaped strings. A pull request shows that something changed, not whether the reference it makes still resolves anywhere in the project.
ign-lint stops at the view
ign-lint checks view.json files one at a time and runs pylint on their scripts. It is slow in CI, it does not read library modules, gateway events, named queries or tags, and so it cannot tell whether a view refers to something that exists.
What it catches
iglint lints views, every kind of Jython 2.7 script (view events and transforms, project library, gateway timers and message handlers, tag events, WebDev and session events), expressions and gateway resources, and checks the references between them across the whole project, inherited projects included.
A binding that can never work
Expressions are parsed with Ignition’s own grammar and checked against the functions a real 8.3 gateway registers, with their argument counts. Property references are resolved in the view.
Expression binding on Rate.props.text
if({view.custom.lineSpeed} > 0,
toString({view.custom.lineSped}))BIND003`if` does not accept 2 arguments (parameters: condition, trueReturn, falseReturn)BIND005`view.custom.lineSped`: custom property `lineSped` is not defined
A call into code that is not there
Scripts embedded in a view are linted as Ignition compiles them, and calls are resolved project-wide: library modules and their function signatures, views, named queries and their parameters, message handlers, style classes, page URLs.
onActionPerformed script on the Details button
rate = line.util.formatRate(self.view.custom.lineSpeed)
system.perspective.openPopup("details", "Line/Detail")REF003`line.util.formatRate` requires 2 arguments, the call passes 1REF001View `Line/Detail` does not existSCR103Local variable `rate` is assigned to but never used
A gateway script that fails at run time
Gateway event scripts are checked the way the gateway runs them. Here the second definition replaces the handler Ignition calls, and every timer tick would fail with a TypeError.
Gateway timer script, handleTimerEvent.py
def handleTimerEvent():
counts = system.tag.readBlocking(["[default]Line1/GoodCount"])
logger = system.util.getLogger("shift")
logger.info("good parts: %d" % counts[0].value)
def handleTimerEvent(event):
passSCR307`handleTimerEvent` is redefined here with parameters that cannot take no arguments: Ignition runs this last definition, not `def handleTimerEvent():`, and every call fails with a TypeErrorSCR104Redefinition of unused `handleTimerEvent` from line 1
Also caught before the gateway sees it
- Scripts Jython parses but refuses to compile, including bodies too large for one Java method
- system.* functions that do not exist in 8.3, or are called with the wrong arguments or outside their scope
- Named queries that do not exist, or get the wrong parameters
- UDT instances of a type that is not defined
110 rules, in ten families
A default set runs without configuration; the rest are opt-in. Every rule has a code, a name and a documentation page with a bad and a good example.
Scripts
Syntax and compile errors, undefined names, system.* functions that do not exist or are called with the wrong arguments or in the wrong scope.
View structure
Duplicate component names, unknown component types, unused custom properties, oversized view data.
References
Views, library functions, named queries, message types, page URLs and parent projects that do not exist.
Bindings and expressions
Expression syntax, unknown functions, argument counts, unresolved property references, fast polling.
Tags and gateway
Unknown UDT types and parameters, tag paths, history providers, alarm bindings, cron expressions.
Security
SQL built from strings, hard-coded credentials, eval and exec, operating system commands.
Performance
Deep nesting, fast query polling, bindings on hidden components, very large views.
Styles
Unknown, unused and empty style classes.
Naming
Configurable naming conventions, custom methods that shadow component members.
Accessibility
Images without alternative text, icon buttons without text.
Built to be right
A linter that cries wolf gets switched off. In iglint a false positive is a bug.
Its own Jython 2.7 parser
The parser accepts exactly what Ignition’s Jython accepts. It is tested differentially against the Jython in Ignition’s own 8.3 gateway image, with recorded cases and randomly generated programs.
Facts from a real gateway
system.* functions, expression functions and Perspective components are generated from a running Ignition 8.3 gateway, not typed in by hand.
Exact locations
Every finding points at a line and column in the file on disk, also inside a script or expression that is stored as an escaped JSON string.
Safe and unsafe fixes
A fix is safe only when iglint can show it does not change behaviour. Anything else is offered as unsafe and needs an explicit flag. --diff shows the edits first, and fixes change the original bytes without reformatting the file.
Fast enough to run on every save
Each file is read and parsed once, in parallel, and results are cached. That makes the full project check a CI step of seconds and a single changed file a matter of milliseconds.
- 20x
- faster than the ign-lint CI step it replaces on a production customer project, on a 2-CPU CI agent
- 42x
- faster than ign-lint’s best configuration on a 5,000-view project, same pylint run
- 0.3 s
- for iglint’s own default rules over that whole production project, on 2 CPUs
- <0.1 s
- for a pre-commit run on one changed file, checked against a 3,000-file workspace
| ign-lint | iglint | Speed-up | |
|---|---|---|---|
| Production customer project, CI step on 2 CPUs (drop-in mode, same pylint run) | 22.2 s | 1.10 s | 20x |
| Same project, iglint’s own default rules (more checks, no pylint) | 22.2 s | 0.33 s | 68x |
| Synthetic project, 500 views (equal work, with pylint) | 16.4 s | 0.59 s | 28x |
| Synthetic project, 5,000 views (equal work, with pylint) | 177 s | 4.25 s | 42x |
Measured on a 16-CPU development machine, the 2-CPU rows pinned to two cores. ign-lint figures for the synthetic projects use its fastest working configuration (pylint with jobs=1); the CI step uses ign-lint as shipped. On very small projects pylint’s own start-up dominates and the gap is smaller. The synthetic projects come from a public generator, so the numbers can be reproduced.
Benchmarks and how to reproduce themHow teams adopt it
- 1
Install
Download the prebuilt binary for Linux, macOS or Windows from the GitHub release, or build it with Cargo. One binary, no Python and no gateway needed.
cargo install --locked --git https://github.com/Mustry-Solutions/iglint iglint iglint check - 2
Baseline what is there
A large project has findings on day one. Record them once and only new findings fail; fix the old ones over time.
iglint baseline git add .iglint-baseline.json - 3
Gate CI
Annotations on the pull request in GitHub, Azure DevOps and GitLab, SARIF for code scanning, JUnit and JSON for everything else.
iglint check --output-format azure - 4
Shift left
A pre-commit hook checks the changed files against the whole project. The language server and the VS Code extension show findings while you type, with quick fixes.
- repo: https://github.com/Mustry-Solutions/iglint hooks: - id: iglint
Configured in one TOML file
iglint.toml (or [tool.iglint] in pyproject.toml) selects rules, sets severities and options, and excludes paths. Scripts take suppression comments; views use path overrides or the baseline.
# iglint.toml
extend-select = ["REF011"]
ignore = ["A11Y"]
exclude = ["projects/Sandbox/**"]
[rules.BIND004]
minimum-interval = 5000Already using ign-lint?
iglint has an equivalent for every ign-lint rule and reads its configuration. iglint migrate turns rule_config.json into iglint.toml and moves whitelisted files into a baseline, so they are linted from now on and only new findings fail.
To change nothing but the binary, iglint ign-lint accepts ign-lint 0.6.4’s arguments and prints its output format, and the pre-commit hook keeps its id. Every finding matches ign-lint’s, except where ign-lint is wrong; each of those differences is documented.
Expect more findings after the switch: iglint checks references ign-lint does not. Run the migration and the baseline first, then gate on iglint check.
iglint migrate
iglint baseline
iglint checkDifferences from ign-lintWhat it will not catch
- Values that only exist at run time: tag values, database contents, a view path or message type built from a string while the session runs.
- Gateway configuration that is not in the repository, and functions of third-party modules it has not been told about.
- Vision windows, and projects in the Ignition 8.1 format.
- Whether the logic is right. iglint finds code that cannot work, not code that does the wrong thing.
Open source, and we can roll it out for you
iglint is MIT or Apache-2.0, built by Mustry and used on our own Ignition projects. It is not affiliated with or endorsed by Inductive Automation.
If you want it working in your pipeline, we set it up: baseline, CI gate, pre-commit and editors. We also do Ignition code-quality reviews of existing projects, with iglint’s findings as the starting point.


