fix(lint): 清零存量 lint 告警并接入 CI / Makefile 门禁

存量清理(首跑 141 errors / 21 warnings → 0 / 0):
- 删除孤儿 disable:SideChatView / Sidebar / selection-popup 三处 exhaustive-deps 豁免与 chunk-artifact 的 no-new-func 豁免(多行理由注释把 disable-next-line 顶离了实报行;selection-popup 改为提取 popupOpen 标志真修复)
- 死代码:未用导入(Sidebar×7、TextEditor、SideCardSection、config、fs-operations 及 6 个 spec)、sidechat-core 死累加器 total、chunk-loader.spec 死助手 registry、sidechat-seed-validation 死助手 toolResultMessage
- 真实小修:agent-opens 三处 throw 补 cause;lazy-chunk any → ComponentType<P>(P extends object);service.ts claimed 初值改定值赋值;chunk-loader task / spec 内 etag / pendingLoad / snapshot 改 const;terminal-links 去掉字符类内多余转义;SubagentView catalogs 空兜底 useMemo 稳定标识;editor-host.spec 组件提出大写 FakeViewer(rules-of-hooks);pty-helpers.spec 内联 require 提升为静态导入;panel-host-css 注释里的零宽空格改为可见写法
- 依赖数组豁免(均带理由):granular scope 字段(EditorHost / PdfView / ChangesTab / GitLens×2)、job.status 粒度(SubagentView×2)、清理期读 ref、mount-only 注册等;ctx(挂载期恒定)直接入 deps(Sidebar×3)、TextEditor previewScroll 补 scope(幂等)
- 刻意控制字符正则(conversation-draft / markdown-images)与宿主槽位 any 签名(context-types)以带理由豁免保留

接入:
- Makefile:lint 目标(typecheck 旁),check 聚合补 lint 保持「对齐 CI」语义
- ci.yml ci / ci-windows 两 job 与 release.yml 在 Typecheck 后加 Lint 步骤(release 注释同步顺序)
This commit is contained in:
Menghuan1918
2026-09-03 20:00:08 +08:00
parent f418692ad0
commit 5a1e75e46b
40 changed files with 112 additions and 90 deletions
+1 -2
View File
@@ -18,8 +18,7 @@ import { renderToString } from 'react-dom/server'
import { createRoot } from 'react-dom/client'
import { act } from 'react-dom/test-utils'
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
import { PLUGIN_TOPIC_URL, type PluginEntry } from '../src/client/plugins-shared.ts'
import { builtinTabPlugins } from '../src/client/plugins-tabs.ts'
import { type PluginEntry } from '../src/client/plugins-shared.ts'
import { builtinViewerPlugins } from '../src/client/plugins-viewers.ts'
import { AddPluginModal, PluginListBody } from '../src/client/add-plugin-modal.tsx'
import { createBetterSidebarService } from '../src/client/service.ts'
-1
View File
@@ -11,7 +11,6 @@ import {
ALLOWED_SIGNALS,
snapshotOf,
tryResizePty,
type AgentTerminalSnapshot,
} from '../src/agent-pty.ts'
/**
-1
View File
@@ -36,7 +36,6 @@ describe.skipIf(!chunksBuilt)('built chunk artifacts', () => {
if (typeof g.removeEventListener !== 'function') g.removeEventListener = () => {}
for (const name of CHUNKS) {
const code = readFileSync(`lib/client-${name}.js`, 'utf8')
// eslint-disable-next-line no-new-func
expect(() => new Function(code)(), name).not.toThrow()
const registry = g.__dshChunks__ as Record<string, unknown>
expect(typeof registry[name], name).toBe('function')
+2 -8
View File
@@ -41,11 +41,6 @@ function removeModuleSystem(): void {
delete (globalThis as Record<string, unknown>).__DSH_MODULES__
}
/** The global registry the chunk scripts populate (mirror of chunk-loader). */
function registry(): Record<string, unknown> {
return (globalThis as { __dshChunks__?: Record<string, unknown> }).__dshChunks__ ?? {}
}
/** Simulate a chunk script executing: it assigns its factory to the registry. */
function simulateScript(name: string, factory: (require: (spec: string) => unknown) => ChunkExports): void {
const g = globalThis as { __dshChunks__?: Record<string, unknown> }
@@ -314,8 +309,7 @@ describe('revalidateChunksOnReactivate (HMR re-activation keeps unchanged chunks
const revalidating = revalidateChunksOnReactivate()
// A lazy open DURING the pending revalidation must NOT get the old cache.
let resolved = false
let pendingLoad: Promise<ChunkExports> | null = null
pendingLoad = loadChunk('editor').then((exports) => { resolved = true; return exports })
const pendingLoad = loadChunk('editor').then((exports) => { resolved = true; return exports })
await new Promise((resolve) => setTimeout(resolve, 5))
expect(resolved, 'loadChunk must await the pending revalidation').toBe(false)
// Release the HEAD; the barrier lifts and the load re-injects fresh exports.
@@ -369,7 +363,7 @@ describe('revalidateChunksOnReactivate (HMR re-activation keeps unchanged chunks
scriptCalls += 1
simulateScript('editor', () => ({ TextEditor: `editor-view:${scriptCalls}` }))
})
let etag = '"v1"'
const etag = '"v1"'
stubBundleHead(() => etag)
await loadChunk('editor')
await settleEtag()
+2 -2
View File
@@ -43,7 +43,7 @@ import type {
const tab: TabDescriptor = {
id: 'my-plugin:db',
title: () => 'Database',
icon: (size: number) => null,
icon: (_size: number) => null,
order: 50,
hidden: false,
available: (ctx, scope, state) => scope.sessionId !== '' && state.panelOpen && ctx !== null,
@@ -116,7 +116,7 @@ service.subscribe(() => {})
const seed: OpenTabSeed = { type: 'my-plugin:db', title: 'DB', path: '/p', id: 'x', meta: { a: 1 } }
service.openTab(seed)
service.openTab(seed, { sessionId: 's1', cwd: '/p' })
service.version
const _version: string = service.version
service.features.includes('badge')
const snapshot: SidebarSnapshot | undefined = service.getSnapshot()
void snapshot
+18 -14
View File
@@ -10,12 +10,12 @@
*/
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest'
import { createElement, useEffect } from 'react'
import { createElement, useEffect, type ReactNode } from 'react'
import { createRoot } from 'react-dom/client'
import { act } from 'react-dom/test-utils'
import type { Context } from '../src/context-types.ts'
import { EditorHost } from '../src/client/EditorHost.tsx'
import { createBetterSidebarService } from '../src/client/service.ts'
import { createBetterSidebarService, type FileViewerProps } from '../src/client/service.ts'
import { allLeaves, createSidebarStore, type SidebarTab } from '../src/client/state.ts'
// The act() environment flag (React 18.2 reads it before flushing effects).
@@ -258,22 +258,26 @@ describe('EditorHost (files window)', () => {
const service = ctx.betterSidebar
const calls: string[] = []
// A viewer with a hoisted toolbar (the TextEditor contract): register
// commands and report the state once on mount.
// commands and report the state once on mount. Capitalized so the hooks
// rules recognize it as a component.
const FakeViewer = (viewerProps: FileViewerProps): ReactNode => {
useEffect(() => {
viewerProps.onToolbarControls?.({
setMode: (next) => { calls.push(`mode:${next}`) },
save: () => { calls.push('save') },
})
viewerProps.onToolbarState?.({ modes: true, mode: 'preview', dirty: true, editable: true, saveState: 'idle' })
return () => { viewerProps.onToolbarControls?.(null) }
// Mount-only: re-running would re-fire the toolbar registration.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return null
}
service.registerFileViewer({
id: 'test:fake',
exts: ['fake'],
fetchStrategy: 'none',
component: (viewerProps) => {
useEffect(() => {
viewerProps.onToolbarControls?.({
setMode: (next) => { calls.push(`mode:${next}`) },
save: () => { calls.push('save') },
})
viewerProps.onToolbarState?.({ modes: true, mode: 'preview', dirty: true, editable: true, saveState: 'idle' })
return () => { viewerProps.onToolbarControls?.(null) }
}, [])
return null
},
component: FakeViewer,
})
service.openTab({ type: 'editor', title: 'x.fake', path: '/tmp/x.fake', id: 'editor:/tmp/x.fake' })
const fileTab = (): SidebarTab =>
+1 -1
View File
@@ -1,5 +1,5 @@
import { afterAll, describe, expect, it } from 'vitest'
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs'
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { writeWorkspaceUpload } from '../src/fs-operations.ts'
+2
View File
@@ -37,6 +37,8 @@ function FakeMarkdownViewer(props: FileViewerProps) {
markDirty = undefined
props.onToolbarControls?.(null)
}
// Only the callback identities must re-wire; content is read at render time.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.onToolbarControls, props.onToolbarState])
return createElement('div', { 'data-testid': 'markdown-content' }, props.content ?? '')
}
+1 -1
View File
@@ -22,7 +22,7 @@ const css = readFileSync('src/client/sidebar.module.css', 'utf8')
/** Body of the `:global([data-dsh-panel-host])` rule (up to the un-indented `}`). */
const hostRule = css.match(/:global\(\[data-dsh-panel-host\]\) \{([\s\S]*?)\n\}/)?.[1]
/** Strip /* ... *​/ comment blocks so prose mentions don't count as declarations. */
/** Strip block comments (`/*` to `*`-`/`) so prose mentions don't count as declarations. */
const stripComments = (cssText: string): string => cssText.replace(/\/\*[\s\S]*?\*\//g, '')
describe('panel host layer css', () => {
+3 -3
View File
@@ -12,6 +12,9 @@ vi.mock('node:os', async (importOriginal) => {
}
})
import { existsSync, statSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { resolveSidebarConfig } from '../src/config.ts'
import {
defaultShell,
@@ -141,9 +144,6 @@ describe('pty helpers', () => {
if (process.platform !== 'darwin') return
ensureSpawnHelper()
ensureSpawnHelper()
const { existsSync, statSync } = require('node:fs') as typeof import('node:fs')
const { dirname, join } = require('node:path') as typeof import('node:path')
const { createRequire } = require('node:module') as typeof import('node:module')
const entry = createRequire(import.meta.url).resolve('node-pty')
const root = dirname(dirname(entry))
// Prebuilt (tarball) or node-gyp-compiled (build/Release): mirror
-1
View File
@@ -22,7 +22,6 @@ import {
SIDE_INJECTION_PLUGIN,
SIDE_LABEL_PREFIX,
type SeedEvent,
type SidechatLogEvent,
} from '../src/sidechat-core.ts'
/** One log event fixture (structural, seq === index like the live contract).
-9
View File
@@ -40,15 +40,6 @@ function assistantMessage(text: string): Record<string, unknown> {
}
}
function toolResultMessage(callId: string, text: string): Record<string, unknown> {
return {
id: `m-${callId}`,
role: 'user',
content: [{ type: 'tool-result', toolCallId: callId, content: [{ type: 'text', text }] }],
source: { kind: 'tool', callId },
}
}
/** A parent log with a completed turn, a pending question, and an open
* in-progress turn (the exact shape a mid-stream thread creation sees). */
function parentLog(): SidebarSessionEvent[] {
+1 -1
View File
@@ -64,7 +64,7 @@ const EVENTS = [
/** A subscribable sessions-list snapshot (mirror of the runtime list feed). */
function makeStore(initial: SidebarSessionList) {
let snapshot = initial
const snapshot = initial
const listeners = new Set<() => void>()
return {
getSnapshot: (): SidebarSessionList => snapshot,