Files
superwork/vscode/src/codeReader/libraryRules.test.ts
T
cruldra 582ac4c987 ✨ feat(vscode): 代码阅读的注释挂到它讲的代码块上成「注释」一行,重写注释改用技能一句话加结构化结果
- 普通注释不再单独成卡片:同一层挂到后面最近的块(隔空行也算),后面没有挂到前面那块,整层没代码挂到父块;只有文件开头隔着空行的注释、模块 docstring、//!、@file 单独成「文件开头的说明」
- 各语言(Python、TypeScript、Rust、Shell、Dockerfile)建好树后统一调 attachComments;块的编辑范围连着挂上的注释,差异、跳转、钉住卡片都按它
- 宿主统一给每张卡片(语言卡片、翻译插件的属性表、库规则卡片、一行译文的语句卡片)插「注释」一行,排在名称、说明后面
- TypeScript 的 JSDoc 也能当常量、变量的说明;sqlx、React 不再单独处理前面的注释
- 「重写注释」每块都有:提示只发 /ecc:writing-code-comments 路径:起-止,模型只读、按 --json-schema 交 body,没交结构化正文就失败不写;扩展换掉块前面的注释或在块上面新写一段
2026-10-08 07:51:21 +08:00

321 lines
17 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 { BlockView, RuleRun } from './blockView'
import type { CodeBlock, LibraryRule, RuleBlock, RuleLocation, RuleSlot, RuleWorkspace, 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 { activeRules, fromLibrary, importsIn, resolveReference } from './libraryRules'
import { propertyLines, WASM_DIR, withBlocks } from './testSupport'
import { pythonZhTranslator } from './translators/pythonZh'
const py = createPythonAdapter(WASM_DIR)
const MODEL = [
'from sqlalchemy.orm import mapped_column',
'',
'class User(Base):',
' __tablename__ = "users"',
' # 基本信息',
' name = mapped_column(String)',
'',
' def greet(self) -> str:',
' return self.name',
'',
].join('\n')
/** 认领每个类块的假规则:并进它的前两个子块。 */
function cardRule(id: string, overrides: Partial<RuleBlock> = {}): LibraryRule {
return {
id,
label: id,
languageIds: ['python'],
libraries: ['sqlalchemy'],
claim: block => block.kind === 'class'
? { kind: `${id}.card`, badge: { label: `${id} 卡片`, color: { dark: '#fff', light: '#000' }, icon: 'box' }, rows: [], absorbs: block.children.slice(0, 1), ...overrides }
: undefined,
}
}
const throwing: LibraryRule = {
id: 'throwing',
label: '抛错',
languageIds: ['python'],
libraries: ['sqlalchemy'],
claim: () => {
throw new Error('坏了')
},
}
async function views(source: string, rules: LibraryRule[], reports: string[] = []): Promise<BlockView[]> {
const run: RuleRun = { rules, report: message => reports.push(message) }
return withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, run))
}
function flatten(list: BlockView[]): BlockView[] {
return list.flatMap(view => [view, ...flatten(view.children)])
}
/** 语法树用完即释放,节点要在里面读完。 */
function activates(source: string, rule: LibraryRule): Promise<boolean> {
return withBlocks(py, 'python', source, blocks => activeRules([rule], importsIn(blocks)).length > 0)
}
describe('库规则:启用', () => {
it('来源等于库名、或是它的子模块才算导入了这个库', () => {
expect(fromLibrary('sqlalchemy', 'sqlalchemy')).toBe(true)
expect(fromLibrary('sqlalchemy.orm', 'sqlalchemy')).toBe(true)
expect(fromLibrary('drizzle-orm/pg-core', 'drizzle-orm')).toBe(true)
expect(fromLibrary('axum::routing', 'axum')).toBe(true)
expect(fromLibrary('sqlalchemy_utils', 'sqlalchemy')).toBe(false)
expect(fromLibrary('.sqlalchemy', 'sqlalchemy')).toBe(false)
})
it.each([
['from sqlalchemy.orm import Mapped, mapped_column', true],
['import sqlalchemy as sa', true],
['import os, sqlalchemy', true],
['def f():\n from sqlalchemy import select\n', true],
['from sqlalchemy_utils import UUIDType', false],
['from .sqlalchemy import Base', false],
['import os', false],
])('%s → %s', async (source, active) => {
expect(await activates(source, cardRule('sqlalchemy'))).toBe(active)
})
it('没导入库的文件不问规则', async () => {
const reports: string[] = []
const list = await views(MODEL.replace('sqlalchemy.orm', 'other'), [throwing], reports)
expect(list.map(view => view.kind)).toEqual(['import', 'class'])
expect(reports).toEqual([])
})
it('libraries 是空的规则不看导入:这种语言的文件都启用', async () => {
expect(await activates('import os', { ...cardRule('any'), libraries: [] })).toBe(true)
expect(await activates('x = 1', { ...cardRule('any'), libraries: [] })).toBe(true)
})
})
describe('库规则:认领', () => {
it('认领的块换成替身:种类、badge、编辑范围是整个类;规则没并进来的子块(方法)照源码顺序留下,讲并进来的子块的注释跟着并进来', async () => {
const rule: LibraryRule = {
...cardRule('orm'),
claim: (block) => {
if (block.kind !== 'class')
return undefined
const [table, column] = block.children
return { kind: 'orm.card', badge: { label: '卡片', color: { dark: '#fff', light: '#000' }, icon: 'box' }, rows: [{ label: '名称', value: [{ text: 'User', role: 'type' }] }], absorbs: [table, column] }
},
}
const [, card] = await views(MODEL, [rule])
expect(card).toMatchObject({
kind: 'orm.card',
badge: { label: '卡片' },
startLine: 2,
endLine: 8,
code: 'class User(Base):\n __tablename__ = "users"\n # 基本信息\n name = mapped_column(String)\n\n def greet(self) -> str:\n return self.name',
attributes: { kind: 'orm.card', access: 'public' },
properties: [{ label: '名称' }],
})
expect(card.children.map(child => [child.id, child.kind, child.startLine])).toEqual([['1.0', 'function', 7]])
})
it('两个字段中间写着的方法:预览里照常是卡片的子块;卡片「编辑」写回整个类', async () => {
const source = [
'from sqlalchemy.orm import mapped_column',
'',
'class User(Base):',
' id = mapped_column(Integer)',
'',
' def greet(self) -> str:',
' return self.name',
'',
' name = mapped_column(String)',
'',
].join('\n')
const fields: LibraryRule = { ...cardRule('orm'), claim: block => block.kind === 'class'
? { kind: 'orm.card', badge: { label: '卡片', color: { dark: '#fff', light: '#000' }, icon: 'box' }, rows: [], absorbs: block.children.filter(child => child.kind === 'variable') }
: undefined }
const [, card] = await views(source, [fields])
expect(card.children.map(child => [child.kind, child.startLine, child.endLine])).toEqual([['function', 5, 6]])
expect([card.startLine, card.endLine]).toEqual([2, 8])
expect(replaceLines(source, card.startLine, card.endLine, card.code)).toBe(source)
const edited = replaceLines(source, card.startLine, card.endLine, card.code.replace('greet', 'hello').replace('String', 'Text'))
expect(edited).toBe(source.replace('greet', 'hello').replace('String', 'Text'))
})
it('能编辑的范围只在块和它的子孙之间重叠:卡片盖住的是它自己留下的子块,别的块互不重叠', async () => {
const list = flatten(await views(MODEL, [cardRule('orm', {})]))
const related = (a: string, b: string): boolean => a.startsWith(`${b}.`) || b.startsWith(`${a}.`)
for (const a of list) {
for (const b of list.filter(other => other !== a && !related(a.id, other.id)))
expect(a.startLine > b.endLine || b.startLine > a.endLine, `${a.id} / ${b.id}`).toBe(true)
}
expect(list.filter(view => view.id.startsWith('1.')).every(view => view.startLine >= list[1].startLine && view.endLine <= list[1].endLine)).toBe(true)
})
it('几个规则都认同一块时先注册的算', async () => {
expect((await views(MODEL, [cardRule('first'), cardRule('second')]))[1].kind).toBe('first.card')
expect((await views(MODEL, [cardRule('second'), cardRule('first')]))[1].kind).toBe('second.card')
})
it('规则抛错:记下来,接着问下一个规则;都没认就照常显示', async () => {
const reports: string[] = []
expect((await views(MODEL, [throwing, cardRule('second')], reports))[1].kind).toBe('second.card')
expect(reports[1]).toBe('库规则 throwing 看第 3 行的 class 块时出错,这块照常显示:坏了')
const alone = await views(MODEL, [throwing])
expect(alone[1]).toMatchObject({ kind: 'class', startLine: 2, headerEndLine: 2, endLine: 8 })
expect(alone[1].children.map(child => child.kind)).toEqual(['variable', 'variable', 'function'])
})
it('替身不合约定(种类 id 没带规则 id、并进来的不是子块):记下来,照常显示', async () => {
const reports: string[] = []
const stranger: CodeBlock[] = []
await withBlocks(py, 'python', 'x = 1', (blocks) => {
stranger.push(...blocks)
})
const list = await views(MODEL, [cardRule('bad', { kind: 'card' }), cardRule('worse', { absorbs: stranger })], reports)
expect(list[1].kind).toBe('class')
expect(reports).toEqual([
'库规则 bad 给第 3 行的 class 块的替身不合约定,这块照常显示:种类 id「card」没以「bad.」开头',
'库规则 worse 给第 3 行的 class 块的替身不合约定,这块照常显示:要并进来的块不是这块的子块',
])
})
it('略去的块不出块,后面的块 id 接着往下数;还有子块、挂着注释的块不能略去,记下来、照常显示', async () => {
const omitting = (kind: string): LibraryRule => ({ ...cardRule('omit'), claim: block => block.kind === kind ? { omit: true } : undefined })
const list = await views(MODEL, [omitting('import')])
expect(list.map(view => [view.id, view.kind])).toEqual([['0', 'class']])
expect(list[0].children.map(child => child.id)).toEqual(['0.0', '0.1', '0.2'])
const reports: string[] = []
expect((await views(MODEL, [omitting('class')], reports)).map(view => view.kind)).toEqual(['import', 'class'])
expect(reports).toEqual(['库规则 omit 给第 3 行的 class 块的替身不合约定,这块照常显示:要略去的块还有子块'])
const commented: string[] = []
const [, kept] = await views(MODEL, [omitting('variable')], commented)
expect(kept.children.map(child => [child.kind, child.startLine])).toEqual([['variable', 4], ['function', 7]])
expect(commented).toEqual(['库规则 omit 给第 6 行的 variable 块的替身不合约定,这块照常显示:要略去的块挂着注释'])
})
it('挂在卡片子行上的子块:照常转成块,记下挂在哪一行的哪个子行;不是留下的子块、没有那一行 / 子行、放了两处的不合约定', async () => {
const rows = [
{ label: '字段', value: [], items: [{ cells: [[{ text: 'name', role: 'variable' as const }]] }], columns: ['名称'] },
{ label: '方法', value: [], items: [{ cells: [[{ text: 'greet', role: 'function' as const }]] }], columns: ['名称'] },
]
const slotting = (slots: (block: CodeBlock) => RuleSlot[], id = 'slot'): LibraryRule => ({
...cardRule(id),
claim: block => block.kind === 'class'
? { kind: `${id}.card`, badge: { label: '卡片', color: { dark: '#fff', light: '#000' }, icon: 'box' }, rows, absorbs: block.children.slice(0, 1), slots: slots(block) }
: undefined,
})
const [, card] = await views(MODEL, [slotting(block => [{ block: block.children[1], row: 0, item: 0 }, { block: block.children[2], row: 1, item: 0 }])])
expect(card.children.map(child => [child.kind, child.slot])).toEqual([['variable', { row: 0, item: 0 }], ['function', { row: 1, item: 0 }]])
const reports: string[] = []
const bad = await views(MODEL, [
slotting(block => [{ block: block.children[0], row: 1, item: 0 }], 'absorbed'),
slotting(block => [{ block: block.children[2], row: 5, item: 0 }], 'row'),
slotting(block => [{ block: block.children[2], row: 1, item: 1 }], 'item'),
slotting(block => [{ block: block.children[2], row: 1, item: 0 }, { block: block.children[2], row: 0, item: 0 }], 'twice'),
], reports)
expect(bad[1].kind).toBe('class')
expect(reports.map(report => report.replace(/^.*照常显示:/, ''))).toEqual([
'挂在子行上的块不是留下的子块',
'挂在卡片第 5 行的块,卡片没有这一行',
'挂在卡片第 1 行第 1 个子行的块,这一行没有这个子行',
'同一块挂在了卡片里的几处',
])
})
it('认成契约里现成的属性表块:种类、属性表、筛选属性照语言插件报的这种块,编辑范围是整个类;并进来的不是子块时照常显示', async () => {
const asEnum = (absorbs: (block: CodeBlock) => CodeBlock[]): LibraryRule => ({
...cardRule('lib'),
claim: block => block.kind === 'class' && block.facts.name
? { as: { kind: 'enum', facts: { name: block.facts.name, access: block.facts.access, decorators: [], bases: [], members: [] } }, absorbs: absorbs(block) }
: undefined,
})
const [, recognized] = await views(MODEL, [asEnum(block => block.children.slice(0, 1))])
expect(recognized).toMatchObject({ kind: 'enum', startLine: 2, endLine: 8, attributes: { kind: 'enum', access: 'public' } })
expect(recognized.badge).toBeUndefined()
expect(propertyLines('properties' in recognized ? recognized.properties : [])).toEqual(['名称: User', '访问级别: 公共', '成员: 无'])
expect(recognized.children.map(child => child.kind)).toEqual(['variable', 'function'])
const reports: string[] = []
const stranger: CodeBlock[] = []
await withBlocks(py, 'python', 'x = 1', blocks => stranger.push(...blocks))
const [, shown] = await views(MODEL, [asEnum(() => stranger)], reports)
expect(shown.kind).toBe('class')
expect(reports).toEqual(['库规则 lib 给第 3 行的 class 块的替身不合约定,这块照常显示:要并进来的块不是这块的子块'])
})
it('筛选:规则的种类和别的种类一样计数,名字取规则给的名字(badge.label)', async () => {
const list = await views(MODEL, [cardRule('orm')])
expect(fileAttributes(list, pythonZhTranslator.attributeNames).find(attribute => attribute.id === 'kind')?.values).toEqual([
{ id: 'import', name: '导入', count: 1 },
{ id: 'orm.card', name: 'orm 卡片', count: 1 },
{ id: 'variable', name: '变量', count: 1 },
{ id: 'function', name: '函数', count: 1 },
{ id: 'return', name: '返回', count: 1 },
])
})
})
describe('库规则:自己查定义的引用', () => {
const workspace: RuleWorkspace = { lookup: async () => [] }
const found: RuleLocation[] = [{ uri: 'file:///w/tasks.py', start: { line: 3, character: 4 }, end: { line: 3, character: 8 } }]
/** 认领类块、在名称那一行标一个引用的规则。 */
function referencing(id: string, reference: Segment, resolve?: LibraryRule['resolve']): LibraryRule {
return {
...cardRule(id, { rows: [{ label: '名称', value: [reference] }] }),
...(resolve && { resolve }),
}
}
it('引用要标自己的规则、规则要给 resolve、段要带下标,不然这块照常显示', async () => {
const reports: string[] = []
const resolve = async (): Promise<RuleLocation[]> => found
const list = await views(MODEL, [
referencing('other', { text: 'x', role: 'string', offset: 20, reference: { rule: 'celery', key: 'x' } }, resolve),
referencing('lazy', { text: 'x', role: 'string', offset: 20, reference: { rule: 'lazy', key: 'x' } }),
referencing('loose', { text: 'x', role: 'string', reference: { rule: 'loose', key: 'x' } }, resolve),
], reports)
expect(list[1].kind).toBe('class')
expect(reports.map(report => report.replace(/^.*照常显示:/, ''))).toEqual([
'引用「x」标的规则是「celery」,不是自己',
'标了引用「x」却没给 resolve',
'引用「x」没带源码下标',
])
const good = await views(MODEL, [referencing('good', { text: 'x', role: 'string', offset: 20, reference: { rule: 'good', key: 'x' } }, resolve)])
expect(good[1].kind).toBe('good.card')
})
it('查引用交给标它的规则;规则不在、没给 resolve、出错时当作没有定义并记下来', async () => {
const reports: string[] = []
const asked: [string, RuleWorkspace][] = []
const rule = referencing('good', { text: 'x', role: 'string', offset: 20 }, async (key, given) => {
asked.push([key, given])
return found
})
const broken = referencing('broken', { text: 'x', role: 'string', offset: 20 }, async () => {
throw new Error('索引坏了')
})
const rules = [rule, broken, cardRule('plain')]
const report = (message: string): number => reports.push(message)
expect(await resolveReference({ rule: 'good', key: 'a.b' }, rules, workspace, report)).toEqual(found)
expect(asked).toEqual([['a.b', workspace]])
expect(await resolveReference({ rule: 'broken', key: 'a.b' }, rules, workspace, report)).toEqual([])
expect(await resolveReference({ rule: 'plain', key: 'a.b' }, rules, workspace, report)).toEqual([])
expect(await resolveReference({ rule: 'gone', key: 'a.b' }, rules, workspace, report)).toEqual([])
expect(reports).toEqual([
'库规则 broken 查「a.b」的定义时出错:索引坏了',
'没有能查引用「a.b」的库规则 plain',
'没有能查引用「a.b」的库规则 gone',
])
})
})