Skip to content

Commit 7860b66

Browse files
ZviBaratzclaude
andcommitted
fix(ego-lint): reduce Just Perfection false positives
Field test #10 on Just Perfection (most popular GNOME extension) revealed 6 false positives and a duplicate-detection bug. Fixes: - License check: add src/ parent-dir fallback (FAIL → PASS) - UUID-dir match: skip when dirname is "src" (src/ layout) - isLocked guard: sessionMode.isLocked is always a guard pattern, not lock screen functionality — exempt from impossible-state and session-modes-consistency checks - Prototype-override: deduplicate by (file, prototype) to avoid counting both override and restore - Comment-density: raise threshold from 40% to 50% to accommodate well-documented hand-written code (JSDoc-heavy) Results: FAILs 3→2, WARNs 414→408 (remaining 400 R-SLOP addressed by PR #23) Closes #26, closes #27, closes #28 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 900b133 commit 7860b66

File tree

17 files changed

+264
-11
lines changed

17 files changed

+264
-11
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Field Test #10: Just Perfection
2+
3+
## Pre-flight
4+
5+
- **Extension**: Just Perfection (`just-perfection-desktop@just-perfection`)
6+
- **Source**: https://github.com/jrahmatzadeh/just-perfection
7+
- **GNOME versions**: 45, 46, 47, 48, 49, 50
8+
- **File count**: 7 JS files, 2 CSS files; ~5,300 JS lines (Manager.js alone is 1,636 non-blank)
9+
- **License**: GPL-3.0-only (in repo root, not in `src/`)
10+
- **Why this extension**: Most popular GNOME extension; exercises connectObject/disconnectObject signal management (67 signals), prototype backup/restore via `#originals` map, `src/` directory layout, GNOME 50 support (widest version range), CSS-heavy (23.7%), private field usage (`#timeoutIds`, `#originals`, `#shellVersion`), 20 Shell.ui imports
11+
12+
## Step 1: ego-lint
13+
14+
```bash
15+
./ego-lint --verbose /tmp/just-perfection/src
16+
```
17+
18+
### Results (before fixes)
19+
20+
| Status | Count |
21+
|--------|-------|
22+
| PASS | 202 |
23+
| FAIL | 3 |
24+
| WARN | 414 |
25+
| SKIP | 12 |
26+
| Exit | 1 |
27+
28+
### Classification of Each FAIL
29+
30+
| Rule | File:Line | TP/FP | Root Cause (if FP) |
31+
|------|-----------|-------|-------------------|
32+
| `license` || **FP** | LICENSE in repo root, not in `src/`; no parent-dir fallback for src/ layout |
33+
| `R-VER48-07` | stylesheet.css:167 | TP | `.quick-menu-toggle` renamed to `.quick-toggle-has-menu` in GNOME 48 |
34+
| `metadata/future-shell-version` || TP | GNOME 50 > CURRENT_STABLE (49) |
35+
36+
### Classification of Key WARNs
37+
38+
| Rule | Count | Classification | Notes |
39+
|------|-------|---------------|-------|
40+
| R-SLOP-01 (JSDoc @param) | 129 | **FP** | Provenance score=3 but threshold was >=4; PR #23 fixes threshold to >=3 |
41+
| R-SLOP-02 (JSDoc @returns) | 271 | **FP** | Same root cause as R-SLOP-01 |
42+
| `lifecycle/prototype-override` | 5 (3 unique) | TP + **bug** | Advisory is valid (prototypes restored via close() chain). Regex catches both override AND restore → duplicates |
43+
| `metadata/uuid-matches-dir` | 1 | **FP** | Dir is `src`, not UUID; meaningless for src/ layout |
44+
| `metadata/session-modes-consistency` | 1 | **FP** | `sessionMode.isLocked` used as guard — not lock screen functionality |
45+
| `quality/impossible-state` | 1 | **FP** | Same isLocked guard pattern |
46+
| `quality/comment-density` | 1 | **FP** | SupportNotifier.js has 40% comments from thorough hand-written JSDoc |
47+
| `metadata/deprecated-version` | 1 | TP | `version` field ignored by EGO for GNOME 45+ |
48+
| `quality/file-complexity` | 1 | TP | Manager.js 1,636 non-blank lines |
49+
| `quality/private-api` | 1 | TP | statusArea access (legitimate for a UI tweaker) |
50+
| `disclosure/private-api` | 1 | TP | Private API not disclosed in metadata description |
51+
52+
## Fixes Implemented
53+
54+
### False Positives Fixed
55+
56+
| Rule/Check | Root Cause | Fix |
57+
|------------|-----------|-----|
58+
| `license` (FAIL) | No parent-dir fallback for src/ layout | `ego-lint.sh`: check parent dir when `basename == "src"` |
59+
| `metadata/uuid-matches-dir` | Dir is `src` for src/ layout | `check-metadata.py`: skip when `dir_name == "src"` |
60+
| `session-modes-consistency` | `isLocked` is always a guard | `check-metadata.py`: narrow regex to `sessionMode.currentMode` only |
61+
| `quality/impossible-state` | `isLocked` is always a guard | `check-quality.py`: remove `isLocked` from check, keep `currentMode` comparisons |
62+
| `quality/comment-density` | 40% threshold too aggressive | `check-quality.py`: raise threshold from 40% to 50% |
63+
| `lifecycle/prototype-override` (dupes) | Regex matches override + restore | `check-lifecycle.py`: deduplicate by `(file, prototype)` key |
64+
65+
### Test Fixtures Added
66+
67+
| Fixture | What It Tests |
68+
|---------|--------------|
69+
| `src-license-fallback@test` | License found in parent dir for src/ layout; UUID-dir skip |
70+
| `islocked-guard@test` | `sessionMode.isLocked` guard not flagged as impossible-state or session-modes inconsistency |
71+
| `proto-override-dedup@test` | Each prototype warned only once despite override + restore pattern |
72+
73+
### Existing Fixture Updated
74+
75+
| Fixture | Change | Reason |
76+
|---------|--------|--------|
77+
| `ai-slop@test` | `sessionMode.isLocked``sessionMode.currentMode !== 'unlock-dialog'` | `isLocked` no longer triggers impossible-state (by design) |
78+
79+
## Results (after fixes)
80+
81+
| Status | Before | After | Change |
82+
|--------|--------|-------|--------|
83+
| FAIL | 3 | 2 | -1 (license FP fixed) |
84+
| WARN | 414 | 408 | -6 (5 FPs + 2 dupes fixed) |
85+
86+
Remaining WARNs breakdown:
87+
- R-SLOP-01/02: 400 (addressed by pending PR #23)
88+
- lifecycle/prototype-override: 3 (true positives — advisory)
89+
- Others: 5 (all true positives)
90+
91+
**With PR #23 merged** (provenance threshold >=3): WARNs would drop to ~8, all true positives.
92+
93+
## Regression Verification
94+
95+
```bash
96+
bash tests/run-tests.sh
97+
# Results: 460 passed, 0 failed
98+
```
99+
100+
| Target | Before | After |
101+
|--------|--------|-------|
102+
| Just Perfection | 3 FAIL, 414 WARN | 2 FAIL, 408 WARN |
103+
| Test suite | 452 passed | 460 passed (+8 new assertions) |
104+
105+
## Calibration Lessons Learned
106+
107+
1. **`sessionMode.isLocked` is always a guard**: Reading lock state to decide behavior is defensive coding, not lock screen functionality. Only `currentMode === 'unlock-dialog'` comparisons suggest active lock screen mode usage.
108+
109+
2. **`src/` layout needs holistic support**: License check and UUID-dir match didn't handle src/ layout. Multiple checks already had src/ fallbacks (CSS, prefs, metadata), but these two were missed. Consider auditing all checks for src/ layout awareness.
110+
111+
3. **Prototype override + restore creates duplicates**: When an extension stores originals and restores them (Just Perfection's `#originals` map pattern), the same regex catches both the override and the restore. Deduplication by `(file, prototype_name)` is essential.
112+
113+
4. **Comment-density threshold interacts with provenance**: Well-documented hand-written code (provenance=3) naturally has higher comment density (40%+) from thorough JSDoc. The 40% threshold was too aggressive; 50% avoids FPs while still catching AI slop (typically 55-70%).
114+
115+
5. **Just Perfection validates PR #23**: Provenance score=3 with 400 JSDoc warnings confirms the threshold change from >=4 to >=3 is needed.
116+
117+
## Issues Created
118+
119+
- [#26](https://github.com/ZviBaratz/gnome-extension-reviewer/issues/26): false-positive: license check fails on src/ layout extensions
120+
- [#27](https://github.com/ZviBaratz/gnome-extension-reviewer/issues/27): false-positive: isLocked guard flagged as impossible-state and session-modes inconsistency
121+
- [#28](https://github.com/ZviBaratz/gnome-extension-reviewer/issues/28): bug: prototype-override check emits duplicate warnings

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,15 +374,23 @@ def check_injection_manager(ext_dir):
374374

375375
# WS1-D: Detect direct prototype overrides
376376
prototype_overrides = []
377+
seen_overrides = set()
377378
for filepath in js_files:
378379
content = strip_comments(read_file(filepath))
379380
rel = os.path.relpath(filepath, ext_dir)
380381
# SomeClass.prototype.methodName = ...
381382
for m in re.finditer(r'(\w+\.prototype\.\w+)\s*=', content):
382-
prototype_overrides.append((rel, m.group(1)))
383+
key = (rel, m.group(1))
384+
if key not in seen_overrides:
385+
seen_overrides.add(key)
386+
prototype_overrides.append(key)
383387
# Object.assign(SomeClass.prototype, ...)
384388
for m in re.finditer(r'Object\.assign\s*\(\s*(\w+\.prototype)', content):
385-
prototype_overrides.append((rel, f"Object.assign({m.group(1)}, ...)"))
389+
label = f"Object.assign({m.group(1)}, ...)"
390+
key = (rel, label)
391+
if key not in seen_overrides:
392+
seen_overrides.add(key)
393+
prototype_overrides.append(key)
386394

387395
if prototype_overrides:
388396
# Check if disable() restores prototypes

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,11 @@ def main():
173173
result("FAIL", "metadata/uuid-format", f"UUID contains invalid characters: {uuid}")
174174

175175
# UUID matches directory name (advisory — cloned repos often differ)
176+
# Skip for src/ layout — dirname is "src", not the UUID
176177
if uuid == dir_name:
177178
result("PASS", "metadata/uuid-matches-dir", f"UUID matches directory name")
179+
elif dir_name == "src":
180+
result("PASS", "metadata/uuid-matches-dir", f"UUID check skipped for src/ layout")
178181
else:
179182
result("WARN", "metadata/uuid-matches-dir", f"UUID '{uuid}' does not match directory '{dir_name}' — must match when installed")
180183

@@ -354,7 +357,9 @@ def check_session_modes_consistency(meta, ext_dir):
354357
"session-modes includes 'unlock-dialog'")
355358
return
356359

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

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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")

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,17 @@ for candidate in LICENSE COPYING LICENSE.rst LICENSE.md LICENSE.txt COPYING.rst
205205
fi
206206
done
207207

208+
# src/ layout fallback: check parent directory for license
209+
if [[ -z "$license_file" && "$(basename "$EXT_DIR")" == "src" ]]; then
210+
parent_dir="$(dirname "$EXT_DIR")"
211+
for candidate in LICENSE COPYING LICENSE.rst LICENSE.md LICENSE.txt COPYING.rst COPYING.md COPYING.txt; do
212+
if [[ -f "$parent_dir/$candidate" ]]; then
213+
license_file="$parent_dir/$candidate"
214+
break
215+
fi
216+
done
217+
fi
218+
208219
if [[ -n "$license_file" ]]; then
209220
head_content=$(head -5 "$license_file" 2>/dev/null || true)
210221
if echo "$head_content" | grep -qiE '(GPL|LGPL|MIT|BSD|Apache|MPL|ISC|Artistic)'; then
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Field test #10: Just Perfection calibration fixes
2+
3+
# --- src-license-fallback (LICENSE in parent dir of src/) ---
4+
echo "=== src-license-fallback ==="
5+
run_lint "src-license-fallback@test/src"
6+
assert_exit_code "exits with 0 (license found in parent dir)" 0
7+
assert_output_contains "license found via parent fallback" "\[PASS\].*license"
8+
assert_output_contains "UUID-dir skipped for src/ layout" "\[PASS\].*metadata/uuid-matches-dir.*src/ layout"
9+
echo ""
10+
11+
# --- islocked-guard (sessionMode.isLocked as guard, not lock screen) ---
12+
echo "=== islocked-guard ==="
13+
run_lint "islocked-guard@test"
14+
assert_exit_code "exits with 0" 0
15+
assert_output_not_contains "no impossible-state for isLocked guard" "\[WARN\].*quality/impossible-state"
16+
assert_output_not_contains "no session-modes-consistency for isLocked" "\[WARN\].*session-modes-consistency"
17+
echo ""
18+
19+
# --- proto-override-dedup (each prototype warned only once) ---
20+
echo "=== proto-override-dedup ==="
21+
run_lint "proto-override-dedup@test"
22+
assert_output_contains "prototype override detected" "\[WARN\].*lifecycle/prototype-override.*BackgroundMenu.prototype.open"
23+
assert_output_contains "search prototype override detected" "\[WARN\].*lifecycle/prototype-override.*SearchController.prototype.startSearch"
24+
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)