import type { BlockView, RuleRun } from './blockView' import type { CodeBlock, LibraryRule, RuleBlock, RuleLocation, 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 = {}): 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' } }, 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 { 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 { 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('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([]) }) }) describe('库规则:认领', () => { it('认领的块换成替身:种类、标签、编辑范围是整个类;规则没并进来的子块(夹在中间的注释、方法)照源码顺序留下', 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' } }, 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', 'comment', 4], ['1.1', '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' } }, 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, 5]]) 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, endLine: 2 }) expect(alone[1].children.map(child => child.kind)).toEqual(['variable', 'comment', '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('认成契约里现成的属性表块:种类、属性表、筛选属性照语言插件报的这种块,编辑范围是整个类;并进来的不是子块时照常显示', 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(['comment', '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('筛选:规则的种类和别的种类一样计数,名字取规则给的标签', 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: 'comment', name: '注释', 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 => 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', ]) }) })