Skip to content

Commit cd0ab2a

Browse files
authored
Merge branch 'main' into docs/field-test-reports
2 parents f5e65e6 + 62c5509 commit cd0ab2a

File tree

26 files changed

+264
-21
lines changed

26 files changed

+264
-21
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

rules/patterns.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,8 @@
10571057
category: version-compat
10581058
fix: "Call window.set_maximize_flags(flags) then window.maximize()"
10591059
min-version: 49
1060+
guard-pattern: "get_maximized\\s*\\?"
1061+
guard-window: 1
10601062

10611063
- id: R-VER49-09
10621064
pattern: "\\bAppMenuButton\\b"
@@ -1084,6 +1086,8 @@
10841086
category: version-compat
10851087
min-version: 49
10861088
fix: "Call unmaximize() without the MaximizeFlags parameter"
1089+
guard-pattern: "get_maximized\\s*\\?"
1090+
guard-window: 1
10871091

10881092
# --- GNOME 50 migration (version-gated) ---
10891093
# GNOME 50 removed X11 support and associated APIs.

skills/ego-lint/references/rules-reference.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,10 +1130,10 @@ Rules for extension lifecycle management: enable/disable hooks, signal cleanup,
11301130
### R-LIFE-01: Signal Balance
11311131
- **Severity**: advisory
11321132
- **Checked by**: check-lifecycle.py
1133-
- **Rule**: Detects imbalance between manual `.connect()` and `.disconnect()` calls (threshold: >2 imbalance). Recognizes `connectObject()`, `connectSmart()`, and `SignalTracker`/`SignalManager` as auto-cleanup patterns.
1133+
- **Rule**: Detects imbalance between manual `.connect()` and `.disconnect()` calls (threshold: >2 imbalance). Recognizes `connectObject()`, `connectSmart()`, and `SignalTracker`/`SignalManager` as auto-cleanup patterns. Excludes: non-signal `.connect()` calls (no string-literal first argument), `'destroy'` signal connections (inherently self-cleaning), and signals on local/ephemeral variables (no `this` reference — garbage collected with scope).
11341134
- **Rationale**: Unmatched signal connections are the #1 cause of extension rejections. Leaked signals cause memory leaks and crash loops.
11351135
- **Fix**: For each `.connect()` call, ensure a matching `.disconnect()` in disable/destroy. Consider using `connectObject()` or a custom signal tracker for automatic cleanup.
1136-
- **Tested by**: `tests/fixtures/lifecycle-basic@test/`, `tests/fixtures/signal-smart-connect@test/`
1136+
- **Tested by**: `tests/fixtures/lifecycle-basic@test/`, `tests/fixtures/signal-smart-connect@test/`, `tests/fixtures/signal-non-gobject@test/`, `tests/fixtures/signal-destroy-connect@test/`, `tests/fixtures/signal-local-variable@test/`
11371137

11381138
#### Example
11391139

@@ -2230,7 +2230,8 @@ Rules for APIs removed or changed in specific GNOME Shell versions. These rules
22302230
- **Rule**: `maximize()` lost the `MaximizeFlags` parameter in GNOME 49.
22312231
- **Rationale**: Passing `MaximizeFlags` to `maximize()` will throw in GNOME 49.
22322232
- **Fix**: Call `window.set_maximize_flags(flags)` first, then `window.maximize()`.
2233-
- **Tested by**: `tests/fixtures/gnome49-maximize@test/`
2233+
- **Guard**: Suppressed when `get_maximized ?` appears on the same line (ternary version-compat pattern).
2234+
- **Tested by**: `tests/fixtures/gnome49-compat@test/`
22342235

22352236
### R-VER49-09: AppMenuButton removal
22362237
- **Severity**: blocking
@@ -2556,6 +2557,7 @@ Rules for APIs removed or changed in specific GNOME Shell versions. These rules
25562557
- **Rule**: `MaximizeFlags` parameter was removed from `unmaximize()` in GNOME 49.
25572558
- **Rationale**: The `unmaximize()` method no longer accepts a flags parameter.
25582559
- **Fix**: Call `unmaximize()` without the `MaximizeFlags` parameter.
2560+
- **Guard**: Suppressed when `get_maximized ?` appears on the same line (ternary version-compat pattern).
25592561
25602562
---
25612563

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:

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,13 @@ def check_signal_balance(ext_dir):
123123
connect_objects += 1
124124
elif re.search(r'\.connectSmart\s*\(', line):
125125
smart_connects += 1
126-
elif re.search(r'\.connect\s*\(', line) and not re.search(r'\.disconnect', line):
127-
pure_connects += 1
126+
elif re.search(r"\.connect\s*\(\s*['\"]", line) and not re.search(r'\.disconnect', line):
127+
if re.search(r"\.connect\s*\(\s*['\"]destroy['\"]", line):
128+
pass # 'destroy' signal — inherently self-cleaning
129+
elif not re.search(r'this[._]', line):
130+
pass # local variable signal — inherently scoped
131+
else:
132+
pure_connects += 1
128133
if re.search(r'\.disconnectObject\s*\(', line):
129134
pass # auto-cleanup
130135
elif re.search(r'\.disconnectSmart\s*\(', line):

skills/ego-lint/scripts/ego-lint.sh

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -421,9 +421,12 @@ for f in "$EXT_DIR"/*.js; do
421421
continue
422422
fi
423423

424-
# Check for any line > 500 chars (strong minification signal)
425-
if awk 'length > 500 { found=1; exit } END { exit !found }' "$f" 2>/dev/null; then
426-
minified_files+=" $rel_path (lines > 500 chars)"$'\n'
424+
# Check for lines > 500 chars — need 3+ such lines to flag as minified.
425+
# A single long line (e.g., keyboard constant chain) in an otherwise
426+
# readable file is not minification.
427+
long_line_count=$(awk 'length > 500 { n++ } END { print n+0 }' "$f" 2>/dev/null || true)
428+
if [[ "$long_line_count" -ge 3 ]]; then
429+
minified_files+=" $rel_path ($long_line_count lines > 500 chars)"$'\n'
427430
fi
428431
done
429432
if [[ -d "$EXT_DIR/lib" ]]; then
@@ -433,8 +436,9 @@ if [[ -d "$EXT_DIR/lib" ]]; then
433436
minified_files+=" $rel_path (webpack bundle)"$'\n'
434437
continue
435438
fi
436-
if awk 'length > 500 { found=1; exit } END { exit !found }' "$f" 2>/dev/null; then
437-
minified_files+=" $rel_path (lines > 500 chars)"$'\n'
439+
long_line_count=$(awk 'length > 500 { n++ } END { print n+0 }' "$f" 2>/dev/null || true)
440+
if [[ "$long_line_count" -ge 3 ]]; then
441+
minified_files+=" $rel_path ($long_line_count lines > 500 chars)"$'\n'
438442
fi
439443
done < <(find "$EXT_DIR/lib" -name '*.js' -print0 2>/dev/null)
440444
fi
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# --- signal-non-gobject (Sub-issue A: non-signal .connect() not counted) ---
2+
echo "=== signal-non-gobject ==="
3+
run_lint "signal-non-gobject@test"
4+
assert_exit_code "exits with 0 (no failures)" 0
5+
assert_output_contains "signal balance OK" "\[PASS\].*lifecycle/signal-balance"
6+
assert_output_not_contains "no signal imbalance warning" "\[WARN\].*lifecycle/signal-balance"
7+
echo ""
8+
9+
# --- signal-destroy-connect (Sub-issue B: destroy signal not counted) ---
10+
echo "=== signal-destroy-connect ==="
11+
run_lint "signal-destroy-connect@test"
12+
assert_exit_code "exits with 0 (no failures)" 0
13+
assert_output_contains "signal balance OK" "\[PASS\].*lifecycle/signal-balance"
14+
assert_output_not_contains "no signal imbalance warning" "\[WARN\].*lifecycle/signal-balance"
15+
echo ""
16+
17+
# --- signal-local-variable (Sub-issue C: local-variable signal not counted) ---
18+
echo "=== signal-local-variable ==="
19+
run_lint "signal-local-variable@test"
20+
assert_exit_code "exits with 0 (no failures)" 0
21+
assert_output_contains "signal balance OK" "\[PASS\].*lifecycle/signal-balance"
22+
assert_output_not_contains "no signal imbalance warning" "\[WARN\].*lifecycle/signal-balance"
23+
echo ""

tests/fixtures/gnome49-compat@test/extension.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ export default class Gnome49Test extends Extension {
99
this._tap = new Clutter.TapAction();
1010
}
1111

12+
// Ternary version-compat guard: should NOT trigger R-VER49-08/11
13+
_maximizeCompat(window) {
14+
window.get_maximized ? window.maximize(Meta.MaximizeFlags.BOTH) : window.maximize();
15+
}
16+
17+
_unmaximizeCompat(window) {
18+
window.get_maximized ? window.unmaximize(Meta.MaximizeFlags.BOTH) : window.unmaximize();
19+
}
20+
1221
disable() {
1322
this._click = null;
1423
this._tap = null;
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+
}

0 commit comments

Comments
 (0)