Files
superwork/vscode/src/codeReader/rules/react.ts
T

279 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, Words> = { 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
},
}