From 5db582c69aa583dd29625fbbca4f66398bb6f614 Mon Sep 17 00:00:00 2001 From: Gary Ritchie Date: Sun, 12 Apr 2026 11:41:27 -0600 Subject: [PATCH] Address Obsidian review lint findings Replace `any` casts with typed App extensions for the undocumented `app.setting` and `app.metadataTypeManager` APIs, remove sentence-case disables by rephrasing the empty-state link and fallback Notice, and drop `async` from `toggleSelection` (which never awaited). Tighten local ESLint config to catch similar issues going forward: enable `reportUnusedDisableDirectives` and re-enable `@typescript-eslint/require-await` (the obsidianmd preset disables it). --- eslint.config.mts | 17 +++++++++++++++++ src/bulk-edit-modal.ts | 27 +++++++++++++++++---------- src/settings.ts | 18 +++++++++++------- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/eslint.config.mts b/eslint.config.mts index 3062c4a..31d64c7 100644 --- a/eslint.config.mts +++ b/eslint.config.mts @@ -5,6 +5,12 @@ import { globalIgnores } from "eslint/config"; export default tseslint.config( { + // Report directive comments that silence rules which never fired. + // Obsidian's plugin reviewer enables this; without it, stale + // eslint-disable comments slip through local lint. + linterOptions: { + reportUnusedDisableDirectives: "error", + }, languageOptions: { globals: { ...globals.browser, @@ -22,6 +28,17 @@ export default tseslint.config( }, }, ...obsidianmd.configs.recommended, + { + // The obsidianmd preset disables require-await, but Obsidian's + // reviewer enforces it. Re-enable so local lint matches. + files: ['**/*.ts', '**/*.tsx'], + plugins: { + '@typescript-eslint': tseslint.plugin, + }, + rules: { + "@typescript-eslint/require-await": "error", + }, + }, globalIgnores([ "node_modules", "dist", diff --git a/src/bulk-edit-modal.ts b/src/bulk-edit-modal.ts index 375b6c1..382518f 100644 --- a/src/bulk-edit-modal.ts +++ b/src/bulk-edit-modal.ts @@ -9,6 +9,17 @@ import { import {withProgress} from "./progress"; import {makeToggleAccessible, updateToggleAriaChecked} from "./accessible-toggle"; +// `app.setting` is an undocumented internal API used by core plugins to +// open the settings pane. Declared here as an optional property so it +// can be accessed type-safely without reaching for `any`; all usages +// are runtime-guarded. +type AppWithSetting = App & { + setting?: { + open?: () => void; + openTabById?: (id: string) => void; + }; +}; + /** * Validates a tag name against Obsidian's naming rules. * Returns null if valid, or a reason string if invalid. @@ -167,7 +178,7 @@ export class BulkEditModal extends Modal { this.fileCheckboxes.set(file, checkbox); row.createEl("span", {text: file.path, cls: "bulk-properties-file-path"}); checkbox.addEventListener("change", () => { - void this.toggleSelection(file, checkbox); + this.toggleSelection(file, checkbox); }); } @@ -190,22 +201,18 @@ export class BulkEditModal extends Modal { if (!hasEditableProperties) { const p = contentEl.createEl("p"); - p.appendText("No properties configured. Add properties in the "); - // eslint-disable-next-line obsidianmd/ui/sentence-case -- mid-sentence text - const link = p.createEl("a", {text: "plugin settings", href: "#"}); + p.appendText("No properties configured. "); + const link = p.createEl("a", {text: "Open settings to configure properties", href: "#"}); link.addEventListener("click", (e) => { e.preventDefault(); this.close(); - /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call -- undocumented Obsidian API */ - const setting = (this.app as any).setting; + const {setting} = this.app as AppWithSetting; if (typeof setting?.open === "function" && typeof setting?.openTabById === "function") { setting.open(); setting.openTabById(this.plugin.manifest.id); } else { - // eslint-disable-next-line obsidianmd/ui/sentence-case -- navigation path - new Notice("Open Settings → Community plugins → Bulk Properties to configure properties."); + new Notice("Open the settings pane to configure properties."); } - /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ }); p.appendText("."); } @@ -301,7 +308,7 @@ export class BulkEditModal extends Modal { this.fileSelection.set(file, selected); } - private async toggleSelection(file: TFile, checkbox: HTMLInputElement) { + private toggleSelection(file: TFile, checkbox: HTMLInputElement): void { const desired = checkbox.checked; checkbox.disabled = true; diff --git a/src/settings.ts b/src/settings.ts index 0c4c6b9..7894450 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -67,6 +67,16 @@ const PROPERTY_TYPE_LABELS: Record = { text: "Text", }; +// metadataTypeManager is an undocumented internal API — not in +// obsidian.d.ts. Declared here as an optional property of App so all +// access is type-safe without reaching for `any`. All access is also +// runtime-guarded below. +type AppWithMetadataTypeManager = App & { + metadataTypeManager?: { + getPropertyInfo?: (name: string) => { widget?: string } | undefined; + }; +}; + // Uses Obsidian's undocumented metadataTypeManager to look up the type // assigned to a property in Settings → Properties. Returns null if the // API is unavailable, the property is unknown, or the widget value @@ -79,13 +89,7 @@ function detectPropertyType(app: App, name: string): PropertyType | null { const known = new Set(getAllPropertyNames(app)); if (!known.has(name)) return null; - // metadataTypeManager is an undocumented internal API — not in - // obsidian.d.ts. All access is runtime-guarded; the any cast and - // unsafe member accesses are intentional. - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - const mtm = (app as any).metadataTypeManager as - | { getPropertyInfo?: (name: string) => { widget?: string } | undefined } - | undefined; + const mtm = (app as AppWithMetadataTypeManager).metadataTypeManager; if (!mtm || typeof mtm.getPropertyInfo !== "function") return null; const info = mtm.getPropertyInfo(name); if (!info || typeof info.widget !== "string") return null;