From 8800b98df476ad3b5b1a2ed6cb4ca36ceb4e99e4 Mon Sep 17 00:00:00 2001 From: Richard McCorkle Date: Sun, 17 May 2026 07:48:09 +0200 Subject: [PATCH] fix(lint): carve out window timer calls in prefer-active-doc rule The local prefer-active-doc rule flagged every `window` identifier as preferring `activeWindow`. obsidianmd 0.3.0's new `prefer-window-timers` rule requires the opposite for timer calls (`window.setTimeout` etc.), so the two rules contradicted, leaving 11 unfixable warnings. Add a `window.` carve-out to the local rule, mirroring the same exception in upstream eslint-plugin-obsidianmd's prefer-active-doc. Lint now reports 0 warnings (4 errors remain: intentional brand-name casing left per project decision). Co-Authored-By: Claude Opus 4.7 --- eslint-rules/prefer-active-doc-fixed.mjs | 24 ++++++++++++++++++++++++ eslint.config.mjs | 9 +++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/eslint-rules/prefer-active-doc-fixed.mjs b/eslint-rules/prefer-active-doc-fixed.mjs index 891589d..b637d3e 100644 --- a/eslint-rules/prefer-active-doc-fixed.mjs +++ b/eslint-rules/prefer-active-doc-fixed.mjs @@ -9,6 +9,29 @@ const REPLACEMENTS = Object.freeze({ }); const BANNED_GLOBALS = new Set(["global", "globalThis"]); +// Timer functions are the one case where `window` is correct, not `activeWindow`: +// obsidianmd's `prefer-window-timers` rule requires `window.setTimeout()` etc. +// Skip `window.` so this rule does not contradict that one. Mirrors +// the same carve-out in upstream eslint-plugin-obsidianmd's prefer-active-doc. +const WINDOW_TIMER_METHODS = new Set([ + "clearInterval", + "clearTimeout", + "requestAnimationFrame", + "setInterval", + "setTimeout", +]); + +function isWindowTimerCall(node) { + const p = node.parent; + return ( + node.name === "window" && + p?.type === "MemberExpression" && + p.object === node && + p.property.type === "Identifier" && + WINDOW_TIMER_METHODS.has(p.property.name) + ); +} + function findVariable(scope, name) { let current = scope; while (current) { @@ -62,6 +85,7 @@ export default { if (!Object.hasOwn(REPLACEMENTS, node.name)) return; if (isSkippableParent(node)) return; + if (isWindowTimerCall(node)) return; const scope = context.sourceCode.getScope(node); if (findVariable(scope, node.name)?.defs.length) return; diff --git a/eslint.config.mjs b/eslint.config.mjs index 0a6bf03..a58f190 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,10 +4,11 @@ import globals from "globals"; import json from "@eslint/json"; import preferActiveDocFixed from "./eslint-rules/prefer-active-doc-fixed.mjs"; -// Upstream eslint-plugin-obsidianmd v0.2.3 `prefer-active-doc` has a prototype- -// lookup bug (REPLACEMENTS[node.name] walks the prototype chain, so every class -// `constructor` is falsely flagged). Swap the rule implementation in-place on -// the plugin object before it gets registered. +// Swap obsidianmd's `prefer-active-doc` for a local implementation. Upstream's +// rule (as of v0.3.0) only flags `document`; this project also wants +// `window` -> `activeWindow` for popout compatibility. The local rule carves out +// `window.` calls so it does not contradict `prefer-window-timers`. +// Patched in-place on the plugin object before it gets registered. if (obsidianmd.rules) { obsidianmd.rules["prefer-active-doc"] = preferActiveDocFixed; }