Skip to content

Commit 81a8020

Browse files
authored
Merge branch 'main' into fix/tiling-shell-calibration
2 parents 0259065 + 900b133 commit 81a8020

File tree

12 files changed

+129
-4
lines changed

12 files changed

+129
-4
lines changed

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

Lines changed: 2 additions & 2 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

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):
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 ""
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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
2+
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
3+
4+
export default class DestroySignalExtension extends Extension {
5+
enable() {
6+
this._settings = this.getSettings();
7+
this._settingsId = this._settings.connect('changed::key', () => {});
8+
9+
// 'destroy' signal — self-cleaning, no disconnect needed
10+
this._button = new St.Button();
11+
this._button.connect('destroy', () => {
12+
this._button = null;
13+
});
14+
15+
// Another destroy signal on a different widget
16+
this._icon = new St.Icon();
17+
this._icon.connect('destroy', () => {
18+
this._icon = null;
19+
});
20+
}
21+
22+
disable() {
23+
this._settings.disconnect(this._settingsId);
24+
this._settings = null;
25+
this._button?.destroy();
26+
this._icon?.destroy();
27+
}
28+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"uuid": "signal-destroy-connect@test",
3+
"name": "Destroy Signal Connect Test",
4+
"description": "Tests that destroy signal connections are not counted in signal balance",
5+
"shell-version": ["48"],
6+
"url": "https://github.com/test/signal-destroy-connect"
7+
}
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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
2+
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
3+
4+
export default class LocalVariableSignalExtension extends Extension {
5+
enable() {
6+
this._settings = this.getSettings();
7+
this._settingsId = this._settings.connect('changed::key', () => {});
8+
}
9+
10+
_showNotification(text) {
11+
// Ephemeral local variable — signal is garbage collected with it
12+
let notification = new MessageTray.Notification(this._source, text);
13+
notification.connect('activated', () => {
14+
Main.extensionManager.openExtensionPrefs(this.uuid);
15+
});
16+
17+
// Another ephemeral local signal
18+
let dialog = new ModalDialog.ModalDialog();
19+
dialog.connect('opened', () => log('dialog opened'));
20+
}
21+
22+
disable() {
23+
this._settings.disconnect(this._settingsId);
24+
this._settings = null;
25+
}
26+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"uuid": "signal-local-variable@test",
3+
"name": "Local Variable Signal Test",
4+
"description": "Tests that signals on local/ephemeral variables are not counted in signal balance",
5+
"shell-version": ["48"],
6+
"url": "https://github.com/test/signal-local-variable"
7+
}
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

0 commit comments

Comments
 (0)