Changelog
All notable changes to iglint are listed here. The format follows Keep a Changelog and versions follow Semantic Versioning. Rule codes and names are stable from 0.1.0 on.
- A configuration file outside the checkout (
--config ../iglint.toml) no longer becomes the workspace root. Run from a linked Git work tree, the walk from that directory skipped the work tree, so every file was linted on its own: cross-file findings such asREF001andREF002were lost andREF009was reported for every file. The configuration file's directory now counts as a workspace root only inside the Git work tree of the linted path, and only when the workspace walk from it reaches the path. SCR001no longer reports the header-onlydo<Method>.pystub of a disabled WebDev method when the resource is linted outside its project (a loose directory, or a project the workspace walk does not reach). A WebDev handler linted on its own is now compiled as Ignition compiles it, under its canonical header and enabled or not as the siblingconfig.jsonsays; withoutconfig.json, a header-only stub is not reported.SCR204only suggests a replacement where it exists. In a gateway, WebDev, tag or Perspective script, a Vision-only replacement (forsystem.util.beepandsystem.util.playSoundClip) is no longer suggested; the message says where it exists instead. In a library module, the message says when the replacement exists only in some scopes (system.security.getUsernameandsystem.vision.getUsernameboth exist only in the Vision Client and the Designer).SCR204names replacements that are notsystem.*functions correctly: forsystem.net.httpGet,httpPost,httpPutandhttpDeleteit suggestssystem.net.httpClient()and its.get(...)(or other) method instead of asystem.net.httpClient.getfunction that does not exist, and forsystem.gui.isTouchscreenModeEnabledandsetTouchscreenModeEnabledit uses the 8.3 namessystem.vision.isTouchScreenModeandsetTouchScreenMode.SCR204no longer suggests a prepared replacement (system.db.execUpdate,system.db.runPrepUpdate, ...) for a deprecatedsystem.dbquery function whose query is aSETstatement (SET IDENTITY_INSERT ... ON). SQL Server keeps aSEToption made by a prepared statement only until that call ends, so the replacement would change what the code does; the message says so instead.- Baseline paths no longer depend on the directory name of the checkout
when
--confignames a configuration file outside the Git work tree: they are relative to the workspace root, as without a configuration file, so one baseline matches in every checkout (a linked work tree, a CI clone). Entries written relative to the configuration file's directory by 0.1.0 still match, and rewriting the baseline stores them relative to the workspace root. NAME001accepts an abbreviation fromallowed-abbreviationsas the whole name or the first word of a camelCase name (ID,PV,IMLocation). Before, it was only accepted after the first word (lineID). The built-in list ofauto-detect-abbreviationsdoes not lead camelCase names, soHTTPServeris still reported.# iglint: ignore[...]at the end of a statement that spans several lines (brackets, a backslash, a triple-quoted string) also suppresses the findings that start on the statement's earlier lines. A finding on a line that opens a triple-quoted string (SEC001onquery = """SELECT ... {}""".format(x)) could not be suppressed before, since a comment there would be inside the string.SCR123judges such comments the same way.
0.1.0 - 2026-10-25
Section titled “0.1.0 - 2026-10-25”First public release.
- Linter for Ignition 8.3 projects on disk: Perspective views, Jython 2.7 scripts (project library, gateway and session events, WebDev, tag events, scripts embedded in views), expressions, style classes, page configuration, session properties, named queries, resource manifests and gateway tag resources.
- More than 100 rules in ten categories (
VIEW,NAME,BIND,SCR,REF,PERF,SEC,A11Y,STYLE,TAG), with cross-resource checks over the whole project and its parent projects. Reference in docs/rules, generated from the rule metadata. - Jython 2.7 parser, semantic model and expression parser tested against the
parsers shipped in the Ignition 8.3.3 image;
system.*, expression function and Perspective component facts generated from a real gateway. - Safe and unsafe auto-fixes (
--fix,--unsafe-fixes,--diff) that only change the bytes they fix. - Configuration in
iglint.toml,.iglint.tomlor[tool.iglint]inpyproject.toml: rule selection, per-rule severity, options and excludes,[[overrides]],strict. - Script suppression comments (
# iglint: ignore[...],# iglint: ignore-file[...]) and a baseline file (iglint baseline,--prune-baseline) that is portable across platforms. - Output formats: text, concise, grouped, JSON, JSON lines, SARIF, GitHub Actions, Azure DevOps, GitLab Code Quality, JUnit and pylint.
- ign-lint 0.6.4 compatibility:
iglint ign-lint(or the binary called asign-lint) with ign-lint's arguments and output,rule_config.jsonand whitelist support, an equivalent for every ign-lint rule, andiglint migrateto convert the configuration. - Result cache and parallel checking.
- Language server (
iglint server) and a VS Code extension. - Distribution as prebuilt binaries (Linux x64 and ARM, macOS, Windows), a
PyPI wheel (
pip install iglint), a VS Code extension on the Visual Studio Marketplace (mustry-solutions.iglint, also attached to the GitHub release), and pre-commit hooks.
Changes during development
Section titled “Changes during development”The entries below record what changed during development and review, before the first release; all of it is part of 0.1.0.
Changed
Section titled “Changed”-
Documentation for the first release: the README, installation, CI and pre-commit pages install the binaries attached to the GitHub release (the CI examples download the pinned v0.1.0 Linux binary); the PyPI wheel is described as planned, not published; the documentation site is published at https://iglint.mustrysolutions.com/. The release workflow publishes to PyPI only when the repository variable
PYPI_PUBLISHistrue. -
Release files: one archive per platform without a version in its name (
iglint-linux-x64.tar.gz,iglint-linux-arm64.tar.gz,iglint-macos.tar.gzwith one binary for Intel and Apple silicon,iglint-windows-x64.zip), oneSHA256SUMS, and one VSIX with the binary for every platform. The wheels go to PyPI only. -
Language server edits (review round 34, Opus nits 2 and 3): a client that supports
workspace.workspaceEdit.documentChangesgets every edit (quick fixes, "fix all", the fix-all command, suppressions) as aTextDocumentEditfor the document version it was computed for; acodeAction/resolvethat makes no edit returns the action disabled with the reason when the client supportsdisabledSupport. -
Rule pages,
iglint ruleand the editor hover state the default of every option in the rule's Options section, taken from the rule metadata (BIND002, BIND004, BIND005, NAME001, SCR900, VIEW012, VIEW013 and VIEW018 did not); a test checks the defaults that pages state themselves. -
Documentation site: the validation page no longer counts the review rounds (it said 24, ending with an approval in round 24); it says the code was reviewed in repeated rounds until two independent reviewers approved it.
-
Documentation: README and CHANGELOG say that nothing is released yet (PyPI wheel, GitHub release binaries, VS Code packages and the
v0.1.0tag are planned for 0.1.0); the README documentsextend,extend-ignore,extend-exclude,unfixable,projects,gateways,respect-gitignoreandcache-dir; DESIGN.md no longer lists aCOMPATcategory; the editor docs say which open files withhold fixes and that a fix's fresh check may run pylint. -
A documentation site (Astro Starlight) in
site/: guides, reference and the rule pages synced fromdocs/rules/at build time. -
Language server latency (review round 30, minor): after an edit, the edited file is published first and the files it may affect are checked again 128 at a time until the next message arrives, so typing in a library module in workspace scope no longer waits for the whole workspace (3,000 generated files: 88.8 ms to 5.2 ms median per keystroke; the private project, 3,143 files: 320 ms to 18 ms median, 26 ms p95; the rest is published about 350 ms after the last keystroke). A view's scripts are summarised, their property accesses found and their vetting judged in parallel: the 3,000-component view of
latency.rs101 ms to 36 ms (CLI 85 ms to 33 ms; the earlier miss of the 50 ms target was this cost, not machine load). -
bench/README.mdre-measured (2026-10-24, small, medium, large and the private project, with and without pylint, all CPUs and 2 CPUs) with every 10x claim qualified: without pylint 10x or more from about 300 views on, 8x to 11x on the 50-view corpus depending on the run; a breakdown of the small corpus shows no fixed start-up cost to cut (2.3 ms for an empty directory; most of the time is indexing the rest of the repository). -
bench/README.mdand the text summary ofbench/parity.pysay plainly that pylint-derived findings (PylintScriptRule,SCR900) are outside the parity comparison, and how they differ by design (the combined-fileC0103andW1405messages dropped, noE0602for injected names,W0702reported once asSCR106). -
docs/rules/SCR101.mdsays which code is not checked at all: a.pyfile outside any project and the tag scripts of gateway resources, where any name could be a library package iglint does not know. -
bench/README.mdanddocs/compat/ign-lint.mdquote the private project's parity and aggregate numbers as of its revision of 2026-10-21 (measured 2026-10-23; the parity table and pylint comparison inbench/README.mdre-measured 2026-10-24 with ign-lint 0.6.4 and 0.6.3, on a copy of the project), and say that they depend on the revision. -
Faster runs: about 3x on a 300-view project and 2x on 5,000 views (whitelist lookups, JSON formatting detected once per file, mimalloc, no teardown at exit,
iglint ign-lintbuilds ign-lint's node model only for the options that show it). Peak memory on 5,000 views is down from 1.9 GB to 1.1 GB: parse trees live for one phase only, and JSON arrays and objects are allocated at their exact size. -
The cache keeps a unit's results when another file changes without changing the facts the project index is built from (most edits inside one view or script), instead of discarding every entry.
-
SCR900 runs pylint while the native rules run and, on POSIX, forks one worker per CPU from a single Python process;
jobsis the number of workers. -
VIEW015 analyses script accesses default-deny: every use of a component, the view, a page or session, a property container or an alias of one must be one of the enumerated forms (listed in
docs/internal/fix-safety.md); anything else reads and writes everything it reaches. Findings that rely on following values into library functions keep being reported, but their fix is unsafe; only findings the view proves on its own keep the safe fix.
-
[fix] vetted-system-functionsand[fix] vetted-expression-functions(default empty):system.*paths and expression functions of third-party modules that the project declares unable to evaluate code, read a frame or read workspace files, so that they do not count as unknown code. Checking that is the user's responsibility;SCR201'sallowandBIND002'sextra-functionsonly silence the findings. -
A drop-in
ign-lintpre-commit hook (review round 14): the same id,filespattern (view\.json$), types, defaultargs(--config=.ignition-lint-precommit.json) and file passing as ign-lint's own hook, runningiglint ign-lint. Documented in the README anddocs/compat/ign-lint.md. -
SCR307 redefined-event-handler(error): a gateway timer, tag change, message, startup, shutdown or update file, or a WebDev handler, whose last module-level binding of the handler name Ignition cannot call with its canonical arguments (def handleTimerEvent(event):after the header, a value that is not callable,del). Ignition runs the module code and calls the last binding by position (verified on an 8.3.3 gateway), so such a script never runs;SCR104only warned about the redefinition (review round 14). -
[fix] assume-scripts-do-not-read-workspace-files(defaultfalse) iniglint.toml: declares that no script reads the files of the workspace, which lifts the file-read condition below (and only that). The user's responsibility; shown by--show-settings. -
stubs-gen/perspective-fields.shmeasures, inside the Ignition image, which binding and transform fields Perspective parses as expressions or property paths and which members of the component script object return its name, intocrates/iglint_project/src/generated/perspective_fields.rs(generated_fields_match_the_generatorfails when the generator changes without regenerating,every_measured_field_is_modelledwhen the view model misses a measured field). -
VIEW021
invalid-view-structurereports the parts of a view (and of the session properties) that have the wrong JSON type, once at the offending value:"root": [],"children": "x","script": 7,"customMethods": [{"name": 5}],events,scriptsorpropConfigthat are not objects, and more. -
SCR003
compile-warningreports what Jython's compiler issues aSyntaxWarningfor, with Jython's message:global xafter an assignment to or a use ofxin the same scope, andimport *below module level (in Perspective and event scripts, everyimport *). The set and the messages are checked against Ignition's Jython (oracle/run-oracle.sh --warnings, thecompile_warning_oracletest). -
An unsafe fix of SCR102, VIEW015 or NAME001 says why when code that is not vetted, or that reads a frame, keeps it from being safe: the construct and its location, in the script or (closed world) the first such use in the workspace. Shown as a note in the text output, as
fix.unsafe_reasonin JSON, as a related location in SARIF, and in the language server's related information and code action title. -
docs/compat/ign-lint.mdlists what changed between ign-lint 0.6.3 and 0.6.4; configurations of both versions are accepted. -
SCR900 option
disable. A configuration read from an ign-lintrule_config.jsonsets it to the messages ign-lint's combined file disables (unused-argument,missing-docstring,redefined-outer-name,function-redefined), also with apylintrcof its own. -
Benchmark and parity harness in
bench/(see bench/README.md). -
SCR124trailing-whitespace (off by default, safe fix). -
SCR900fixes pylint'strailing-whitespacelike ign-lint'sPylintScriptRule(every script linerstrip()ed).
-
Language server and a byte order mark the editor keeps (review round 35, Opus minor): positions on the first line of a file that starts with a mark were always given after it, so an editor that keeps U+FEFF in the document's text saw diagnostics and the hover one character to the left, and "suppress on this line" inserted its comment before the last character of the line (
import jso # iglint: ignore[SCR102]n). Positions are now shifted only when the editor's text of the file lacks the mark (for a file that is not open, when the client left it out of the documents it opened). -
Language server and byte order marks or other encodings (review round 34, Opus minor): VS Code hides a leading byte order mark and writes it back on save, so an open file with one counted as modified forever and withheld every fix, and the file itself was linted without the mark (no
SCR001, fixesiglint checkdoes not make). Now an open document whose file has a hidden mark, or decodes to its text as iglint reads it, counts as saved; a file that is not valid UTF-8 withholds fixes with its own reason; a file that starts with a mark or is not valid UTF-8 gets no fix itself; the editor showsSCR001for the mark, with first-line positions after it. -
Language server: an unsaved edit of a view that is also the project's manifest (a hard link) could still get a fix computed with the manifest on disk (review round 34, Codex minor): the check refused the text for the manifest, but a later step counted it as used because the view took it. A refused text now stays refused.
-
The language server applies a fix only when every other open document is saved (review round 33, blocker, Opus B1 and Codex 1). The round 32 rule withheld fixes while an open file "the check reads" had unsaved changes, decided by path, and missed files open under another name: a module open through a symbolic link outside the folder, a
project.jsonopen through a symbolic or hard link or as the external file it links to, a configuration file the configurationextends (any extension). Now a fix edit (quick fix, "fix all",source.fixAllon save, the fix-all command) is computed only when every open document other than the one fixed holds its file's bytes on disk, and the fresh run overlays only that document's text (by path and file identity), so it isiglint check --fixafter saving it; the document open under several names with different texts, or with unsaved text the run cannot take (a manifest, a configuration file), gets no fix either. Clients withcodeAction.disabledSupport(VS Code) list the fixes disabled with the reason "Save the other modified files to apply iglint fixes". The cost: no fixes while any other open file has unsaved changes, also one that cannot matter. -
The language server no longer reuses fix edits between requests (review round 33, Codex 2): the one-second reuse for clients without
codeAction/resolvetrusted a fingerprint of sizes and modification times, and a same-size rewrite of the configuration with its old modification time kept a removed fix on offer. -
The language server treated pylint findings (
SCR900), which only a fresh check computes, as the editor's workspace drifting: every fix computation discovered the folder again, the published findings flapped, and a list of code actions could come back empty (review round 33, Codex 3). They are left out of that comparison and of what is published after it. -
The language server watches every file the configuration load read (files it extends,
rule_config.json, whitelist files, the baseline), compared by path, link target, identity, size and times, so a change to an extended configuration file is shown at the next code action (review round 33). -
The language server computed the fix of every finding of the file for a list of code actions of clients without
codeAction/resolve, even for a range without findings (review round 33, Opus M1); it now computes only the fixes of findings in the range, and none when there are none. On the largest views and modules of the private project, a range without findings went from 1,139 ms p95 to 0.4 ms p95, a range on one finding from 1,153 ms p95 to 147 ms p95. Resolving a fix there takes 146 ms median (182 ms p95), listing code actions 15 ms median. -
The language server computes every fix it applies with
iglint checkitself (review round 32, blocker, Opus and Codex). Rounds 29 to 32 each found another way for the server's incrementally kept workspace to differ from a fresh check and so offer a stale safe fix: a module replaced on disk by a symbolic link to another module (also with the same bytes), a link retargeted, an unsaved edit ofproject.jsonthat changes the parent project, a baseline written without a notification. Now every edit the server returns or applies for a fix (quick fixes, "fix all" as a code action,source.fixAllon save, the fix-all command) comes from a run of the same function asiglint check --fixin the folder: configuration and baseline loaded again, the workspace discovered again, the editor's text of every open document in place of the file on disk (also through symbolic and hard links to it), nothing reused from the incremental workspace. A quick fix's edit is the fix of a finding that run reports, applied on its own. While an open manifest, configuration or baseline file below the folder had unsaved changes, or an open document was missing on disk, no fix was computed (round 33 replaced this rule by location, which missed files open under other names, by "every other open document saved"; see above). The incremental workspace still shows diagnostics, hovers and the list of code actions; when a fresh run disagrees with it, the folder is discovered again and the fresh findings are published. Clients withcodeAction/resolveget the edits on resolve; for other clients one run computes the edit of every quick fix in the list (reused for at most a second while no notification arrived and the disk looked unchanged; removed in round 33). Cost (release build, private project, 3,143 files): resolving a fix takes 143 ms median (169 ms p95), listing code actions 16 ms median; typing is unchanged (largest library modules 19.4 ms median, 23.0 ms p95). -
The language server did not show findings of rules a configuration enables only for some files (an
[[overrides]]entry for**/view.json, as migrated ign-lint configurations have) in views with athumbnail.png: the view's settings were taken from the thumbnail. On the private project, NAME001 and VIEW018 findings in 175 files now appear in the editor asiglint checkreports them (review round 32, found by comparing the server with a fresh check). -
Discovery's directory digests now tell a file replaced by a symbolic link, a link retargeted and a link replaced by a file; the server's disk check treats a file whose identity, number of hard links or link status changed as a change of the workspace, even with the same bytes; a baseline written without a notification reloads the folder (review round 32, Opus B1 and N1, Codex 1). These keep what is shown current; fixes do not depend on them.
-
The language server offered stale safe fixes when a file it read was outside the workspace folder or changed without a notification (review round 31, blocker, Opus and Codex; since round 24 for changes on disk). A folder opened on
projects/childreads its parent projectprojects/basetoo, but the editor text of an openbasescript, and watched-file notifications for it, reached only the folder that publishes it; nested and overlapping folders ([W, W/.../lib],[R, R/.../users]) had the same gap; and a change on disk that no notification reported (git pull, another editor) was never seen. So a code action onlib.utiloffered the safe removal of animport jsonthat the edited script now uses (also fix all, resolve and the fix-all command). Now every live workspace whose index holds an open document gets its text (on open, change, close, folder addition, removal and reload; tracked per workspace), watched-file changes go to every workspace that holds or may hold the file, the server watches the directories of each workspace outside the folders (relative patterns when the client supports them, else absolute globs), and before any fix is computed the server compares every file and directory of the workspace with what it read (size, times, identity; contents when the stamp is within 2 s of the read), reads changed files again and discovers the folder again when files or directories were added or removed or the configuration changed. Cost (release build): 0.2 ms median on 3,005 generated files and 1,510 directories, 0.5 ms (p95 0.9 ms) on the private project's 4,262 files and 2,470 directories; a code action with fixes there takes 36 ms median. -
docs/rules/REF002.mdsays that a use behindhasattr(read, delete or augmented assignment) is reported like any other while no code creates the name (review round 31, nit); the three forms are tested together. -
A star import of a module named through another module's import was not followed by a further star import (review round 30, blocker; new in round 29): with
m0doingfrom lib import exports,m1doingfrom m0.exports import *andm2doingfrom m1 import *,m2.leaf.json.dumps(1)(okon 8.3.3,AttributeErrorwithout the import) did not usejsonofcore.leaf, andSCR102removed it as safe. Star-import targets are resolved through imports, aliases and chains, and a star import that cannot be resolved exactly to one module (a package, a variable, a target behind another star import) makes its module's names unknown to the modules star-importing it. Measured on 8.3.3 with 7chains.casesand 600 generated star-import graphs (chains-stars.expected). -
The language server computed fixes from stale text of other open documents (review round 30, blocker; incomplete in round 29): with
run: onSave, oronTypewith a long debounce, an unsaved or not yet analysed edit ofm1was not seen by the fresh check of a code action onlib.util, which offered a safe removal a check of the editor's texts does not report (also fix all, resolve and the fix-all command). Every fix computation now first gives the server's workspace the editor text of every open document, whatever the run trigger; publishing still follows it. -
SCR102reported names a caller'sfrom m import *uses whenmbinds__all__once otherwise than by an assignment to the bare name (review round 30, minor):from x import __all__,for __all__ in ...,with c as __all__,__all__, y = ...were read as an empty export list. Such a module now exports any name (4chains.cases). -
The unsafe reason of an import removal that a store elsewhere makes unsafe (
lib.util.json = 1in a view script) named no site: what, file and location were null in JSON and the SARIF related location had no physical location (review round 30, nit). It names the first store, also from the cache and after the store moves. -
REF002did not reportdel lib.util.nothere(aKeyErroron 8.3.3) orlib.util.nothere += 1(anAttributeError) (review round 30, nit). Both are checked, also through aliases, chains and packages; a delete or augmented assignment elsewhere no longer counts as creating the name (7chains.cases). -
Deleting or augmenting a library module's name elsewhere did not use it (review round 29, blocker; since the first project index):
del lib.util.jsonin a view script or another library module (aKeyErroron 8.3.3 onceimport jsonis gone) andlib.util.json += 1(TypeErrorwith the import,AttributeErrorwithout) were recorded only as stores, soSCR102removedimport jsonfromlib.utilas safe; also through aliases, other modules' imports (del m0.util.json,del m0.m1.util.json) and tuple targets. Delete and augmented-assignment targets are now uses of the name, through the one resolver; an assignment from another resource (also afor,exceptor tuple target) or from a function of the module throughglobalreplaces the binding and makes the removal unsafe. Measured with the library-routes oracle (chains.cases, each case also with the import removed);docs/internal/fix-safety.md, "Stores, deletes and augmented assignments through library paths". -
The language server kept a stale safe
SCR102quick fix after an edit of a module in the middle of a chain (review round 29, blocker; new in round 28): withm2importinglib.utilandbreturningm1.m2.util.json.dumps(1), changingm1toimport m2leftlib.util's "jsonimported but unused" published and offered its removal as the preferred fix, also after a save and a watched-file change;VIEW015stayed stale through two library calls. Any change of a library module's summary, or of what a resource contributes to the whole project, now re-checks every file; a quick fix, fix-all or code action resolve is applied only while a fresh check reports the same finding and fix, and stale findings are published again with nothing offered. Property test of random edits against a freshiglint check. -
REF002andREF003false positives for names stored through a chain of library imports (review round 29, minor):m0.util.h = lambda: 'stored'thenlib.util.h()(returns'stored'on 8.3.3) was reported missing, andm0.util.f = lambda a, b: 5thenlib.util.f(1, 2)(returns 5) as a wrong call. Stores are now recorded where they land through imports as well as as written. -
Star imports between library modules took time about cubic in their number (review round 29, minor, new in round 28): 1,000 modules each star-importing the ten before 22 s, 69 s with names bound nowhere, 150 modules each star-importing every earlier one minutes. Where a name may come from through star imports is now found once per module and name and kept for every consumer: 0.06 s, 0.09 s and 0.05 s (release). Every module that may provide the name is found (the old search stopped after 64 modules and counted a module reached twice like a cycle); a module that can neither provide nor pass on the name is not followed. New release stress test.
-
__all__changed in a way iglint does not read (__all__[:] = [...],__all__[1] = 'json',insert,+=, an alias, a change from a function, several or conditional bindings) was taken as the literal list (review round 29, nit): a name a caller'sfrom lib.util import *uses was reported unused. Such an__all__now exports any name, also one starting with_. -
A path through another library module's import was not followed (review round 28, blocker; since the first project index): for
m0.util.json.dumps(1)wherem0doesfrom lib import util, the index countedm0.utilonly, soSCR102removedimport jsonfromlib.utilas safe and the caller then failed withAttributeError;REF012reported functions only reached that way andREF002missedm0.util.nothere(). The same forimport lib.util as util,import lib.util,import m1and chains of any length,from m0 import util,from m0.util import json,import m0.util.jsonandrunScript("m0.util.json.dumps", 0, 1). Measured with Ignition 8.3.3's ScriptManager (38 chains,chains.casesof the library-routes oracle). There is now one resolver of dotted library paths (LibraryTree::resolve_path), which follows library imports with a bound of 64 and records every name a path reads; usage counts, values, dynamic references, exposed names, star imports, the shared-object reach, stores, rebinding, vetting,REF002,REF003,REF012,SCR102,VIEW015's call following and the language server's dependencies all use it (some followed imports on their own, with a bound of 8, others not at all). Default deny where a path cannot be followed with certainty: a name bound several times through imports makes each import's target used as a value, a missing name in a module with star imports continues in each star-imported module, and a cycle or the bound makes what it reaches used as a value. A library module used as a value now also makes the library modules it imports used as values (get().x.jsonreached a modulegetreturns), and a name that may hold an import from outside the library through several bindings reaches a shared object (del m0.j.dumps).REF003now checks calls through imports (m0.util.fmt()), andREF002reportsm0.util.notherewith its segment in the path as written. Documented indocs/internal/fix-safety.md, "Paths through library imports"; oracle replay, property test (400 generated libraries) and generated end-to-end chains. -
SCR120 did not count the items of a literal
*argsas arguments (review round 28, Codex minor):max(*((v * t for v in [1]),))in a loop was reported, thoughmaxconsumes the generator (Jython prints[0, 1, 2]), and so wasmap(*(lambda x: x * t, [1])). They are now arguments at their positions;max(*(g, None))andmax(*(lambda: t, lambda: t))are still reported (measured withoracle/run-oracle.sh --run). -
A
runScripttarget written as a dotted name was never analysed as code (review round 27, blocker; since round 8). Measured with Ignition 8.3.3's ownScriptFunctionand Perspective'sPerspectiveRunScriptFunction:runScriptcompiles"__RESULT = " + script, runs it with the library,systemandsys(Perspective's alsoself,view,pageandsession) as locals, and calls the result with the other arguments when it is callable, also with none. SorunScript("m.f.func_globals.has_key", 0, "re")readm's namespace whileSCR102removedimport refrommas safe (the tag's value changed fromtruetofalse), andrunScript("eval", 0, ...),"execfile","open","system.file.readFileAsString","system.util.execute","system.tag.configure"or an unknownsystem.*function kept every safe fix safe. Now a dotted name is the callname(*args)with unknown arguments, analysed like a script (vetting, frame routes, closed world, file reads, references), its object a library value (a module, package or import that is not callable becomes the expression's value); any other literal is the code__RESULT = <literal>. In Perspective expressions, code that namesself,view,pageorsession, or a computed script, is unknown code and makesVIEW015count every property of the view as read. The project index judges every segment of every dotted library reference, whatever produced it (scripts, imports, expressions), default deny: after a function any attribute but its name and documentation, after a class, variable or import any special attribute or member of Jython's function, method, class, instance and generator objects (measured), makes the module a value use, likevars(m). The library-routes oracle measures those members and 56runScriptexpressions; the replayed table has 273 routes. OnlyrunScriptevaluates code among the expression functions of the 8.3.3 image (everyFunctionclass of common, gateway and the modules scanned). -
SCR120 exempted
max(g)andmin(g)of a generator expression made in a loop when*argscould add a positional argument (review round 27, Codex:out.append(max((v * t for v in [1]), *[None]))keeps the generator, Jython prints[[2], [2], [2]]). The single-iterable exemption now counts the items of a literal*argsand is not granted for any other;**kwargsadds no positional argument (verified on Jython). The other builtin exemptions do not depend on the number of arguments. -
Closures that read a function's local took time quadratic in their number when the local is bound many times (review round 27, Opus nit: 32,000 lambdas reading one rebound local, 3.4 s): each read copied and hashed every binding of the name. The list is built once per function and name and shared; 32,000 closures now take 0.44 s with every rule. 32,000
tryblocks whose handler keeps an earlier binding of a name reaching remain quadratic with a small constant (1.4 s; the set of reaching bindings grows with every block), a known limit far above Jython's method size limit. Both shapes are in the release stress testlibrary_modules_take_linear_time_in_their_names. -
SCR102 removed an import of a library module as safe while another module reached it through the module object (review round 26, blocker):
m.__findattr_ex__('re'),m.dict['re'],m.getDict()['re'],m.__reduce_ex__(2)[2]['re']and the other special attributes of Ignition's library module and package objects (measured on 8.3.3:ProjectScriptModule,ScriptPackage). Default deny: a special attribute anywhere in a chain from another module (m.__dict__,m.__class__,pkg.m.__getattribute__,m.f.__call__, ...) uses the object before it as a value, likevars(m); the literal accessors__getattribute__,__getattr__,__findattr__and__findattr_ex__with a literal name count likegetattr(m, 'n'),__setattr__and__delattr__likesetattranddelattr; the Java members of the module and package objects (dict,getDict,setCode,reloadModule,name,path,pkg.clear,pkg.setDict, ...) make the module a value unless it binds the name itself (a name it binds wins, measured). REF012 no longer reports a function of such a module (m.__getattribute__(n)()was a false positive), and REF002 no longer reports the members as missing. Every measured member and 80 routes (aliases, containers, package chains,operator,inspect,copy,pickle,object.__getattribute__,type(m).__getattribute__) are replayed from the oracle (oracle/run-oracle.sh --library-routes,rules::refs::library_routes, 137 routes). -
The namespace of a library module holds its
__builtins__, som.dict['__builtins__']['locals']()reads the caller's frame (measured): SCR102 removed the caller's own import as safe.dict,getDict,setCode,__reduce__and__reduce_ex__are frame routes on any value (fixes unsafe). On a value that may be a module (a name bound by an import or bound nowhere, and attributes of one),vars(m),m.__dict__,m.dict,m.getDict(),m.__reduce_ex__andgetattr(m, '__dict__')observe names, so the findings they would make false (SCR102, SCR103, REF002, REF012 in the script that uses them) are not reported (review round 26, M2:vars(json)['__builtins__']). -
SCR123 reported, and its safe fix deleted, suppressions in scripts that do not parse, where no rule that reads the syntax tree ran (review round 26, M1; fix-all on save in the middle of an edit). Such scripts are not judged, and they count as referencing anything for the unused rules elsewhere (REF011 to REF014, and SCR102's safe fix of a library module's import).
-
SCR120 exempted every argument of
sorted,min,max,map,filterandreduce(review round 26, Codex:max(lambda: t, lambda: t)returns the lambda). Only the arguments a builtin calls (cmpandkeyofsorted,keyofminandmax, the function ofmap,filterandreduce) or consumes are exempt. A generator expression kept in a loop (out.append(v * i for v in y)) is now reported, and a generator function whose every call is consumed in place (total += sum(h())) is not. -
A module where
nfunctions bind one global that module code readsntimes took cubic time (review round 26, M4: 4,000 of each, 7.1 s with--select SCR001and 21 s with every rule; now 0.01 s and 0.04 s). The lists of bindings that many references find are built once and shared. Long lists of reaching bindings are shared too: a name bound in 10,000 branches and read 10,000 times took 1.2 GB and 3.0 s, now 87 MB and 0.5 s (their merges stay quadratic with a small constant). SCR104, SCR108, SCR113 and SCR900 count lines with line indexes. -
bench/parity.pyresolves a component's ownpropConfig: a component custom property with a change script and ign-lint's definitions from a component'spropConfigkeys are the documented differences (0 unexplained on the fixtures). -
A library module whose namespace another module reads through
__dict__(m.__dict__[name]) counts as used as a value, likevars(m)andgetattr(m, name)(review round 25, N2): REF012 no longer reports its functions and SCR102 its imports. Fixes that depend on the module's names only become more conservative. No change on the private corpus. -
SCR120 reports generators made in a loop that read its variable: calling a generator function (
def h(): yield t, thenout.append(h())) runs none of its body, and a generator expression returned by a function called at once ((lambda: (t for _ in [0]))()) runs when it is consumed, so each gives the last value (Jython:[[2], [2], [2]]; review round 25, Codex). It no longer reports a lambda passed directly to the builtinsorted,min,max,map,filterorreduce, which call it only while they run ([sorted(rows, key=lambda r: r[c]) for c in cols]; review round 25, N1), nor a generator expression such a function passes to a builtin that consumes it (sum,any,''.join, ...). No change on the private corpus. -
Project library modules with many top-level names were analysed in time about cubic in their number (review round 25, M3): the project index decided for every library value from scratch whether its module may hold a shared object and looked symbols up by a linear search; the flow analysis copied every local at each comprehension, branch, loop and
tryand merged binding sets one id at a time (it now shares the state in a persistent tree and merges sorted sets); the vetting walker listed every binding of a name at each use;SCR003,SCR102andSCR103searched the module once perglobaldeclaration, import or finding; and every module-level read went through every module binding of its name.Binding::shadowedof the semantic model is now recorded only for the kinds the analyses read (definitions, imports, augmented assignments). Release builds on one machine: 2,000 statementsbI = a4.7 s to 0.01 s, 2,000 pairscI = 1; bI = cI12.5 s to 0.01 s, 8,000b = [c for c in a]71 s to 0.05 s, 8,000 such comprehensions with distinct names more than 150 s to 0.05 s, 10,000bI = amore than 150 s to 0.03 s, and the 16,000-branchelifchain of the stress tests more than 150 s to 0.7 s. The release stress testlibrary_modules_take_linear_time_in_their_nameskeeps twelve shapes of 10,000 statements under 1.5 s (all between 0.03 s and 0.4 s here) and linear where the result is (four times the statements, at most six times the time);language_server_survives_deep_documentspasses again. Results on the private corpus are byte-identical. -
importlib.import_module('__builtin__')hands out the builtins like__import__('__builtin__'), and.locals()on it reads the caller's frame (review round 25, M2), but it only made fixes unsafe, so SCR102 reported an import read that way as unused. A direct call is now judged like__import__(a literal plain module keeps the findings), and every other use of it,imp's loaders,sys.builtins,sys.getBuiltins(),sys.getDefaultBuiltins(),sys.modules_reloading,sys.__dict__and thesysattribute of other modules (re.sys) are routes that read names, likesys.modules. Measured on 8.3.3's Jython. -
SCR124's safe fix changed what a script does after a string Jython drops (review round 25, B1): Jython's lexer drops a single-quoted string that a line break ends, with the line break, and a backslash in it escapes the whitespace after it, so
v4 = 'x \theny' # cisv4 = y; without the whitespace the backslash continues the string andv4is'x y'(measured on 8.3.3's Jython). Such whitespace is now part of the string: SCR124 does not report it and SCR900's fix is unsafe. Every layout-only fix (SCR123, SCR124 and SCR900's trailing-whitespace fix) is safe only when every token of every script stays as it is, proven by lexing; otherwise it is unsafe with the reason (whitespace after a stray backslash,print 1, \, or a comment removed after one). The safety net also compares the reads that a fix removes and the literals of each script. A differential test (layout_oracle,oracle/run-oracle.sh --run) ran 8,000 generated programs through these fixes and Jython: no behaviour change (714 of 2,000 with the old code). SCR124's fix safety is now "depends". On the private corpus no finding or fix changed. -
A safe fix in a module that fails to compile on its size either way was reported safe by
checkand refused by--fixas making the file worse (review round 25, M1): the health check took SCR002's message with another byte count for a new error. Both now compare size errors without their numbers. The rule tests check that every fix they see reported safe is applied by--fix. -
A reflection or introspection route that is only a first hop no longer silences findings: any of the 145 measured reflective Java member names (
.type,.base,.fields,.module,getType(),getColumnType(i),__class__, ...), a computedgetattr,sys.exc_info(),import tracebackor a line number anywhere in a module used to make every scope read its locals, so SCR102, SCR103, REF002, REF003 and REF012 (and SCR104 and SCR118) reported nothing in that module. Routes are now of two kinds (dynamic::Route, table indocs/internal/fix-safety.md, "Frame routes: findings and fixes"): those that read or write a frame's or a module's names by themselves (sys._getframe,.f_locals,.tb_frame,e = eval,import __builtin__,get = getattr, ...) still silence findings; every route of either kind still makes the safe fixes that depend on what code reads unsafe (SCR102's reason names the route), keeps the module's code unknown for the closed world, keeps library symbols variables for every fix-side consumer, and the safety net drops a safe fix that adds or removes one. On the private corpus: SCR102 5 to 10, SCR103 39 to 55, REF002 75 to 76, REF012 21 to 24, every other rule and every existing fix unchanged; all 25 new findings checked by hand and true, and each of their fixes unsafe. -
A Java class or
type(x)in a comparison other thanis/is not, inin, as the operand of%on anything but a string literal, or as the first argument ofissubclassis unknown code: these hand the class to special methods user code may define (E() == AbstractMapgaveE.__eq__the class, which coulddel o.SimpleEntryfor every later script; measured on 8.3.3 for==,!=,%,inand__subclasscheck__). No change on the private corpus. -
docs/internal/fix-safety.md("The binding rule") said that a module object in a read-only place (a comparison,print,str,%) is allowed; the code allows read-only places only for Java classes andtypeand fails closed for module objects. The text now says so. -
The safe-fix net was quadratic when a script's compile status is not known: it rejected the safe fixes together and then checked each alone. An edit that leaves every token where it was (trailing whitespace, an edit in a comment) is proven neutral by lexing both versions and passes whatever the status; a script with a certain error (a lower size bound above a limit included) has a known status (it fails), and the net compares which limit fails rather than the numbers; failing fixes are found by halving groups (O(k log n) evaluations, at most
2n) incheck,--fixand the language server; and a safe fix that may change a token of a script whose status is not known is dropped without evaluating the net. Measured in release builds on one machine (1,400 statements padded with comment lines, every line ending in whitespace;--select SCR102,SCR124): 1,400 lines 1.7 s to 0.03 s, 12,000 lines 18 s to 0.12 s (--fix0.6 s), 16,000 lines 26 s to 0.16 s (--fix1.0 s), and 2,000 functions whose every safe fix fails from more than 150 s to 0.4 s. A release-only stress test (unknown_compile_status_is_not_quadratic) keeps these shapes under 20 s. -
SCR120 reports lambdas in comprehensions that read a comprehension variable (
[lambda: k for k in range(3)], also generator expressions and set and dict comprehensions: Jython gives[2, 2, 2]for each) and closures in a loop that read anexcepttarget (except ValueError, error: callbacks.append(lambda: error.args[0])gives[1, 1]). It no longer reports a closure that runs in the iteration that creates it: a lambda called where it is written ((lambda: u)()) and adefor a name bound to a lambda whose every use is a call later in the same block (Jython:[0, 1, 2]). -
An import in a class body is unknown code at the import:
class K: import mathstores the module as the attributeK.math, which the vetting walker did not follow, sodel K.math.pi,K.sys.modules['math'] = None,K.os.system(...),K.System.loadLibraryandK.os.open/K.os.readof a file a fix edits (also throughK(),self,cls, a parameter, an alias, a subclass or a nested class) kept SCR102's and SCR124's fixes safe (Jython printedmissingbefore SCR102's fix and7after). The rule is general: an object the vetting tracks may be held only by a name whose every use the walker analyses; every other holder receives it as a value, checked where the value is computed. A star import in a class body counts too. The project index no longer takes a function, class or variable of a library module for plain when that module's code may store such an object where it is reachable (del l.K.math.pifrom another module is reported at the change).x.__class__,x.__objclass__,getattr(x, '__class__')and the builtintypeused as a value are handled liketype(x): unknown unless only read (ArrayList().__class__.size = fchanges the Java class). Findings and fixes on the private corpus are unchanged. -
SCR120 reports closures that read a module global a module-level loop rebinds (
for value in xs: callbacks.append(lambda: value)gives every callback the last value), and a global a function's loop rebinds underglobal; loops in methods were attributed to the module and missed. A loop in a class body is not reported (its names are class attributes, which closures do not read by name). -
SCR102 no longer removes an import safely when code anywhere in the workspace changes the shared object the import's evidence describes: after
import math; del math.pi, removing an unusedfrom math import piturned anImportErrorinto success. Measured facts (a module's attributes, a Java class's members, thesystemAPI, the builtins) hold only while no code changes these objects, which every script of the gateway shares. So a store, deletion or augmented assignment into a module, a Java class,system/fpmi,sysor a function, class or constant of a vetted module or the builtins (del math.pi,m.pi = 3throughimport math as m,del os.path.join,System.out = x,ArrayList.size = f,sys.stdout = f,len.x = 1),setattr/delattron one,__setattr__/__delattr__/__dict__named on one, a Java class or member class kept or passed as a value (c = System,e = Map.Entry),type(x)other than read (compared, printed, an attribute read) and a change through a library module's import or a library module handed out as a value (del lib.math.pi,k = lib.math,f(lib)whenlibimportsmath) are unknown code, with the site named, for every fix that needs the closed world.__builtin__andreloadwere already not vetted. The member classes of the vetted Java classes are measured in the image (JAVA_MEMBER_CLASSES). -
BIND005 reports property references with nothing after the scope or the property type (
{view},{this},{session},{view.custom},{this.props},{../Label.props}, also as property binding paths): they match Perspective's reference grammar, so expressions read them as property references, but Ignition 8.3.3's gateway rejects them ("Invalid property identifier ..., no scope specified" or "no property specified") and the binding fails. -
A tag path whose property is empty or only dots (
{..},T.) names no property, as Ignition 8.3.3'sTagPathParserreads it, so it is no longer taken for a read of tag configuration (which made{..}withhold every safe fix as a possible file read while{this}did not). -
NAME001 no longer offers a safe rename to a name that broken references already use: renaming
my_labeltoMyLabelmadeself.getSibling('MyLabel'), a binding path../MyLabel.props.textor an expression{/root/MyLabel.props.text}start to resolve. The new name gets the guards of the old one (any occurrence as a word in a string of the view or a script's string literal, in any case, makes the rename unsafe and names the site; fixed values of Perspective's schema, such as component, binding and action types, do not count for either name), and the safety net compares the view's reference-resolution map (expression and binding references, script lookups, tag and view paths, components identified by their place in the view) before and after every safe fix to aview.json; any change drops the fix, which is reported unsafe with the reason (review round 21). -
SCR104 reports a redefinition when the only read of the first definition is in a top-level class body that runs after both definitions (
class A: f = f), which reads only the second one. -
NAME001 offers no suggestion or fix for a name with a character whose case mapping changes length (
file_namebecameFIleName), and says why. -
SCR112 checks format specs with a Latin-1 fill character (
u'{0:é>10s}'.format(0)raisesValueError: Unknown format code 's'on Jython), in unicode and byte format strings; byte strings are read as Latin-1. Verified on 614,400 generated cases with Latin-1 fills (oracle/format-spec.sh nonascii), all matching Jython. -
Code iglint does not know no longer counts as vetted (review round 18, Opus, B1). A
system.*member that the measured 8.3 stubs do not list (a third-party module'ssystem.perspective.runJavaScriptAsync, a package that does not exist), an expression function the gateway does not register (objectScript, a simulation module's functions) and a Perspective component or navigate action that can run JavaScript in the browser (a component type that is not in the 8.3 list, inline frames, amCharts and time series charts, maps, PDF and report viewers, the menu components, SVG drawings with elements that are not plain shapes, Markdown that renders HTML, links and navigate actions whose URL may be ajavascript:URL) are unknown code for the closed world and possible file reads, with the site named. So are stores into the scripting API (system.tag.readBlocking = f), its legacy aliasfpmi, andsystem.perspective.navigatewith a URL that is not a literal that runs no script. Before,SCR102,VIEW015,NAME001andSCR124kept their safe fixes. -
A safe fix near Jython's size limits is no longer applied (review round 18, Opus, B2):
SCR002's bounds are lower bounds, so the safety net could not see a fix turn a function Jython rejects ("Module or method too large") into one it compiles. Every code unit, the class's<init>andcall_functionand the constant pool now also have a sound upper bound, and a fix of a script whose bounds lie on both sides of a limit is unsafe, with the bounds in the reason. The upper bounds were calibrated on the oracle with 5,794 generated programs (3,000 random function bodies near the method limit, and programs near the<init>and constant pool limits): no method or pool exceeds its bound, method bounds are at least a quarter above the method's size (15,659 bytes above any unit of more than 60,000 bytes) and pool bounds at least 5% above the count. -
SCR002reports modules with too many functions, classes, lambdas and generator expressions for Jython's class initializer (<init>builds a code object per body and is one Java method too): Jython fails from about 765 functions of eight parameters,SCR002reports these from 840, with 0 false reports on 300 generated modules near the limit. -
The upper bounds of Jython's code size follow its code generator (review round 19, Opus, blocker): the round 18 bound charged a closure's free variables once per unit, while Jython builds the cells at every creation, so it undercounted functions full of closures by up to 4.6 times, and
SCR102removed an import from a function Jython rejects and compiles without it. Every Java method's bound now mirrors Jython 2.7.3'sCodeCompiler(the image's Jython emits the same code) instruction by instruction: cells per creation and free variable, also through the units in between, every copy of afinallybody and of the units in it, the locals and stack ayieldsaves, wide locals and jumps, and the class's<init>andcall_functionper compiled unit. The derivation is a table indocs/internal/fix-safety.md("Size limits"). Calibrated on 10,000 generated programs with a family of adversarial shapes per term: no method or constant pool above its bound, median margin 7%. The review's repro now gets "compile status is not known". -
Long names no longer escape the upper bounds of Jython's code size (review round 20, Codex, blocker): Jython loads a name of more than 16,000 characters in chunks, and the bounds charged one constant per name, so a function of 1,683 reads of an attribute of 16,001 characters seemed to fit, and
SCR102blanked an import Jython needs removed to compile it. Every name, attribute, keyword, imported name, class name and the names of every code object are now charged by their length (as Jython mangles them in a class), checked on the oracle with a family of long names in every position (smallest margin 7%). Independently, a script with an identifier of more than 1,000 characters has an unknown compile status ("identifier of N characters: Jython's code size cannot be bounded precisely"), so no fix of it is safe; the private project's longest identifier has 79 characters. -
SCR002reports a function or class whose name makes the name of its Java method longer than a class file allows (65,535 bytes,UTF8 string too large; a function of the module fails from 65,534 characters), measured on the oracle. The documentation said that identifiers have no length limit: names in every other position compile at 1,000,000 characters. -
SCR002reports two more crashes of Jython's code generator (review round 20, Opus): ayieldin the exception type of anexceptclause (except (yield):,NegativeArraySizeException) and ayieldin an attribute or subscript target inside an unpacking target (b, a[(yield)] = 1, 2, also infor,withandexcepttargets,IllegalArgumentException). Single targets, augmented assignments,deland unreachable copies compile and are not reported; the cross product oforacle/compile_cross.py(13,202 cases) has no difference from Jython. -
oracle/jython_method_size_oracle.pyno longer aborts on a compile error message with non-ASCII text (review round 20, Opus), and records method names of more than 64 characters shortened. -
Size bounds saturate instead of overflowing (review round 19, Codex): nested
try/finallychains with areturnin eachfinallymade the bound wrap in release builds (theSCR124fix of a 64-level chain was safe) and panic in debug builds. Such a script's compile status is now not known and no fix of it is safe. -
SCR002's lower bound no longer exceeds Jython's size for simple slices, stores without a temporary and unpacking,delof a tuple, anif,withortrywhose body ends,execwith both parts, comprehensions with several generators,fromimports and the class initializer of small modules (found by the new calibration programs;SCR002never reported a script Jython compiles from these). The class initializer's bound counts each code object per instruction:SCR002reports modules of functions of eight parameters from 900 (was 840; Jython fails from about 765). -
BIND002no longer reportspropertyandisAuthorizedin Perspective bindings, transforms and the other expressions of a view, which can call them (measured on an 8.3.3 gateway: Perspective's own function factory adds them, and replacesrunScript), and still reports them in tag expressions and alarm and tag property bindings, where an 8.3.3 gateway rejects them.BIND003checks their argument counts. A call ofpropertystill keeps other rules' fixes from being safe (it reads properties by a path computed at run time) unless the project vets it. The generated lists come fromstubs-gen(new:expression_contexts.py,introspect/perspective_expression.py). -
check, the language server's diagnostics and its quick fixes report a safe fix that the post-fix safety net would drop as unsafe, with the net's reason (review round 18, Opus, N1). Before, it was listed as safe and--fixsaid "fix not applied". -
No more panics (exit 101) on text with multi-byte characters where a byte index was assumed to be a character boundary (review round 18, Codex). A script path literal whose component starts with such a character (
os.path.exists(u"/é/report.txt")) abortedcheckand the language server's initialization; so did a view property key ending in multi-byte characters, a string literal with a multi-byte character across its seventh byte (SEC004), and a configured NAME001 abbreviation whose uppercase form is shorter (ı,ſ), which could also split the name at the wrong place. Lexer error columns, pylint positions and source map offsets inside a character now snap to its start. Clippy'sstring_slicelint is denied in the whole workspace: every byte slice of astreither uses a checked API or says why its indices are boundaries.TextRange::sliceclamps a range that splits a character or leaves the text instead of panicking (TextRange::getreturnsNone), andapply_editsrejects such an edit as an error. A generated-workspace test puts random Unicode in every name and string position and runs every command and the language server over it. -
A linked Git work tree (
git worktree add,.gitis a file that points at<git dir>/worktrees/<name>) is a checkout of its own, wherever it lies (review round 17, Opus): a file of.worktrees/xor.claude/worktrees/xwas linted against the main checkout's workspace, whose walk skips hidden directories, so the file-read gate and the closed world came from the main checkout's code (a feature branch'sopen(p).read()did not make SCR123 and SCR124 fixes unsafe), REF009 said the walk did not reachproject.jsonandresource.json, real REF002 and REF012 findings were missed and baseline paths started with.worktrees/x. The search for the workspace root stops at a linked work tree; the main checkout's runs (and the language server's folder) no longer enter one, hidden or not. Submodules and repositories made with--separate-git-dirstill belong to the enclosing work tree. -
A workspace root candidate counts only when its walk reaches the linted file's project or collection (no hidden or otherwise skipped directory on the way), otherwise the nearest one that does is the root: a project below
node_modules/pkg/projects/is linted innode_modules/pkg. A file whose project the walk did not reach anyway (a language server folder) gets no fixes, and REF009 says that iglint lints it on its own, without saying that the gateway does not load it. In the language server such a file gets a folder of its own. -
Rules that point at the first character of a file (REF011, REF013, STYLE002, REF014) no longer panic (exit 101, no output) when the file starts with a character longer than a byte, such as a UTF-8 byte order mark (review round 17, Opus). Every finding's range is widened to whole characters in one place, line and column conversions accept any offset, and a fix with an edit inside a character is dropped; debug builds assert that rules compute ranges on character boundaries.
-
SCR307 no longer reports a handler binding that code the rule does not follow may replace (review round 17, Codex): a
globaldeclaration of the handler name in a class body or function (also when a later call assigns it), a module-level list comprehension whose variable is the name (Python 2 leaks it),exec,import *or writes throughglobals()in any scope, or a route to a frame. Bindings that stay in their scope (locals, class attributes, generator variables) still do not hide a finding. Verified on the Jython of Ignition 8.3.3. -
The workspace root no longer depends on what is linted (review round 15): it is the outermost of the configuration directory, every Ignition layout root around the path (up to the Git work tree) and the Git work tree when a project or collection lies anywhere below it. Linting a tag file in
gateway/data/config/resources/next toprojects/, orsrc/projects/Anext togateway/config/resources/, now sees the projects too, so its fixes are as safe as in a run over the repository. -
Projects and collections named
target,node_modulesor__pycache__are discovered, linted and part of the closed world; before they were skipped (review round 15). -
Hidden resource directories and folders inside a project or collection (
script-python/.hidden/,views/.folder/V) are linted and part of the closed world: an 8.3.3 gateway loads them. Before they were skipped. -
A workspace directory that cannot be read (permissions, a broken symbolic link, a symbolic link to a directory) makes the closed world unknown: safe fixes become unsafe with a reason that names the directory. REF009 says when a resource file lies in a project the workspace walk did not reach, instead of calling it outside every project.
-
SCR307, SCR105, SCR112 and the library constants of the closed world treat
True,FalseandNoneas constants only where they read Jython's builtins: all three are names a module can rebind (True = lambda: 7,import x as None; verified on Ignition 8.3.3), soTrue = lambda: 7followed byhandleTimerEvent = Trueis no longer an SCR307 error (review round 15, Codex). -
The "nothing to lint" warning shows the working directory as
.instead of an empty path. -
Symbolic links to directories are no longer followed, like Ignition 8.3.3, which does not follow them (review round 16, verified on a throwaway gateway: a linked project never starts, a linked
corecollection keeps the gateway from starting, a linked library folder,script-pythonorignitiondirectory givesImportError, a linked tag folder has no tags). A link inside a project or collection, a project or collection directory that is a link, and a link to a directory that holds projects count as a directory that was not read: every safe fix is unsafe, with the link named. The files behind such a link are linted on their own, reported by REF009 ("behind ..., a symbolic link to a directory") and get no fixes, so--fixno longer edits files where a link points, also outside the repository. -
A safe fix of a file that is a symbolic link, the target of a symbolic link of the workspace, or a file with hard links is unsafe, with the other paths named (review round 16): the gateway reads files through links, so the fix changed every resource that reaches the file (
SCR102removed an import that another project used through its link). Every file is known by its identity (device and inode). Symbolic links to manifests (project.json,resource.json,config-mode.json) and data files are read, as the gateway reads them. -
--fixkeeps symbolic links and hard links: a fix of a link is written to the file it points to, and a file with hard links is rewritten in place. Before, the link was replaced by a regular file (review round 16). -
The workspace root of a file in a nested Git work tree (a submodule, or a repository made inside another one) is the outermost work tree that holds a project or collection, not the nearest
.git, so linting one file of the nested repository sees the gateway configuration of the enclosing one (review round 16). -
Without a configuration file, baseline paths and fingerprints are relative to the workspace root instead of the working directory, so an explicit baseline matches, and is pruned, the same from every subdirectory (review round 16, Codex). Entries written by earlier builds from another directory still match, and are rewritten relative to the workspace root by
--update-baseline; the default.iglint-baseline.jsonis looked up at the workspace root first.--show-settingsprints the directory (Baseline paths relative to:). -
SCR307 no longer reports
handleTimerEvent = a == b(or<,in,is) and backquotes as values that cannot be called: a rich comparison may return a function (__eq__returning a lambda is called by Jython). Only literals of non-callable types and the builtin constants count (review round 16, Codex). -
bench/parity.pyno longer lets ign-lint write its debug files into the corpus or the source tree when the configuration has no debug settings (ign-lint pickstests/debugor.ignition-lint/debugby itself):PylintScriptRulealways getsdebugoff and adebug_dirin--out(review round 16, Codex). Pylint findings are set aside before matching instead of being counted as identical when their text matched; the summary gives their number per side undernot compared(the private project's parity row is now 1,463 identical and 915 expected, 0 unexplained). -
SCR112missed formatting that certainly fails on Unicode (review round 14, Codex): a unicode format spec with a character beyond U+00FF (u'{0:\U0001f980^4}'.format(1):ValueError), a non-ASCII unicode literal made a byte string by a byte string format or!s(UnicodeEncodeError), a non-ASCII byte string inserted with%sinto a unicode result (u'%s' % '\xff', also after a unicode value:UnicodeDecodeError), and%cof a number from 256 on into a byte string (OverflowError). Checked against Jython on 16,947 generated cases (oracle/format-unicode.sh): no finding where Jython formats, every such failure with literal values reported. -
With lone CR line ends, findings were shown a line further down than Jython reports them (review round 14): Jython numbers lines by
\nonly, and a lone CR is a character of the line. Every output format (text, concise, JSON, SARIF and the others) now shows Jython's line and the column of the same character, checked against new parser oracle cases (crates/iglint_python/tests/oracle/line_endings.*). The language server keeps the protocol's lines (a lone CR ends a line there), so the editor highlights the same text. -
Large views took superlinear time (review round 14): 80,000 components took 11.5 s, 46 times as long as 10,000. The view model looked up the
propConfigentries, properties and events of each component by scanning the whole view,NAME001collected the names of every sibling, looked up references and searched every string of the view once per renamed component, andVIEW018grouped expressions with a linear search. Lookups are now indexed: 80,000 components take 1.1 s (all rules: 1.3 s), 160,000 take 2.3 s. A release-only stress test checks that time grows linearly for views and modules. -
The scope oracle compares Python 2 tuple parameters too (review round 14): the dump has the argument Jython loads (
load fast (b, (c, d))) and the names its prologue stores, and the generator makes tuple parameters, nested ones included (a fresh run: 20,053 programs, 6,602 of them with tuple parameters, 0 differences). -
Linting part of a workspace (one file, as the pre-commit hooks do, a project directory,
cd projects/A && iglint check --fix ., standard input, a language server folder opened on one project) analysed only the projects around the linted paths, so a safeSCR102fix removed an import that a gateway tag script used asm.os, and a tag script that is not vetted did not make the fix unsafe (review round 14). The workspace (closed world, uses of library names, every cross-resource fact) is now everything below the workspace root of the linted paths: the configuration file's directory, the directory that holds theprojects/orconfig/resources/layout, the Git work tree or the working directory. Excluded and ignored files still count, since they still run. Without a root the closed world is unknown and the fixes that need it are unsafe.--show-settingsprints the workspace roots. A warm single-file run on a 3,000-file workspace takes about 55 ms (17 ms before, when it saw only one project). -
Every unsafe fix says why (review round 14):
SCR102imports not known to be free of effects, names still read after the import or before afrom __future__import, names other resources may use;SCR103,SCR110,SCR115,SCR123,SCR206,SCR301,SCR900,VIEW012,VIEW013,VIEW015andNAME001fixes that are unsafe by nature. -
SCR002missed "Module or method too large" far below its threshold of 10,922 statement lines: Jython fails from 680 lines ofd["kN"] = {"a": N, "b": [1, 2, 3]}(review round 14). It now adds a lower bound of the bytecode of every statement and expression, measured on Jython's code generator and checked against every body of about 2,300 generated programs (oracle/run-oracle.sh --method-size), and reports a body from about 740 such lines. -
SCR102removed imports that class bodies read, safely, and--fixbroke the module (review round 13). A class body reads by name: a name it binds anywhere is looked up in the class namespace and then in the module's globals and the builtins, never in an enclosing function, sox = m.pibefore the class's ownm = 1reads the module'simport math as m. And Jython mangles private names in class bodies (__mis_D__min classD, also for import aliases,def,class,global,except,withandfornames) and attribute names anywhere inside a class, but not names in methods.SCR101reported such names as undefined (from math import pi as _D__piread as__piin the class); it now shows the mangled spelling when it does report one. The semantic model resolves names the way Jython's compiler does, checked by a new differential oracle (oracle/scope/): which accessor Jython's code generator picks for every name read, store and delete of a program (by name, global, fast local, cell or free variable) with its run-time spelling, and every attribute name; 100,000 generated programs, the 1,642 scripts of the private project and hand-written cases agree. -
Imports the interpreter reads without the code naming them counted as unused (review round 13, M1):
import math as __metaclass__decides how a class without bases is created, andSCR102removed it safely. The model now records the implicit reads, measured withoracle/run-oracle.sh --reads:__name__when a class body starts and when a function, lambda or generator expression is created,__metaclass__after a class body without bases,__name__,__package__and__path__at every import,__debug__andAssertionErroratassert. The safe-fix net also compares which kinds of bindings each read may find, not only how it resolves. -
SCR002missed compile failures in code Jython generates but cannot reach (review round 13, M2): ayieldin a list comprehension, or in the first iterable of a set or dict comprehension, after atrythat returns from both its body andfinally(and 47 other control flow prefixes) crashes the bytecode verifier, because a generator resumes after everyyield. Reachability is now one model for the compile errors and the scope oracle, checked against a cross product of every block-ending prefix, every compile error construct and 14 nesting contexts (oracle/compile_cross.py, 5,796 cases).SCR002also reports a private name in a class whose name is only underscores (Jython's compiler crashes),execwith free names andimport *in a class nested in two functions, andreturnwith a value afteryieldin module and class bodies, as Jython does. -
--diffwrote hunk headers that did not match their lines for the most commonSCR102safe fix (an emptied import line before a blank line:@@ -1,3 +2,2 @@), whichgit applyandpatchrejected. iglint now computes the hunks itself; a property test applies random diffs (blank line edits, CRLF, missing final newlines, several hunks) with both tools and checks every header. -
Every fix was safe although scripts read the content of the files it edits without naming them (review round 12). The
system.*audit classified functions by their parameters only; it now classifies each of the 683 functions and objects of the stubs by what its outputs carry, and a test fails on one that is not classified. Possible reads now includesystem.tag.getConfiguration,exportTagsto any path (exportTags('/tmp/x.json', ...)thenopen('/tmp/x.json')showed a tag event script's comments),query,browse,getAttribute,getAlarmStates, tag reads (readBlocking,read,getTagValue, ...) of a path that may name a configuration property such asT.eventScripts(literal value,QualityandTimestamppaths stay fine),system.perspective.getProjectInfo, the audit log, Vision windows, and every network client (system.net.httpClient, thehttp*functions,java.net.URLwith a network URL): a gateway sharing the workspace's files serves them over HTTP. Expressions, Perspective tag bindings, reference and derived tags, tag-bound alarm properties and tag change scripts whose tag path may name a configuration property ({[.]T.eventScripts},tag(path)with a computed path) count too, since the value they produce carries the configuration. The measured Java members that hand out such content (getEventScripts,findViewConfig,getPropertyConfig, ...) count like file handles.system.tag.movecounts as code that stores tag scripts, likecopy, and the legacysystem.db.exportCSV,exportExcelandexportHTMLas file facilities. All verified on Ignition 8.3.3. -
SCR112 missed
*widths and precisions with a dict as the only operand ('%*s' % {'x': 1},'%.*f' % {'x': 1}), which raiseTypeError: * wants inton Jython. -
bench/parity.pycompares the severity of matched findings and explains the three documented differences on the bundled fixtures;cargo test -p iglint --test bench_parityfails on any unexplained finding or severity difference. -
SCR102's safe fix removed the only import of a library module that reads
__package__: in a library module every import statement that runs (failing, Java and__future__imports too, in any scope) binds__package__in the module's globals, so a function returning it gaveNonebefore the fix and raisedNameErrorafter (measured on Ignition 8.3.3 through its ownScriptManager,oracle/run-oracle.sh --implicit, recorded incrates/iglint_python_semantic/tests/oracle/implicit_names.expected). The semantic model now binds__package__at each such import, so the safe-fix net sees the change, and the fix is unsafe whenever anything may observe__package__or__warningregistry__: a read or binding in the module, a binding of__name__, a read of the globals by name, or another resource reading them, the module's__dict__or the module as a value. Event scripts run without__name__, and their imports bind nothing (unchanged). SCR101 now reports a__package__that no import of the library module binds. -
SCR112 missed format specs with characters after the type (
'{0:dd}'.format(1),'{0:%Y}'.format(1),'{0:.2.2}'.format(1.5),'{0:s.}'.format('ab'),'{0:#x#}'.format(5)) and a.without digits ('{0:5.}'.format(1)), which Jython rejects for every literal ("Invalid conversion specification", "Format specifier missing precision"); objects with a__format__of their own (datetime) are still left alone. Jython also never reads an alignment character as a fill ('{0:<<}'is alignment<and type<), unlike CPython; SCR112 now parses it that way.oracle/format-spec.shgenerates characters after the type too: all 5,414,400 cases match Jython, and the replayed sample was redrawn. -
The rule catalogue (
docs/internal/rules.md) labelledNAME001's fix unsafe although it offers safe renames, andVIEW013's although it is safe when there is no stored value. Rules whose fixes are safe or unsafe per finding (NAME001,VIEW013,VIEW015,SCR102,SCR123,SCR900) are nowS/U; each rule declares its fix safety in the registry (fix_safety), the catalogue test checks the column against it, and the rule pages show it. -
iglint check <dir>on a directory without an Ignition project (a folder of loose.pyfiles) printed only "All checks passed!". It now warns that no Ignition project or lintable files were found under it (exit status still0); a.pyfile named on the command line is still linted as a standalone script. -
bench/__pycache__is no longer committed;__pycache__/and*.pycare ignored. -
File access is default deny for the file-read condition of every fix: every vetted facility that takes a path counts unless every path argument is a literal outside the workspace, whatever its mode or direction.
os.renameof a module to/tmp/m.pyfollowed by a read of/tmp/m.pykeptSCR102's fix safe, andopen(p, "a")thentell()(which reveals the size) counted as write-only; nowopenandfilein every mode,os.rename,renames,remove,unlink,makedirs,mkdir,rmdir,utime,listdir,walk,os.path.exists,isfile,isdir,realpathand the other path-taking attributes,system.file.writeFile,fileExists,getTempFile,system.tag.exportTags, the dataset exports and the othersystem.*functions that name local files, and the JavaFileOutputStream,FileWriter,PrintWriterandFormattercount. Every vetted attribute ofos,os.pathandsys, and everysystem.*function with a file-like parameter, is classified, and a test fails on one that is not. -
Fixes of every rule were safe even when a script read the file they edit:
open(...).read()of a module sawSCR102remove an import,SCR124trim a comment,SCR123remove a suppression andVIEW015remove a custom property from aview.jsonthe view's own event reads. While any code in the workspace may read a workspace file (a read ofopen,file,os.stat,os.access,os.path.getsizeand the other metadata functions,system.file.readFileAsStringand friends, the legacysystem.net.http*functions,java.io.File,FileInputStream,FileReader,java.net.URLorURIwhose path is not a literal outside the workspace, a measured Java member that hands out a file handle such asgetParentFileorgetProjectManager, a Vision client script, or code that is not vetted or reads frames), every fix is now unsafe, and the unsafe reason names the first such site and the new setting. Reads of literal paths outside the workspace (/tmp/x.csv), write-only opens, directory listings and existence checks keep fixes safe.docs/internal/fix-safety.mdhas a new "File reads" section and no longer claims that whitespace and comments cannot be observed. -
Gateway managers reachable without
system.*store or run code and configuration (request['context'].getTagManager().importTagsAsync(...)in a WebDev handler imports tags with their scripts from JSON). The generator now measures the gateway types that store or run code and configuration from data and the members that hand one out (getTagManager,getGatewayContext,getScriptManager, ...); naming one, or using a WebDev handler'srequestin a way that may reach itscontext, is unknown code for the closed world, with its site. -
SCR112 missed format specs that raise for a literal argument:
'{0:z}'.format(1),'{0:.2}'.format(1),'{0:+s}'.format('ab'). It now checks the whole spec per literal type (presentation type, sign, alignment,#,,, zero padding, precision), matching Jython on every one of the generated cases (oracle/format-spec.sh, 5,414,400 since the entry below; a stratified sample of 4,000 is replayed by the tests). -
SCR102's documentation no longer says that removing an import of a missing JDK class is a remaining effect of the safe fix (that removal is unsafe since the previous round).
-
Review round 10 (views): every field of a Perspective binding or transform that Ignition parses as an expression is now an expression of the view model, so all BIND rules, VIEW015 and NAME001 see it: HTTP bodies, header values and authorization values, tag history date ranges, most-recent values and return sizes, map transform fallbacks, list items of expression structures and indirect tag binding references. The fields are measured in the Ignition 8.3.3 image (
stubs-gen/gen_perspective_fields.py, see "Added"). An HTTPurlis always parsed, so a literal URL is reported (BIND001) instead of being skipped: Perspective rejects it ("Error parsing url: Invalid expression"). -
NAME001 no longer offers a safe rename that leaves references dangling: references in the newly modelled fields are renamed (the fix is then unsafe, as for every referenced component), and the safe rename is withheld, naming the place, when a script reads the component's name through
nameorgetName()(also for a component reached throughchildren,getChildren(), a loop or an index; the name members are measured), when a string iglint does not read as an expression holds a{...}reference, when a reference is written with whitespace inside its braces, or when a string holds the component's name where the fix does not update it. -
BIND005 reports
{ view.custom.x }: whitespace inside the braces makes Perspective read a property reference as a tag path (checked against Ignition 8.3.3's parsers). VIEW015 counts such a reference as a read of the property, so its fix never deletes it. -
Pages and sessions hand out other views (
page.views,getViews(),page.view,session.pages,getPages(),getPage(id)), with their custom properties and components. Such a use anywhere in the workspace (view, component and session property scripts, session and page event scripts, library functions handed a component, view, page or session) is unknown code for the closed world, so VIEW015 and NAME001 fixes become unsafe and name the site. So is a component, view, page or session used as a value the analysis does not follow. The members of the page, session, view and component script objects are measured in the Ignition 8.3.3 image (stubs-gen/gen_vetted.py) and each is classified. Loops over children and truth or identity tests of components are followed instead of reading everything. -
SCR102: removing an import is safe only with positive evidence that it succeeds. Java classes must be measured to exist in the image (public classes of the vetted packages), and
from m import nneedsnto be a measured attribute ofm; removingfrom java.util import Missingchanged whether a caller'sexceptran. -
SCR102 no longer reports a library module's import that another module reads through the module object:
getattr(a.b, "json")andhasattr(...)with a literal name are uses ofa.b.json, and a module used as a value (a default argument, a container,getGlobals(),sys.modules["a.b"], also reached asre.sys.modules) may have any of its names read. -
REF009 reports resource manifests and data files outside every project and gateway configuration collection (a
config/resources/coretree withoutconfig-mode.json): Ignition does not load them (verified on 8.3.3; acoredirectory without the manifest keeps the gateway from starting), and iglint does not read their tag event scripts. -
Malformed view parts that may hold scripts (a component,
events,scripts,propConfig, a custom method without a string name, a script that is not a string) are unknown code for the closed world; malformed tag folders too. -
SCR112 reports literal arguments of the wrong type: numeric conversions and
*with strings, complex numbers, containers orNone('%d' % 'ab'),%cout of range, and presentation types the literal does not support instr.format('{:d}'.format('a'),'{:s}'.format(1), any type butsafter!ror!s). Checked against 3,000 generated cases on Jython: no finding on code that formats. -
Code that runs from expressions and run-time strings counts for the closed world.
runScriptwith a computed script, or with code that is not vetted or does not parse, is unknown code; literal code is analysed like a script and a literal dotted name is a library reference. Resources iglint does not read (reports, alarm pipelines, SFC charts, Vision, transaction groups, event streams, legacy and unknown resources) andrunScripttext outside modelled expressions are unknown code. An audit of everysystem.*function marks those that store or run code from run-time values (system.tag.configure,importTags,system.util.execute,system.project.requestScan, ...) as not vetted, andsystem.tagwrites as vetted only with value paths. -
The machine-measured Java reflection table now covers every type a script can hold: a transitive fixed point over the public members of
Object,Throwable,Thread, class proxies and reflected functions, the vetted classes and packages, the typessystem.*returns in every scope and the objects Ignition hands to scripts (Perspective wrappers and events, datasets, qualified values, loggers, tag, alarm, WebDev, Vision and SFC objects), through return, field, generic, exception and callback types, stopping at reflection and interpreter types (ScriptManager,PySystemState, frames, modules). It grows from 43 to 145 names (module,argslist,getColumnTypes,getScriptManager,type, ...), each with the member it was found on. Jython's lookups by name (invoke,__findattr__) are reflective accessors. String literals are scanned for these names and the introspection attributes only in sink positions (accessor names, literal format strings, code forevalandexec); a sink with any other argument, andvars(o), is an escape. Literals that are data ({'type': 'popup'}) no longer hide findings. -
The vetted-code guard resolves names through the semantic model's scopes, flow-insensitively, instead of in the order its walk met the imports. A function defined before
import rethat readsre.sys(and from thereeval) was not seen, so SCR102 removed an import that code read by name and VIEW015 removed a property; now every binding of a name in the scope it resolves to counts, a single unvetted one makes the use unknown, and names the model cannot resolve fail closed. -
SCR002 reports a
yieldinside a list comprehension, or in the first iterable of a set or dict comprehension: Jython's compiler crashes on it (java.lang.VerifyError). The exact crashing and compiling forms were mapped against the compile oracle. -
The vetted-code guard no longer trusts hand lists for two routes that defeated them. Java reflection reached through a bean property (
x.class,x.classLoader,x.methods) and vetted modules that hand outsys(aPySystemStatein Jython:re.sys,calendar.sys) are now caught by tables measured inside the Ignition image (stubs-gen/gen_vetted.py,stubs-gen/run.sh --vetted); subclassing a Java class is unknown code too. -
Safety scans decode a
strliteral that is not valid UTF-8 as Latin-1 (Jython's byte-to-char mapping) instead of reading it as empty, so a'{0.f_locals}\xff'format string orgetChildargument can no longer slip a safe fix through. -
Closed world: a script that reads a frame or the interpreter, anywhere in the shared-runtime workspace, now blocks the safe fixes of SCR102 (including function-local imports), VIEW015 and NAME001, not only when it is in the same resource.
-
--fixreaches a fixpoint in one run: a safe fix rejected only in combination with another is retried in a later pass, so removing unused imports whose joint deletion would promote a docstring no longer needs a second run.--fixreports a clear error if fixes do not converge. -
Safe fixes now assume only vetted code (
docs/internal/fix-safety.md, "Threat model"), instead of a list of dynamic routes that six review rounds each defeated. A script or library module keeps the safe fixes of SCR102, VIEW015 and NAME001 only when every module, Java class, builtin and module attribute it uses is on a default-deny allowlist (checked on Jython: none can evaluate code, read a frame or look up names from data), and every library module it imports or calls is vetted too.cPickle(whosec__builtin__opcodes callevalorglobals()in the caller's frame),warnings,logging,traceback,org.python.*,com.inductiveautomation.*,java.lang.Thread,Throwable,Classand anything else not listed make those fixes unsafe; findings are unchanged. Library-module imports also need every resource of the project to be vetted. -
Safe fixes never move a line: SCR102 and SCR123 empty the line of a deleted statement or comment and keep it (the line-removing fix is now the unsafe one). Line numbers read through
warnings,loggingrecords, tracebacks or Java stack traces no longer change under--fix. -
A central safety net drops any safe fix that would move a line of a script, change whether a script compiles (in either direction: removing
import osbefore a misplacedfrom __future__ import divisionmade it valid), change a docstring, the__future__imports, the scopes or a generator, or what any remaining name resolves to (removingimport math as lenafterprint len(x)in a function turned anUnboundLocalErrorinto a builtin call). It covers.pyfiles and every script embedded in view and resource JSON (two deletions in a view script no longer promote a string to a docstring). -
SCR102 no longer offers a safe fix for an import that is the only binding of a name still read in its scope, or that comes before a
from __future__import. -
Frame detection treats reflective accessors used under another name or as values (
get = getattr,map(getattr, ...),operator.attrgetteras a value),getattr(o, 'class'), format strings that are not literals (fmt.format(o)reads the attributes the string names),__subclasses__,__mro__,__bases__and Java class loaders as frame routes, besides introspection attributes named in source or strings, reflective accessors with computed names and the interpreter and reflection APIs. -
SCR002 reports a recursive construct once, not once per nesting level.
-
Language server quick fixes go through the same fix function as fix-all and
--fix: a fix that would make the file worse, or a safe fix that fails the safety net, is no longer offered. -
VIEW015 no longer removes properties that a library function reads into a local it never uses (
x = c.custom.foo, also insidetry), calls (f = c.custom.foo; f()) or reads through an alias of the container (cu = c.custom; x = cu.foo); that a script or library function reads through a method of the property wrapper used as a value (map(self.custom.get, ['foo']),g = c.custom.get; g('foo'),invokeAsynchronous(c.custom.keys)), through a page or session, an unknown member of a component or a component method used as a value; that__builtin__.eval,from __builtin__ import locals as lor a library variable holdingevalread through the frame; or that a library function reads after being replaced through an alias (alias = handle; alias.func_code = ..., a helper that receives it),importlib.import_module,func_globalsor__globals__. -
SCR102 and the other unused-name rules no longer remove imports or names that a frame builtin reached under another name can read (
from __builtin__ import locals as capture,e = eval,__builtin__.eval,sys._getframe). -
Safe fixes of NAME001, SCR102 and SCR123 become unsafe when a script may read names or frames dynamically:
evalorlocalsunder another name (also a library variable holding one, checked through the project index), calls of unnamed values (factory()(),handlers[k]()), or, for SCR123, a deleted line in a script that reads frames. SCR102 keeps module imports when the module reads its globals with a computedgetattr. VIEW019 and BIND unresolved property references treat such names as writing anything. NAME001 and VIEW019 now run in the project phase. -
SCR002 no longer reports invalid assignment targets in unreachable code that Jython generates but never runs into (after
break, atrywhose body and handlers or whosefinallydo not complete, a loop whoseelsedoes not complete). -
SCR112 reports
%cwith a string literal that is not one character ('%c' % 'ab') and checks format strings concatenated with+(('%s' + '%s') % (1,)). -
Deleting a statement (SCR102 import removal and other deleting fixes) no longer changes a docstring: removing the first statement of a module, class or function body would promote a following bare string to the body's
__doc__. The shared deletion helper keeps apass, and a post-fix safety net drops any.pyfix that changes the set of docstrings, covering several deletions that together expose a later string. -
The command line
--selectnow wins over[[overrides]]as the README promises: it replaces the selection everywhere, so an override'sselect/extend-selectcan no longer re-add a rule the command line left out, and--ignoreremoves a rule an override selects. This holds in ign-lint compatibility mode (theview.jsonscope overrides) too. -
Library-name detection in script accesses now resolves every name through its real Python 2.7 scope (the semantic model), so a name bound only in a nested function, class or comprehension no longer hides a library reference of the same spelling in the enclosing scope. Before,
def f():with a nesteddef dummy(lib): passmadelib.util.ev(...)invisible, so SCR102 removed an import and VIEW015 removed a property that the call reached. Module-level names, imports and injected library packages stay library-name candidates, so the module's own functions are still followed. -
The rule catalogue (
docs/internal/rules.md) Fix column is kept in step with the registry by a test:SCR110,SCR115,VIEW012andVIEW013were labelled wrongly (safe or none) although their fixes are unsafe. -
VIEW015no longer trusts the body of a library function that may be replaced at run time, so its safe fix no longer removes properties the function that actually runs reads. A module's top-level name is no longer a function (for every rule) when the module may rebind it:exec(also inside a function, where the code may declare a nameglobal),eval,execfile, writes toglobals()orsys.modules[__name__], a laterfrom x import *, or a replacedfunc_code/func_defaults. The project index records library names that scripts assign, delete orsetattr(also through aliases,__dict__,sys.modules['pkg.mod']and literalexec/evalcode):VIEW015andREF003do not use the definitions of such names, andREF002does not report a name another script creates.VIEW015also treats functions of modules used as values (or reached throughsys.modulesor code run from strings) as unknown. -
Literal code run with
exec,evalorcompilecounts as references: a library name used only there is no longer unused forSCR102(whose safe fix removed the import) andREF012.execfileand__import__with a literal name make the modules they reach escape. -
SCR002counts only the code Jython generates: nothing afterreturn,raise,continueor anif/elsewhose branches all end that way (not for the module's own statement list). A function with 12,000 deadpasslines, or 40,000 dead string literals, is no longer reported. Errors of Jython's code generator ('break' outside loop,'return' outside functionin a class body,import *in a function, a bareexcept:that is not last, ...) are no longer reported in dead code, which Jython compiles. -
VIEW015no longer follows a library function's parameter stored in a name the function declaresglobalas a local alias: the store is an escape, so the safe fix no longer removes a property that another function of the module reads later through the global. A**kwargsor*argsparameter named like a library package is no longer taken for the package, and code that reaches stack frames (sys._getframe(),inspect.currentframe(),.f_locals) counts as reading everything, in library functions and in view scripts. -
SCR102no longer reports a top-level import of a library module that another resource star-imports (from pkg.mod import *binds it there): every name not starting with_, or only the names of__all__when the module assigns it unconditionally, counts as used. -
SCR103's fix keeps the value of a list comprehension (removes onlyx =): Python 2 leaks its variables, so removing the statement broke code that read them later. Backticks count as a call ofrepr, forSCR103andSCR114(which no longer reports backtick or list comprehension statements). -
--diffoutput applies with GNUpatch -p1when a path holds spaces (views/My View/...): the file name is followed by a tab, asdiff -uwrites it. Paths with",\or control characters are quoted like git does. Bothpatchandgit applyaccept the result. -
REF009reports a Python file that is not the resource's script once, on the file, instead of a second time onresource.jsonas an unlisted file (listing it would not make the gateway load it). -
SCR112reports.formatcalls Jython always fails on that it missed: a negative field index ('{-1}'.format(1, 2),ArrayIndexOutOfBoundsException), a format spec width or precision too large for a Javaint('{0:2147483648}'.format(1),ValueError) for literal arguments and!r/!sfields, and missing arguments when*argsor**kwargsare literals ('{0}{1}'.format(*(1,)),'{a}'.format(1, **{})). -
SCR112treats a.formatfield too large for a Javaint({2147483648}, also{2147483648.x}and in nested specs) as the keyword it is in Jython: no finding when**kwargsmay supply it. -
iglint ign-lint --versionprints the ign-lint version it emulates:ign-lint 0.6.4 (iglint 0.1.0 compatibility mode). -
The oracle scripts name their containers (
iglint-oracle-<pid>-...) and remove them when interrupted. -
VIEW015follows the library functions view scripts handself, the view or a property container to (lib.util.handle(self),handle(self=self)): a function that does not exist reads nothing (the call fails;REF002reports it), and an existing one is followed through its body (param.view.custom.x,param.getSibling('A'), further library calls up to 4 deep). Anything that cannot be followed, and functions a child project overrides, still count as reading everything. The fix is unsafe when something writes the property. -
iglint migratekeeps ign-lint's scope: the rules ign-lint adds to iglint's defaults (above allSCR900) are selected forview.jsonfiles only, through an[[overrides]]entry, instead of for the whole repository. pylint no longer runs on library modules and WebDev scripts after a migration (thousands of extra messages, among themW0311from ign-lint's tab-indented rcfile on space-indented code), and the migratediglint checkis several times faster.iglint checkwith arule_config.jsonuses the same scope. -
Resources without
resource.jsonare no longer silently skipped: a directory that holds the main file of a resource type (view.jsonbelowviews/, a librarycode.py, an event script, a style class, tags, ...) is linted as that resource, andview.jsonand Python files outside every resource directory are linted standalone (with the project's library names).REF009reports that the gateway does not load them; references do not resolve to them and they are not inherited.iglint ign-lintlints such views too. Python files of resource types iglint does not model (and WebDev file content) are named in the run summary (note: ...: not linted: <reason>) instead of being counted as checked. -
Safe fixes no longer change behaviour (audit in
docs/internal/fix-safety.md):SCR102leaves imports in class bodies alone and its fix is safe only for imports without side effects (JDK classes, verified standard modules);SCR115resolves the handler name like Python and its fix is unsafe;NAME001renames are unsafe when a view script hands a component to code outside the view;SCR110,VIEW012, andVIEW013with a stored value, have unsafe fixes;SCR123is unsafe when the comment holds more than the suppression. -
SCR103's fix removes the whole assignment when the value calls nothing, instead of leaving a pointless statement. -
SCR112follows Jython's format grammar: one length modifier at most (%llsfails),*values of%%are consumed, hugestr.formatindexes are keywords (no overflow), signed indexes,Missing ']'and recursion errors. -
SCR108foldsTrue,FalseandNoneonly where they are builtins. -
SCR900passes every argument of thepylintcommand to pylint, also when forking (quoted words are supported), andcategory-mappingwins over the rule severity for WebDev scripts too. -
SCR204names documented replacements Ignition does not name itself (system.tag.readBlockingforsystem.tag.read, ...). -
Deeply nested or very long scripts no longer abort the run with a stack overflow (
elifchains,a.b.c...,x[0][0]...,1 + 1 + ..., prefix operators, in modules and in view scripts, also through the language server). Long chains are parsed with loops and freed without recursion, and linting runs on threads with 256 MiB stacks (reserved, not committed). A statement deeper than 16,384 tree levels is reported as SCR001 "expression nested too deeply" (Ignition's Jython fails much earlier on such code). Expressions may now nest 2,000 levels instead of 100, which is more than Jython accepts on a gateway thread. -
JSON nested deeper than 512 levels is no longer reported as invalid (
VIEW001"nesting too deep"): Gson reads any depth. iglint parses, walks and frees JSON of any depth without recursion and analyses files nested up to 8,192 levels;VIEW001reports deeper ones, which Perspective cannot open (its view loader and property trees overflow the stack at 1,200 to 3,300 levels).VIEW006measures deepcustomdata in linear memory. -
iglint ign-lintno longer rejects views nested more than 128 levels deep as invalid JSON; it lints them likeiglint check(ign-lint itself crashes from about 500 levels). -
SCR002reports scripts Jython cannot load because they are too large: first two lines of 100,000 characters or more (Mark invalid; exact), bodies with more than 10,922 statement lines (Module or method too large) and modules with more than 32,767 distinct string literals (Class too large). Thresholds indocs/internal/ignition-format.md. -
The text output no longer slows down quadratically with many findings on one very long line (a script in a view is one JSON line): 100,000 findings took 74 s, now 0.3 s.
-
iglint ign-lintprints ign-lint node paths for every finding, including BIND005 findings in query binding parameters and other binding expressions. -
\N{...}escapes in unicode literals resolve every name IA's Jython 2.7.3 accepts (its Unicode 9.0 table, with its lookup quirks); unknown names are reported as SCR001, as Jython rejects them. -
iglint ign-lintgroups pylint messages by category (Pylint - Convention (C):) like ign-lint. -
pylint messages that a native rule also reports (same place and name) are shown once.
-
SARIF artifact URIs are percent-encoded (RFC 3986, per path segment) in results, related locations and fixes; files outside the working directory get absolute
file:URIs instead of a path relative to%SRCROOT%. -
Azure Pipelines messages no longer show
%3B/%5D: only%,\rand\nare escaped in the message,;and]only in properties. -
Fixes that delete statements (
SCR102,SCR103) no longer empty a block when several of them remove every statement of it: the last one becomespass. -
Fixing never makes a file worse: when the fixes of a pass make a script or expression stop parsing or compiling, empty a Perspective script or break the JSON, they are applied one at a time and the offending ones are dropped and reported (
warning: ...: fix not applied). -
SCR102no longer reports top-level imports of library modules that other resources use aspkg.mod.name, and its fix is unsafe when such uses cannot be ruled out. -
SCR101no longer reports names created byexec code in globals()(and the other qualifiedexecforms that write the module or the function's locals), bysetattr(sys.modules[__name__], ...),sys.modules[__name__].x = ...or by writes to the builtins. -
SCR108compares keys with Python 2.7 equality:Trueand1,0jand0, equal tuples,1e16and10000000000000000are duplicates. -
except ValueError, TypeError:is reported once, bySCR115;SCR103andSCR117no longer report the same name. -
iglint ign-lint: the timestamps of the--timing-outputreport and of the aggregated--results-outputsummary are in local time, like ign-lint's. -
All checks passed!(andiglint ign-lint's "No style inconsistencies found!") is no longer printed when the run had errors. -
Files that are not valid UTF-8 are linted instead of failing the run: the new rule
VIEW007 invalid-utf8reports the invalid bytes (Ignition replaces them with U+FFFD), and such files get no fixes.
Copied from the repository when the site is built. Source: CHANGELOG.md.