chore(eslint): enable no-unnecessary-type-assertion and no-misused-promises (#2441)

* chore(eslint): enable no-unnecessary-type-assertion and no-misused-promises

Auto-fixed all 40 no-unnecessary-type-assertion violations via `eslint --fix`.

For no-misused-promises, configured `checksVoidReturn` with two relaxations
(documented inline):
- `attributes: false` — `onClick={async () => ...}` is the standard React pattern
- `inheritedMethods: false` — Obsidian's Plugin.onload/onunload are async

Fixed the remaining 10 violations by hand:
- Wrapped async subscribers/listeners with `void` (projectManager, useChatFileDrop,
  main.ts, webViewerServiceSelection)
- Widened ConfirmModal subclass callback types to `() => void | Promise<void>`
- Widened MobileCardDropdownAction.onClick to match parent prop types
- Made SettingsMainV2 handleReset sync (it never awaited anything)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(eslint): move no-misused-promises to TS-only config block

The @typescript-eslint plugin is registered only for .ts/.tsx files via
obsidianmd's recommendedTypeChecked. Enabling the rule in the
JS+TS-shared block caused CI to fail with "could not find plugin
@typescript-eslint" because the plugin isn't loaded for .js/.mjs files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Zero Liu 2026-05-13 21:27:35 -07:00 committed by GitHub
parent 3b8af4d341
commit 8499b85a1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 80 additions and 68 deletions

View file

@ -95,8 +95,6 @@ export default [
// --- Medium: promise / method ergonomics ---
"@typescript-eslint/unbound-method": "off", // 68 violations
"@typescript-eslint/no-misused-promises": "off", // 46 violations
"@typescript-eslint/no-unnecessary-type-assertion": "off", // 40 violations
"@typescript-eslint/no-floating-promises": "off", // 39 violations
// --- Quick wins: small enough to fix and enable in a single PR ---
@ -172,6 +170,14 @@ export default [
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
// checksVoidReturn relaxed for:
// - attributes: async event handlers in JSX (onClick={async () => ...}) are
// the standard React pattern; React already handles them correctly.
// - inheritedMethods: Obsidian's Plugin.onload/onunload are commonly async.
"@typescript-eslint/no-misused-promises": [
"error",
{ checksVoidReturn: { attributes: false, inheritedMethods: false } },
],
// TypeScript handles undefined-identifier detection (and does so cross-realm
// correctly); per typescript-eslint's own guidance, disable no-undef on TS.
"no-undef": "off",

View file

@ -243,7 +243,7 @@ export class ChatOpenRouter extends ChatOpenAI {
role: "tool",
content: msg.content,
tool_call_id: msg.tool_call_id,
} as OpenRouterMessageParam;
};
}
if (msg.additional_kwargs?.function_call) {

View file

@ -101,7 +101,7 @@ export class GitHubCopilotChatModel extends ChatOpenAICompletions {
const provider = GitHubCopilotProvider.getInstance();
// scorecard: streaming requires fetch — cannot use requestUrl
const baseFetch = fetchImplementation ?? (configuration?.fetch as FetchImplementation) ?? fetch;
const baseFetch = fetchImplementation ?? configuration?.fetch ?? fetch;
const authedFetch = GitHubCopilotChatModel.buildAuthedFetch(provider, baseFetch);
super({
@ -115,7 +115,7 @@ export class GitHubCopilotChatModel extends ChatOpenAICompletions {
configuration: {
...(configuration ?? {}),
// Reason: OpenAI SDK appends "/chat/completions" to baseURL automatically
baseURL: (configuration?.baseURL as string) ?? COPILOT_API_BASE,
baseURL: configuration?.baseURL ?? COPILOT_API_BASE,
fetch: authedFetch,
},
});

View file

@ -90,7 +90,7 @@ export class GitHubCopilotResponsesModel extends ChatOpenAI {
const { fetchImplementation, configuration, apiKey, ...rest } = fields;
const provider = GitHubCopilotProvider.getInstance();
const baseFetch = fetchImplementation ?? (configuration?.fetch as FetchImplementation) ?? fetch;
const baseFetch = fetchImplementation ?? configuration?.fetch ?? fetch;
const authedFetch = buildGitHubCopilotAuthedFetch(provider, baseFetch);
super({
@ -100,7 +100,7 @@ export class GitHubCopilotResponsesModel extends ChatOpenAI {
streamUsage: false,
configuration: {
...(configuration ?? {}),
baseURL: (configuration?.baseURL as string) ?? COPILOT_API_BASE,
baseURL: configuration?.baseURL ?? COPILOT_API_BASE,
fetch: authedFetch,
},
});

View file

@ -57,11 +57,11 @@ export default class ProjectManager {
this.loadTracker = ProjectLoadTracker.getInstance(this.app);
// Set up subscriptions
subscribeToModelKeyChange(async () => {
await this.getCurrentChainManager().createChainWithNewModel();
subscribeToModelKeyChange(() => {
void this.getCurrentChainManager().createChainWithNewModel();
});
subscribeToChainTypeChange(async () => {
subscribeToChainTypeChange(() => {
// When switching from other modes to project mode, no need to update the chain.
if (isProjectMode()) {
return;
@ -72,14 +72,14 @@ export default class ProjectManager {
settings.indexVaultToVectorStore === VAULT_VECTOR_STORE_STRATEGY.ON_MODE_SWITCH &&
(getChainType() === ChainType.VAULT_QA_CHAIN ||
getChainType() === ChainType.COPILOT_PLUS_CHAIN);
await this.getCurrentChainManager().createChainWithNewModel({
void this.getCurrentChainManager().createChainWithNewModel({
refreshIndex: shouldAutoIndex,
});
});
// Subscribe to Project changes
subscribeToProjectChange(async (project) => {
await this.switchProject(project);
subscribeToProjectChange((project) => {
void this.switchProject(project);
});
// Subscribe to project cache changes to monitor project modifications

View file

@ -111,7 +111,7 @@ describe("processedPrompt()", () => {
.mockResolvedValueOnce("here is the note content for note0")
.mockResolvedValueOnce("note content for note1");
const { getFileName } = jest.requireMock("@/utils") as any;
const { getFileName } = jest.requireMock("@/utils");
getFileName.mockReturnValueOnce("Variable1 Note").mockReturnValueOnce("Variable2 Note");
const mockNote1 = mockTFile({ path: "path/to/note1.md", basename: "Variable1 Note" });
@ -175,7 +175,7 @@ describe("processedPrompt()", () => {
const selectedText = "";
(getFileContent as jest.Mock).mockResolvedValue("Content of the active note");
const { getFileName } = jest.requireMock("@/utils") as any;
const { getFileName } = jest.requireMock("@/utils");
getFileName.mockReturnValue("Active Note");
const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);
@ -238,7 +238,7 @@ describe("processedPrompt()", () => {
(getNotesFromTags as jest.Mock).mockReturnValue([mockNoteForTag]);
// Mock getFileName to return the basename
const { getFileName } = jest.requireMock("@/utils") as any;
const { getFileName } = jest.requireMock("@/utils");
getFileName.mockReturnValue("Tagged Note");
// Mock getFileContent to return content for the note
@ -270,7 +270,7 @@ describe("processedPrompt()", () => {
(getNotesFromTags as jest.Mock).mockReturnValue([mockNoteForTag1, mockNoteForTag2]);
// Mock getFileName to return the basename
const { getFileName } = jest.requireMock("@/utils") as any;
const { getFileName } = jest.requireMock("@/utils");
getFileName.mockImplementation((file: TFile) => file.basename);
// Mock getFileContent to return content for each note
@ -324,7 +324,7 @@ describe("processedPrompt()", () => {
(extractTemplateNoteFiles as jest.Mock).mockReturnValue([mockNoteFile]);
const { getFileName } = jest.requireMock("@/utils") as any;
const { getFileName } = jest.requireMock("@/utils");
getFileName.mockReturnValue("Test Note");
(getFileContent as jest.Mock).mockResolvedValue("Test note content");
@ -362,7 +362,7 @@ describe("processedPrompt()", () => {
// Only Note1 should be extracted since it's wrapped in {[[]]}
(extractTemplateNoteFiles as jest.Mock).mockReturnValue([mockNote1]);
const { getFileName } = jest.requireMock("@/utils") as any;
const { getFileName } = jest.requireMock("@/utils");
getFileName.mockImplementation((file: TFile) => file.basename);
(getFileContent as jest.Mock).mockImplementation((file: TFile) => {
@ -459,7 +459,7 @@ describe("processedPrompt()", () => {
const selectedText = "";
// Mock getFileName and getFileContent
const { getFileName } = jest.requireMock("@/utils") as any;
const { getFileName } = jest.requireMock("@/utils");
getFileName.mockReturnValue("Active Note");
(getFileContent as jest.Mock).mockResolvedValue("Content of the active note");
@ -511,7 +511,7 @@ describe("processedPrompt()", () => {
// Mock getFileContent for the active note when processed via {}
(getFileContent as jest.Mock).mockResolvedValue("Content of the active note");
const { getFileName } = jest.requireMock("@/utils") as any;
const { getFileName } = jest.requireMock("@/utils");
getFileName.mockReturnValue("Active Note");
const result = await processPrompt(doc.content, selectedText, mockVault, mockActiveNote);

View file

@ -183,7 +183,7 @@ export function AtMentionTypeahead({
return (
<TypeaheadMenuPopover
options={searchResults as any[]}
options={searchResults}
selectedIndex={selectedIndex}
onSelect={handleSelect}
onHighlight={handleHighlight}

View file

@ -63,9 +63,7 @@ jest.mock("obsidian", () => {
};
});
const { __renderMarkdownMock: renderMarkdownMock } = jest.requireMock("obsidian") as {
__renderMarkdownMock: jest.Mock;
};
const { __renderMarkdownMock: renderMarkdownMock } = jest.requireMock("obsidian");
// ---------------------------------------------------------------------------
// Verifies that the HTML string passed to MarkdownRenderer.renderMarkdown

View file

@ -48,7 +48,7 @@ export function useAtMentionSearch(
key: `note-${file.basename}-${index}`,
title: file.basename,
subtitle: file.path,
category: "notes" as AtMentionCategory,
category: "notes",
data: file,
content: undefined,
icon: React.createElement(FileText, { className: "tw-size-4" }),
@ -64,7 +64,7 @@ export function useAtMentionSearch(
key: `tool-${tool}`,
title: tool,
subtitle: getToolDescription(tool),
category: "tools" as AtMentionCategory,
category: "tools",
data: tool,
content: getToolDescription(tool),
icon: React.createElement(Wrench, { className: "tw-size-4" }),
@ -79,7 +79,7 @@ export function useAtMentionSearch(
key: `folder-${folder.path}`,
title: folder.name,
subtitle: folder.path,
category: "folders" as AtMentionCategory,
category: "folders",
data: folder,
content: undefined,
icon: React.createElement(Folder, { className: "tw-size-4" }),
@ -97,7 +97,7 @@ export function useAtMentionSearch(
key: `webtab-${tab.url || tab.title || index}-${index}`,
title: tab.title || "Untitled",
subtitle: isLoaded ? tab.url : "Tab not loaded",
category: "webTabs" as AtMentionCategory,
category: "webTabs",
data: tab,
content: undefined,
disabled: !isLoaded,
@ -129,7 +129,7 @@ export function useAtMentionSearch(
key: "active-web-tab",
title: "Active Web Tab",
subtitle: undefined,
category: "activeWebTab" as AtMentionCategory,
category: "activeWebTab",
data: activeWebTab,
content: undefined,
icon: React.createElement(Globe, { className: "tw-size-4" }),
@ -142,7 +142,7 @@ export function useAtMentionSearch(
key: `active-note-${currentActiveFile.path}`,
title: "Active Note",
subtitle: undefined,
category: "activeNote" as AtMentionCategory,
category: "activeNote",
data: currentActiveFile,
content: undefined,
icon: React.createElement(FileClock, { className: "tw-size-4" }),

View file

@ -186,7 +186,7 @@ export function AtMentionCommandPlugin({
<>
{state.isOpen && (
<TypeaheadMenuPortal
options={displayOptions as any[]}
options={displayOptions}
selectedIndex={state.selectedIndex}
onSelect={handleSelect}
onHighlight={handleHighlight}

View file

@ -2,7 +2,7 @@ import { App } from "obsidian";
import { ConfirmModal } from "./ConfirmModal";
export class RebuildIndexConfirmModal extends ConfirmModal {
constructor(app: App, onConfirm: () => void) {
constructor(app: App, onConfirm: () => void | Promise<void>) {
super(
app,
onConfirm,

View file

@ -2,7 +2,7 @@ import { ConfirmModal } from "@/components/modals/ConfirmModal";
import { App } from "obsidian";
export class ResetSettingsConfirmModal extends ConfirmModal {
constructor(app: App, onConfirm: () => void) {
constructor(app: App, onConfirm: () => void | Promise<void>) {
super(
app,
onConfirm,

View file

@ -218,7 +218,7 @@ function AddProjectModalContent({
try {
setIsSubmitting(true);
await onSave(saveData as ProjectConfig);
await onSave(saveData);
} catch (e) {
new Notice(err2String(e));
setTouched((prev) => ({

View file

@ -435,7 +435,7 @@ export class QuickAskOverlay {
const getVisibleRect = (pos: number | null): AnchorRect | null => {
if (typeof pos !== "number") return null;
const coords = this.options.view.coordsAtPos(pos) as AnchorRect | null;
const coords = this.options.view.coordsAtPos(pos);
if (!coords) return null;
return this.isAnchorRectVisible(coords, visibleRect) ? coords : null;
};

View file

@ -15,7 +15,7 @@ import { ChevronDown, ChevronRight, GripVertical, MoreVertical } from "lucide-re
export interface MobileCardDropdownAction<T = any> {
icon: React.ReactNode;
label: string;
onClick: (item: T) => void;
onClick: (item: T) => void | Promise<void>;
variant?: "default" | "destructive";
}

View file

@ -15,7 +15,7 @@ const mockElectron = {
jest.mock("electron", () => mockElectron);
window.TextEncoder = TextEncoder as any;
window.TextEncoder = TextEncoder;
window.TextDecoder = TextDecoder as any;
// Now we can import our modules

View file

@ -241,16 +241,20 @@ export function useChatFileDrop(props: UseChatFileDropProps): UseChatFileDropRet
}
};
const handleDropEvent = (e: DragEvent) => {
void handleDrop(e);
};
// Attach event listeners
container.addEventListener("dragover", handleDragOver);
container.addEventListener("dragleave", handleDragLeave);
container.addEventListener("drop", handleDrop);
container.addEventListener("drop", handleDropEvent);
// Cleanup
return () => {
container.removeEventListener("dragover", handleDragOver);
container.removeEventListener("dragleave", handleDragLeave);
container.removeEventListener("drop", handleDrop);
container.removeEventListener("drop", handleDropEvent);
};
}, [app, contextNotes, selectedImages, onAddImage, setContextNotes, containerRef]);

View file

@ -103,13 +103,15 @@ export default class CopilotPlugin extends Plugin {
async onload(): Promise<void> {
await this.loadSettings();
this.settingsUnsubscriber = subscribeToSettingsChange(async (prev, next) => {
if (next.enableEncryption) {
await this.saveData(await encryptAllKeys(next));
} else {
await this.saveData(next);
}
registerCommands(this, prev, next);
this.settingsUnsubscriber = subscribeToSettingsChange((prev, next) => {
void (async () => {
if (next.enableEncryption) {
await this.saveData(await encryptAllKeys(next));
} else {
await this.saveData(next);
}
registerCommands(this, prev, next);
})();
});
this.addSettingTab(new CopilotSettingTab(this.app, this));

View file

@ -249,7 +249,7 @@ describe("MiyoServiceDiscovery", () => {
host: "127.0.0.1",
port: 8742,
pid: 999,
} as MiyoServiceConfig),
}),
},
}
);

View file

@ -114,7 +114,7 @@ describe("SearchCore retrieve", () => {
});
it("should pass queries and salient terms as recall queries", async () => {
const searchCore = new SearchCore(mockApp as any);
const searchCore = new SearchCore(mockApp);
const retrieveResult = await searchCore.retrieve("#ProjectAlpha/Phase1 update");
@ -128,7 +128,7 @@ describe("SearchCore retrieve", () => {
});
it("should bypass ceilings when returnAll is enabled", async () => {
const searchCore = new SearchCore(mockApp as any);
const searchCore = new SearchCore(mockApp);
buildFromCandidatesMock.mockResolvedValueOnce(5);
batchCachedReadGrepMock.mockResolvedValueOnce(["note1.md", "note2.md"]);

View file

@ -90,7 +90,7 @@ describe("TieredLexicalRetriever", () => {
if (path === "note1.md" || path === "note2.md") {
const file = new (TFile as any)(path);
Object.setPrototypeOf(file, (TFile as any).prototype);
(file as any).stat = { mtime: 1000, ctime: 1000 };
file.stat = { mtime: 1000, ctime: 1000 };
return file;
}
return null;
@ -209,7 +209,7 @@ describe("TieredLexicalRetriever", () => {
if (path === "test.md") {
const file = new (TFile as any)(path);
Object.setPrototypeOf(file, (TFile as any).prototype);
(file as any).stat = { mtime: 1000, ctime: 1000 };
file.stat = { mtime: 1000, ctime: 1000 };
return file;
}
return null;
@ -260,7 +260,7 @@ describe("TieredLexicalRetriever", () => {
if (path === "large.md" || path === "other.md") {
const file = new (TFile as any)(path);
Object.setPrototypeOf(file, (TFile as any).prototype);
(file as any).stat = { mtime: 1000, ctime: 1000 };
file.stat = { mtime: 1000, ctime: 1000 };
return file;
}
return null;

View file

@ -251,7 +251,7 @@ function tryExtractPluginApi(entry: unknown): WebViewerPluginApi | null {
typeof maybe.openUrl === "function" || typeof maybe.handleOpenUrl === "function";
if (hasOpenCapability) {
return maybe as WebViewerPluginApi;
return maybe;
}
return null;
}

View file

@ -142,10 +142,12 @@ export class WebSelectionTracker {
private scheduleNext(): void {
if (!this.isRunning) return;
this.timeoutId = window.setTimeout(async () => {
await this.checkSelection();
// Schedule next only after current check completes
this.scheduleNext();
this.timeoutId = window.setTimeout(() => {
void (async () => {
await this.checkSelection();
// Schedule next only after current check completes
this.scheduleNext();
})();
}, this.intervalMs);
}

View file

@ -185,7 +185,7 @@ describe("sanitizeSettings - legacy Miyo settings cleanup", () => {
miyoRemoteVaultPath: "\\\\Mac\\Home\\Downloads\\graham-essays-main",
};
const sanitized = sanitizeSettings(legacySettings as any);
const sanitized = sanitizeSettings(legacySettings);
expect(sanitized.enableMiyo).toBe(true);
expect(sanitized.miyoServerUrl).toBe("http://127.0.0.1:8742");
@ -209,7 +209,7 @@ describe("sanitizeSettings - legacy Miyo settings cleanup", () => {
miyoRemoteVaultPath: "\\\\Mac\\Home\\Downloads\\graham-essays-main",
};
const sanitized = sanitizeSettings(legacySettings as any);
const sanitized = sanitizeSettings(legacySettings);
const sanitizedRecord = sanitized as unknown as Record<string, unknown>;
expect(sanitized.userId).toBeTruthy();

View file

@ -86,8 +86,8 @@ const SettingsMainV2: React.FC<SettingsMainV2Props> = ({ plugin }) => {
const [resetKey, setResetKey] = React.useState(0);
const { latestVersion, hasUpdate } = useLatestVersion(plugin.manifest.version);
const handleReset = async () => {
const modal = new ResetSettingsConfirmModal(app, async () => {
const handleReset = () => {
const modal = new ResetSettingsConfirmModal(app, () => {
resetSettings();
// Increment the key to force re-render of all components
setResetKey((prev) => prev + 1);

View file

@ -41,7 +41,7 @@ export const obsidianDailyNoteTool = createLangChainTool({
func: async (args) => {
const { command, vault } = args;
const params = buildCliParams(args as Record<string, unknown>);
const params = buildCliParams(args);
const result = await runObsidianCliCommand({ command, vault, params });
if (!result.ok) throwCliFailure(result);
@ -107,7 +107,7 @@ export const obsidianPropertiesTool = createLangChainTool({
throw new Error("name is required for property:read");
}
const params = buildCliParams(args as Record<string, unknown>);
const params = buildCliParams(args);
const result = await runObsidianCliCommand({ command, vault, params });
if (!result.ok) throwCliFailure(result);
@ -158,7 +158,7 @@ export const obsidianTasksTool = createLangChainTool({
schema: tasksSchema,
func: async (args) => {
const { command, vault } = args;
const params = buildCliParams(args as Record<string, unknown>);
const params = buildCliParams(args);
const result = await runObsidianCliCommand({ command, vault, params });
if (!result.ok) throwCliFailure(result);
@ -215,7 +215,7 @@ export const obsidianLinksTool = createLangChainTool({
schema: linksSchema,
func: async (args) => {
const { command, vault } = args;
const params = buildCliParams(args as Record<string, unknown>);
const params = buildCliParams(args);
const result = await runObsidianCliCommand({ command, vault, params });
if (!result.ok) throwCliFailure(result);
@ -262,7 +262,7 @@ export const obsidianTemplatesTool = createLangChainTool({
throw new Error("name is required for template:read");
}
const params = buildCliParams(args as Record<string, unknown>);
const params = buildCliParams(args);
const result = await runObsidianCliCommand({ command, vault, params });
if (!result.ok) throwCliFailure(result);
@ -343,7 +343,7 @@ export const obsidianBasesTool = createLangChainTool({
throw new Error(`file or path is required for ${command}`);
}
const params = buildCliParams(args as Record<string, unknown>);
const params = buildCliParams(args);
const result = await runObsidianCliCommand({ command, vault, params });
if (!result.ok) throwCliFailure(result);