Sentry 前端设计系统 ESLint 规则开发指南:基于 eslintPluginScraps 的规则创建、测试与自动修复全流程
【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry
导读
本文是一份面向 Sentry 开源仓库前端团队的实战指南,围绕仓库内lint-new技能文档(.agents/skills/lint-new/SKILL.md)及其三份参考文档(rule-archetypes、schema-patterns、style-collector-guide)展开,系统讲解如何在eslintPluginScraps插件中从零创建一个带测试、可注册、支持自动修复(autofix)的 ESLint 规则。读者将掌握:如何按意图选择合适的规则原型(Archetype)、如何复用插件内置的 AST 分析工具、如何编写规则与测试骨架、何时实现 autofix、以及如何完成规则注册与配置驱动的规则扩展,从而为 Sentry 的设计系统(@sentry/scraps)维护一套可持续演进的样式与结构约束。
一、背景:eslintPluginScraps 是什么
在 Sentry 仓库中,前端采用 Emotion(CSS-in-JS)进行样式开发,并逐步迁移到@sentry/scraps核心组件与语义化 token 体系(参见 static/AGENTS.md 中的 Design System 规范:布局用Flex/Grid/Stack/Container,排版用Text/Heading,禁用手写display: flex/grid等)。
eslintPluginScraps就是承载这些约束的 ESLint 插件,位于 static/oxlint/eslintPluginScraps/。从规则索引 static/oxlint/eslintPluginScraps/src/rules/index.ts 可以看到当前已内置 7 条规则:
| 规则名 | 职责方向 |
|---|---|
no-core-import | 禁止从sentry/components/core导入,自动改写为@sentry/scraps |
no-double-dollar-interpolation | 模板字符串中的双重$$插值检测 |
no-token-import | 禁止直接导入 token |
prefer-info-text | 用InfoTip/InfoText替代裸Tooltip |
prefer-stack-for-column-flex | 优先用Stack替代display: flex列布局 |
restrict-jsx-slot-children | 限制特定 prop 槽位中允许出现的 JSX 元素 |
use-semantic-token | 强制theme.tokens.*只能配合语义匹配的 CSS 属性使用 |
lint-new技能描述明确其适用场景:"create a lint rule"、"add an eslint rule"、"scaffold a rule"、"write a new scraps rule"、"new design system lint rule",即当开发者需要为设计系统新增一条约束时,按本文档流程操作。
二、规则开发工作流总览
SKILL.md给出的核心指令是:在eslintPluginScraps插件中创建一条名为$ARGUMENTS的新 ESLint 规则。完整流程分四步,外加 autofix、扩展、命名三个专项主题:
- 选择规则原型(Archetype)——决定 AST 访问策略;
- 检查共享工具——复用插件内置的 AST 工具,避免重复造轮子;
- 创建规则文件与测试——套用模板生成
规则.ts与规则.spec.ts; - 注册规则——加入规则索引与 ESLint 配置并验证。
下面逐节展开。
三、第一步:选择规则原型(Archetype)
在编写任何 AST 遍历逻辑之前,先阅读 references/rule-archetypes.md,根据规则意图匹配对应的原型。该文档提供了一张决策表:
| 你想做的事 | 原型(Archetype) | 关键模式 | 示例规则 |
|---|---|---|---|
| 重写导入路径 | Import rewrite | ImportDeclaration访问器 +fixer.replaceText(node.source, ...) | no-core-import |
| 校验 token/值可用于哪些 CSS 属性 | Property validation | createStyleCollector+Program:exit延迟校验 | use-semantic-token |
| 限制特定 prop 中的 JSX 元素 | JSX structural | 导入追踪 + 递归 JSX 树遍历 + 配置 schema | restrict-jsx-slot-children |
| 检测静态 CSS 文本中的模式 | Template text analysis | TaggedTemplateExpression→ 遍历quasi.quasis静态文本 | no-dom-coupling |
文档强调:每种原型决定了应使用哪些 AST 访问器、哪些共享工具适用,以及哪些模式不适用于该方案。写代码前必须读完对应参考。
原型 1:Import Rewrite(导入重写)
适用场景:规则检查 import 来源并重写之。模式:单个ImportDeclaration访问器,autofix 替换 source 字符串:
create(context) { return { ImportDeclaration(node) { const importPath = node.source.value; if (typeof importPath === 'string' && importPath.startsWith(FORBIDDEN)) { context.report({ node, messageId: '...', fix(fixer) { return fixer.replaceText(node.source, `'${newPath}'`); }, }); } }, }; }Autofix 安全性:几乎总是安全的——修复只是修改一个字符串字面量。边界情况:类型导入(import type)、混合命名导入、re-export 都被自动处理,因为只替换 source 路径字符串。
仓库中的规范实现是 noCoreImport.ts(SKILL 文档中称为no-core-import.ts,实际仓库文件名采用 camelCase)。它定义FORBIDDEN_PATH = 'sentry/components/core/'、REPLACEMENT_PATH = '@sentry/scraps',在fix中还会解析子路径并保留一级组件名(importPath.split('/')[3]),将sentry/components/core/foo改写为@sentry/scraps/foo,说明导入重写不仅限于"改前缀",还可以做路径结构的映射。
原型 2:Property Validation(属性校验,基于 Style Collector)
适用场景:校验动态值(theme token、变量)被用在哪些 CSS 属性上。核心思路:两阶段法——遍历时收集,遍历后校验:
createStyleCollector(context)返回{collector, visitors},把visitors展开进规则的返回对象;- 在
Program:exit中遍历collector.getAll()校验每个StyleDeclaration; - 结束时调用
collector.clear()做清理。
create(context) { if (!shouldAnalyze(context)) return {}; // 快速退出 const {collector, visitors} = createStyleCollector(context); return { ...visitors, 'Program:exit'() { for (const decl of collector.getAll()) { // decl.property.name — CSS 属性(已归一化) // decl.values — 数组 {rawNode, tokenInfo: {tokenPath, node}} validateDeclaration(decl); } collector.clear(); }, }; }关键要点:
- collector 只处理模板字面量中的插值表达式(
${...}部分),不分析quasis 中的静态 CSS 文本。若需检测静态文本本身的模式(如裸十六进制颜色、嵌套选择器),应改用原型 4。 - 配置驱动规则:若校验规则随类别变化,把映射放进
src/config/并从那里加载(参见tokenRules.ts),这样新增类别无需改动规则逻辑。 shouldAnalyze:始终作为快速预扫描的退出条件。它通过正则检查 Emotion 导入/使用模式,跳过明显不使用 styled-components 的文件。
仓库中的规范实现是 useSemanticToken.ts:在create()首行调用shouldAnalyze(context)快速退出;通过createStyleCollector收集后,在Program:exit中对每条声明做校验:跳过--开头的 CSS 自定义属性,通过findRuleForToken(tokenPath)找到 token 所属规则,若属性不在该规则的allowedProperties内,则依据PROPERTY_TO_RULE反向映射给出带建议类别的错误消息(invalidPropertyWithSuggestion),否则给出invalidProperty。
原型 3:JSX Structural Constraint(JSX 结构约束)
适用场景:限制特定 prop 或槽位中可以出现哪些 JSX 元素。模式:用createImportTracker(来自 src/ast/tracker/imports.ts)做导入解析,配合JSXAttribute访问器做树遍历:
createImportTracker()—— 合并其visitors,再用resolve(localName)或findLocalNames(source, name)检查导入;JSXAttribute—— 发现配置的 prop 后,递归遍历 JSX 树,逐个检查元素是否属于允许集合。
create(context) { const importTracker = createImportTracker(); return { ...importTracker.visitors, JSXAttribute(node) { // 用 importTracker.resolve(displayName) 检查元素来源 // 用 importTracker.findLocalNames(source, name) 查找本地别名 }, }; }关键模式:
- 处理导入别名:
import {Foo as Bar}中Bar是本地名——importTracker.resolve('Bar')返回{source, imported: 'Foo'}; - 处理成员表达式:
MenuComponents.Alert必须匹配${localName}.${member}; - 递归遍历:直接 JSX 子节点、三元表达式、逻辑表达式(
&&、||、??)、JSXExpressionContainer、JSXFragment、箭头函数表达式体; - 跳过
React.Fragment/<Fragment>(透明包装器); - 在不允许的元素处停止递归(上报后返回)。
Schema:使用带嵌套数组的复杂 options schema,完整模式参考restrict-jsx-slot-children规则。Autofix:一般不安全——替换 JSX 元素需要理解组件 API,超出了 AST 单独能提供的信息。
原型 4:Template Text Analysis(模板文本分析)
适用场景:检测模板字面量静态 CSS 文本中的模式(而非插值表达式)。模式:使用createQuasiScanner(SKILL 文档指向 src/ast/scanner/index.ts),它内部替你处理了shouldAnalyze退出、通过getStyledCallInfo做标签检测、以及 quasi 迭代:
import {createQuasiScanner} from '../ast/scanner/index'; create(context) { return createQuasiScanner(context, (cssText, quasi, info) => { // cssText: 该 quasi 段的静态 CSS 文本 // quasi: TemplateElement 节点(用于错误上报) // info: { kind: 'element' | 'component' | 'css', name?: string } for (const match of cssText.matchAll(MY_PATTERN)) { context.report({ node: quasi, messageId: '...' }); } }); }扫描器会对文件中每个 styled/css 标签模板的每个 quasi 元素调用analyze回调,并自动跳过没有 Emotion 使用的文件。
原型 2 vs 原型 4 的选择:若在 CSS文本本身中找模式(原始颜色、嵌套选择器、属性名),用createQuasiScanner;若校验通过插值(${theme.tokens.X})传给 CSS 属性的值,用createStyleCollector。
标签检测工具:getStyledCallInfo(node)(来自 src/ast/utils/styled.ts)把任意TaggedTemplateExpression或CallExpression分类为{kind: 'element', name}、{kind: 'component', name}、{kind: 'css'}或null,覆盖styled.div、styled('div')、styled(Component)、styled(Component).attrs(...)及css模式。扫描器内部使用它,也可在自定义访问器中直接调用。
需要说明的是,当前仓库ast/目录实际包含extractor/、tracker/、utils/三个子目录(见 static/oxlint/eslintPluginScraps/src/ast/),scanner/目录与no-dom-coupling规则在本仓库快照中尚未出现,SKILL 文档中的描述可视为该原型的既定设计目标,实现落地时以实际代码为准。
四、第二步:检查共享 AST 工具
在编写 AST 遍历逻辑之前,先检查 src/ast/ 下是否有可复用代码。SKILL.md给出了完整工具清单:
| 工具 | 位置 | 用途 |
|---|---|---|
getStyledCallInfo | src/ast/utils/styled.ts | 将 styled/css 调用分类为 element、component 或 css |
createQuasiScanner | src/ast/scanner/index.ts | 扫描模板字面量中的静态 CSS 文本(原型 4) |
createImportTracker | src/ast/tracker/imports.ts | 解析本地名称从何处导入 |
createStyleCollector | src/ast/extractor/index.ts | 收集 CSS-in-JS动态值声明(非静态文本) |
shouldAnalyze | src/ast/extractor/index.ts | 快速预扫描,跳过无 Emotion 使用的文件 |
normalizePropertyName | src/ast/utils/normalizePropertyName.ts | 归一化 CSS 属性名 |
decomposeValue | src/ast/extractor/value-decomposer.ts | 把复杂表达式分解为所有可能的值 |
| Theme tracker | src/ast/tracker/theme.ts | 追踪useTheme()与回调式 theme 绑定 |
复用原则:如果已有规则解决了类似问题,把共享逻辑提取到src/ast/utils/并复用,而不是复制粘贴。
从源码看,extractor/index.ts 是这套工具的核心聚合点:createStyleCollector内部先创建createThemeTracker()(extractor 依赖它),再创建createStyledExtractor()(处理styled.div\...`与styled(X)`...`)、createCssPropExtractor()(处理css={}与css`...`prop)、createStylePropExtractor()(处理style={{}}prop),最后通过mergeVisitors合并四份 visitor 并返回{collector, visitors, themeTracker}。shouldAnalyze的正则预扫描同时检查@emotion/styled、@emotion/react导入与useTheme、styled[.(、`` css[({] ``、css=/style=等使用模式。
五、第三步:创建规则文件与测试
文件清单
- 规则文件:
static/oxlint/eslintPluginScraps/src/rules/$ARGUMENTS.ts - 测试文件:
static/oxlint/eslintPluginScraps/src/rules/$ARGUMENTS.spec.ts
规则模板
import {ESLintUtils} from '@typescript-eslint/utils'; export const $RULE_NAME = ESLintUtils.RuleCreator.withoutDocs({ meta: { type: 'problem', docs: { description: '[Rule description]', }, fixable: 'code', // 若规则有 autofix 则包含——参见 Autofix 指导 schema: [], messages: { forbidden: 'Error message shown to user', }, }, create(context) { return { // AST 访问器方法——参见所选原型 }; }, });如果规则需要可配置选项,加载 references/schema-patterns.md(详见下文第八节)。
测试模板
import {RuleTester} from '@typescript-eslint/rule-tester'; import {$RULE_NAME} from './$ARGUMENTS'; const ruleTester = new RuleTester(); ruleTester.run('$ARGUMENTS', $RULE_NAME, { valid: [ { code: '// valid code', filename: '/project/src/file.tsx', }, ], invalid: [ { code: '// invalid code', filename: '/project/src/file.tsx', errors: [{messageId: 'forbidden'}], output: '// expected output after autofix', // 可修复规则必填 }, ], });运行测试
pnpm test-ci "static/oxlint/eslintPluginScraps/src/rules/$ARGUMENTS.spec.ts"仓库既有规则的测试采用与规则同名同目录的.spec.ts文件组织(如 noCoreImport.spec.ts、useSemanticToken.spec.ts),AST 工具层也有独立测试(如 imports.spec.ts、styled.spec.ts),可对照学习。
六、Autofix 实现指南
默认立场:实现 autofix,除非转换存在歧义或可能改变运行时行为。
安全的 autofix 模式
- 导入路径重写(以
no-core-import.ts为规范示例); - 添加/移除已知值的 JSX 属性;
- 用已知组件包裹表达式;
- 无遮蔽风险的标识符重命名。
禁止 autofix 的场景
- 存在多个合理修复方案、正确选择需要人工判断;
- 修复需要 AST 无法提供的类型信息;
- 转换会改变控制流或运行时行为;
- 修改跨越多个文件。
Fixer API
context.report({ node, messageId: 'forbidden', fix(fixer) { return fixer.replaceText(node, newText); // 还有:fixer.replaceTextRange([start, end], text) // fixer.insertTextBefore(node, text) // fixer.insertTextAfter(node, text) // fixer.remove(node) // 返回单个修复或修复数组 }, });铁律:规则一旦可修复,每个 invalid 测试用例都必须包含output,声明修复后的期望代码。仓库中的noCoreImport正是这样做的:meta.fixable: 'code',并在fix回调里用fixer.replaceText(node.source, ...)改写导入源字符串。
七、第四步:注册规则
1. 规则索引
在 src/rules/index.ts 中注册:
import {$RULE_NAME} from './$ARGUMENTS'; export const rules = { // existing rules... $ARGUMENTS: $RULE_NAME, };对照现有索引可以看到,插件采用"kebab-case 注册名 → camelCase 导出"的映射方式(如'no-core-import': noCoreImport、'use-semantic-token': useSemanticToken),新增规则照此模式追加即可。
2. ESLint 配置
在eslint.config.ts的name: 'plugin/@sentry/scraps'配置块中添加:
'@sentry/scraps/$ARGUMENTS': 'error', // 或带选项: '@sentry/scraps/$ARGUMENTS': ['error', { /* options */ }],3. 验证
pnpm test-ci "static/oxlint/eslintPluginScraps/src/rules/$ARGUMENTS.spec.ts"八、规则选项 Schema 模式
当规则需要可配置选项时,按 references/schema-patterns.md 选择匹配的 schema 形态。
无选项(默认)
大多数规则无需选项,使用空 schema。注意类型参数用never[]而非[]——[]会违反@typescript-eslint/no-restricted-types:
ESLintUtils.RuleCreator.withoutDocs<never[], MessageIds>({ meta: { schema: [] }, defaultOptions: [], create(context) { ... }, });简单选项:字符串数组
适合"可配置启用特性集合"的规则:
interface Options { enabledCategories?: string[]; } ESLintUtils.RuleCreator.withoutDocs<[Options], MessageIds>({ meta: { schema: [{ type: 'object', properties: { enabledCategories: { type: 'array', items: { type: 'string' }, }, }, additionalProperties: false, }], }, defaultOptions: [{}], create(context, [options = {}]) { const enabled = options.enabledCategories ? new Set(options.enabledCategories) : null; // null = 全部启用 ... }, });配置写法:'@sentry/scraps/rule-name': ['error', {enabledCategories: ['background', 'border']}]。
这正是use-semantic-token采用的形式:enabledCategories为null时全部启用,否则仅校验集合内的类别(见 useSemanticToken.ts 中的isCategoryEnabled)。
复杂选项:嵌套配置
适合槽位限制这类结构化配置:
interface Options { slots: Array<{ propNames: [string, ...string[]]; allowed: Array<{ source: string; names: [string, ...string[]]; }>; componentNames?: string[]; }>; }Schema 与 TypeScript 接口一一对应(propNames、allowed[].source、allowed[].names、componentNames均需minItems: 1,对象用required+additionalProperties: false收紧)。
配置驱动规则(Config-Driven)
当校验逻辑通用、只有数据随类别变化时,把配置拆到独立文件:
src/config/tokenRules.ts ← 类别定义、属性映射 src/rules/use-semantic-token.ts ← 通用校验逻辑这样新增类别只需改配置、不动规则逻辑。仓库中的 tokenRules.ts 正是如此:它是 token 检测关键词、属性校验白名单、autofix 建议反向映射的单一事实来源(SINGLE SOURCE OF TRUTH),并采用"最具体者胜"的匹配策略——当多个关键词命中时,路径中最深/最后的关键词胜出(如interactive.border.content命中 content 规则)。
反向映射模式(Reverse Mapping)
配置是"类别 → 允许属性"时,通常还需要反向的"属性 → 期望类别"用于错误消息:
function buildPropertyToRule(rules: TokenRule[]) { const result = new Map<string, string>(); for (const rule of rules) { for (const property of rule.allowedProperties) { result.set(property, rule.name); // Last writer wins } } return result; }顺序很重要:当多个类别共享同一属性(如box-shadow同时属于focus和shadow),数组中最后一个类别赢得反向映射,这决定了错误消息中建议哪个类别。新增类别时务必注意此副作用。
九、扩展现有规则
如果修改现有规则而非新建,按以下步骤:
- 先读现有规则及其配置文件,理解架构;
- 配置驱动规则(如
use-semantic-token):改动通常只需编辑配置文件(如src/config/tokenRules.ts),无需动规则逻辑; - 警惕反向映射副作用——新增类别可能改变共享属性建议的类别(
buildPropertyToRule中"后写者胜"); - 为任何行为变更更新现有测试,然后补充新测试用例。
十、命名规范
SKILL.md明确了命名三件套:
- 规则名(kebab-case):
my-rule-name,采用"动词-名词"模式(如no-token-import、use-semantic-token); - 导出名(camelCase):
myRuleName; - 文件名:与规则名完全一致(
my-rule-name.ts、my-rule-name.spec.ts)。
需要说明的是,当前仓库既有的 7 条规则实际采用了 camelCase 文件名(如noCoreImport.ts、useSemanticToken.ts),但注册名仍为 kebab-case。新增规则时建议遵循 SKILL 文档规范,并保持与rules/index.ts注册映射的一致性。
十一、Style Collector 深入指南
references/style-collector-guide.md 是原型 2 的配套深度文档。
适用边界
使用createStyleCollector:校验 theme token 用于哪些 CSS 属性、检查插值值是否符合期望类型/类别、分析 CSS 属性与动态值的关系。不要使用它来:检测静态 CSS 文本模式(改用createQuasiScanner)、检查导入路径(用ImportDeclaration访问器)、限制 JSX 元素用法(用 JSX 树遍历 +createImportTracker)。
架构
File: src/ast/extractor/index.ts createStyleCollector(context) ├── createThemeTracker() ← 追踪 useTheme() / 回调式 theme 绑定 ├── createStyledExtractor() ← 处理 styled.div`...` 和 styled(X)`...` ├── createCssPropExtractor() ← 处理 css={} 和 css`...` prop └── createStylePropExtractor() ← 处理 style={{}} prop 返回: { collector, visitors, themeTracker }Collector 捕获的内容
collector.getAll()中的每条StyleDeclaration:
interface StyleDeclaration { property: { name: string; // 归一化后的 CSS 属性(如 'background-color') node: TSESTree.Node; // 属性名 AST 节点 }; values: Array<{ rawNode: TSESTree.Node; tokenInfo?: { tokenPath: string; // 如 'content.primary'、'border.secondary' node: TSESTree.Node; // token 访问的 AST 节点 }; }>; }Collector 不捕获的内容
- 模板字面量 quasis 中的静态文本(非插值部分);
- 只出现在静态文本中、无动态值的 CSS 属性名;
- 注释、空白、格式。
为何延迟校验
一个 styled 块中,属性与值可能分布在多个 AST 节点上(template quasis + expressions),collector 先聚合全部信息,再在Program:exit校验完整图景。collector.clear()必调——为下一个文件做清理。
常见陷阱:Collector 与静态文本
头号错误:需要分析静态 CSS 文本时却用了createStyleCollector:
const Box = styled.div` color: #ff0000; ← 这是 quasi 中的静态文本——collector 看不到 background: ${p => p.theme.tokens.background.primary}; ← 这个会被捕获 `;检测原始十六进制颜色、嵌套选择器等文本本身的模式时,改用createQuasiScanner(见 rule-archetypes.md 的 "Template Text Analysis" 原型)。
十二、实战流程速查
把以上内容压缩为一份可执行清单:
- 定意图→ 查决策表选 Archetype(1 导入重写 / 2 属性校验 / 3 JSX 结构 / 4 模板文本);
- 查工具→ 遍历
src/ast/确认可复用工具,有相似规则则提取共享逻辑; - 建文件→ 用规则模板与测试模板创建
rules/$name.ts与rules/$name.spec.ts,可配置则按 schema-patterns 三选一; - 写 autofix→ 默认实现,遵守安全/禁止清单,invalid 用例必填
output; - 注册→
rules/index.ts添加导出,eslint.config.ts的plugin/@sentry/scraps块中置为'error'; - 验证→
pnpm test-ci "static/oxlint/eslintPluginScraps/src/rules/$name.spec.ts"; - 维护→ 配置驱动规则优先改
src/config/,留意反向映射"后写者胜"副作用,同步更新测试。
遵循这套流程,开发者即可为 Sentry 设计系统持续沉淀新的 lint 约束,将设计规范转化为可自动化执行的工程质量防线。
【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考