eslint-plugin-unicorn 的 consistent-compound-words 规则:统一标识符中复合词拼写的完整指南
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
本篇指南以 eslint-plugin-unicorn 仓库中consistent-compound-words规则为主线,系统讲解该规则如何统一标识符中复合词的拼写风格、内置的 52 组默认替换词表、全部 8 个可配置选项(replacements、allowList、checkProperties等)以及底层的正则匹配与安全重命名实现原理。读完本文,你将能够在自己的 ESLint 配置中精准启用、定制并理解这条规则,从而让代码库中passWord/password、userName/username这类写法不一致的问题被自动发现并一键修复。
规则是什么:为什么复合词拼写需要被约束
在遵循 camelCase 等标识符命名规范的代码库中,同一个复合词往往会被不同开发者写成不同形态:有人写passWord,有人写password;有人写isInViewPort,有人写isInViewport。这些写法在 JavaScript 语义上完全等价,但会导致同一概念在代码中出现多种大小写形态,破坏检索一致性、增加心智负担。
consistent-compound-words规则的职责正是解决这个问题:把复合词当作一个整体来套用标识符大小写约定,从而保证同一复合词在全部标识符中拼写一致。该规则文档位于 docs/rules/consistent-compound-words.md,其实现位于 rules/consistent-compound-words.js。
需要特别强调的是,这条规则不是拼写检查器,也不是散文风格规则——它只针对一份保守精选的、代码标识符中常见的复合词拼写错误清单进行检查,避免误伤正常命名。
基本示例
// ❌ 错误:passWord 应为 password const passWord = 'secret'; // ✅ 正确 const password = 'secret';// ❌ 错误:ViewPort 应作为一个整体单词 const isInViewPort = true; // ✅ 正确 const isInViewport = true;// ❌ 错误:unSubscribe 应为 unsubscribe function unSubscribe() {} // ✅ 正确 function unsubscribe() {}从源码的 meta 定义可见,该规则类型为suggestion,recommended级别为unopinionated,并且hasSuggestions: true,意味着它可以通过编辑器建议(editor suggestions)手动修复,即不需要--fix也会在编辑器中给出可点击的一次性重命名建议(rules/consistent-compound-words.js)。同时该规则已启用在上文文档头部声明的recommended与unopinionated两套预设配置中(对应 configs/flat-config-base.js 等配置文件所导出的规则集)。
内置默认替换词表:一份保守的复合词清单
规则的全部默认替换映射定义在源码的defaultReplacements对象中(rules/consistent-compound-words.js)。下表完整列出默认检查的 52 组词(被禁止写法→推荐写法):
| 禁止写法 | 推荐写法 | 禁止写法 | 推荐写法 |
|---|---|---|---|
backGround | background | sideBar | sidebar |
callBack | callback | subClass | subclass |
checkBox | checkbox | subDirectory | subdirectory |
clipBoard | clipboard | subDomain | subdomain |
codeBase | codebase | subMenu | submenu |
dataBase | database | subProcess | subprocess |
downLoad | download | subString | substring |
feedBack | feedback | subTree | subtree |
foreGround | foreground | subType | subtype |
frameWork | framework | subTitle | subtitle |
headLine | headline | timeOut | timeout |
keyBoard | keyboard | timeStamp | timestamp |
keyFrame | keyframe | toolBar | toolbar |
lifeCycle | lifecycle | toolKit | toolkit |
metaData | metadata | toolTip | tooltip |
midPoint | midpoint | touchScreen | touchscreen |
nameSpace | namespace | unSubscribe | unsubscribe |
overRide | override | underScore | underscore |
passWord | password | upLoad | upload |
payLoad | payload | userName | username |
placeHolder | placeholder | viewPort | viewport |
preView | preview | webCam | webcam |
screenShot | screenshot | webHook | webhook |
whiteSpace | whitespace | webSite | website |
wildCard | wildcard | weekEnd | weekend |
workFlow | workflow | workSpace | workspace |
匹配的大小写适应性
该规则对大小写是自适应的:上述词表以驼峰形态给出,但匹配时同时考虑小写首字母形态与大写首字母形态。根据源码中的注释(rules/consistent-compound-words.js),小写首字母形态只匹配标识符开头,而大写首字母形态可以匹配标识符的任意复合段。这意味着:
passWord、myPassWord、passWordAndUserName都会被命中(测试用例见 test/consistent-compound-words.js);- 类名
ViewPortState、class ViewPort {}也会被报告,替换时会把ViewPort段替换为Viewport; - 全大写常量
VIEW_PORT不会被检查(源码getNameReplacement中显式跳过isUpperCase(name),见 rules/consistent-compound-words.js),因为全大写形态本身就是对复合词的可接受表达。
单词边界的判断
规则不是简单子串替换。匹配使用了一个精心构造的边界正则(?=$|[\d_$]|\p{Uppercase_Letter})(rules/consistent-compound-words.js),保证只命中“真正的复合词段”。因此测试中以下命名都被判定为合法:
foo_userName、version2userName——下划线和数字会打断匹配;compassWord、myViewPortion、endPoint、postFix、preFix、protoType、roadMap——这些并不是清单中的禁止写法,只是形似;XMLHttpRequest——全大写形态被跳过。
(以上合法用例均可在 test/consistent-compound-words.js 的 valid 列表中核对。)
刻意不检查的内容:保护外部 API 面
规则文档专门用一节说明“Intentionally not checked”(docs/rules/consistent-compound-words.md),这一点在源码与测试中有完整印证:
- 字符串键、计算属性、属性读取、JSX 属性、导出别名不检查。它们往往是外部 API 表面,保持原拼写比强行规范化更重要。例如
const options = {"timeOut": 1000}、foo[timeOut] = 1、<input passWord="current" />均合法;export {username as userName}也不会被报告(见 test/consistent-compound-words.js 与 test/consistent-compound-words.js 的 JSX 用例)。 - 歧义或常见 API 拼写被显式排除:如
fileName、setUp、lookUp、newLine,以及源码注释中点名的onLine、offLine、styleSheet、superClass(rules/consistent-compound-words.js)。这些词在“各词保持独立含义”的场景下是自然标识符——例如“一个新创建的行”而不是“换行符”。测试中const newLine = "\n"、navigator.onLine = isOnline、document.styleSheet = styleSheet、class Foo { superClass = Base; }均为合法(见 valid 用例列表)。
完整选项指南
规则接受一个对象作为第二参数,全部选项及默认值如下(对应源码prepareOptions与 schema 定义,见 rules/consistent-compound-words.js 和 rules/consistent-compound-words.js):
replacements(object,默认{})
在默认替换表的基础上扩展自定义替换。值为false可以显式禁用某个默认替换,值为字符串则是自定义的“禁止写法 → 推荐写法”映射:
'unicorn/consistent-compound-words': [ 'error', { replacements: { fooBar: 'foobar', passWord: false, // 禁用内置的 passWord → password }, }, ]注意 schema 约束:键名长度至少为 1,值必须是长度至少为 1 的字符串或false(rules/consistent-compound-words.js)。测试中也覆盖了自定义替换的验证(test/consistent-compound-words.js)。
extendDefaultReplacements(boolean,默认true)
当设为false时,replacements将完全覆盖默认替换表而不是扩展:
'unicorn/consistent-compound-words': [ 'error', { extendDefaultReplacements: false, replacements: { fooBar: 'foobar', }, }, ]该逻辑在源码中体现为extendDefaultReplacements ? {...defaultReplacements, ...replacements} : replacements(rules/consistent-compound-words.js)。
allowList(object,默认{})
按大小写精确跳过整个标识符名称,值必须是true。适用于希望在个别位置保留历史命名的场景:
'unicorn/consistent-compound-words': [ 'error', { allowList: { legacyUserName: true, }, }, ]允许列表在源码中转换为Set并参与getNameReplacement的前置判断(rules/consistent-compound-words.js 与 rules/consistent-compound-words.js)。schema 强制其值必须为true(rules/consistent-compound-words.js),测试中allowList: {userName: false}会触发校验错误(test/consistent-compound-words.js)。
checkVariables(boolean,默认true)
是否检查变量名。设为false可关闭对变量(包括函数名、类名等绑定标识符)的检查,只保留属性检查能力(配合checkProperties使用):
'unicorn/consistent-compound-words': [ 'error', { checkVariables: false, checkProperties: true, }, ]源码中变量检查在Program:exit阶段基于 scope 变量统一进行(rules/consistent-compound-words.js)。
checkProperties(boolean,默认false)
设为true后,将检查属性定义与属性写入。源码中reportProperty通过Identifier与PrivateIdentifier事件处理(rules/consistent-compound-words.js),具体覆盖:
- 属性写入:
foo.userName = 1、foo.userName++、++foo.userName; - 对象字面量属性:
({passWord: 1})(非简写形式); - 类成员与 TypeScript 声明:类字段
viewPort、私有字段#passWord、interface/type成员、accessor、抽象成员等(见 TypeScript 测试组 test/consistent-compound-words.js); - 特别地,
__proto__与ExportSpecifier会被跳过(rules/consistent-compound-words.js)。
checkDefaultAndNamespaceImports('internal' | boolean,默认'internal')
控制默认导入与命名空间导入(含静态require())的变量名是否检查:
'internal'(默认):只检查指向内部模块的导入,即模块路径以.或/开头且不包含node_modules(判断逻辑见 rules/shared/identifier-checks.js)。因此import userName from "user-name"(外部包)默认合法,而import userName from "./user-name.js"会被报告;true:所有默认/命名空间导入都检查(import userName from "user-name"也会被报告,见 test/consistent-compound-words.js);false:完全不检查这类导入变量。
checkShorthandImports('internal' | boolean,默认'internal')
与上一项对称,控制简写导入(import {userName} from "...")的本地绑定名是否检查。'internal'同样只检查内部模块的简写导入(rules/shared/identifier-checks.js)。测试中import {userName} from "user-name"默认合法,开启checkShorthandImports: true后会被报告。
checkShorthandProperties(boolean,默认false)
设为true后,检查对象解构模式中作为简写属性声明的变量,例如const {userName} = object中的userName。默认关闭是因为简写属性同时是变量声明,其重命名会影响外部契约,源码中通过isShorthandPropertyValue判断(rules/consistent-compound-words.js)。
选项合法性校验
规则的 JSON schema 对上述选项做了严格约束:不允许出现未声明属性(如extendDefaultAllowList会直接报错)、checkDefaultAndNamespaceImports/checkShorthandImports只能是'internal'或布尔值、replacements的键名和值均有最小长度要求、allowList的值必须为true。这些约束都有对应的 schema 校验测试兜底(test/consistent-compound-words.js)。
底层原理:单次正则扫描与安全重命名
合并正则:一次扫描全部词表
规则将每个禁止写法生成小写首字母与大写首字母两种形态,并把它们合并到一个正则中(buildReplacementRegExp,rules/consistent-compound-words.js)。源码注释解释了这样做的动机:如果不合并,规则就要为每个替换词、对文件里每个标识符分别编译并执行一次正则,性能开销巨大;合并后每个标识符只需一次replaceAll扫描即可完成所有词的匹配。
替换时保持大小写
getReplacementForPart会根据被匹配段的首字母大小写,将推荐写法相应调整为upperFirst或lowerFirst(rules/consistent-compound-words.js),保证ViewPort→Viewport、viewPort→viewport这种大小写自适应的替换行为。
编辑器建议修复:先查冲突再重命名
当命中变量时,规则不会盲目建议重命名,而是先做两件事:
- 作用域冲突检查:通过
getAvailableVariableName在同作用域内寻找无冲突的替换名,并用scopeToNamesGeneratedByFixer记录本次报告已生成的名称,避免多个建议互相冲突(rules/consistent-compound-words.js 与 rules/consistent-compound-words.js); - 安全重命名判断:
shouldRenameVariable(rules/shared/identifier-checks.js)会跳过被导出的标识符(避免破坏export的外部契约)与 JSX 标签名(<UserNameField />这类组件名重命名会引发解析错误)。此外,若变量被 Vue 模板引用(reference.vueUsedInTemplate),也只报错不给出重命名建议。
满足条件时,规则会生成一条带有fix的 suggestion,实际执行重命名的是 rules/fix/rename-variable.js,它通过getVariableIdentifiers收集该变量的全部标识符(定义、引用、解构别名等),逐一用replaceReferenceIdentifier替换为新名称。这也是为什么测试中能对function getUserName(userName) { return userName; }这类跨引用场景给出完整修复(见 test/consistent-compound-words.js 的 invalid 用例)。
类作用域与 TypeScript 参数属性的特殊处理
源码还处理了两个相对隐蔽的场景:
- 类名合成变量:ESLint 的 scope 分析会为类名创建合成变量,规则通过
isClassVariable识别并合并外层类变量的引用,避免重复报告(rules/consistent-compound-words.js,辅助函数见 rules/shared/identifier-checks.js); - TypeScript 构造函数参数属性:如
constructor(private userName: string) {},这类标识符兼具“参数”与“属性”双重身份。规则中的isTSParameterPropertyName专门判断该场景:当checkVariables关闭时仍可作为属性被检查,而当两个开关都开启时,测试显示它会被当作变量给出带修复的重命名建议(输出constructor(private username: string) {},见 test/consistent-compound-words.js)。
使用建议与注意事项
- 从默认配置开始:该规则已包含在
recommended与unopinionated预设中,直接启用预设即可获得默认词表的检查;单独启用可写作'unicorn/consistent-compound-words': 'error'。 - 渐进式落地:对存量代码库,可先用
allowList放行无法立即改动的历史名称(如legacyUserName),或通过replacements: {passWord: false}单独关闭某组争议词,再逐步收敛。 - 区分变量与属性:默认
checkProperties: false意味着属性名(尤其是对象字面量键)默认不受影响;如果你的项目属性命名也需要统一,再显式开启,并留意它不会触碰字符串键、计算属性与 JSX 属性等外部 API 表面。 - 导入名默认只查内部模块:
checkDefaultAndNamespaceImports与checkShorthandImports的'internal'默认值保证了外部依赖的命名不会被强行改写,这通常是想要的行为;只有对自研模块的导入名有强约束需求时再改为true。
需要提醒的是,该规则刻意不检查fileName、setUp、lookUp、newLine等常见歧义拼写(详见“刻意不检查的内容”一节),因此它解决的是“同一概念不同写法”的一致性问题,而不是完整的命名风格规范——后者需要与仓库中其他命名类规则(如 name-replacements)配合使用。若在编辑器中看到“PreferusernameoveruserName.”或“Rename tousername.”的提示,前者是规则报错信息,后者是可点击执行的编辑器建议,点击后规则会基于作用域分析自动完成全部引用的安全重命名。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考