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).
This commit is contained in:
Gary Ritchie 2026-04-12 11:41:27 -06:00
parent fdd32c79a6
commit 5db582c69a
No known key found for this signature in database
GPG key ID: A37A622ED6F91DD0
3 changed files with 45 additions and 17 deletions

View file

@ -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",

View file

@ -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;

View file

@ -67,6 +67,16 @@ const PROPERTY_TYPE_LABELS: Record<PropertyType, string> = {
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;