Rome 命名规范 Lint 规则 useNamingConvention 完整指南:覆盖全代码库的命名约定检查与自动修复
【免费下载链接】toolsUnified developer tools for JavaScript, TypeScript, and the web项目地址: https://gitcode.com/gh_mirrors/to/tools
本文全面解读 Rome(该项目仓库)中useNamingConvention这条 lint 规则:它用于强制整个代码库统一命名约定,涵盖变量、函数、类、TypeScript 类型、枚举、命名空间、导入导出别名等几乎所有标识符。读完本文,你将掌握该规则默认的命名规范矩阵、两条可配置选项(strictCase与enumMemberCase)的完整语义与默认值、规则诊断与安全修复(Safe Fix)的行为,并了解其底层源码实现与测试验证方式。
规则概览:为什么需要统一命名规范
useNamingConvention的目标是对整个代码库中的一切命名强制约定(Enforce naming conventions for everything across a codebase)。在 规则实现 中,该规则通过declare_rule!宏声明,元数据如下:
- 名称:
useNamingConvention - 版本:
next(即文档标题中的 "since vnext",属于尚未正式发布的版本) - recommended:
false(不属于推荐规则集,需要显式在配置中启用)
统一命名约定的价值在于:让代码库保持一致,降低思考"某个变量应该用哪种大小写风格"的认知开销(reduces overhead when thinking about the name case of a variable)。这是一个典型的团队协作与代码可维护性收益——审查代码时不再需要纠结命名风格,一切有明确的、可机器检查的规则。
从源码结构看,规则定义在语义分析器(semantic analyzer)分组中:crates/rome_js_analyze/src/semantic_analyzers/nursery/use_naming_convention.rs,并通过 nursery.rs 汇总注册。查询类型(Query)为Semantic<AnyIdentifierBindingLike>,即它会基于语义模型遍历所有"可命名的绑定节点"(AnyIdentifierBindingLike 联合类型),包括:
JsIdentifierBinding(JS 标识符绑定)JsLiteralMemberName(字面量成员名)JsPrivateClassMemberName(类私有成员名)JsLiteralExportName(字面量导出名)TsIdentifierBinding(TS 标识符绑定)TsTypeParameterName(TS 类型参数名)
这意味着该规则不是简单的文本匹配,而是基于 Rome 的语义模型,能正确区分局部变量、顶层变量、类成员、类型成员等不同"身份",再为每种身份套用不同的命名规范。
命名规范总览:各标识符类型的默认约定
规则的通用前提是:所有名字都可以带前缀和后缀的下划线_和美元符号$(All names can be prefixed and suffixed by underscores_and dollar signs$)。例如_unusedParam或value$都是被允许的——下划线和$只作为装饰性前缀/后缀,真正参与大小写判定的是去掉它们之后的"核心名"。
在源码中,这一逻辑由trim_underscore_dollar函数实现(use_naming_convention.rs),它会把名字两侧的_和$全部剥离后再进行 Case 识别。
下面按标识符类型逐一展开默认约定(对应文档正文与源码中Named::allowed_cases的实现)。
变量名(Variable names)
所有变量,包括函数参数(function parameters)和 catch 参数(catch parameters),都必须是camelCase。
额外的例外规则:顶层变量(top-level variables)如果声明为const或var,可以放宽为CONSTANT_CASE或PascalCase。所谓"顶层"指在模块或脚本层级声明的变量;在 TypeScript 的module或namespace中声明的变量也被视为顶层变量。
文档给出的完整合法示例:
function f(param, _unusedParam) { let localValue = 0; try { /* ... */ } catch (customError) { /* ... */ } } export const A_CONSTANT = 5; export const Person = class {} let aVariable = 0; export namespace ns { export const ANOTHER_CONSTANT = ""; }注意其中几个细节:
_unusedParam以下划线开头,属于合法前缀,核心名unusedParam是 camelCase;A_CONSTANT是顶层const,允许 CONSTANT_CASE;Person是顶层const,允许 PascalCase(常用于类表达式的赋值);let aVariable是顶层let,不享受顶层例外(文档和源码中只有顶层const/var允许额外风格),因此只能是 camelCase。
从源码看,变量命名判定由Named::from_variable_declarator完成:它会向上查找最近的"控制流根"(AnyJsControlFlowRoot),如果是JsModule或JsScript则视为顶层;再依据JsVariableKind(Const/Let/Var/Using)与是否顶层组合出TopLevelConst、TopLevelLet、TopLevelVar、LocalConst、LocalLet、LocalVar、LocalUsing等细分命名类别,并分别给出允许的 Case 集合:
| 类别 | 允许的 Case |
|---|---|
| 局部 const/let/var/using | camelCase |
| 顶层 let | camelCase |
| 顶层 const / 顶层 var | camelCase、PascalCase、CONSTANT_CASE |
| 函数参数 / catch 参数 | camelCase |
测试用例 invalidLocalVariable.js 中,函数内部的const X、const PascalCaseConst、let PascalCaseLet、var PascalCaseVar、const CONSTANT_CASE_CONST等全部被判定为不合规,因为局部变量只允许 camelCase;其快照 invalidLocalVariable.js.snap 展示了实际诊断输出。
错误示例:
let a_value = 0;a_value是 snake_case,且是顶层let(不享受顶层例外),因此被诊断为 "Thistop-level letname should be incamelCase.",并给出建议名aValue与安全修复(Safe fix: Rename this symbol in camelCase):
function f(FirstParam) {}函数参数FirstParam是 PascalCase,不符合 camelCase 要求,建议改名为firstParam。
函数名(Function names)
function的名称允许camelCase或PascalCase。
function trimString(s) { /*...*/ } function Component() { return <div></div>; }trimString是 camelCase,Component是 PascalCase(典型的 React 组件命名),两者均合法。源码中Function类别的allowed_cases返回[Case::Camel, Case::Pascal],与文档一致。
TypeScriptenum名称
TypeScriptenum的名称必须是PascalCase。
enum成员默认也必须是PascalCase(与 TypeScript 编译器团队文档推荐的约定一致),但可以通过enumMemberCase选项修改成员的大小写要求(详见下文 Options 章节)。
enum Status { Open, Close, }类(Classes)
类的命名规范分为三层:
- 类名:
PascalCase; - 静态属性名与静态 getter 名:
camelCase或CONSTANT_CASE; - 类属性名与类方法名:
camelCase。
文档中的完整示例:
class Person { static MAX_FRIEND_COUNT = 256; static get SPECIAL_PERSON_INSTANCE() { /*...*/ } initializedProperty = 0; specialMethod() {} }这里MAX_FRIEND_COUNT是静态属性,允许 CONSTANT_CASE;SPECIAL_PERSON_INSTANCE是静态 getter,同样允许 CONSTANT_CASE;initializedProperty与specialMethod分别是普通属性与方法,要求 camelCase。
源码中,类成员通过Named::from_class_member结合static修饰符的检测,细分出Class、ClassGetter、ClassStaticGetter、ClassMethod、ClassStaticMethod、ClassProperty、ClassStaticProperty、ClassSetter、ClassStaticSetter、IndexParameter等十余种类别,allowed_cases的映射为:
| 类成员类别 | 允许的 Case |
|---|---|
| 类名 | PascalCase |
| 静态属性 / 静态 getter | camelCase、CONSTANT_CASE |
| 静态方法 / 静态 setter | camelCase |
| 普通属性 / 方法 / getter / setter | camelCase |
| 索引签名参数(IndexParameter) | camelCase |
注意:静态 setter 与静态方法只允许 camelCase(静态 getter 和静态属性才额外允许 CONSTANT_CASE),这一点在 allowed_cases 实现 中有明确区分。
TypeScripttype别名与interface
type别名和interface名称:PascalCase;- type/interface 中的属性名与方法名:
camelCase或CONSTANT_CASE; readonly属性名和 getter 名:可以额外使用CONSTANT_CASE。
type Named = { readonly fullName: string; specialMethod(): void; }; interface Named { readonly fullName: string; specialMethod(): void; } interface PersonConstructor { readonly MAX_FRIEND_COUNT: number; get SPECIAL_PERSON_INSTANCE(): Person; new(): Person; }其中PersonConstructor接口展示了两个细节:readonly MAX_FRIEND_COUNT是 readonly 属性,允许 CONSTANT_CASE;get SPECIAL_PERSON_INSTANCE()是 getter,同样允许 CONSTANT_CASE。new(): Person是构造签名,其本身不涉及属性命名检查。
错误示例:
type person = { fullName: string };type别名person是 camelCase,不满足 PascalCase 要求,诊断为 "Thistype aliasname should be inPascalCase.",建议改为Person,并附带安全修复。
源码中,type 成员通过Named::from_type_member处理,并特别检查readonly_token()是否存在以区分TypeProperty与TypeReadonlyProperty;只有后者(以及TypeGetter)的允许集合才是[Case::Camel, Case::Constant],普通属性TypeProperty只允许[Case::Camel]。
字面量对象属性与方法名(Literal object property and method names)
字面量对象的属性名和方法名必须是camelCase。
const alice = { fullName: "Alice", }错误示例:
const alice = { FULL_NAME: "Alice", }FULL_NAME是 CONSTANT_CASE,不满足对象属性的 camelCase 要求,诊断为 "Thisobject propertyname should be incamelCase.",建议改名为fullName。(注意这个例子没有显示 FIXABLE 标记,说明对象属性名的修复能力与变量不同——源码中JsLiteralMemberName类节点不进入AnyJsRenamableDeclaration分支,因此不生成安全修复 action。)
导入与导出的模块别名(Imported and exported module aliases)
模块命名空间别名(namespace import/export)必须是camelCase:
import * as myLib from "my-lib"; export * as myLib from "my-lib";import/export别名(具名导入导出时的as重命名)允许camelCase、PascalCase或CONSTANT_CASE:
import assert, { deepStrictEqual as deepEqual, AssertionError as AssertError } from "node:assert";其中deepEqual是 camelCase 别名,AssertError是 PascalCase 别名,均合法。
错误示例:
import * as MyLib from "my-lib";MyLib是 PascalCase 的命名空间别名,不符合 camelCase 要求,诊断为 "Thisimport namespacename should be incamelCase.",建议改为myLib,并附带安全修复。
源码中,导入导出的判定通过Named::from_binding_declaration与Named::from_name中的JsLiteralExportName分支完成,细分出ImportNamespace(camelCase)、ImportAlias/ExportAlias(camel、Pascal、Constant 三种均可)、以及ImportSource/ExportSource(源码中这两类的allowed_cases返回空集合,即不检查,因为它们是模块路径字符串而非用户自定义标识符)。
TypeScript 类型参数名(Type parameter names)
TypeScript 类型参数名必须是PascalCase:
function id<Val>(value: Val): Val { /* ... */}源码中TsTypeParameterName直接映射为Named::TypeParameter,允许集合为[Case::Pascal]。
TypeScriptnamespace名称
namespace名称允许camelCase或PascalCase:
namespace mathExtra { /*...*/ } namespace MathExtra { /*...*/ }Options:规则的两条可配置项
useNamingConvention提供两条选项,可通过rome.json中规则级别的options字段配置(与 linter/index.mdx 中 "Rule options" 章节描述的方式一致)。完整示例:
{ "//": "...", "options": { "strictCase": false, "enumMemberCase": "CONSTANT_CASE" } }源码层面,选项类型为NamingConventionOptions,位于 use_naming_convention.rs,通过rome_deserialize的 JSON 访问器(VisitNode<JsonLanguage>)解析。它只接受两个已知键strictCase与enumMemberCase(KNOWN_KEYS常量),未知键会触发诊断——例如 naming_convention_incorrect_options.json 中的strictCaseTYPO就是刻意构造的非法选项测试。
strictCase
- 设为
true时,禁止camelCase与PascalCase中出现连续大写字符。例如HTTPServer或aHTTPServer都会报错,应改名为HttpServer和aHttpServer。 - 设为
false时,允许连续大写字符,HTTPServer和aHTTPServer均视为合法。 - 默认值:
true。
这一行为在 case.rs 的Case::identify(value, strict)中有精确实现:当strict为true且检测到连续两个大写字符时,camelCase/PascalCase判定直接返回Case::Unknown(不属于任何合法 Case,从而触发规则诊断)。测试快照 validClassNonStrictPascalCase.options.json 对应的用例即验证了strictCase: false场景下HTTPSServer之类的 PascalCase 名称可以通过检查。
另外值得注意的 Case 识别细节(来自 case.rs 的文档注释与单元测试):
- 数字被视为既不大写也不小写,因此
V8_ENGINE属于 CONSTANT_CASE,V8Engine属于 PascalCase; Case::identify("aHTTPServer", true)返回Unknown,而Case::identify("aHTTPServer", false)返回Camel;- 单字母如
T、T1被识别为NumberableCapital(可数字化大写),它与Constant、Pascal、Upper均兼容,这也是为什么const X顶层常量这样的短名字能够通过检查。
enumMemberCase
- 默认行为:遵循 TypeScript 编译器团队 的约定,
enum成员必须是PascalCase。 - 可以通过
enumMemberCase改为其他约定,支持的值:PascalCase、CONSTANT_CASE、camelCase。
源码中该选项类型为EnumMemberCase枚举,其合法取值集合(KNOWN_VALUES)为["camelCase", "CONSTANT_CASE", "PascalCase"],解析时通过with_only_known_variants校验,传入非法值(如测试文件 malformedOptions.options.json 中的"snake_case")会产生配置诊断错误。
测试用例 validEnumMemberConstantCase.ts 配合 validEnumMemberConstantCase.options.json 展示了实际效果:在配置"enumMemberCase": "CONSTANT_CASE"后,enum Status { OPEN, CLOSE }这样的 CONSTANT_CASE 成员即被视为合法;同理,validEnumMemberCamelCase.options.json 对应 camelCase 成员场景。
规则是如何工作的:源码实现要点
从 AST 节点到"命名类别"
规则的核心流程(Rule::run)如下:
- 拿到语义查询节点
AnyIdentifierBindingLike; - 通过
Named::from_name(node)推断出该节点的命名类别(如TopLevelLet、ClassStaticGetter、TypeReadonlyProperty等 40 余种); - 调用
element.allowed_cases(options)取得允许的 Case 集合;若集合为空(如导入/导出源名),直接跳过; - 读取名字文本,先用
is_js_ident过滤非标识符字符串,再用trim_underscore_dollar去掉首尾_/$; - 调用
Case::identify(trimmed_name, options.strict_case)识别实际 Case; - 若实际 Case 与任一允许 Case 兼容(
is_compatible_with),则通过;否则生成诊断状态。
Named::from_name的分派逻辑非常细致:JsIdentifierBinding/TsIdentifierBinding走from_binding_declaration(依据绑定声明类型区分变量、参数、catch、函数、类、接口、枚举、命名空间、导入别名等);JsLiteralMemberName则根据其父节点分别走类成员、类型成员、对象成员、枚举成员四套分派。
兼容性判断(Case 是超集关系)
Case::is_compatible_with不是简单的相等比较,而是大小写风格之间的包含关系:
lowercase(如httpserver)兼容 camelCase、kebab-case、snake_case;NumberableCapital(如T、T1)兼容 CONSTANT_CASE、PascalCase、UPPERCASE;UPPERCASE(如HTTPSERVER)兼容 CONSTANT_CASE;- 任意 Case 都与自身及
Unknown兼容。
这意味着const X = 0(单个大写字母,属于NumberableCapital)作为顶层常量时,能够通过 CONSTANT_CASE 检查;而纯小写单字母变量也天然兼容 camelCase。
诊断消息与安全修复
规则产生的诊断(Rule::diagnostic)包含三部分信息:
- 主消息:如 "Thistop-level letname should be incamelCase."(当名字带有
_/$装饰时会追加 "trimmed asxxx" 提示); - note:如 "The name could be renamed to
aValue."; - Safe fix(
Rule::action):类别为ActionCategory::QuickFix、适用性为Applicability::Always的安全修复,消息为 "Rename this symbol incamelCase."
修复时会取允许集合中的首选 Case(列表第一个)调用Case::convert生成新名字。需要特别注意的是,修复不是对所有节点都生效:只有JsIdentifierBinding/TsIdentifierBinding绑定才会进入AnyJsRenamableDeclaration重命名分支,且导出的绑定(is_exported)与 TypeScript 属性参数(TsPropertyParameter)会被排除——导出符号的改名会影响公共 API,Rome 选择不自动修复。因此文档中export const A_CONSTANT这类例子即使触发诊断,也只会给出建议而不会自动重命名。
从测试快照 invalidLocalVariable.js.snap 可以看到真实的诊断渲染:局部const X被诊断为 "This local const name should be in camelCase.",附带建议名x与修复后的代码行对比(const X = 0→const x = 0)。
配置与启用方式
由于recommended: false,该规则需要显式启用。在rome.json中,useNamingConvention位于nursery分组(新规则先在 nursery 中孵化),配置示例:
{ "linter": { "rules": { "nursery": { "useNamingConvention": { "level": "warn", "options": { "strictCase": true, "enumMemberCase": "PascalCase" } } } } } }level可设为"warn"、"error"或"off"(参见 linter/index.mdx 中 "Enable a lint rule" 与 "Change the diagnostic severity" 章节)。规则在服务端配置结构中注册于 linter/rules.rs,与 JSON Schema(configuration_schema.json)联动,编辑器与 CLI 都能获得完整的配置提示与校验。
测试目录 crates/rome_js_analyze/tests/specs/nursery/useNamingConvention/ 中提供了 90+ 个用例(valid/invalid 成对出现,覆盖类成员、枚举、对象、类型、导入导出、命名空间、索引参数、属性参数等全部类别),可以作为该规则实际行为的权威参考;malformedOptions与invalid/naming_convention_incorrect_options则验证了非法选项的报错路径。
相关链接
- 禁用某条规则(Disable a lint rule)
- 规则选项配置(Rule options)
- 规则源码:use_naming_convention.rs
- Case 判定与转换工具:case.rs
【免费下载链接】toolsUnified developer tools for JavaScript, TypeScript, and the web项目地址: https://gitcode.com/gh_mirrors/to/tools
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考