关于 code-quality
code-quality 是面向 TypeScript 应用的代码审查技能。它先指导运行 lint 和类型检查,再检查类型安全、组件结构、重复逻辑、依赖注入、可测试性、命名及死代码,并按文件和行号报告发现。
主要功能
- 01运行 lint 和严格 TypeScript 检查
- 02审查类型安全与状态建模
- 03检查组件规模和职责
- 04查找重复逻辑与依赖问题
- 05审查可测试性和编码模式
- 06检查命名与死代码
适用场景
- 01审查 TypeScript 应用的代码质量
- 02查找不安全类型和重复代码
- 03评估依赖注入与测试模式
- 04整理带文件和行号的审查发现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cognitedata/ builder-skills --skill code-quality安装后在 Claude Code 中输入/code-quality确认已加载。
详细介绍
代码质量审查
审查 $ARGUMENTS(如果未提供参数,则审查整个应用)中的代码质量问题。按顺序完成以下所有步骤,并报告所有问题及其文件路径和行号。
步骤 1 — 先运行代码检查工具
在手动阅读任何代码之前,先使用自动化工具获取基线:
pnpm run lint
列出每个错误和警告。继续之前修复所有错误——代码检查错误必须解决。应检查并解决警告,除非有记录在案的例外情况。
还要在严格模式下运行 TypeScript 编译器,以发现任何隐藏的类型问题:
pnpm exec tsc --noEmit
列出每个类型错误。必须修复这些错误。
步骤 2 — TypeScript 类型安全
2a — 消除 any 类型
搜索整个代码库中 any 的用法:
grep -rn --include="*.ts" --include="*.tsx" -E ": any|as any|<any>" src/
对于每个匹配项,替换为正确的类型。常见替换方式:
| 替代项 | 使用 |
|---|---|
any 表示未知的外部数据 | unknown + 类型守卫或 Zod 解析 |
any 用于事件处理程序 | React.ChangeEvent<HTMLInputElement>、React.MouseEvent 等 |
any 用于 CDF 响应 | SDK 自带的响应类型(从 @cognite/sdk 导入) |
用于数组的 any[] | 使用正确泛型的 T[] |
as any 类型断言 | 正确的类型收窄或显式的重载函数签名 |
目标是让 src/ 中不含任何 any。如果第三方库要求使用 any,请将调用封装在类型化适配器函数中,避免 any 泄漏到应用中。
2b — 让不可能的状态无法表示
利用类型系统,让无效状态在编译时就报错。可达状态越少,代码就越易于阅读和修改。
品牌类型 — 为基本类型添加品牌标记,避免混淆。只需在边界处验证一次;后续代码即可信任该类型。
type PhoneNumber = string & { __brand: "PhoneNumber" };
function parsePhone(input: string): PhoneNumber {
if (!/^\+?\d{10,15}$/.test(input)) throw new Error(`Invalid: ${input}`);
return input as PhoneNumber;
}
如果项目使用的库原生支持品牌类型(例如 Effect),请使用该库提供的原语,而不是自行实现。
优先使用可辨识联合类型,而不是标志对象 — 将布尔值和可选值的组合替换为完整的联合类型:
// Don't — invalid combos representable
type State = { loading: boolean; user?: User; error?: string };
// Do — only valid states exist
type State =
| { status: "loading" }
| { status: "success"; user: User }
| { status: "error"; error: string };
搜索标志对象模式:
grep -rn --include="*.ts" --include="*.tsx" -E "loading\?|isLoading.*isError|isSuccess.*isError" src/
标记所有包含布尔标志、且只有某些组合才有效的类型。这些类型应改为可辨识联合类型。
2c — 让类型端到端流转
DB 架构 → 服务端 → 客户端应共享类型,避免手动重复定义。不要重复声明可以推导出的类型——编写新接口之前,先考虑使用 Pick、Omit、Parameters、ReturnType、Awaited、typeof。
// Don't — duplicate shape, drifts when the row changes
type UserSummary = { id: string; email: Email };
function renderUser(u: UserSummary) { /* ... */ }
// Do — derive from the source of truth
type User = Awaited<ReturnType<typeof db.query.users.findFirst>>;
function renderUser(u: Pick<User, "id" | "email">) { /* ... */ }
# Find manually duplicated type shapes
grep -rn --include="*.ts" --include="*.tsx" -E "^(export )?type \w+Summary|^(export )?interface \w+DTO" src/
标记那些手动重复声明 SDK 或 DB 类型中已有字段的接口——这些接口应使用 Pick/Omit。
2d — 传递对象,而不是位置参数
如果函数有两个或更多相同基本类型的参数,应改为接收带命名属性的对象,避免调用者无意中调换参数顺序。
// Don't — swap two args, still compiles
sendEmail("Welcome!", "Hi there");
// Do — order-independent, self-documenting
sendEmail({ to: "[email protected]", subject: "Welcome!", body: "Hi there" });
# Find functions with multiple string/number parameters (potential swap bugs)
grep -rn --include="*.ts" --include="*.tsx" -E "^\s*(export\s+)?(function|const)\s+\w+\s*\([^)]*string[^)]*string" src/
步骤 3 — 检查组件大小和单一职责
列出所有 .tsx 文件及其行数:
node -e "const fs=require('fs'),path=require('path');function walk(d){return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>{const p=path.join(d,e.name);return e.isDirectory()?walk(p):p.endsWith('.tsx')?[p]:[]})}walk('src').map(p=>({p,l:fs.readFileSync(p,'utf8').split('\n').length})).sort((a,b)=>b.l-a.l).forEach(({l,p})=>console.log(l,p))"
标记所有超过 150 行的组件文件。逐一阅读并检查:
- 它是否承担了多项职责?(获取数据、渲染 UI、处理表单状态)
- 获取数据的逻辑能否移至自定义 Hook(
useAssetData)? - 能否将子部分提取为具名子组件?
仅当拆分能让职责划分真正更清晰时才进行拆分——不要只为控制行数而拆分。一个命名得当的 200 行组件,胜过三个命名不佳的 60 行组件。
步骤 4 — 查找并删除重复逻辑(DRY)
搜索 Hook、工具函数和组件中复制粘贴的模式:
# Find repeated fetch patterns
grep -rn --include="*.ts" --include="*.tsx" -E "sdk\.(assets|timeseries|events|files)\.(list|retrieve)" src/
# Find repeated formatting functions
grep -rn --include="*.ts" --include="*.tsx" -E "toLocaleDateString|toLocaleString|new Date\(" src/
# Find repeated className strings longer than 40 chars
grep -rn --include="*.tsx" -E 'className="[^"]{40,}"' src/
对于每组重复逻辑:
- 如果是纯函数,提取到
src/utils/ - 如果包含 React 状态或副作用,提取到
src/hooks/ - 如果是 JSX,提取为共享组件
步骤 5 — 强制对外部调用进行依赖注入
组件和 Hook 不得直接导入 CDF 客户端。必须从上下文获取 SDK 客户端(通过 useCogniteClient() 或作为 prop 传入),以便单独测试组件。
grep -rn --include="*.ts" --include="*.tsx" -E "new CogniteClient|createCogniteClient" src/
标出应用引导/身份验证配置文件之外的所有直接客户端实例化。应始终采用以下模式:
// GOOD — client comes from context
export function useMyData() {
const sdk = useCogniteClient(); // from Flows auth context
// ...
}
// BAD — direct construction inside a hook or component
const sdk = new CogniteClient({ project: "my-project", ... });
同样,Atlas 工具应通过 execute 闭包接收 Hook 提供的 ref 作为依赖,而不是导入全局单例。
步骤 6 — 验证编码模式和可测试性
检查代码库是否遵循 Flows 应用审核流程要求的三种核心模式。这些模式能确保代码可测试、可维护且保持一致。
6a — 通过 React 上下文进行依赖注入
Hook 必须通过上下文类型声明依赖项,并通过 useContext 使用这些依赖项,而不是直接导入。这样无需模块级模拟即可进行测试。
# Find hooks that import other hooks/services directly (potential DI violation)
grep -rn --include="*.ts" --include="*.tsx" -E "^import.*from\s+['\"]\.\./" src/hooks/
# Find hooks that use useContext for dependency injection (good pattern)
grep -rn --include="*.ts" --include="*.tsx" "useContext" src/hooks/
首选模式:
// GOOD — injectable via context
const defaultDependencies = { useDataSource, useAnalytics };
export type UseMyHookContextType = typeof defaultDependencies;
export const UseMyHookContext = createContext<UseMyHookContextType>(defaultDependencies);
export function useMyHook() {
const { useDataSource } = useContext(UseMyHookContext);
}
// BAD — hard-coded import, requires vi.mock to test
import { useDataSource } from '../data/useDataSource';
export function useMyHook() { const data = useDataSource(); }
对于非 React 代码(实用工具、服务),请使用带有部分依赖覆盖的工厂函数:
type Deps = { serviceFactory: () => SomeService };
const defaultDeps: Deps = { serviceFactory: () => new SomeServiceImpl() };
export const doSomething = async (props: Props, depOverrides?: Partial<Deps>) => {
const deps = { ...defaultDeps, ...depOverrides };
const service = deps.serviceFactory();
};
标出所有直接导入依赖项、而不是通过上下文接收依赖项的 Hook。即使目前已有测试,这仍然属于可测试性问题。
6b — 基于接口的服务
服务类必须实现明确的 TypeScript 接口。这样生产代码可替换,测试替身也能保证类型安全。
# Find service/class definitions and check for interface implementations
grep -rn --include="*.ts" --include="*.tsx" -E "class\s+\w+(Service|Client|Repository|Manager)" src/
# Find unsafe casts in production AND test code
grep -rn --include="*.ts" --include="*.tsx" "as unknown as" src/
标出:
- 未实现明确接口的服务类
- 生产代码或测试代码中的
as unknown as T类型转换——这表明接口设计不佳
6c — ViewModel 模式
页面级 Hook(useSomethingViewModel)必须将业务逻辑与展示分离。UI 组件只接收数据和回调;不得包含数据获取、副作用逻辑或直接 SDK 调用。
# Find page/view components
grep -rn --include="*.tsx" --include="*.ts" -l "useQuery\|useMutation\|sdk\.\|client\." src/pages/ src/views/ 2>/dev/null
# Find ViewModel hooks
grep -rn --include="*.ts" --include="*.tsx" -l "ViewModel" src/hooks/ 2>/dev/null
标出:
- 包含
useQuery、useMutation或直接 SDK 调用的页面组件——这些逻辑应放在 ViewModel Hook 中 - 包含非简单数据逻辑的页面缺少 ViewModel Hook
6d — 测试模拟的质量
# Find vi.mock usage — each should have a comment justifying why context injection wasn't used
grep -rn --include="*.ts" --include="*.tsx" "vi\.mock" src/
# Find unsafe test casts
grep -rn --include="*.ts" --include="*.tsx" "as unknown as" src/ | grep -E "\.test\.|\.spec\."
标出:
- 使用
vi.mock却未添加说明注释,解释为何无法进行上下文注入 - 测试文件中的
as unknown as T类型转换——这表明生产代码的接口设计不佳
步骤 7 — 检查命名约定
阅读有代表性的一组文件并验证:
| 项目 | 约定 | 示例 |
|---|---|---|
| 文件和目录 | kebab-case | asset-panel.tsx、use-asset-data.ts |
| React 组件 | PascalCase | AssetPanel、NavigationBar |
| 变量、函数、Hook | camelCase | isLoading、fetchAssets、useAssetData |
| 常量(模块级) | SCREAMING_SNAKE_CASE | MAX_ITEMS、AGENT_EXTERNAL_ID |
| TypeScript 类型和接口 | PascalCase | AssetNode、ChartConfig |
| 布尔变量 | 使用助动词前缀 | isLoading、hasError、canEdit |
搜索常见违规情况:
# TSX components not in PascalCase (filename starts with lowercase)
node -e "const fs=require('fs'),path=require('path');function walk(d){return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>{const p=path.join(d,e.name);return e.isDirectory()?walk(p):p.endsWith('.tsx')?[p]:[]})}walk('src').filter(p=>/^[a-z]/.test(path.basename(p))).forEach(p=>console.log(p))"
# Hook files not prefixed with "use"
node -e "const fs=require('fs');fs.readdirSync('src/hooks').filter(f=>f.endsWith('.ts')&&!f.startsWith('use')).forEach(f=>console.log('src/hooks/'+f))"
步骤 8 — 删除无用代码
# Find commented-out code blocks (3+ consecutive commented lines)
Get-ChildItem -Recurse -Include "*.ts","*.tsx" src | ForEach-Object {
$file = $_; $lines = Get-Content $file.FullName
$count = 0; $startLine = 0
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match '^\s*//') {
if ($count -eq 0) { $startLine = $i + 1 }
$count++
} else {
if ($count -ge 3) { "$($file.FullName):$startLine — $count consecutive comment lines" }
$count = 0
}
}
if ($count -ge 3) { "$($file.FullName):$startLine — $count consecutive comment lines" }
}
# Find console.log/debug statements
grep -rn --include="*.tsx" --include="*.ts" -E "console\.(log|debug|warn|error|info)" src/
# Find TODO/FIXME/HACK comments
grep -rn --include="*.tsx" --include="*.ts" -E "(TODO|FIXME|HACK|XXX):" src/
搜索无法访问的页面(在路由器中定义、但其组件从未导入或渲染的路由)以及完全未使用的文件:
# Find all .ts/.tsx files and check if they are imported anywhere
for file in $(find src -name "*.ts" -o -name "*.tsx" | grep -v ".test." | grep -v ".spec." | grep -v "node_modules"); do
basename=$(basename "$file" | sed 's/\.[^.]*$//')
imports=$(grep -rn --include="*.ts" --include="*.tsx" "$basename" src/ | grep -v "$file" | wc -l)
if [ "$imports" -eq 0 ]; then
echo "UNUSED: $file"
fi
done
# Find route definitions and verify their components are imported
grep -rn --include="*.tsx" --include="*.ts" -E "path:\s*['\"]|<Route" src/
规则:
- 发布前必须删除
console.log和console.debug(console.error应使用适当的错误日志记录)。 - 必须删除注释掉的代码块——版本控制会保留历史记录。
- 应解决早于当前迭代的
TODO和FIXME注释,或将其转为已跟踪的问题。 - 未使用的导入会由代码检查工具发现(步骤 1);请确认已将其删除。
硬性门槛: 批准前必须删除无法访问的页面、完全未使用的文件以及重要的无用代码块。这些问题会阻止审批。
步骤 9 — 验证文件和导出结构
每个功能区域都应遵循一致的结构。检查应用的布局是否符合以下模式:
src/
├── components/ # Shared presentational components
│ └── <name>/
│ ├── <name>.tsx
│ └── index.ts # re-exports the public API
├── hooks/ # Custom hooks (each file = one hook)
├── utils/ # Pure utility functions (no React)
├── contexts/ # React context providers
├── pages/ or views/ # Route-level components
└── types/ # Shared TypeScript types
标记以下问题:
- 业务逻辑直接写在页面组件中(应放在 hooks 中)
- 工具函数位于组件文件中(应放在
utils/中) - 类型在多个文件中使用,却内联定义在组件文件中(应放在
types/中) - 组件目录缺少
index.ts汇总文件(会导致导入语句冗长)
步骤 10 — 报告发现
按类别整理并生成结构化报告:
| 类别 | 文件 | 行号 | 问题 | 建议 |
|---|---|---|---|---|
| TypeScript | src/hooks/useData.ts | 18 | 使用 response as any 进行类型转换 | 从 @cognite/sdk 导入并使用 NodeItem 类型 |
| 大小 | src/components/Dashboard.tsx | — | 共 340 行,混合了数据获取和渲染逻辑 | 提取 useDashboardData hook(约 120 行) |
| DRY | src/components/A.tsx、src/components/B.tsx | 45、62 | 日期格式化函数重复 | 提取到 src/utils/formatDate.ts |
| 命名 | src/hooks/data.ts | — | 文件名未以 use 开头 | 重命名为 useData.ts |
| 死代码 | src/App.tsx | 88 | console.log("debug response", data) | 删除 |
如果某个步骤未发现问题,请注明“未发现问题”。不要默默跳过任何步骤。
完成
按类别汇总发现的问题总数,并列出应优先处理、影响最大的项目。任何 any 类型和 lint 错误都必须视为阻塞问题,并单独列出。