|
25 | 25 | - R-LIFE-20: Bus name ownership without release |
26 | 26 | - R-LIFE-21: GSettings signal leak (bare connect without disconnect) |
27 | 27 | - R-LIFE-22: Stage actor leak (add_child/addChrome without remove) |
| 28 | + - R-LIFE-23: .destroy without parentheses (property access, not method call) |
28 | 29 | - R-LIFE-24: MessageTray source leak (add without destroy) |
29 | 30 | - R-SEC-16: Clipboard + keybinding cross-reference |
30 | 31 | - R-FILE-07: Missing export default class |
@@ -947,6 +948,79 @@ def check_bus_name_lifecycle(ext_dir): |
947 | 948 | # If no bus name ownership, skip silently |
948 | 949 |
|
949 | 950 |
|
| 951 | +def check_destroy_without_call(ext_dir): |
| 952 | + """R-LIFE-23: .destroy without () is property access, not method call.""" |
| 953 | + js_files = find_js_files(ext_dir, exclude_prefs=True) |
| 954 | + if not js_files: |
| 955 | + return |
| 956 | + |
| 957 | + violations = [] |
| 958 | + for filepath in js_files: |
| 959 | + content = strip_comments(read_file(filepath)) |
| 960 | + rel = os.path.relpath(filepath, ext_dir) |
| 961 | + |
| 962 | + for lineno, line in enumerate(content.splitlines(), 1): |
| 963 | + # Match .destroy at word boundary (excludes .destroyAll etc.), not followed by () or ?.() (excludes actual calls) |
| 964 | + for m in re.finditer(r'\.destroy\b(?!\s*(\?\.)?\s*\()', line): |
| 965 | + before = line[:m.start()] |
| 966 | + after = line[m.end():] |
| 967 | + |
| 968 | + # Skip: callback reference in signal connection |
| 969 | + if re.search(r'(?<!dis)connect\w*\s*\(', before): |
| 970 | + continue |
| 971 | + # Skip: Function.prototype reference (.bind/.call/.apply) |
| 972 | + if re.search(r'\.(bind|call|apply)\s*\(', before): |
| 973 | + continue |
| 974 | + if re.search(r'\.(bind|call|apply)\s*\(', after): |
| 975 | + continue |
| 976 | + |
| 977 | + # Skip: property existence check (only when .destroy is inside the condition) |
| 978 | + if re.search(r'\btypeof\b', before): |
| 979 | + continue |
| 980 | + if_match = re.search(r'\bif\s*\(', before) |
| 981 | + if if_match: |
| 982 | + # Check .destroy is inside the condition, not in the body |
| 983 | + after_if_open = before[if_match.end():] |
| 984 | + depth = 1 |
| 985 | + for ch in after_if_open: |
| 986 | + if ch == '(': |
| 987 | + depth += 1 |
| 988 | + elif ch == ')': |
| 989 | + depth -= 1 |
| 990 | + if depth == 0: |
| 991 | + break |
| 992 | + if depth > 0: |
| 993 | + # Condition still open — .destroy is being tested |
| 994 | + continue |
| 995 | + |
| 996 | + # Skip: ternary existence check (e.g., actor.destroy ? actor.destroy() : null) |
| 997 | + after_stripped = after.lstrip() |
| 998 | + if after_stripped.startswith('?') and not after_stripped.startswith('?.'): |
| 999 | + continue |
| 1000 | + |
| 1001 | + # Skip: property assignment (not comparison) |
| 1002 | + if after_stripped.startswith('=') and not after_stripped.startswith('=='): |
| 1003 | + continue |
| 1004 | + |
| 1005 | + # Skip: destructuring |
| 1006 | + if re.search(r'\{\s*destroy\b', line): |
| 1007 | + continue |
| 1008 | + |
| 1009 | + violations.append(f"{rel}:{lineno}") |
| 1010 | + |
| 1011 | + if violations: |
| 1012 | + locs = violations[:5] |
| 1013 | + for loc in locs: |
| 1014 | + result("WARN", "lifecycle/destroy-no-call", |
| 1015 | + f"{loc}: .destroy without () — property access, not method call") |
| 1016 | + if len(violations) > 5: |
| 1017 | + result("WARN", "lifecycle/destroy-no-call", |
| 1018 | + f"(and {len(violations) - 5} more)") |
| 1019 | + else: |
| 1020 | + result("PASS", "lifecycle/destroy-no-call", |
| 1021 | + "All .destroy references include parentheses") |
| 1022 | + |
| 1023 | + |
950 | 1024 | def check_widget_lifecycle(ext_dir): |
951 | 1025 | """Detect widgets created in enable() but not destroyed in disable().""" |
952 | 1026 | ext_file = os.path.join(ext_dir, 'extension.js') |
@@ -1239,6 +1313,7 @@ def main(): |
1239 | 1313 | check_clipboard_network(ext_dir) |
1240 | 1314 | check_soup_session_abort(ext_dir) |
1241 | 1315 | check_destroy_then_null(ext_dir) |
| 1316 | + check_destroy_without_call(ext_dir) |
1242 | 1317 | check_widget_lifecycle(ext_dir) |
1243 | 1318 | check_stage_actor_lifecycle(ext_dir) |
1244 | 1319 | check_message_tray_lifecycle(ext_dir) |
|
0 commit comments