mcpField
F
fix-knip-unused-exports

fix-knip-unused-exports

github.com/factory-ai开发117140 次安装3 天前更新
代码仓库

关于 fix-knip-unused-exports

用于处理 Knip“未使用导出”告警的开发技能,先分类问题,再选择删除导出、移除重导出或抽取到新文件等修复方式。它还指导检查跨包引用、循环依赖及相关 lint 规则。

主要功能

  1. 01识别并分类未使用导出
  2. 02按类别选择对应修复策略
  3. 03将仅供测试使用的代码抽取到新文件
  4. 04检查 barrel 重导出与包子路径
  5. 05验证 Knip、类型检查和测试

适用场景

  1. 01修复 `npm run knip` 报告的未使用导出
  2. 02整理仅被测试引用的函数或类
  3. 03移除未使用的 `index.ts` 重导出

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/factory-ai/factory-plugins --skill fix-knip-unused-exports

安装后在 Claude Code 中输入/fix-knip-unused-exports确认已加载。

详细介绍

修复 Knip 未使用的导出

修复 Knip 的“未使用的导出”违规。有多种违规类别,每种都有不同的修复策略。

适用情况

  • npm run knip 报告“未使用的导出”

不适用情况

  • 该导出被另一个文件中的非测试生产代码使用——说明还有其他问题

工作流程

1. 找出违规项
npm run knip

输出如下:

Unused exports (3)
::error file=packages/foo/src/bar.ts,line=42,title=Unused exports::myFunction
2. 对每项违规分类

对于每个被标记的导出,使用 grep 搜索整个仓库(而不仅是当前包):

rg "myFunction"

确定其所属类别:

类别调用方修复方法
仅供测试使用的导出仅在同一文件和测试文件中使用提取到新文件
无效的 barrel 重新导出从 index.ts 重新导出,但生产代码通过相对路径或其他子路径导入从 barrel 中移除该重新导出
仅在内部使用的导出仅在同一文件中使用,测试或其他文件均未使用移除 export 关键字
死代码没有任何调用方删除该导出
存在生产代码使用方被另一个文件中的非测试代码使用不是 Knip 问题——进一步调查

重要提示:使用 grep 时,排除测试文件,以便找出生产代码使用方:

rg "myFunction" --glob '!**/*.test.*'

修复:仅供测试使用的导出(提取到新文件)

当某个函数仅为测试访问而导出,但也在同一文件中供内部使用时。

规划提取

编写代码前,先回答以下问题:

a) 哪些内容要移到新文件?

  • 被标记的导出函数、类或常量
  • 它所依赖的所有私有辅助函数
  • 它所依赖的所有私有常量和类型

b) 是否有辅助函数会被保留在原文件中的函数共用?

  • 如果有,该辅助函数必须从新文件导出,并由原文件导入
  • 这意味着新文件会有 2 个或更多导出(对任何文件名与导出名称匹配的 lint 规则来说都没问题)

c) 新文件是否恰好导出一个函数?

  • 如果项目强制执行 filename-match-export lint 规则,文件名必须与该导出函数同名:myFunction.ts
  • 如果文件导出 2 个或更多函数,文件名可以灵活选择

d) 是否存在名称匹配的测试文件?

  • 如果 bar.ts 保留且存在 bar.test.ts,测试必须仍从 ./bar 导入内容(前提是项目强制执行 test-imports-source 规则)
  • 如果删除了 bar.ts(所有内容都已移出),该规则通常只适用于存在名称匹配的源文件时

e) 是否存在循环依赖风险?

  • 绘制导入关系图:新文件 -> 原文件 -> 新文件会形成循环
  • 修复方法:将共享依赖移到新文件或第三个文件中

f) 是否导出了常量?

  • 如果项目强制执行 constants-file-organization lint 规则,导出的常量必须放在名为 constants.ts 的文件中
  • 如果提取出的函数依赖某个原文件中其他函数也会使用的常量,请勿从新文件导出该常量。应改为调用函数(例如,将 BUDGET[effort] 替换为 getBudget(effort)),以免需要单独的 constants.ts
执行提取

在同一目录中创建新文件:

// myFunction.ts (new file)
import { SomeType } from '../types';

function privateHelper(): void { /* ... */ }

export function myFunction(): SomeType {
  return privateHelper();
}

更新原文件,使其从新文件导入:

// bar.ts (original file, updated)
import { myFunction } from './myFunction';

function otherFunction() {
  const result = myFunction(); // Now imports from new file
}

更新测试文件,使其从新文件导入:

// bar.test.ts (updated)
import { myFunction } from './myFunction';
// If bar.ts still exists, you may need to also import something from './bar'
// to satisfy any test-imports-source rule
留意连锁违规

提取后,再次运行 npm run knip。如果函数 A 与它调用的函数 B 一起被提取到新文件中,但 B 在外部也仅由测试使用,Knip 仍会标记 B。此时需要将 B 提取到自己的文件中,使 A 所在文件对 B 形成真正的生产代码导入。

例如,假设先将 throwMappedError 与 mapResponseFailure 一起提取到 error-mappers.ts。如果 throwMappedError 只在该文件内部被调用(由 mapResponseFailure 调用),它仍会被标记。修复方法:将它提取到 throwMappedError.ts,使 error-mappers.ts 对它的导入成为真正的生产代码使用。

修复:无效的 Barrel 再导出(从 index.ts 中移除)

如果 barrel index.ts 再导出了某项内容,但没有任何生产代码通过该 barrel 导入它,就会出现这种情况。具体包括:

  • 同一软件包中的生产代码使用相对导入(例如,import { x } from './source'),而不是通过 barrel 导入
  • 其他软件包中的生产代码直接从子路径导入(例如,@scope/pkg/feature/handlers),而不是通过 barrel 导入
  • 再导出是推测性添加的,但从未被使用
如何识别

使用 grep 搜索,但排除测试文件。如果唯一的命中项是:

  • barrel index.ts 本身
  • 在同一软件包中使用相对导入的源文件
  • 测试文件

那么该 barrel 再导出未被使用。只需将它从 index.ts 中移除。

跨软件包测试导入

如果另一个软件包中的测试通过 barrel 导入符号(例如,import { x } from '@scope/pkg/feature'),移除 barrel 再导出后,你需要提供替代导入路径:

  1. 在源软件包的 package.json 中添加子路径导出:

    {
      "exports": {
        "./feature": "./src/feature/index.ts",
        "./feature/doSomething": "./src/feature/doSomething.ts"
      }
    }
    
  2. 更新测试,使其从新的子路径导入:

    import { doSomething } from '@scope/pkg/feature/doSomething';
    

此模式遵循 monorepo 中常见的子路径导出约定。

修复:仅在内部使用的导出(取消导出)

如果某项导出只在同一文件内使用,且没有被其他任何内容(甚至测试)导入,只需移除 export 关键字:

// Before
export const MySchema = z.object({ ... });

// After
const MySchema = z.object({ ... });

对于仅在同一文件中作为其他 schema 的构建模块使用的 Zod schema,这种情况很常见。

验证

对受影响的软件包运行以下所有检查:

# Knip passes (the whole point)
npm run knip

# Types still compile
npm run typecheck

# Tests still pass
npm run test

# Lint passes (catches filename-match-export, test-imports-source, constants-file-organization, etc.)
npm run lint

如果存在跨软件包导入,也要验证使用该导入的软件包。

相关 Lint 规则

许多 TypeScript monorepo 会在 knip 之上叠加额外的自定义 lint 规则。请根据项目使用的规则调整以下修复方法。

filename-match-export(或类似规则)

如果一个文件恰好导出了一个函数(不是 React 组件),文件名必须与函数名一致。

  • loadConfig.ts 中的 export function loadConfig —— 通过
  • helpers.ts 中的 export function loadConfig —— 不通过
  • helpers.ts 中有两个导出 —— 规则不适用(有多个导出)
test-imports-source(或类似规则)

如果 foo.test.ts 和 foo.ts 同时存在,测试必须从 ./foo 导入。

  • 类似 import { x } from './foo' 的导入满足此规则
  • 通常也接受从 '.' 或 './index' 导入,前提是 index.ts 从 foo.ts 再导出
  • 如果删除了 foo.ts,则此规则不适用
constants-file-organization(或类似规则)

导出的常量必须定义在名为 constants.ts 的文件中。

  • 如果提取的函数依赖共享常量,请勿从函数所在文件导出该常量
  • 应改用函数调用替代直接访问常量(例如,将 BUDGET[effort] 改为 getBudget(effort))
  • 或将常量移至 constants.ts 文件
Knip 如何追踪导出
  • Knip 会忽略测试文件(**/*.test.*、**/*.spec.*)
  • knip.json 中的 ignoreIssues 会抑制所列文件上的警告,但不会使源导出变为“已使用”
  • 带有 ignoreIssues 的 index.ts 中的 barrel 再导出(export { x } from './source')不计作源导出的使用
  • 只有来自项目中非测试、非忽略文件的实际导入才会被计为使用
  • includeEntryExports: true(如果已设置)表示也会检查入口文件中的导出,因此迁移、脚本等入口点风格的文件可能需要显式设置 ignoreIssues
软件包子路径导出

移除跨软件包测试所依赖的 barrel 再导出时,请在 package.json 中添加子路径导出:

{
  "exports": {
    "./feature": "./src/feature/index.ts",
    "./feature/doSomething": "./src/feature/doSomething.ts"
  }
}

不要做的事

  • 除非文件是真正的入口脚本(迁移脚本、CLI),否则不要将其添加到 knip.json 的 ignoreIssues 中
  • 不要为了减少导出而把所有函数合并到一个文件中——从 knip 的角度来看,在同一文件中使用导出不算作使用
  • 如果测试需要 export 关键字,不要将其移除——否则测试会失败
  • 不要在新文件和原始文件之间创建循环导入
  • 如果项目强制执行 constants-file-organization lint 规则,不要从非 constants.ts 文件中导出常量

评论

支持 Markdown