✨ feat(vscode): 代码阅读加枚举卡片,Python enum 库规则和 TypeScript enum 画成同一张表
This commit is contained in:
1 parent
2251e67ef8
commit
de355661f0
24 files changed
+858
-83
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { IndexedFile, IndexEntry, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleBlock, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
|
||||
import type { IndexedFile, IndexEntry, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, Segment, ThemeColor, WorkspaceIndex } from '../contract'
|
||||
import type { Call } from './pythonTree'
|
||||
import { aliasesOf, callOf, lastName, stringContent, stringValue, syntaxChildren } from './pythonTree'
|
||||
import { hint, joined, name, plain, range, word } from './segments'
|
||||
@@ -482,7 +482,7 @@ function appOfConf(node: Node | null | undefined, attribute: string): Node | und
|
||||
}
|
||||
|
||||
/** 定时任务卡片:应用 / 说明 / (conf.update 里别的配置各一行)/ 任务表。 */
|
||||
function beatBlock(app: Node, schedule: Node, settings: [string, Node][], doc: Node | undefined, env: Env): RuleBlock {
|
||||
function beatBlock(app: Node, schedule: Node, settings: [string, Node][], doc: Node | undefined, env: Env): RuleCard {
|
||||
const { words, context } = env
|
||||
const { labels, heads } = words
|
||||
const entries = entriesOf(schedule)
|
||||
@@ -506,7 +506,7 @@ function beatBlock(app: Node, schedule: Node, settings: [string, Node][], doc: N
|
||||
// ─── Celery 应用 ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** Celery 应用卡片:名称 / 说明 / 应用名 / 消息队列 / 结果存储,别的参数照写的顺序各一行,行名是参数名。 */
|
||||
function appBlock(target: Node, call: Call, doc: Node | undefined, env: Env): RuleBlock {
|
||||
function appBlock(target: Node, call: Call, doc: Node | undefined, env: Env): RuleCard {
|
||||
const { words, context } = env
|
||||
const { labels } = words
|
||||
const parameters = new Map<string, Node>()
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('库规则只经 contract 拿信息', () => {
|
||||
const files = readdirSync(__dirname).filter(file => file.endsWith('.ts') && !file.endsWith('.test.ts'))
|
||||
|
||||
it('目录里有规则', () => {
|
||||
expect(files).toEqual(expect.arrayContaining(['sqlalchemy.ts', 'celery.ts']))
|
||||
expect(files).toEqual(expect.arrayContaining(['sqlalchemy.ts', 'celery.ts', 'enum.ts']))
|
||||
})
|
||||
|
||||
it.each(files)('%s 不 import 语言插件、翻译插件和宿主', (file) => {
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { Inline, PropertyRow, Segment } from '../contract'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { fileAttributes } from '../attributes'
|
||||
import { toBlockViews } from '../blockView'
|
||||
import { replaceLines } from '../editText'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
import { createTypeScriptAdapter } from '../languages/typescript'
|
||||
import { flatSegments, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||
import { typescriptZhTranslator } from '../translators/typescriptZh'
|
||||
import { celeryRule } from './celery'
|
||||
import { enumRule } from './enum'
|
||||
import { sqlalchemyRule } from './sqlalchemy'
|
||||
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
const ts = createTypeScriptAdapter(WASM_DIR)
|
||||
const SNAPSHOT = readFixture('enumModels.py')
|
||||
/** 和扩展里注册的顺序一样。 */
|
||||
const RULES = [sqlalchemyRule, celeryRule, enumRule]
|
||||
|
||||
async function views(source: string, reports: string[] = []): Promise<BlockView[]> {
|
||||
return withBlocks(py, 'python', source, blocks =>
|
||||
toBlockViews(blocks, source, py, pythonZhTranslator, { rules: RULES, report: message => reports.push(message) }))
|
||||
}
|
||||
|
||||
function tsViews(source: string): Promise<BlockView[]> {
|
||||
return withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, source, ts, typescriptZhTranslator, { rules: RULES, report: () => {} }))
|
||||
}
|
||||
|
||||
function properties(view: BlockView | undefined): PropertyRow[] {
|
||||
if (!view || !('properties' in view))
|
||||
throw new Error(`块 ${view?.id} 不是属性表`)
|
||||
return view.properties
|
||||
}
|
||||
|
||||
function named(list: BlockView[], name: string): BlockView {
|
||||
const found = list.find(view => 'properties' in view && propertyLines(view.properties)[0] === `名称: ${name}`)
|
||||
if (!found)
|
||||
throw new Error(`没有 ${name}`)
|
||||
return found
|
||||
}
|
||||
|
||||
/** 属性表里所有带下标的词(连表格里的格子)。 */
|
||||
function linkedSegments(rows: PropertyRow[]): Segment[] {
|
||||
const pieces: Inline[] = rows.flatMap(row => [
|
||||
...('markdown' in row.value ? [] : row.value),
|
||||
...(row.items ?? []).flatMap(item => item.cells.flat()),
|
||||
])
|
||||
return flatSegments(pieces).filter(segment => segment.offset !== undefined)
|
||||
}
|
||||
|
||||
const RULE_CASES = [
|
||||
'from enum import Enum as E, IntFlag, auto, member, nonmember',
|
||||
'import enum',
|
||||
'',
|
||||
'',
|
||||
'class Color(E):',
|
||||
' """颜色。"""',
|
||||
'',
|
||||
' _ignore_ = ["TEMP"]',
|
||||
' __slots__ = ()',
|
||||
' _order_ = "RED GREEN"',
|
||||
' # 红色',
|
||||
' # 最常用',
|
||||
' RED = auto()',
|
||||
' GREEN = 2 # 绿',
|
||||
' """绿色。"""',
|
||||
' TEMP = 9',
|
||||
' __secret = 1',
|
||||
' helper = lambda self: 1',
|
||||
' label = property(lambda self: self.name)',
|
||||
' table = nonmember({})',
|
||||
'',
|
||||
' def describe(self) -> str:',
|
||||
' return self.name',
|
||||
'',
|
||||
' BLUE = member(3)',
|
||||
' _private = 5',
|
||||
' ALIAS: int = 4',
|
||||
' _value_: int',
|
||||
'',
|
||||
' class Meta:',
|
||||
' pass',
|
||||
'',
|
||||
' # 收尾',
|
||||
'',
|
||||
'',
|
||||
'@enum.unique',
|
||||
'class Perm(IntFlag):',
|
||||
' R = 4',
|
||||
' W = 2',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
describe('enum 库规则(Python)', () => {
|
||||
it('luffy 的枚举:和原型一样的枚举卡片,说明取行尾注释,类的 docstring 当说明', async () => {
|
||||
const list = await views(SNAPSHOT)
|
||||
expect(list.map(view => view.kind)).toEqual(['comment', 'import', 'import', 'import', 'enum', 'enum', 'enum', 'enum', 'enum'])
|
||||
const commission = named(list, 'CommissionStatus')
|
||||
expect(commission.badge).toBeUndefined()
|
||||
expect(propertyLines(properties(commission))).toEqual([
|
||||
'名称: CommissionStatus',
|
||||
'访问级别: 公共',
|
||||
'继承: str, enum.Enum',
|
||||
'成员: 5 个',
|
||||
' PENDING | "pending" | 待结算',
|
||||
' AVAILABLE | "available" | 可提现',
|
||||
' WITHDRAWING | "withdrawing" | 提现中',
|
||||
' WITHDRAWN | "withdrawn" | 已提现',
|
||||
' INVALID | "invalid" | 已失效',
|
||||
])
|
||||
expect(properties(commission).at(-1)?.columns).toEqual(['成员', '值', '说明'])
|
||||
expect(propertyLines(properties(named(list, 'SecretScope')))).toEqual([
|
||||
'名称: SecretScope',
|
||||
'说明: 条目作用域: user 全局 / app 级 (同名覆盖 user).',
|
||||
'访问级别: 公共',
|
||||
'继承: enum.StrEnum',
|
||||
'成员: 2 个',
|
||||
' USER | "user"',
|
||||
' APP | "app"',
|
||||
])
|
||||
expect(properties(named(list, 'SecretScope')).at(-1)?.columns).toEqual(['成员', '值'])
|
||||
})
|
||||
|
||||
it('筛选:种类是 enum、叫「枚举」,访问级别同原来的类;卡片默认的编辑范围是整个类', async () => {
|
||||
const list = await views(SNAPSHOT)
|
||||
const commission = named(list, 'CommissionStatus')
|
||||
expect(commission.attributes).toEqual({ kind: 'enum', access: 'public' })
|
||||
expect(fileAttributes(list, pythonZhTranslator.attributeNames).find(attribute => attribute.id === 'kind')?.values)
|
||||
.toContainEqual({ id: 'enum', name: '枚举', count: 5 })
|
||||
const lines = SNAPSHOT.split('\n')
|
||||
expect(lines[commission.startLine]).toBe('class CommissionStatus(str, enum.Enum):')
|
||||
expect(lines[commission.endLine]).toBe(' INVALID = "invalid" # 已失效')
|
||||
})
|
||||
|
||||
it('成员照 Python 的规则认:dunder、sunder、私有名、_ignore_ 里的、lambda、property、nonmember、只写注解的不是成员;auto() 写「自动」,member(x) 的值是 x', async () => {
|
||||
const list = await views(RULE_CASES)
|
||||
const color = named(list, 'Color')
|
||||
expect(propertyLines(properties(color))).toEqual([
|
||||
'名称: Color',
|
||||
'说明: 颜色。',
|
||||
'访问级别: 公共',
|
||||
'继承: E',
|
||||
'成员: 5 个',
|
||||
' RED | 自动 | 红色 最常用',
|
||||
' GREEN | 2 | 绿 绿色。',
|
||||
' BLUE | 3 | ',
|
||||
' _private | 5 | ',
|
||||
' ALIAS | 4 | ',
|
||||
])
|
||||
const auto = properties(color).at(-1)?.items?.[0].cells[1]
|
||||
expect(auto).toEqual([{ text: '自动', role: 'hint' }])
|
||||
expect(propertyLines(properties(named(list, 'Perm')))).toEqual([
|
||||
'名称: Perm',
|
||||
'访问级别: 公共',
|
||||
'装饰器: @enum.unique',
|
||||
'继承: IntFlag',
|
||||
'成员: 2 个',
|
||||
' R | 4',
|
||||
' W | 2',
|
||||
])
|
||||
})
|
||||
|
||||
it('方法、嵌套类、不是成员的属性不管写在哪都照常是子块;成员前面的注释并进卡片,最后面的注释留下;卡片「编辑」写回整个类', async () => {
|
||||
const list = await views(RULE_CASES)
|
||||
const color = named(list, 'Color')
|
||||
const lines = RULE_CASES.split('\n')
|
||||
expect(color.children.map(child => lines[child.startLine].trim())).toEqual([
|
||||
'_ignore_ = ["TEMP"]',
|
||||
'__slots__ = ()',
|
||||
'_order_ = "RED GREEN"',
|
||||
'TEMP = 9',
|
||||
'__secret = 1',
|
||||
'helper = lambda self: 1',
|
||||
'label = property(lambda self: self.name)',
|
||||
'table = nonmember({})',
|
||||
'def describe(self) -> str:',
|
||||
'_value_: int',
|
||||
'class Meta:',
|
||||
'# 收尾',
|
||||
])
|
||||
expect([lines[color.startLine], lines[color.endLine]]).toEqual(['class Color(E):', ' # 收尾'])
|
||||
expect(replaceLines(RULE_CASES, color.startLine, color.endLine, color.code)).toBe(RULE_CASES)
|
||||
const edited = replaceLines(RULE_CASES, color.startLine, color.endLine, color.code.replace('GREEN = 2', 'GREEN = 20'))
|
||||
expect(edited).toBe(RULE_CASES.replace('GREEN = 2', 'GREEN = 20'))
|
||||
})
|
||||
|
||||
it('跳到定义、高亮出现处:卡片是类名的定义,成员行是成员名的定义,带下标的词都指着源码里的这个名字', async () => {
|
||||
const list = await views(SNAPSHOT)
|
||||
const commission = named(list, 'CommissionStatus')
|
||||
expect(SNAPSHOT.slice(commission.defines?.startIndex, commission.defines?.endIndex)).toBe('CommissionStatus')
|
||||
const members = properties(commission).at(-1)?.items ?? []
|
||||
expect(members.map(item => SNAPSHOT.slice(item.defines?.startIndex, item.defines?.endIndex))).toEqual(['PENDING', 'AVAILABLE', 'WITHDRAWING', 'WITHDRAWN', 'INVALID'])
|
||||
const linked = linkedSegments(properties(commission))
|
||||
expect(linked.map(segment => segment.text)).toEqual(expect.arrayContaining(['CommissionStatus', 'str', 'enum', 'Enum', 'PENDING', '"pending"']))
|
||||
for (const segment of linked)
|
||||
expect(SNAPSHOT.slice(segment.offset, (segment.offset ?? 0) + segment.text.length), segment.text).toBe(segment.text)
|
||||
})
|
||||
|
||||
it('不是 enum 的枚举类型不认:没导入 enum、父类是 SQLAlchemy 的 Enum、父类不是枚举类型', async () => {
|
||||
const plain = await views('class Color(Enum):\n RED = 1\n')
|
||||
expect(plain.map(view => view.kind)).toEqual(['class'])
|
||||
const other = await views('import enum\nfrom sqlalchemy import Enum\n\n\nclass A(Enum):\n X = 1\n\n\nclass B(enum.auto):\n Y = 1\n\n\nclass C(str):\n Z = 1\n')
|
||||
expect(other.map(view => view.kind)).toEqual(['import', 'import', 'class', 'class', 'class'])
|
||||
const aliased = await views('import enum as e\n\n\nclass A(e.IntEnum):\n X = 1\n')
|
||||
expect(aliased.map(view => view.kind)).toEqual(['import', 'enum'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('枚举:TypeScript 和 Python 同一张卡片', () => {
|
||||
it('同样的成员:种类、种类名、属性表的行、列名、格子都一样(TypeScript 没有「继承」)', async () => {
|
||||
const tsSource = 'export enum Status {\n PENDING = "pending", // 待结算\n /** 可提现 */\n AVAILABLE = "available",\n}\n'
|
||||
const [tsEnum] = await tsViews(tsSource)
|
||||
const [pending] = properties(tsEnum).at(-1)?.items ?? []
|
||||
expect(pending.cells[0]).toEqual([{ text: 'PENDING', role: 'variable', offset: tsSource.indexOf('PENDING') }])
|
||||
expect(pending.defines).toEqual({ startIndex: tsSource.indexOf('PENDING'), endIndex: tsSource.indexOf('PENDING') + 'PENDING'.length })
|
||||
const [, pyEnum] = await views('import enum\n\n\nclass Status(enum.Enum):\n PENDING = "pending" # 待结算\n # 可提现\n AVAILABLE = "available"\n')
|
||||
const shape = (view: BlockView): unknown => {
|
||||
const rows = properties(view).filter(row => row.label !== '继承')
|
||||
return {
|
||||
kind: view.kind,
|
||||
badge: view.badge,
|
||||
attributes: view.attributes,
|
||||
lines: propertyLines(rows),
|
||||
columns: rows.map(row => row.columns),
|
||||
roles: rows.at(-1)?.items?.map(item => item.cells.map(cell => flatSegments(cell).map(segment => segment.role))),
|
||||
}
|
||||
}
|
||||
expect(shape(tsEnum)).toEqual(shape(pyEnum))
|
||||
expect(propertyLines(properties(tsEnum))).toEqual([
|
||||
'名称: Status',
|
||||
'访问级别: 公共',
|
||||
'成员: 2 个',
|
||||
' PENDING | "pending" | 待结算',
|
||||
' AVAILABLE | "available" | 可提现',
|
||||
])
|
||||
})
|
||||
|
||||
it('typeScript 没写值的成员写算出来的数,算不出来写「自动」,都是说明词', async () => {
|
||||
const [direction] = await tsViews('enum Direction { Up, Down = 5, Left, Mask = 1 << 2, After }')
|
||||
expect(propertyLines(properties(direction))).toEqual([
|
||||
'名称: Direction',
|
||||
'访问级别: 私有',
|
||||
'成员: 5 个',
|
||||
' Up | 0',
|
||||
' Down | 5',
|
||||
' Left | 6',
|
||||
' Mask | 1 << 2',
|
||||
' After | 自动',
|
||||
])
|
||||
expect(properties(direction).at(-1)?.items?.map(item => item.cells[1])).toEqual([
|
||||
[{ text: '0', role: 'hint' }],
|
||||
[{ text: '5', role: 'number' }],
|
||||
[{ text: '6', role: 'hint' }],
|
||||
expect.any(Array),
|
||||
[{ text: '自动', role: 'hint' }],
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,152 @@
|
||||
import type { ClassBlock, CodeBlock, EnumMemberFacts, ImportFacts, LibraryRule, Node, RuleFacts } from '../contract'
|
||||
import { leadingComments, withLeadingComments } from './children'
|
||||
import { stringValue, syntaxChildren } from './pythonTree'
|
||||
|
||||
/**
|
||||
* Python 自带 enum 库的规则:父类里有 enum 的枚举类型(Enum、IntEnum、StrEnum、Flag、IntFlag、ReprEnum;写成 `enum.X`、
|
||||
* 从 enum 导入的名字或它的别名都算,可以和别的父类混着写:`class Status(str, enum.Enum)`)的类,认成契约里的枚举块(EnumFacts):
|
||||
* 和 TypeScript 的 enum 是同一种块、同一张属性表,用词归翻译插件,规则只给 enum 库的知识——哪些类是枚举、哪些赋值是成员。
|
||||
* 成员照 Python 的规则认(见 memberOf);成员连同讲它的注释并进来,方法、嵌套类、别的属性照常是子块。
|
||||
* 父类是同一文件里别的枚举类(`class Sub(Base)`)的认不出来:规则只看这一块。
|
||||
*/
|
||||
|
||||
const LIBRARY = 'enum'
|
||||
const ENUM_TYPES = new Set(['Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum'])
|
||||
/** 值是这几种内置描述符的类属性不是成员。 */
|
||||
const DESCRIPTORS = new Set(['property', 'classmethod', 'staticmethod'])
|
||||
|
||||
/** 这份源码里怎么叫 enum 库的东西:库自己(`import enum`、`import enum as e`),和从它导入的名字(本地名 → 原名)。 */
|
||||
interface Names {
|
||||
modules: Set<string>
|
||||
imported: Map<string, string>
|
||||
}
|
||||
|
||||
function namesOf(imports: readonly ImportFacts[]): Names {
|
||||
const names: Names = { modules: new Set(), imported: new Map() }
|
||||
for (const facts of imports) {
|
||||
const fromEnum = facts.sources.some(source => source.text === LIBRARY)
|
||||
for (const item of facts.items) {
|
||||
if (item.kind === 'module' && item.module.text === LIBRARY)
|
||||
names.modules.add(item.alias?.text ?? LIBRARY)
|
||||
else if (item.kind === 'named' && fromEnum)
|
||||
names.imported.set(item.alias?.text ?? item.name.text, item.name.text)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
/** 节点指的是 enum 库里的哪个名字:`enum.Enum`、从 enum 导入的 `Enum`(别名换回原名);别的是 undefined。 */
|
||||
function enumName(node: Node | null | undefined, names: Names): string | undefined {
|
||||
if (node?.type === 'identifier')
|
||||
return names.imported.get(node.text)
|
||||
const object = node?.type === 'attribute' ? node.childForFieldName('object') : null
|
||||
return object?.type === 'identifier' && names.modules.has(object.text) ? node?.childForFieldName('attribute')?.text : undefined
|
||||
}
|
||||
|
||||
/** 调用的是 enum 库里的哪个函数(`auto()`、`enum.nonmember(...)`)。 */
|
||||
function enumCall(node: Node | null | undefined, names: Names): string | undefined {
|
||||
return node?.type === 'call' ? enumName(node.childForFieldName('function'), names) : undefined
|
||||
}
|
||||
|
||||
/** 类主体里给一个名字赋值的子块(`A = 1`、`A: int = 1`、只写注解的 `A: int`):名字和值。 */
|
||||
function assignmentOf(block: CodeBlock): { name: Node, value?: Node } | undefined {
|
||||
if (block.kind !== 'variable' && block.kind !== 'field')
|
||||
return undefined
|
||||
const assignment = block.node.firstNamedChild
|
||||
const name = assignment?.type === 'assignment' ? assignment.childForFieldName('left') : null
|
||||
if (name?.type !== 'identifier')
|
||||
return undefined
|
||||
return { name, value: assignment?.childForFieldName('right') ?? undefined }
|
||||
}
|
||||
|
||||
/** `_ignore_` 里列的名字:空格隔开的字符串,或字符串的列表、元组。 */
|
||||
function ignoredNames(children: readonly CodeBlock[]): Set<string> {
|
||||
const value = children.map(assignmentOf).find(assignment => assignment?.name.text === '_ignore_')?.value
|
||||
const text = stringValue(value)
|
||||
if (text !== undefined)
|
||||
return new Set(text.split(/\s+/).filter(Boolean))
|
||||
const items = value?.type === 'list' || value?.type === 'tuple' ? syntaxChildren(value).map(stringValue) : []
|
||||
return new Set(items.filter((item): item is string => item !== undefined))
|
||||
}
|
||||
|
||||
/** 名字能当成员:不是 `__x__`(dunder)、`_x_`(enum 保留的 sunder)、`__x`(私有,会被改名)。 */
|
||||
function memberName(name: string): boolean {
|
||||
const dunder = name.length > 4 && name.startsWith('__') && name.endsWith('__')
|
||||
const sunder = name.length > 2 && name.startsWith('_') && name.endsWith('_') && name[1] !== '_' && name.at(-2) !== '_'
|
||||
const privateName = name.startsWith('__') && !name.endsWith('__')
|
||||
return !dunder && !sunder && !privateName
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个子块是不是成员,是的话它的值:名字能当成员、不在 `_ignore_` 里,赋了值,值不是函数(lambda)、内置描述符
|
||||
* (property、classmethod、staticmethod)、`nonmember(...)`、连着赋值(`A = B = 1`)。
|
||||
* `auto()` 的值交给库自动取(没有 value),`member(x)` 的值是 x。
|
||||
*/
|
||||
function memberOf(block: CodeBlock, ignored: ReadonlySet<string>, names: Names): { name: Node, value?: Node } | undefined {
|
||||
const assignment = assignmentOf(block)
|
||||
const value = assignment?.value
|
||||
if (!assignment || !value || !memberName(assignment.name.text) || ignored.has(assignment.name.text))
|
||||
return undefined
|
||||
const callee = value.type === 'call' ? value.childForFieldName('function') : null
|
||||
if (value.type === 'lambda' || value.type === 'assignment' || (callee?.type === 'identifier' && DESCRIPTORS.has(callee.text)))
|
||||
return undefined
|
||||
switch (enumCall(value, names)) {
|
||||
case 'nonmember':
|
||||
return undefined
|
||||
case 'auto':
|
||||
return { name: assignment.name }
|
||||
case 'member':
|
||||
return { name: assignment.name, value: syntaxChildren(value.childForFieldName('arguments'))[0] }
|
||||
default:
|
||||
return { name: assignment.name, value }
|
||||
}
|
||||
}
|
||||
|
||||
/** 注释块里的各条注释(几条行注释连成的一块是好几个节点);当注释用的字符串是那个字符串。 */
|
||||
function commentNodes(block: CodeBlock): Node[] {
|
||||
if (block.node.type !== 'comment')
|
||||
return syntaxChildren(block.node).slice(0, 1)
|
||||
const nodes: Node[] = []
|
||||
for (let node: Node | null = block.node; node?.type === 'comment' && node.endIndex <= block.header.endIndex; node = node.nextSibling)
|
||||
nodes.push(node)
|
||||
return nodes
|
||||
}
|
||||
|
||||
/** 成员的说明,照源码顺序:紧挨在它上面的注释块、它这一行行尾的注释、紧跟在它后面的属性 docstring。 */
|
||||
function memberDocs(children: readonly CodeBlock[], block: CodeBlock): Node[] {
|
||||
const trailing = block.node.nextSibling
|
||||
const doc = block.kind === 'variable' ? block.facts.doc : block.kind === 'field' ? block.doc : undefined
|
||||
return [
|
||||
...leadingComments(children, block).flatMap(commentNodes),
|
||||
...(trailing?.type === 'comment' && trailing.startPosition.row === block.node.endPosition.row ? [trailing] : []),
|
||||
...(doc ? [doc] : []),
|
||||
]
|
||||
}
|
||||
|
||||
function enumBlock(block: ClassBlock, names: Names): RuleFacts | undefined {
|
||||
const { facts, children } = block
|
||||
if (!facts.name || !facts.bases.some(base => ENUM_TYPES.has(enumName(base.value, names) ?? '')))
|
||||
return undefined
|
||||
const ignored = ignoredNames(children)
|
||||
const members: EnumMemberFacts[] = []
|
||||
const blocks: CodeBlock[] = []
|
||||
for (const child of children) {
|
||||
const member = memberOf(child, ignored, names)
|
||||
if (!member)
|
||||
continue
|
||||
members.push({ ...member, docs: memberDocs(children, child) })
|
||||
blocks.push(child)
|
||||
}
|
||||
return {
|
||||
as: { kind: 'enum', facts: { name: facts.name, doc: facts.doc, access: facts.access, decorators: facts.decorators, bases: facts.bases, members } },
|
||||
absorbs: withLeadingComments(children, blocks),
|
||||
}
|
||||
}
|
||||
|
||||
export const enumRule: LibraryRule = {
|
||||
id: 'enum',
|
||||
label: 'Python 枚举(enum)',
|
||||
languageIds: ['python'],
|
||||
libraries: [LIBRARY],
|
||||
claim: (block, context) => block.kind === 'class' ? enumBlock(block, namesOf(context.imports)) : undefined,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ClassBlock, CodeBlock, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleBlock, RuleContext, ThemeColor, TokenRole } from '../contract'
|
||||
import type { ClassBlock, CodeBlock, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleCard, RuleContext, ThemeColor, TokenRole } from '../contract'
|
||||
import type { Call } from './pythonTree'
|
||||
import { withLeadingComments } from './children'
|
||||
import { aliasesOf, callOf, canonicalName, lastName, stringValue, syntaxChildren } from './pythonTree'
|
||||
@@ -414,7 +414,7 @@ function tableRow(label: string, heads: string[], items: PropertyItem[], env: En
|
||||
return items.length > 0 ? [{ label, value: [hint(env.words.count(items.length))], columns: heads, items }] : []
|
||||
}
|
||||
|
||||
function tableBlock(block: ClassBlock, model: Model, env: Env): RuleBlock {
|
||||
function tableBlock(block: ClassBlock, model: Model, env: Env): RuleCard {
|
||||
const { words, context } = env
|
||||
const { facts } = block
|
||||
const { labels } = words
|
||||
|
||||
Reference in new issue
Block a user