Skip to content

Commit 93f50c9

Browse files
authored
Merge branch 'main' into fix/r-slop-38-identifier-fps
2 parents 19b9528 + 5e86aa6 commit 93f50c9

File tree

6 files changed

+100
-6
lines changed

6 files changed

+100
-6
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,10 +1318,10 @@ Rules for code that runs at module load or constructor time, before `enable()` i
13181318
13191319
- **Severity**: blocking
13201320
- **Checked by**: check-init.py
1321-
- **Rule**: Must not create GObjects from any GI namespace (St, Clutter, Gio, GLib, GObject, Meta, Shell, Pango, Soup, Cogl, Atk, GdkPixbuf) at module scope or in `extension.js` constructors. GObject constructors in helper file constructors are not flagged (runtime-only, instantiated from `enable()`). Shell globals are flagged everywhere. **Exemptions**: `GObject.registerClass()` (class registration, not instantiation), arrow function definitions (lazy evaluation), value types (`GLib.Bytes`, `GLib.Variant`, `GLib.DateTime`, `GLib.TimeZone`, `GLib.Regex`, `GLib.Uri`, `Cogl.Color`, `Clutter.Color`).
1321+
- **Rule**: Must not create GObjects from any GI namespace (St, Clutter, Gio, GLib, GObject, Meta, Shell, Pango, Soup, Cogl, Atk, GdkPixbuf) at module scope or in `extension.js` constructors. GObject constructors in helper class constructors are not flagged — whether in separate files or in `extension.js` alongside the Extension class (runtime-only, instantiated from `enable()`). Constructor checks in `extension.js` only apply to the Extension class (identified by `extends Extension` or `export default class`). Shell globals are flagged everywhere. **Exemptions**: `GObject.registerClass()` (class registration, not instantiation), arrow function definitions (lazy evaluation), value types (`GLib.Bytes`, `GLib.Variant`, `GLib.DateTime`, `GLib.TimeZone`, `GLib.Regex`, `GLib.Uri`, `Cogl.Color`, `Clutter.Color`).
13221322
- **Rationale**: GObjects created before enable() cannot be properly cleaned up; #1 rejection cause
13231323
- **Fix**: Move all GObject creation to enable()
1324-
- **Tested by**: `tests/fixtures/init-modification@test/`, `tests/fixtures/init-helper-constructor@test/`, `tests/fixtures/constructor-lifecycle-expanded@test/`
1324+
- **Tested by**: `tests/fixtures/init-modification@test/`, `tests/fixtures/init-helper-constructor@test/`, `tests/fixtures/constructor-lifecycle-expanded@test/`, `tests/fixtures/init-helper-in-extension@test/`
13251325

13261326
#### Example
13271327

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ def is_skip_line(line):
9494
r'|Cogl\.Color|Clutter\.Color)\b'
9595
)
9696

97+
# Extension class identification for scoping constructor checks.
98+
# Only the Extension class constructor runs at instantiation time —
99+
# helper class constructors only run when called from enable().
100+
EXTENSION_CLASS_DEF = re.compile(
101+
r'class\s+\w+\s+extends\s+(?:\w+\.)*Extension\s*\{'
102+
)
103+
# Fallback: export default class without extends
104+
DEFAULT_EXPORT_CLASS_DEF = re.compile(
105+
r'export\s+default\s+class\s+\w+\s*\{'
106+
)
107+
97108

98109
def extract_module_scope_lines(content_lines):
99110
"""Extract lines that are at module scope (outside any class/function body).
@@ -114,12 +125,42 @@ def extract_module_scope_lines(content_lines):
114125
return module_lines
115126

116127

117-
def extract_constructor_lines(content_lines):
128+
def find_extension_class_range(content_lines):
129+
"""Find the line range of the Extension class body.
130+
131+
Returns (start_line, end_line) or None if not found.
132+
Priority: extends Extension > export default class.
133+
"""
134+
for lineno, line in enumerate(content_lines, 1):
135+
if EXTENSION_CLASS_DEF.search(line) or \
136+
DEFAULT_EXPORT_CLASS_DEF.search(line):
137+
depth = line.count('{') - line.count('}')
138+
if depth <= 0 and '{' in line:
139+
# Single-line class body, e.g., `class Foo extends Extension { }` —
140+
# unlikely but handled for completeness
141+
return (lineno, lineno)
142+
start = lineno
143+
for end_lineno, end_line in enumerate(
144+
content_lines[lineno:], lineno + 1):
145+
depth += end_line.count('{') - end_line.count('}')
146+
if depth <= 0:
147+
return (start, end_lineno)
148+
return (start, len(content_lines))
149+
# No match found. This includes multiline class definitions where
150+
# `class` and `{` are on different lines. Returning None causes all
151+
# constructors to be checked (conservative — no false negatives).
152+
return None
153+
154+
155+
def extract_constructor_lines(content_lines, class_range=None):
118156
"""Extract lines inside constructor() method bodies.
119157
120158
Returns a list of (original_lineno, line_text) tuples.
121159
Skips constructors inside GObject.registerClass() class bodies, since those
122160
only run when explicitly instantiated (not at module init time).
161+
162+
If class_range is provided as (start, end), only returns constructors
163+
whose definition line falls within that range.
123164
"""
124165
constructor_lines = []
125166
in_constructor = False
@@ -147,6 +188,9 @@ def extract_constructor_lines(content_lines):
147188
if not in_constructor and re.search(r'\bconstructor\s*\(', line):
148189
if in_register_class:
149190
continue
191+
# Only include constructors within the Extension class range
192+
if class_range and not (class_range[0] <= lineno <= class_range[1]):
193+
continue
150194
in_constructor = True
151195
open_braces = line.count('{')
152196
close_braces = line.count('}')
@@ -248,11 +292,12 @@ def check_init_modifications(ext_dir):
248292
violations.append(f"{rel}:{lineno}")
249293

250294
# Check constructor() lines
251-
# Helper file constructors are runtime-only (only run when
252-
# instantiated from enable()), so only flag in extension.js
295+
# Helper class constructors are runtime-only (only run when
296+
# instantiated from enable()), so only flag the Extension class
253297
is_extension_js = os.path.basename(filepath) == 'extension.js'
254298
if is_extension_js:
255-
ctor_lines = extract_constructor_lines(lines)
299+
ext_range = find_extension_class_range(lines)
300+
ctor_lines = extract_constructor_lines(lines, class_range=ext_range)
256301
ctor_lines = filter_signal_callbacks(ctor_lines)
257302
for lineno, line in ctor_lines:
258303
if is_skip_line(line):
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Tests that helper class constructors in extension.js are not flagged
2+
# while Extension class constructors still are
3+
# Sourced by run-tests.sh
4+
5+
echo "=== init-helper-in-extension ==="
6+
run_lint "init-helper-in-extension@test"
7+
assert_exit_code "exits with 1 (Extension constructor violation)" 1
8+
assert_output_contains "flags Extension constructor Shell global" "\[FAIL\].*init/shell-modification.*extension.js:20"
9+
assert_output_not_contains "no FP on helper class constructor" "init/shell-modification.*extension.js:8"
10+
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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
2+
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
3+
import Gio from 'gi://Gio';
4+
5+
// Helper class — instantiated at runtime from enable()
6+
class Helper {
7+
constructor() {
8+
this._cancellable = new Gio.Cancellable();
9+
}
10+
destroy() {
11+
this._cancellable.cancel();
12+
this._cancellable = null;
13+
}
14+
}
15+
16+
export default class TestExtension extends Extension {
17+
constructor(metadata) {
18+
super(metadata);
19+
// VIOLATION: Shell global in Extension constructor
20+
this._hasOverview = Main.sessionMode.hasOverview;
21+
}
22+
23+
enable() {
24+
this._helper = new Helper();
25+
}
26+
27+
disable() {
28+
this._helper.destroy();
29+
this._helper = null;
30+
}
31+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"uuid": "init-helper-in-extension@test",
3+
"name": "Init Helper In Extension Test",
4+
"description": "Tests that helper class constructors in extension.js are not flagged",
5+
"shell-version": ["48"],
6+
"url": "https://example.com"
7+
}

0 commit comments

Comments
 (0)