Skip to content

Commit e46e2df

Browse files
ZviBaratzclaude
andauthored
fix(ego-lint): reduce init-time safety false positives (#21)
## Summary - **Arrow function definitions exempt**: `const fn = () => Main.layoutManager.monitors` at module scope is lazy — the body isn't executed at load time, so it's not an init-time violation - **Constructor checks restricted to extension.js**: Helper file constructors are runtime-only (instantiated from `enable()`), matching the existing GObject constructor restriction Closes #15 ## Test plan - [x] New fixture: `tests/fixtures/init-time-safety@test/` with `extension.js` (true violation + arrow fn) and `helper.js` (constructor with Shell globals) - [x] Assertions verify: FAIL on module-scope Main access, no FP on arrow fn definition, no FP on helper constructor - [x] `bash tests/run-tests.sh` passes (514 assertions) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 900b133 commit e46e2df

File tree

7 files changed

+77
-11
lines changed

7 files changed

+77
-11
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ bash skills/ego-lint/scripts/ego-lint.sh tests/fixtures/<fixture-name>
5656
- `apply-patterns.py` — Tier 1 pattern engine (inline YAML parser, no PyYAML dependency). Supports `guard-pattern` + `guard-window` (sliding `deque` lookback) + `guard-window-forward` (forward peeking), `replacement-pattern`, `exclude-dirs`, version-gating, `fix-min-version` (suppresses fix text when extension min shell-version is below threshold), `skip-comments` (skips matches inside `//` and `/* */` comments)
5757
- `check-quality.py` — Tier 2 heuristic AI slop detection (try-catch density, impossible states, pendulum patterns, empty catches, _destroyed density, mock detection, constructor resources, run_dispose comment, clipboard disclosure, network disclosure, excessive null checks, repeated getSettings, obfuscated names, mixed indentation, excessive logging, code provenance)
5858
- `check-metadata.py` — JSON validity, required fields, UUID format/match, shell-version (with VALID_GNOME_VERSIONS allowlist), session-modes, settings-schema, version-name, donations, gettext-domain consistency
59-
- `check-init.py` — Init-time Shell modification, GObject constructor detection (extension.js only; all GI namespaces, GObject.registerClass exempt, GLib value types exempt), Gio._promisify placement
59+
- `check-init.py` — Init-time Shell modification (module-scope + extension.js constructors only; arrow function definitions exempt), GObject constructor detection (extension.js only; all GI namespaces, GObject.registerClass exempt, GLib value types exempt), Gio._promisify placement
6060
- `check-lifecycle.py` — enable/disable symmetry, signal cleanup (connectSmart/SignalTracker-aware), timeout removal verification, InjectionManager, lock screen signals, selective disable detection, unlock-dialog comment, clipboard+keybinding cross-ref, prototype override detection, pkexec target validation, Soup.Session abort, D-Bus export/unexport, bus name own/unown lifecycle, timeout reassignment, subprocess cancellation, clipboard+network cross-ref, widget lifecycle, settings cleanup
6161
- `check-prefs.py` — Preferences file validation (ExtensionPreferences base class, GTK4/Adwaita patterns, memory leak detection, supports `src/` layout)
6262
- `check-gobject.py` — GObject.registerClass patterns and GTypeName validation

skills/ego-lint/scripts/check-init.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ def is_skip_line(line):
6161
return False
6262

6363

64+
# Arrow function definitions: the body after => is lazy (not executed at
65+
# module scope). Detect `const fn = (...) => expr` patterns.
66+
ARROW_FN_DEF = re.compile(
67+
r'(?:const|let|var)\s+\w+\s*=\s*' # variable assignment
68+
r'(?:\([^)]*\)|\w+)' # parameter list or single param
69+
r'\s*=>' # arrow
70+
)
71+
72+
6473
# Shell globals that should only be accessed inside enable()/disable()
6574
SHELL_GLOBALS = re.compile(
6675
r'\bMain\.(panel|overview|layoutManager|sessionMode|messageTray|wm|'
@@ -183,6 +192,10 @@ def check_init_modifications(ext_dir):
183192
if is_skip_line(line):
184193
continue
185194
if SHELL_GLOBALS.search(line):
195+
# Arrow function definitions are lazy — the body after =>
196+
# is not executed at module scope
197+
if ARROW_FN_DEF.search(line):
198+
continue
186199
violations.append(f"{rel}:{lineno}")
187200
elif GOBJECT_CONSTRUCTORS.search(line):
188201
# GObject.registerClass() returns a class, not an instance
@@ -191,17 +204,18 @@ def check_init_modifications(ext_dir):
191204
violations.append(f"{rel}:{lineno}")
192205

193206
# Check constructor() lines
194-
# GObject constructors in helper file constructors are runtime-only
195-
# (only run when instantiated from enable()), so only flag in extension.js
207+
# Helper file constructors are runtime-only (only run when
208+
# instantiated from enable()), so only flag in extension.js
196209
is_extension_js = os.path.basename(filepath) == 'extension.js'
197-
ctor_lines = extract_constructor_lines(lines)
198-
for lineno, line in ctor_lines:
199-
if is_skip_line(line):
200-
continue
201-
if SHELL_GLOBALS.search(line):
202-
violations.append(f"{rel}:{lineno}")
203-
elif is_extension_js and GOBJECT_CONSTRUCTORS.search(line):
204-
violations.append(f"{rel}:{lineno}")
210+
if is_extension_js:
211+
ctor_lines = extract_constructor_lines(lines)
212+
for lineno, line in ctor_lines:
213+
if is_skip_line(line):
214+
continue
215+
if SHELL_GLOBALS.search(line):
216+
violations.append(f"{rel}:{lineno}")
217+
elif GOBJECT_CONSTRUCTORS.search(line):
218+
violations.append(f"{rel}:{lineno}")
205219

206220
if violations:
207221
for loc in violations:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SPDX-License-Identifier: GPL-2.0-or-later
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
2+
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
3+
4+
// TRUE VIOLATION: module-scope Shell global access
5+
const primaryIdx = Main.layoutManager.primaryIndex;
6+
7+
// OK: arrow function definition is lazy (body not executed at module scope)
8+
const getMonitors = () => Main.layoutManager.monitors;
9+
10+
export default class InitTimeTest extends Extension {
11+
enable() {
12+
// OK: Shell globals inside enable()
13+
Main.panel.addToStatusArea('test', this);
14+
this._monitors = getMonitors();
15+
}
16+
17+
disable() {
18+
this._monitors = null;
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
2+
3+
// Helper class — constructor only runs when instantiated from enable()
4+
export class TilingManager {
5+
constructor(monitorIndex) {
6+
// Shell globals in helper constructor: OK (only called from enable())
7+
this._workArea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
8+
this._monitors = Main.layoutManager.monitors;
9+
}
10+
11+
destroy() {
12+
this._workArea = null;
13+
this._monitors = null;
14+
}
15+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"uuid": "init-time-safety@test",
3+
"name": "Init Time Safety Test",
4+
"description": "Tests init-time Shell modification detection",
5+
"shell-version": ["48"],
6+
"url": "https://example.com"
7+
}

tests/run-tests.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,15 @@ assert_exit_code "exits with 1 (has failures)" 1
191191
assert_output_contains "fails on minified JS" "\[FAIL\].*minified-js"
192192
echo ""
193193

194+
# --- init-time-safety ---
195+
echo "=== init-time-safety ==="
196+
run_lint "init-time-safety@test"
197+
assert_exit_code "exits with 1 (has module-scope violation)" 1
198+
assert_output_contains "fails on module-scope Main access" "\[FAIL\].*init/shell-modification"
199+
assert_output_not_contains "no FP on helper constructor" "init/shell-modification.*helper.js"
200+
assert_output_not_contains "no FP on arrow function definition" "init/shell-modification.*extension.js:8"
201+
echo ""
202+
194203
# --- lifecycle-imbalance ---
195204
echo "=== lifecycle-imbalance ==="
196205
run_lint "lifecycle-imbalance@test"

0 commit comments

Comments
 (0)