Sentry eslintPluginScraps 的 Style Collector 指南:用 createStyleCollector 编写 CSS-in-JS 语义 lint 规则
【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry
本文聚焦 Sentry 仓库内置的设计系统 ESLint 插件eslintPluginScraps(源码位于 static/oxlint/eslintPluginScraps),深度讲解其核心共享工具Style Collector(createStyleCollector)的架构、数据结构与两阶段使用模式。读者将掌握如何编写一类特殊规则——不是检查静态 CSS 文本,而是校验styled.div\...`、css={}、style={}中**通过插值传入的动态值**(尤其是theme.tokens.*语义化 token)与 CSS 属性的搭配关系,并清楚其与createQuasiScanner` 等静态文本分析工具的边界。
1. Style Collector 是什么、何时该用
1.1 定位与出处
style-collector-guide.md是.agents/skills/lint-new技能(SKILL.md)为"新增一条 lint 规则"流程准备的参考资料之一。技能的核心流程是:
- 阅读 rule-archetypes.md,选择匹配你意图的archetype(原型);
- 检查 src/ast 下可复用的共享工具,避免重复实现 AST 遍历;
- 按模板创建
src/rules/$RULE_NAME.ts与其.spec.ts测试,注册规则并运行pnpm test-ci。
其中第四类 archetype"Property validation(属性校验)"就是 Style Collector 的主战场:你想校验"某个动态值(theme token、变量)被用在了哪个 CSS 属性上"。仓库中use-semantic-token规则是该 archetype 的规范范例。
1.2 适用场景:动态值分析
根据 style-collector-guide.md,当规则需要以下能力时使用createStyleCollector:
- 校验某个 theme token 被用于哪些 CSS 属性(如
theme.tokens.content.primary是否被错误地赋给background); - 检查插值值是否符合期望的类型或类别;
- 分析 CSS 属性与其动态值之间的关系。
这三条共同点:关注的永远是interpolation(插值表达式),也就是模板字符串${...}、css={{...}}对象属性值、style={{...}}值——而不是静态写死的 CSS 文本。
1.3 明确不适用:什么时候别用它
文档给出三条清晰的反向清单,违反它们正是新手最常见的错误:
| 你的需求 | 应该用 |
|---|---|
| 检测静态 CSS 文本中的模式(十六进制颜色、嵌套选择器等) | createQuasiScanner(src/ast/scanner/index.ts,见 skill 文档中 "Template Text Analysis" archetype) |
| 检查 import 路径 | ImportDeclarationvisitor(见no-core-import规则) |
| 限制 JSX 元素在特定 prop 中出现 | JSX 树遍历 +createImportTracker(见restrict-jsx-slot-children规则) |
从当前插件源码目录结构看,ast 下已落地的是extractor/、tracker/、utils/三组模块;scanner/尚属技能文档规划中面向未来静态文本扫描 archetype 的 API。写规则前先对照 SKILL.md 第 2 步的共享工具表做判断,是最省力的方式。
2. 架构:createStyleCollector 的内部构成
文档给出的架构图与实际源码 extractor/index.ts 完全吻合:
File: src/ast/extractor/index.ts (仓库真实路径见 static/oxlint/eslintPluginScraps/src/ast/extractor/) createStyleCollector(context) ├── createThemeTracker() ← 追踪 useTheme() / 回调式 theme 绑定 ├── createStyledExtractor() ← 处理 styled.div`...`、styled(X)`...`、css`...`、styled.div({...}) ├── createCssPropExtractor() ← 处理 JSX 上的 css={} / css`...` prop └── createStylePropExtractor() ← 处理 style={{}} prop 返回: { collector, visitors, themeTracker }关键实现细节值得逐一对照源码:
- 工厂顺序有依赖:
createStyleCollector先创建themeTracker,再把{collector, themeTracker, ruleContext}组装成ExtractorContext传给三个 extractor——因为 styled/css 提取器在分解表达式时需要询问 theme tracker"当前作用域里哪个变量是 theme"(见 index.ts 与 types.ts)。 - visitors 会被合并:
mergeVisitors把 theme tracker 与三个 extractor 返回的监听器按节点类型合并;同一节点类型存在多个 handler 时依次链式调用(见 index.ts)。这就是为什么使用方只需一行...visitors展开,就能同时获得所有提取能力。
Styled extractor 覆盖面最广。源码注释与遍历逻辑(styled.ts)显示它统一处理:
styled.div\...`/styled(Component)`...`(TaggedTemplateExpression` visitor);styled.div({...})/styled('div')({...})对象语法(CallExpressionvisitor);- 以及
css\...`` 标签模板。
而 cssProp.ts 面向 JSX 的cssprop(<div css={css\...`} />、css={{...}}、css={[...]}、css={(theme) => ({...})}),[styleProp.ts](https://link.gitcode.com/i/9ef7d0e14391501828caed200bd44146) 则覆盖<div style={{...}} />。四条采集路径最终都汇入同一个collector`。
3. Collector 捕获的数据模型
3.1 每个 StyleDeclaration 的形态
collector.getAll()返回的StyleDeclaration,是属性声明(property)与所有可能取值(values)的中间表示(IR)。style-collector-guide.md给出的是经过简化的视图,真实定义在 extractor/types.ts,信息更完整:
interface StyleDeclaration { context: StyleContext; // 来源文件、scopeId、当前生效的 themeBinding kind: 'styled' | 'css-prop' | 'style-prop' | 'theme'; // 声明出现在哪种样式上下文 property: { name: string; // 归一化后的 CSS 属性名,如 'background-color' node: TSESTree.Node; // 属性名 AST 节点(用于报错定位) }; raw: { containerNode: TSESTree.Node; // 承载容器节点(TemplateLiteral / ObjectExpression) sourceNode: TSESTree.Node; // 根部的 styled/css/style 节点 }; values: StyleValue[]; }每个values[i](types.ts)除了文档提到的rawNode与tokenInfo,还包含两个对校验规则很有用的字段:
interface StyleValue { confident: boolean; // 该值能否被静态分析确定(不能确定则为 false) kind: 'literal' | 'template-quasi' | 'member' | 'call' | 'conditional' | 'logical' | 'unknown'; // 值表达式的类型 node: TSESTree.Node; // 值节点(定位/报告用) tokenInfo: TokenInfo | null; }其中TokenInfo记录主题 token 引用:node(精确高亮用)、tokenName(如'primary')、tokenPath(如'content.primary')。
3.2 values 的设计含义:一个属性对应多个候选值
values是数组而非单值,这是理解 collector 的关键。源码中的decomposeValue(valueDecomposer.ts,styled.ts/cssProp.ts 均 import 它)负责把复杂表达式"拆解"成所有可能取值,以覆盖三元表达式、逻辑运算等情况:
background: ${status === 'active' ? p => p.theme.tokens.background.primary : 'transparent'}这种写法会被拆解成两条values(token 引用 + literal),规则在校验时逐条处理即可,无需自己实现 AST 拆解。
3.3 context 与 themeBinding:作用域感知
StyleContext携带file、scopeId与themeBinding。每个声明被创建时都会记录当时"有效的 theme 绑定"(styled.ts)。ThemeBinding(types.ts)描述绑定来源——useTheme、styled-callback或css-callback,以及本地变量名(theme/t/p都可能)。
3.4 Collector 容器接口
StyleCollector是一个极其精简的接口(types.ts):add(decl)、getAll()、clear()。底层就是一个数组,clear()通过declarations.length = 0实现(index.ts)——所以跨文件复用同一个 collector 前必须调用clear(),否则会残留上一个文件的状态。
3.5 它故意不捕获什么
回到文档的提示,collector 对以下内容天然不可见(这正是它与createQuasiScanner的根本差异):
- 模板字面量 quasis(非插值部分)中的静态 CSS 文本;
- 只以静态文本出现、没有任何动态值跟随的 CSS 属性名;
- 注释、空白与格式信息。
从实现可验证这一边界:styled.ts的extractCssProperty只从每个插值前的那个 quasi 片段里用正则(?:^|[{;])\s*([a-z-]+)\s*:\s*[^;{]*$抠出属性名(styled.ts),随后才decomposeValue(expr)处理该插值。若某一行color: #ff0000后面没有${},它根本没有机会进入 collector。
4. 两阶段模式(Two-Phase Pattern):先收集、后校验
4.1 为什么必须延迟校验
一条 styled 块中的属性和值会横跨多个 AST 节点(多个 quasi + 多个插值表达式)。若在TaggedTemplateExpression进入时就立即做校验,你永远只能看到片段。因此 collector 采用延迟校验(deferred validation):遍历阶段只负责把声明聚合起来,真正的业务校验放到Program:exit(整个文件解析完、所有声明都已收集齐)统一执行。
文档给出最小骨架:
create(context) { if (!shouldAnalyze(context)) return {}; const {collector, visitors} = createStyleCollector(context); return { ...visitors, // 展开 collector 的 visitors(内含全部提取逻辑) 'Program:exit'() { for (const decl of collector.getAll()) { // 你的校验逻辑写在这里 } collector.clear(); // 必做:为下一个文件清理 }, }; }4.2 规范范例:use-semantic-token 规则
仓库中真正落地的 useSemanticToken.ts 完整演示了这一骨架。它的语义是:"theme.tokens.*token 只能与它所属语义类别允许的 CSS 属性搭配"。核心逻辑分四步:
// 1) 快速预扫描(第一行)——见第 5 节 if (!shouldAnalyze(context)) return {}; // 2) 创建 collector,校验配置中启用的类别 const {collector, visitors} = createStyleCollector(context); // 3) Program:exit 逐个校验 'Program:exit'() { for (const declaration of collector.getAll()) { validateDeclaration(declaration); } collector.clear(); }validateDeclaration(useSemanticToken.ts)展示了消费StyleDeclaration数据的标准姿势,值得逐行读:
- 先取
decl.property.name,若以--开头(CSS 自定义属性)直接跳过——--foo: ${token}这类传给自定义属性的写法不做约束; - 遍历
decl.values,跳过没有tokenInfo的值; - 用
tokenPath查询分类规则findRuleForToken(tokenPath); - 命中规则后,检查
rule.allowedProperties是否包含当前属性;不包含则 report。若还能通过反向映射PROPERTY_TO_RULE找到该属性"应该用哪类 token",就给出带建议的报错:
context.report({ node: tokenNode, // 精确高亮到 token 访问节点 messageId: 'invalidPropertyWithSuggestion', data: {tokenPath, property: normalizedProperty, suggestedCategory}, });4.3 配置是"单一事实来源"
规则本身几乎没有硬编码的类别知识,真正的校验矩阵放在 config/tokenRules.ts:
interface TokenRule { name: string; // 人类可读类别名,如 'content' keywords: string[]; // token 路径匹配关键词,如 ['content', 'link'] allowedProperties: Set<string>; // 该类别允许搭配的 CSS 属性集合 }该配置同时承担三份职责(文件头注释明确写着SINGLE SOURCE OF TRUTH):
- token 检测——
tokenPath是否含某类别关键词; - 属性校验——该类别允许哪些属性;
- autofix 建议——由
PROPERTY_TO_RULE反向映射,从属性找应归属的类别。
匹配采用"most specific wins(最具体者胜)":如interactive.border.content同时命中 border 与 content 关键词,由于 content 层级更深,最终归属 content 规则。给这种"类别驱动型"规则加新能力时,通常只需改配置文件(如新增一条TokenRule),而不用动规则遍历逻辑——这是 SKILL.md "Extending an Existing Rule" 一节反复强调的设计原则。
5. shouldAnalyze:进入遍历前的快速预扫描
5.1 用法与返回语义
文档要求在create()中把shouldAnalyze(context)作为任何使用 style collector 或分析 Emotion 模式的规则的第一行。它的作用不是精确判定,而是正则级预筛:文件明显不含 Emotion/styled 模式时直接返回空监听器,为成千上万个无关 TS/TSX 文件节省 AST 全量遍历的开销。
5.2 底层判定逻辑
源码 extractor/index.ts 暴露了完整规则:import 命中或用法命中任一即可。
- import 命中:源码文本包含
'@emotion/styled'或'@emotion/react'; - 用法命中(正则):
useTheme调用、styled[.(](含styled.div/styled(/styled`)、`` css[({](`css\/css(/css{)、或 JSX 属性css=/style=。
代码注释特别解释了"同时检查 import 与 usage"的原因:
- 有 import 但无实际用法的文件虽然少见但确实存在(例如 re-export 文件);
- 没有直接 import 却在使用
styled/css的文件也可能存在(这些名称来自别处注入)。
shouldAnalyze允许误报(false positive)——宁可多做几次无谓的遍历,也要保证不漏掉真正需要分析的文件。注意它做的是源码文本的includes/正则测试,不是 token 级 AST 分析,因此非常廉价。
6. 最常见的坑:Collector 与静态文本的边界
style-collector-guide.md用一整节强调头号错误:规则需要分析静态 CSS 文本时误用了createStyleCollector。
const Box = styled.div` color: #ff0000; // ← quasi 中的静态文本,collector 看不到它 background: ${p => p.theme.tokens.background.primary}; // ← 这个才会被捕获 `;行为差异非常直观:
- 第一行的
color: #ff0000是 quasis 里的静态文本,没有任何插值表达式紧随其后——collector采集不到; - 第二行由于存在
${...}插值,会被采集为{property: 'background', values: [token 引用]}。
因此:
如果你的规则要检测的是文本本身的模式——裸十六进制颜色、嵌套选择器、静态属性名——请使用静态文本分析("Template Text Analysis" archetype,见 rule-archetypes.md 的决策表),而不是 Style Collector。
两条路线的选择可归纳为一个简单问题:**你想分析"这段 CSS 写死了什么",还是"这个插值/ token 被用于哪个属性"?**前者走 quasi 文本扫描,后者才走createStyleCollector。在 style-collector-guide.md 里,作者还特别处理了伪选择器干扰:属性提取正则要求属性名出现在{、;或行首之后,从而避免把a:hover中的a误认成属性。
7. 配套工具全景:如何组合出完整的样式规则
Style Collector 只是 ast 工具链的一员。依据 SKILL.md 第 2 步的共享工具表,写规则时可复用的能力如下(路径均以仓库根目录换算):
| 工具 | 仓库内真实路径 | 用途 |
|---|---|---|
createStyleCollector | ast/extractor/index.ts | 采集 CSS-in-JS 动态值声明(本文主题) |
shouldAnalyze | ast/extractor/index.ts | 快速预筛,跳过无 Emotion 用法的文件 |
getStyledCallInfo | ast/utils/styled.ts | 把 styled/css 调用归类为 element / component / css |
normalizePropertyName | ast/utils/normalizePropertyName.ts | CSS 属性名归一化(如驼峰转 kebab-case) |
decomposeValue | ast/extractor/valueDecomposer.ts | 把复杂表达式拆成所有可能取值 |
createThemeTracker | ast/tracker/theme.ts | 追踪useTheme()与回调式 theme 绑定 |
createImportTracker | ast/tracker/imports.ts | 解析本地名从何处 import(配合 JSX 结构类规则) |
createStyleCollector内部已经替你组装了 theme tracker 与三个 extractor,普通规则无需直接接触它们。但如果你的规则还要做JSX 结构约束(如restrict-jsx-slot-children),则需自行引入createImportTracker+ 递归 JSX 树遍历——这两类需求通常不会同时出现在一条规则里。
7.1 Theme Tracker 内部做了什么
theme.ts 值得单独认识,因为它是 collector 能够识别"p就是 theme"的原因。它跟踪的绑定形态包括:
import {useTheme} from '@emotion/react'(同时记住本地别名);const theme = useTheme()/const t = useTheme();const {tokens} = theme(对象解构后tokens.xxx也算 theme 绑定);- 回调参数绑定:
(theme) => ...、(p) => p.theme(经registerCallbackBinding注册到当前作用域)。
作用域管理使用显式的scopeStack:每进入一个ArrowFunctionExpression/FunctionExpressionpush 一个新 scopeId,退出时把该作用域注册的绑定从集合中删除(theme.ts),从而保证getActiveBinding()在任意时刻返回的确实是当前作用域可见的 theme。
8. 端到端实践:从 archetype 到一条可用的新规则
8.1 编写步骤回顾
判断意图:若你要做的是"按 CSS 属性校验 token/值的使用",锁定Property validationarchetype,加载 style-collector-guide.md;若是检测静态文本,则切到 Template text analysis,不要用 collector。
检查共享工具:见上表,已有逻辑不要重写。
创建文件:
- 规则:
static/oxlint/eslintPluginScraps/src/rules/$RULE_NAME.ts - 测试:
static/oxlint/eslintPluginScraps/src/rules/$RULE_NAME.spec.ts
命名约定(SKILL.md):规则名
kebab-case动词-名词(如no-token-import、use-semantic-token),导出名camelCase(useSemanticToken),文件名与规则名一致。- 规则:
套用规则模板:基于
ESLintUtils.RuleCreator.withoutDocs,在create()第一行放shouldAnalyze预筛。测试驱动:用
RuleTester写 valid/invalid 用例,跑:pnpm test-ci "static/oxlint/eslintPluginScraps/src/rules/$RULE_NAME.spec.ts"
8.2 注册与启用
- 把规则加入 rules/index.ts 的
rules映射(key 为 kebab-case 规则名); - 在插件配置的
name: 'plugin/@sentry/scraps'块中启用,键名为'@sentry/scraps/$RULE_NAME',可取'error'或['error', {options}](SKILL.md 第 4 步); - 规则若声明为 fixable,每个 invalid 测试用例必须带
output字段(即期望的修复后代码),这是 fixable 规则的硬性要求。
8.3 关于 autofix 的边界建议
SKILL.md 的默认立场是尽量实现 autofix,但明确列出不可自动修复的情形,其中与本主题直接相关的一条是:修复需要 AST 之外的类型信息。CSS-in-JS 动态值属于典型的"类型不敏感但语义敏感"数据——例如把 token 从一个类别换到另一个类别,需要理解theme.tokens的类型结构,仅凭 AST 无法安全判断——因此这类规则往往只 report 并给出建议文本,而不提供自动改写。
8.4 相关测试与延伸阅读
- 现有规则的可参考实现:useSemanticToken.ts 及其
.spec.ts;另有 noTokenImport.ts、noDoubleDollarInterpolation.ts 可从不同角度理解插件惯例。 - archetype 总览与更多示例:rule-archetypes.md。
- 若要为规则增加可配置项(schema),参考 references/schema-patterns.md。
9. 关键文件速查
| 文件(仓库根目录相对路径) | 作用 |
|---|---|
| style-collector-guide.md | 本文讲解的核心关联文档 |
| SKILL.md | 新规则编写总流程与共享工具表 |
| rule-archetypes.md | 规则意图 → 技术路线的决策表 |
| ast/extractor/index.ts | createStyleCollector与shouldAnalyze实现 |
| ast/extractor/types.ts | StyleDeclarationIR 的权威类型定义 |
| ast/extractor/styled.ts | styled/css 标签模板与对象语法提取 |
| ast/extractor/cssProp.ts | JSXcssprop 提取 |
| ast/tracker/theme.ts | theme 绑定追踪 |
| config/tokenRules.ts | token→属性 校验矩阵(单一事实来源) |
| rules/useSemanticToken.ts | Style Collector 的规范使用范例 |
总结
Style Collector 是 Sentry 设计系统 lint 基础设施中最关键的抽象之一:它把"从 CSS-in-JS 里抓出带动态值的属性声明"这件重复劳动下沉为共享工具,让规则作者专注业务判断。写这类规则时,只需记住三个要点——预扫描先行(shouldAnalyze)、遍历期只收集、Program:exit统一校验并clear();同时守住一条边界——它只看得见插值动态值,静态 CSS 文本请交给 quasi 文本扫描。以 useSemanticToken.ts 为模板、以 tokenRules.ts 为配置载体,即可用最小的样板成本扩展出新的属性校验规则。
【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考