import type { CodeBlock, Inline, KindBadge, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, SegmentGroup, ThemeColor, WorkspaceIndex } from '../contract' import type { BindingFacts, ComponentFacts, FieldFacts, FieldText, PropsFacts } from './reactTree' import { clientDirective, commentText, componentAt, componentOf, componentsIn, declaredTypes, propsOf, sameNode } from './reactTree' import { hint, name, plain, range } from './segments' /** * React 库规则:TSX / JSX 里的 React 函数组件(见 reactTree.ts 的 componentAt)整块换成「React 组件」卡片,不再是普通函数卡片的输入参数 / 输出参数。 * 卡片:名称(后面跟导出方式、客户端组件、记忆化、转发 ref 这些小标签)/ 说明 / 属性(解构的每个属性一行,类型取属性类型里对应的字段; * 属性类型是接口、类型别名时写来自哪个类型,导入的类型经工作区索引查它的字段),再按钩子分区。 * 文件开头的 "use client" 讲成了「客户端组件」、组件上面的 JSDoc 讲成了「说明」,都不再单独成块。 * 新的 JSX 写法不用导入 react,规则不看导入:TSX / JSX 文件都启用。用词都在词表里,按翻译插件的显示语言挑。 */ const RULE_ID = 'react' const KIND = 'react.component' /** 组件色:React 的青色,渲染大纲里的组件、「客户端组件」标签也用它;白底上照原色看不清,亮色主题取同色相深一些的。 */ const COMPONENT_COLOR: ThemeColor = { dark: '#61dafb', light: '#0a7ea4' } /** 导出方式的标签:同 import / export 关键字的颜色。 */ const EXPORT_COLOR: ThemeColor = { dark: '#c586c0', light: '#af00db' } /** 记忆化、转发 ref 的标签:同 const / function 关键字的颜色。 */ const WRAPPER_COLOR: ThemeColor = { dark: '#569cd6', light: '#0000ff' } interface Words { kinds: Record<'component', string> labels: Record<'name' | 'doc' | 'props', string> tags: Record<'named' | 'default' | 'client' | 'memo' | 'forwardRef', string> /** 属性名后面的标签:函数类型的属性、children */ roles: Record<'callback' | 'children', string> /** 属性表的列:名称 | 类型 | 默认值 | 必填,有属性带说明时多一列说明 */ propHeads: string[] propDoc: string yes: string no: string /** 表里没有的值 */ nothing: string /** 没写类型 */ unmarked: string count: (n: number) => string /** 属性来自哪个类型:「 · 来自 」 */ from: string /** `...rest` 的类型:其余的属性 */ rest: string /** 解构时改了名:「(组件里叫 x)」 */ renamed: [string, string] } const ZH: Words = { kinds: { component: 'React 组件' }, labels: { name: '名称', doc: '说明', props: '属性' }, tags: { named: '具名导出', default: '默认导出', client: '客户端组件', memo: '记忆化', forwardRef: '转发 ref' }, roles: { callback: '回调', children: '子元素' }, propHeads: ['名称', '类型', '默认值', '必填'], propDoc: '说明', yes: '是', no: '否', nothing: '—', unmarked: '未标注', count: n => n > 0 ? `${n} 个` : '无', from: ' · 来自 ', rest: '其余的属性', renamed: ['(组件里叫 ', ')'], } const WORDS: Record = { zh: ZH } interface Env { context: RuleContext words: Words } /** 小标签(见 GroupStyle 的 tag)。 */ function tag(label: string, color: ThemeColor): SegmentGroup { return { style: 'tag', label, segments: [], color } } /** 规则自己的用词里当关键字着色的:是、否。 */ function yesNo(value: boolean, words: Words): Inline[] { return [{ text: value ? words.yes : words.no, role: 'storage' }] } // ─── 名称、说明 ─────────────────────────────────────────────────────────────── /** 名称:组件名用组件色,后面跟导出方式、客户端组件、记忆化、转发 ref 这些小标签。 */ function nameRow(component: ComponentFacts, { words }: Env): PropertyRow { const tags = [ ...(component.exported ? [tag(words.tags[component.exported], EXPORT_COLOR)] : []), ...(component.client ? [tag(words.tags.client, COMPONENT_COLOR)] : []), ...component.wrappers.map(wrapper => tag(words.tags[wrapper], WRAPPER_COLOR)), ] return { label: words.labels.name, value: [{ ...name(component.name, 'function'), color: COMPONENT_COLOR }, ...tags.flatMap(item => [plain(' '), item])], } } /** 说明:组件上面的 JSDoc。 */ function docRows(component: ComponentFacts, { context, words }: Env): PropertyRow[] { const doc = component.doc && context.doc(component.doc) return doc ? [{ label: words.labels.doc, value: doc }] : [] } // ─── 属性 ───────────────────────────────────────────────────────────────────── /** 工作区里各文件顶层声明的接口、类型别名:名字 → 各字段的文字(JSON,见 FieldText),组件的属性类型是导入的时候查它。 */ const PROP_TYPES_INDEX: WorkspaceIndex = { id: `${RULE_ID}.propTypes`, languageId: 'typescriptreact', include: '**/*.{ts,tsx}', mentions: source => /\b(?:interface|type)\s+[A-Z]/.test(source), entries: root => declaredTypes(root).map(({ name: typeName, fields }) => ({ key: typeName.text, name: range(typeName), value: JSON.stringify(fields) })), } /** 属性表的一行讲的一个属性:这个文件里的字段、查索引得到的别的文件里的字段,和解构出来的那一项(都可能没有)。 */ interface PropEntry { key: string field?: FieldFacts text?: FieldText binding?: BindingFacts } /** 查索引得到的别的文件里的字段;值不是这个规则存的样子时当没查到。 */ function importedFields(from: Node | undefined, { context }: Env): FieldText[] | undefined { const value = from && context.lookup(PROP_TYPES_INDEX, from)?.value if (!value) return undefined try { const parsed: unknown = JSON.parse(value) return Array.isArray(parsed) ? parsed as FieldText[] : undefined } catch { return undefined } } /** * 属性表的各行:属性类型的字段照类型里的顺序(解构出来的那项给默认值、定义处),类型里没有的解构项接在后面,`...rest` 最后; * 类型查不到字段、又没解构时是参数本身一行。 */ function propEntries(props: PropsFacts, env: Env): PropEntry[] { const texts = props.fields ? undefined : importedFields(props.from, env) const keys = props.fields?.map(field => field.name.text) ?? texts?.map(text => text.name) const bindingOf = (key: string): BindingFacts | undefined => props.bindings.find(binding => !binding.rest && binding.key === key) const declared: PropEntry[] = props.fields ? props.fields.map(field => ({ key: field.name.text, field, ...withBinding(bindingOf(field.name.text)) })) : (texts ?? []).map(text => ({ key: text.name, text, ...withBinding(bindingOf(text.name)) })) const extra = props.bindings.filter(binding => binding.rest || !keys?.includes(binding.key)).map(binding => ({ key: binding.key, binding })) const entries = [...declared, ...extra.filter(entry => !entry.binding.rest), ...extra.filter(entry => entry.binding.rest)] if (entries.length === 0 && props.parameter && !keys) return [{ key: props.parameter.text }] return entries } function withBinding(binding: BindingFacts | undefined): { binding?: BindingFacts } { return binding ? { binding } : {} } /** 名称一格:属性名(定义处能跳),children、回调后面跟小标签,解构时改了名写出组件里叫什么。 */ function propName(entry: PropEntry, env: Env): Inline[] { const { words } = env const { binding, field, text } = entry const renamed = binding && !binding.rest && binding.name.text !== entry.key const head: Inline[] = field ? [name(field.name, 'variable')] : binding && !renamed ? [...(binding.rest ? [plain('...')] : []), name(binding.name, 'variable')] : [{ text: entry.key, role: 'variable' }] const role = entry.key === 'children' ? words.roles.children : (field?.callback ?? text?.callback) ? words.roles.callback : undefined return [ ...head, ...(role ? [plain(' '), tag(role, COMPONENT_COLOR)] : []), ...(renamed ? [hint(words.renamed[0]), name(binding.name, 'variable'), hint(words.renamed[1])] : []), ] } /** 类型一格:字段的类型照常翻,方法签名写成函数签名;别的文件里的字段照写文字;`...rest` 是其余的属性。 */ function propType(entry: PropEntry, props: PropsFacts, { context, words }: Env): Inline[] { const { field, text, binding } = entry if (field?.type?.type === 'formal_parameters') return [...context.translate(field.type), plain(' => '), ...(field.returns ? context.translate(field.returns) : [hint(words.unmarked)])] if (field?.type) return context.translate(field.type) if (text?.type) return [{ text: text.type, role: 'type' }] if (binding?.rest) return [hint(words.rest)] if (!binding && props.type) return context.translate(props.type) return [hint(words.unmarked)] } /** 必填一格:类型里写了 `?` 的不必填;没有类型可看时解构写了默认值的不必填,否则不知道。 */ function propRequired(entry: PropEntry, words: Words): Inline[] { const optional = entry.field?.optional ?? entry.text?.optional if (optional !== undefined) return yesNo(!optional, words) if (entry.binding?.default || entry.binding?.rest) return yesNo(false, words) return [hint(words.nothing)] } /** 说明一格:字段上面的 JSDoc / 注释。 */ function propDoc(entry: PropEntry): Inline[] { const text = entry.field ? commentText(entry.field.docs) : entry.text?.doc return text ? [{ text: text.replace(/\s*\n\s*/g, ' '), role: 'comment' }] : [] } /** 属性:解构的每个属性一行(名称 / 类型 / 默认值 / 必填,有属性带说明时多一列说明);属性类型是一个名字时「属性」后面写来自哪里。 */ function propsRow(component: ComponentFacts, env: Env): PropertyRow { const { context, words } = env const props = propsOf(component) const entries = propEntries(props, env) const docs = entries.map(propDoc) const described = docs.some(doc => doc.length > 0) const items = entries.map((entry, index): PropertyItem => { const defines = entry.binding?.name ?? entry.field?.name return { cells: [ propName(entry, env), propType(entry, props, env), entry.binding?.default ? context.translate(entry.binding.default) : [hint(words.nothing)], propRequired(entry, words), ...(described ? [docs[index]] : []), ], ...(defines && { defines: range(defines) }), } }) return { label: words.labels.props, value: [hint(words.count(items.length)), ...(props.from ? [hint(words.from), name(props.from, 'type')] : [])], ...(items.length > 0 && { items, columns: described ? [...words.propHeads, words.propDoc] : words.propHeads }), } } // ─── 认领 ───────────────────────────────────────────────────────────────────── /** 组件卡片。 */ function componentCard(component: ComponentFacts, env: Env): RuleCard { return { kind: KIND, badge: badge(env.words), rows: [nameRow(component, env), ...docRows(component, env), propsRow(component, env)], defines: range(component.name), absorbs: [], } } function badge(words: Words): KindBadge { return { label: words.kinds.component, color: COMPONENT_COLOR, icon: 'atom' } } /** * 讲在了组件卡片里、不再单独成块的:文件开头的 "use client"(文件里有组件时,讲成「客户端组件」标签), * 紧贴在组件前面、没并进组件块头部的 JSDoc(值是 memo(…)、直接返回 JSX 的箭头函数这类常量块,讲成「说明」)。 */ function omitted(block: CodeBlock): boolean { const root = block.node.tree.rootNode if (block.kind === 'statement') return sameNode(clientDirective(root), block.node) && componentsIn(root).length > 0 if (block.kind === 'comment') { const next = block.node.nextSibling const component = next ? componentAt(next) : undefined return !!component?.doc && sameNode(component.doc, block.node) } return false } export const reactRule: LibraryRule = { id: RULE_ID, label: 'React 组件', languageIds: ['typescriptreact', 'javascriptreact'], libraries: [], claim: (block, context) => { const env: Env = { context, words: WORDS[context.displayLanguage ?? ''] ?? ZH } const component = componentOf(block) if (component) return componentCard(component, env) return omitted(block) ? { omit: true } : undefined }, }