mcpField
C
code-quality

code-quality

github.com/cognitedata测试与质量62.0k 次安装1 天前更新
代码仓库

关于 code-quality

code-quality 是面向 TypeScript 应用的代码审查技能。它先指导运行 lint 和类型检查,再检查类型安全、组件结构、重复逻辑、依赖注入、可测试性、命名及死代码,并按文件和行号报告发现。

主要功能

  1. 01运行 lint 和严格 TypeScript 检查
  2. 02审查类型安全与状态建模
  3. 03检查组件规模和职责
  4. 04查找重复逻辑与依赖问题
  5. 05审查可测试性和编码模式
  6. 06检查命名与死代码

适用场景

  1. 01审查 TypeScript 应用的代码质量
  2. 02查找不安全类型和重复代码
  3. 03评估依赖注入与测试模式
  4. 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-caseasset-panel.tsx、use-asset-data.ts
React 组件PascalCaseAssetPanel、NavigationBar
变量、函数、HookcamelCaseisLoading、fetchAssets、useAssetData
常量(模块级)SCREAMING_SNAKE_CASEMAX_ITEMS、AGENT_EXTERNAL_ID
TypeScript 类型和接口PascalCaseAssetNode、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 — 报告发现

按类别整理并生成结构化报告:

类别文件行号问题建议
TypeScriptsrc/hooks/useData.ts18使用 response as any 进行类型转换从 @cognite/sdk 导入并使用 NodeItem 类型
大小src/components/Dashboard.tsx—共 340 行,混合了数据获取和渲染逻辑提取 useDashboardData hook(约 120 行)
DRYsrc/components/A.tsx、src/components/B.tsx45、62日期格式化函数重复提取到 src/utils/formatDate.ts
命名src/hooks/data.ts—文件名未以 use 开头重命名为 useData.ts
死代码src/App.tsx88console.log("debug response", data)删除

如果某个步骤未发现问题,请注明“未发现问题”。不要默默跳过任何步骤。


完成

按类别汇总发现的问题总数,并列出应优先处理、影响最大的项目。任何 any 类型和 lint 错误都必须视为阻塞问题,并单独列出。

评论

支持 Markdown