Getting started
This tutorial runs iglint on a small, made-up Perspective project with a dozen deliberate mistakes. Every output below was captured from the real binary (iglint 0.1.0); site/scripts/run-example.sh in the repository replays the whole tutorial.
The example project
Section titled “The example project”The project lives in the iglint repository under site/examples/quickstart. It is laid out the way an Ignition 8.3 gateway stores projects on disk:
projects/demo-hmi/├── project.json├── com.inductiveautomation.perspective/views/Line/│ ├── Overview/view.json a view with a title, clock, logo and button│ └── PumpDetail/view.json a popup with a `pumpId` parameter└── ignition/ ├── script-python/plant/pumps/code.py a library module ├── named-query/Pumps/ByArea/ a named query with an `area` parameter └── timer/refresh-pumps/handleTimerEvent.py a gateway timer scriptCopy it to a directory of its own, outside the iglint checkout, and make it a Git repository, as your project would be:
cp -R iglint/site/examples/quickstart ~/demo-ignitioncd ~/demo-ignitiongit init -q && git add -A && git commit -qm "example project"First run
Section titled “First run”Run iglint in the directory that holds projects/. Without arguments, iglint is the same as iglint check .:
iglintprojects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:4:5: VIEW015 [warning] Custom property `legacyFilter` is never read |4 | "legacyFilter": "" | ^^^^^^^^^^^^^^ | = note: at view.custom.legacyFilter = help: [*] Remove unused custom property 'legacyFilter'
projects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:34:57: BIND005 [error] `view.custom.aera`: custom property `aera` is not defined |34 | "expression": "\"Area \" + {view.custom.aera}" | ^^^^ | = note: at root/Title.props.text@binding
projects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:56:43: BIND004 [warning] `now` polls every 1000 ms, faster than the minimum of 10000 ms |56 | "expression": "dateFormat(now(1000), \"HH:mm:ss\")" | ^^^ | = note: at root/Clock.props.text@binding
projects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:69:19: A11Y001 [warning] Image `Logo` has no alternative text (`props.altText`) |69 | "name": "Logo" | ^^^^^^ | = note: at root/Logo
projects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:84:75: REF001 [error] View `Line/PumpDetial` does not exist |84 | ...m.perspective.openPopup(\"pumpDetail\", \"Line/PumpDetial\", params={\"pumpId\": 7})\n\tplant.pumps.refresh(self.view.cu... | ^^^^^^^^^^^^^^^^^^^ | = note: at root/Details@events.component.onActionPerformed
projects/demo-hmi/ignition/script-python/plant/pumps/code.py:1:8: SCR102 [warning] `os` imported but unused |1 | import os | ^^ | = note: at plant.pumps = help: [*] Remove unused import `os`
projects/demo-hmi/ignition/script-python/plant/pumps/code.py:7:9: SCR204 [warning] `system.db.runNamedQuery` is deprecated; use `system.db.execQuery` or `system.db.execUpdate` instead |7 | rows = system.db.runNamedQuery("Pumps/ByAreaa", {"area": area}) | ^^^^^^^^^^^^^^^^^^^^^^^ | = note: at plant.pumps
projects/demo-hmi/ignition/script-python/plant/pumps/code.py:7:33: REF004 [error] Named query `Pumps/ByAreaa` does not exist |7 | rows = system.db.runNamedQuery("Pumps/ByAreaa", {"area": area}) | ^^^^^^^^^^^^^^^ | = note: at plant.pumps
projects/demo-hmi/ignition/script-python/plant/pumps/code.py:10:6: SCR110 [warning] `is` compares identity with a literal; use `==` |10 | if status is "fault": | ^^^^^^^^^^^^^^^^^ | = note: at plant.pumps = help: Replace `is` with `==` (unsafe fix available with `--unsafe-fixes`) = note: the fix is unsafe because `==` calls `__eq__` and can give a different result than the identity test
projects/demo-hmi/ignition/script-python/plant/pumps/code.py:11:4: SCR101 [error] Undefined name `log` |11 | log.warn("Pump %s is faulted" % row["name"]) | ^^^ | = note: at plant.pumps
projects/demo-hmi/ignition/script-python/plant/pumps/code.py:18:21: SCR115 [error] `TypeError` is bound to the caught exception, not caught; use parentheses to catch both |18 | except ValueError, TypeError: | ^^^^^^^^^ | = note: at plant.pumps = help: Add parentheses (unsafe fix available with `--unsafe-fixes`) = note: the fix is unsafe because the fix changes what the handler catches and no longer binds the second name
projects/demo-hmi/ignition/timer/refresh-pumps/handleTimerEvent.py:3:2: REF003 [error] `plant.pumps.refresh` accepts at most 1 argument positionally, the call passes 2 |3 | plant.pumps.refresh("South", True) | ^^^^^^^^^^^^^^^^^^^ | = note: at refresh-pumps
Found 6 errors and 6 warnings.[*] 2 fixable with the `--fix` option (2 hidden fixes can be enabled with the `--unsafe-fixes` option).Twelve findings in three files, without any configuration. Most of them would only show up at run time on a gateway, and some not even there: a view that does not exist makes the popup silently fail, the mistyped property reference makes the title evaluate to null with a bad quality, and the except ValueError, TypeError: line looks right but does not catch TypeError at all.
Reading a finding
Section titled “Reading a finding”Take the BIND005 finding:
projects/.../Line/Overview/view.json:34:57: BIND005 [error] `view.custom.aera`: custom property `aera` is not defined |34 | "expression": "\"Area \" + {view.custom.aera}" | ^^^^ | = note: at root/Title.props.text@bindingview.json:34:57is the line and column in the file on disk. The expression is a JSON string inside the view, and iglint maps the position back through the JSON escapes, so the caret is underaeraitself.BIND005is the rule code;iglint rule BIND005(or the rule page) explains it. Every rule also has a name (unresolved-property-reference) that you can use instead of the code anywhere.[error]is the severity:error,warningorinfo.note: at root/Title.props.text@bindingis the place in the view, as a component path: theTitlecomponent underroot, the binding on itsprops.text.help: [*] ...marks a finding with a fix that--fixapplies. A help line with "(unsafe fix available with--unsafe-fixes)" has a fix that iglint could not prove safe, followed by the reason.
The exit status is 1 because there are findings of severity warning or higher. It is 0 when nothing reaches the --error-on level (default warning), and 2 for usage, configuration or internal errors.
To get an overview instead of the details, count the findings per rule:
iglint check --statistics1 A11Y001 image-without-alt1 BIND004 polling-interval1 BIND005 unresolved-property-reference1 REF001 unknown-view1 REF003 library-function-arity1 REF004 unknown-named-query1 SCR101 undefined-name1 SCR102 [*] unused-import1 SCR110 comparison-with-literal-identity1 SCR115 except-tuple-without-parentheses1 SCR204 deprecated-system-function1 VIEW015 [*] unused-custom-propertyPreview and apply fixes
Section titled “Preview and apply fixes”--diff shows what --fix would change, without writing anything:
iglint check --diff--- a/projects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json+++ b/projects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json@@ -1,14 +1,10 @@ { "custom": {- "area": "North",- "legacyFilter": ""+ "area": "North" }, "params": {}, "propConfig": { "custom.area": {- "persistent": true- },- "custom.legacyFilter": { "persistent": true } },--- a/projects/demo-hmi/ignition/script-python/plant/pumps/code.py+++ b/projects/demo-hmi/ignition/script-python/plant/pumps/code.py@@ -1,5 +1,5 @@-import os
+ logger = system.util.getLogger("plant.pumps")
Would fix 2 issue(s) in 2 file(s).Two things to notice:
- The view fix removes the property and its
propConfigentry and touches nothing else: fixes are byte-exact edits, so the rest of the file keeps the Designer's formatting. - The import is replaced by an empty line instead of being deleted. Removing the line would shift every following line up by one, and line numbers are something a script can observe (tracebacks and logged stack traces show them). A safe fix keeps them.
Apply them:
iglint check --fix --output-format conciseprojects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:30:57: BIND005 [error] `view.custom.aera`: custom property `aera` is not definedprojects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:52:43: BIND004 [warning] `now` polls every 1000 ms, faster than the minimum of 10000 msprojects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:65:19: A11Y001 [warning] Image `Logo` has no alternative text (`props.altText`)projects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:80:75: REF001 [error] View `Line/PumpDetial` does not existprojects/demo-hmi/ignition/script-python/plant/pumps/code.py:7:9: SCR204 [warning] `system.db.runNamedQuery` is deprecated; use `system.db.execQuery` or `system.db.execUpdate` insteadprojects/demo-hmi/ignition/script-python/plant/pumps/code.py:7:33: REF004 [error] Named query `Pumps/ByAreaa` does not existprojects/demo-hmi/ignition/script-python/plant/pumps/code.py:10:6: SCR110 [warning] `is` compares identity with a literal; use `==`projects/demo-hmi/ignition/script-python/plant/pumps/code.py:11:4: SCR101 [error] Undefined name `log`projects/demo-hmi/ignition/script-python/plant/pumps/code.py:18:21: SCR115 [error] `TypeError` is bound to the caught exception, not caught; use parentheses to catch bothprojects/demo-hmi/ignition/timer/refresh-pumps/handleTimerEvent.py:3:2: REF003 [error] `plant.pumps.refresh` accepts at most 1 argument positionally, the call passes 2Fixed 2 issues.Found 6 errors and 4 warnings.--output-format concise prints one line per finding. --fix-only applies the fixes without reporting what is left.
Safe and unsafe fixes
Section titled “Safe and unsafe fixes”The two remaining fixable findings have unsafe fixes. A fix is safe only when iglint can show that it cannot change what the project does in Ignition. These two change behaviour on purpose:
status is "fault"tostatus == "fault":==calls__eq__, which can give a different result than the identity test (in this case the intended one, but iglint cannot know that).except ValueError, TypeError:toexcept (ValueError, TypeError):: the handler then catchesTypeErrorand no longer assigns the exception to a variable namedTypeError.
Both are almost certainly what the author meant, which is exactly why they are offered, but a reviewer should look at them. Preview them with --unsafe-fixes:
iglint check --unsafe-fixes --diff--- a/projects/demo-hmi/ignition/script-python/plant/pumps/code.py+++ b/projects/demo-hmi/ignition/script-python/plant/pumps/code.py@@ -7,7 +7,7 @@ rows = system.db.runNamedQuery("Pumps/ByAreaa", {"area": area}) for row in rows: status = row["status"]- if status is "fault":+ if status == "fault": log.warn("Pump %s is faulted" % row["name"]) return len(rows)
@@ -15,5 +15,5 @@ def parse_speed(text): try: return float(text)- except ValueError, TypeError:+ except (ValueError, TypeError): return None
Would fix 2 issue(s) in 1 file(s).and apply them with iglint check --fix --unsafe-fixes. A fix that is safe in one project can be unsafe in another: if any code in the workspace reads frames, evaluates computed strings or reads the files a fix would edit, the fixes that depend on it become unsafe and say where that code is. What iglint does not catch explains why.
The rest (the mistyped view, property and query names, the extra argument, the undefined log) need a human: iglint reports what is wrong but does not guess the intended name.
Selecting rules
Section titled “Selecting rules”The default set (88 of the 110 rules) runs without configuration. Rules are selected by code, name, code prefix or category:
iglint check --select BIND,REF # only these two categoriesiglint check --ignore SCR204 # everything elseiglint check --extend-select REF011 # the defaults plus an opt-in ruleSelecting a whole category also turns on its opt-in rules. With --select BIND,REF, the REF011, REF012 and REF013 rules report unused resources:
projects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:1:1: REF011 [info] View `Line/Overview` is not referenced anywhereprojects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:30:57: BIND005 [error] `view.custom.aera`: custom property `aera` is not definedprojects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:52:43: BIND004 [warning] `now` polls every 1000 ms, faster than the minimum of 10000 msprojects/demo-hmi/com.inductiveautomation.perspective/views/Line/Overview/view.json:80:75: REF001 [error] View `Line/PumpDetial` does not existprojects/demo-hmi/com.inductiveautomation.perspective/views/Line/PumpDetail/view.json:1:1: REF011 [info] View `Line/PumpDetail` is not referenced anywhereprojects/demo-hmi/ignition/named-query/Pumps/ByArea/resource.json:1:1: REF013 [info] Named query `Pumps/ByArea` is not referenced anywhereprojects/demo-hmi/ignition/script-python/plant/pumps/code.py:7:33: REF004 [error] Named query `Pumps/ByAreaa` does not existprojects/demo-hmi/ignition/script-python/plant/pumps/code.py:15:5: REF012 [info] Library function `plant.pumps.parse_speed` is never referencedprojects/demo-hmi/ignition/timer/refresh-pumps/handleTimerEvent.py:3:2: REF003 [error] `plant.pumps.refresh` accepts at most 1 argument positionally, the call passes 2Found 4 errors, 1 warning and 4 info.Line/PumpDetail is unused because the only reference to it is the misspelled one. (Line/Overview is unused because this small project has no page configuration that opens it.)
To browse the rules from the terminal:
iglint rules # every rule, with severity, fix and defaultiglint rules --category REF # one categoryiglint rule SCR115 # the documentation of one ruleSettings you want every time go into an iglint.toml file.
Adopting iglint on an existing project
Section titled “Adopting iglint on an existing project”A real project that has never been linted can have hundreds of findings. You do not have to fix them before iglint can protect you: a baseline records today's findings, and from then on only new ones fail. Start again from the original example (git checkout .) and write one:
iglint baselineWrote 12 finding(s) to the baseline .iglint-baseline.json.iglint checkAll checks passed!12 finding(s) hidden by the baseline .iglint-baseline.json.Commit .iglint-baseline.json. Now add a new function with a typo to plant/pumps/code.py:
def area_label(area): return "Area " + aeraprojects/demo-hmi/ignition/script-python/plant/pumps/code.py:23:19: SCR101 [error] Undefined name `aera` |23 | return "Area " + aera | ^^^^ | = note: at plant.pumps
Found 1 error.12 finding(s) hidden by the baseline .iglint-baseline.json.The new finding fails the run, the old ones stay hidden. Entries are matched by rule, file and a fingerprint that survives moving code around, so editing other parts of the file does not bring old findings back. When you fix old findings (here, log.warn becomes logger.warn, and the new typo is fixed too), drop their entries:
iglint check --prune-baselineRemoved 1 fixed finding(s) from the baseline .iglint-baseline.json.All checks passed!11 finding(s) hidden by the baseline .iglint-baseline.json.More in the baseline guide.
Next steps
Section titled “Next steps”- Configuration: rule selection, severities, rule options and per-path overrides in
iglint.toml. - CI integration: annotations on pull requests, SARIF, GitLab and Azure DevOps.
- Editors: findings while you type in VS Code or any LSP editor.
- What iglint catches and, just as important, what it does not catch.