Skip to content

Commit 69c6254

Browse files
ZviBaratzclaude
andauthored
fix(ego-lint): exempt isLocked guard from impossible-state and session-modes checks (#31)
## Summary Field test #10 (Just Perfection) revealed that `sessionMode.isLocked` is always a guard pattern — reading lock state to decide behavior, not evidence of lock screen functionality. Extensions like Just Perfection use it defensively to avoid modifying UI when locked. - **impossible-state**: Remove `isLocked` from check, keep `currentMode` comparisons (both `===` and `!==`) - **session-modes-consistency**: Narrow regex to only flag `sessionMode.currentMode` references - **comment-density**: Raise threshold from 40% to 50% — well-documented JSDoc code hits 40% legitimately Closes #27 ## Test plan - [x] New fixture `islocked-guard@test` with `sessionMode.isLocked` guard - [x] Updated `ai-slop@test` to use `currentMode !== 'unlock-dialog'` - [x] 455 tests pass (452 baseline + 3 new) - [ ] Verify no regressions on other field-tested extensions 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f8b55f7 commit 69c6254

File tree

8 files changed

+54
-16
lines changed

8 files changed

+54
-16
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -921,9 +921,9 @@ Heuristic rules that detect code patterns commonly seen in AI-generated or over-
921921
### R-QUAL-02: Impossible state checks
922922
- **Severity**: advisory
923923
- **Checked by**: check-quality.py
924-
- **Rule**: Extension code should not check for states that are impossible given its configuration. For example, checking `Main.sessionMode.isLocked` without declaring `unlock-dialog` in `session-modes`.
925-
- **Rationale**: If the extension does not declare `unlock-dialog` in `session-modes`, it is never active on the lock screen, so checking lock state is dead code. This pattern is common in AI-generated extensions that copy-paste lock-screen handling without understanding when it applies.
926-
- **Fix**: Either add `"unlock-dialog"` to `session-modes` in `metadata.json` (if lock-screen support is intended) or remove the lock-state checking code.
924+
- **Rule**: Extension code should not check for states that are impossible given its configuration. For example, comparing `Main.sessionMode.currentMode` with `'unlock-dialog'` without declaring `unlock-dialog` in `session-modes`. Note: `sessionMode.isLocked` is exempt — reading lock state is always a defensive guard pattern, not evidence of lock screen functionality.
925+
- **Rationale**: If the extension does not declare `unlock-dialog` in `session-modes`, it is never active on the lock screen, so checking `currentMode` against lock-screen values is dead code. This pattern is common in AI-generated extensions that copy-paste lock-screen handling without understanding when it applies.
926+
- **Fix**: Either add `"unlock-dialog"` to `session-modes` in `metadata.json` (if lock-screen support is intended) or remove the `currentMode` comparison code.
927927

928928
### R-QUAL-03: Over-engineered async coordination
929929
- **Severity**: advisory
@@ -1561,7 +1561,7 @@ destroy() {
15611561
### R-QUAL-11: Comment Density
15621562
- **Severity**: advisory
15631563
- **Checked by**: check-quality.py
1564-
- **Rule**: Flags files where >40% of lines (after first 10) are comments.
1564+
- **Rule**: Flags files where >50% of lines (after first 10) are comments.
15651565
- **Rationale**: Excessive comments explaining obvious code is a strong AI slop signal.
15661566
- **Fix**: Remove redundant comments. Only keep comments that explain non-obvious logic or API quirks.
15671567
- **Tested by**: `tests/fixtures/quality-signals@test/`
@@ -1952,7 +1952,7 @@ Rules for APIs removed or changed in specific GNOME Shell versions. These rules
19521952
### metadata/session-modes-consistency: SessionMode usage without declaration
19531953
- **Severity**: advisory
19541954
- **Checked by**: check-metadata.py
1955-
- **Rule**: Warns if the extension code references `Main.sessionMode.currentMode` or `sessionMode.isLocked` but `metadata.json` does not declare `session-modes` with `unlock-dialog`.
1955+
- **Rule**: Warns if the extension code references `Main.sessionMode.currentMode` but `metadata.json` does not declare `session-modes` with `unlock-dialog`. Note: `sessionMode.isLocked` is exempt — reading lock state is a defensive guard, not evidence of lock screen usage.
19561956
- **Rationale**: Code that checks session mode without the proper declaration is either dead code (the extension won't run in lock screen mode) or a misunderstanding of the lifecycle.
19571957
- **Fix**: Either add `"session-modes": ["user", "unlock-dialog"]` to metadata.json, or remove the session mode checks from the code.
19581958

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,9 @@ def check_session_modes_consistency(meta, ext_dir):
354354
"session-modes includes 'unlock-dialog'")
355355
return
356356

357-
session_mode_re = re.compile(r"sessionMode\.(currentMode|isLocked)")
357+
# Only flag currentMode checks — isLocked is always a guard pattern
358+
# (reading lock state to decide behavior, not evidence of lock screen usage)
359+
session_mode_re = re.compile(r"sessionMode\.currentMode")
358360
found = []
359361
for root, _dirs, files in os.walk(ext_dir):
360362
for fname in files:

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
Performs structural analysis that goes beyond simple pattern matching:
77
- Excessive try-catch density
8-
- Impossible state checks (isLocked without lock session-mode)
8+
- Impossible state checks (currentMode unlock-dialog without lock session-mode)
99
- Over-engineered async coordination (_pendingDestroy + _initializing)
1010
- Module-level mutable state
1111
- Empty catch blocks
@@ -114,7 +114,7 @@ def check_try_catch_density(ext_dir, js_files):
114114

115115

116116
def check_impossible_state(ext_dir, js_files):
117-
"""R-QUAL-02: Flag isLocked/unlock-dialog checks without matching session-modes."""
117+
"""R-QUAL-02: Flag currentMode unlock-dialog checks without matching session-modes."""
118118
session_modes = get_session_modes(ext_dir)
119119
# If session-modes absent or ["user"], extension doesn't run on lock screen
120120
has_lock = (isinstance(session_modes, list) and
@@ -130,12 +130,10 @@ def check_impossible_state(ext_dir, js_files):
130130
rel = os.path.relpath(filepath, ext_dir)
131131
with open(filepath, encoding='utf-8', errors='replace') as f:
132132
for lineno, line in enumerate(f, 1):
133-
if re.search(r'sessionMode\.isLocked', line):
134-
result("WARN", "quality/impossible-state",
135-
f"{rel}:{lineno}: checks isLocked but extension "
136-
f"does not run in lock screen")
137-
found = True
138-
elif re.search(r"currentMode\s*===?\s*['\"]unlock-dialog['\"]", line):
133+
# sessionMode.isLocked is always a guard (reading lock state
134+
# to decide behavior) — not evidence of lock screen mode usage.
135+
# Only flag currentMode === 'unlock-dialog' checks.
136+
if re.search(r"currentMode\s*[!=]==?\s*['\"]unlock-dialog['\"]", line):
139137
result("WARN", "quality/impossible-state",
140138
f"{rel}:{lineno}: checks for unlock-dialog but "
141139
f"extension does not declare this session-mode")
@@ -699,7 +697,7 @@ def check_comment_density(ext_dir, js_files):
699697
code_lines += 1
700698

701699
total = comment_lines + code_lines
702-
if total > 0 and comment_lines / total > 0.4:
700+
if total > 0 and comment_lines / total > 0.5:
703701
result("WARN", "quality/comment-density",
704702
f"{rel}: {comment_lines}/{total} lines are comments "
705703
f"({comment_lines * 100 // total}%) — may indicate AI-generated verbose comments")

tests/assertions/islocked-guard.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Field test #10: isLocked guard exemption (closes #27)
2+
3+
# --- islocked-guard (sessionMode.isLocked as guard, not lock screen) ---
4+
echo "=== islocked-guard ==="
5+
run_lint "islocked-guard@test"
6+
assert_exit_code "exits with 0" 0
7+
assert_output_not_contains "no impossible-state for isLocked guard" "\[WARN\].*quality/impossible-state"
8+
assert_output_not_contains "no session-modes-consistency for isLocked" "\[WARN\].*session-modes-consistency"
9+
echo ""

tests/fixtures/ai-slop@test/extension.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default class AiSlopExtension extends Extension {
1515
this._pendingDestroy = false;
1616
this._indicator = null;
1717

18-
if (!Main.sessionMode.isLocked && this._indicator === null)
18+
if (Main.sessionMode.currentMode !== 'unlock-dialog' && this._indicator === null)
1919
this._initAsync();
2020
}
2121

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
export default class TestExtension extends Extension {
5+
enable() {
6+
if (!this._isLocked()) {
7+
this._applySettings();
8+
}
9+
}
10+
11+
disable() {
12+
this._revertSettings();
13+
}
14+
15+
_isLocked() {
16+
return Main.sessionMode.isLocked;
17+
}
18+
19+
_applySettings() {}
20+
_revertSettings() {}
21+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"uuid": "islocked-guard@test",
3+
"name": "Test isLocked Guard",
4+
"description": "Tests that sessionMode.isLocked guard is not flagged",
5+
"shell-version": ["48"],
6+
"url": "https://example.com"
7+
}

0 commit comments

Comments
 (0)