Files
superwork/vscode/src/codeReader/languages/python.test.ts
T

593 lines
25 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 { IterationFacts, Node } from '../contract'
import type { Outline } from '../testSupport'
import { describe, expect, it } from 'vitest'
import { factsByLine, nodeFacts, nodesToText, outline, readFixture, runtime, walk, WASM_DIR, withBlocks } from '../testSupport'
import { createPythonAdapter } from './python'
const adapter = createPythonAdapter(WASM_DIR)
function tree(source: string): Promise<Outline[]> {
return withBlocks(adapter, 'python', source, outline)
}
function facts(source: string): Promise<Record<number, unknown>> {
return withBlocks(adapter, 'python', source, factsByLine)
}
// fixtures/sample.py:行号 1 起
const SAMPLE: Outline[] = [
{ at: 1, kind: 'comment' },
{ at: 3, kind: 'import' },
{ at: 4, kind: 'import' },
{ at: 6, kind: 'const' },
{ at: 7, kind: 'variable' },
{ at: [10, 11], kind: 'function', kids: [
{ at: 13, kind: 'variable', notes: [12] },
{ at: 14, kind: 'if', kids: [{ at: 15, kind: 'return' }] },
{ at: 16, kind: 'else', kids: [{ at: 17, kind: 'variable' }] },
{ at: 18, kind: 'else', kids: [{ at: 19, kind: 'statement' }] },
{ at: 20, kind: 'loop', kids: [
{ at: 21, kind: 'if', kids: [{ at: 22, kind: 'statement' }] },
] },
{ at: 23, kind: 'try', kids: [
{ at: 24, kind: 'statement', kids: [{ at: 25, kind: 'statement' }] },
] },
{ at: 26, kind: 'catch', kids: [{ at: 27, kind: 'statement' }] },
{ at: 28, kind: 'finally', kids: [{ at: 29, kind: 'statement' }] },
{ at: 30, kind: 'return' },
] },
{ at: 33, kind: 'class', kids: [
{ at: 34, kind: 'field' },
{ at: 35, kind: 'field' },
{ at: 36, kind: 'field' },
{ at: 37, kind: 'variable' },
{ at: 39, kind: 'function', kids: [{ at: 40, kind: 'return' }] },
] },
]
describe('python 代码块树', () => {
it('样例覆盖 import / def / docstring / if-elif-else / for / try-except-finally / with / 推导式 / class 字段', async () => {
expect(await tree(readFixture('sample.py'))).toEqual(SAMPLE)
})
it('装饰器算进头部;冒号后、主体前的注释挂到主体第一条语句上;循环的 else 是兄弟块;match 的 case 是子块 if', async () => {
const source = [
'@cache',
'def load(key):',
' # 先查缓存',
' while key:',
' key = key[1:]',
' else:',
' pass',
' match key:',
' case \'a\':',
' return 1',
' case _:',
' return 2',
].join('\n')
expect(await tree(source)).toEqual([
{ at: [1, 2], kind: 'function', kids: [
{ at: 4, kind: 'loop', notes: [3], kids: [{ at: 5, kind: 'variable' }] },
{ at: 6, kind: 'else', kids: [{ at: 7, kind: 'statement' }] },
{ at: 8, kind: 'if', kids: [
{ at: 9, kind: 'if', kids: [{ at: 10, kind: 'return' }] },
{ at: 11, kind: 'if', kids: [{ at: 12, kind: 'return' }] },
] },
] },
])
})
it('docstring 只认主体开头;不是开头、也没挂到赋值上的字符串是注释,挂到后面那条语句上,f-string、字节串照旧是语句', async () => {
const source = [
'def f():',
' x = 1',
' """not a docstring"""',
' "a" \'b\'',
' f"{x}"',
' b"x"',
' "a" f"b"',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'function', kids: [
{ at: 2, kind: 'variable' },
{ at: 5, kind: 'statement', notes: [3, 4] },
{ at: 6, kind: 'statement' },
{ at: 7, kind: 'statement' },
] },
])
expect(await tree('def f(): f"not a docstring"')).toEqual([{ at: 1, kind: 'function', kids: [{ at: 1, kind: 'statement' }] }])
})
})
describe('python 属性 docstring', () => {
/** 字段(一行译文的块)的说明,按 header 起始行(1 起)索引。 */
function fieldDocs(source: string): Promise<Record<number, unknown>> {
return withBlocks(adapter, 'python', source, (blocks) => {
const byLine: Record<number, unknown> = {}
walk(blocks, (block) => {
if (block.kind === 'field' && block.doc)
byLine[block.header.startRow + 1] = nodesToText(block.doc)
})
return byLine
})
}
it('attributeDocs.py:模块、类主体里紧跟在赋值后面的字符串并进那块,头部延到字符串结尾;隔着注释、不跟在赋值后面的是注释,挂到讲的块上', async () => {
expect(await tree(readFixture('attributeDocs.py'))).toEqual([
{ at: 1, kind: 'comment' },
{ at: [3, 4], kind: 'const' },
{ at: [6, 8], kind: 'variable' },
{ at: [11, 12], kind: 'class', kids: [
{ at: [14, 15], kind: 'variable' },
{ at: [17, 19], kind: 'field', notes: [21, 22] },
] },
{ at: 25, kind: 'function', kids: [
{ at: 26, kind: 'variable' },
{ at: 31, kind: 'statement', notes: [[27, 30]] },
{ at: 32, kind: 'statement' },
{ at: 33, kind: 'return' },
] },
])
})
it('进常量、变量的 facts.doc 和字段的 doc,是那个字符串节点;函数体里的赋值没有', async () => {
const source = readFixture('attributeDocs.py')
const found = await facts(source)
expect(found[3]).toMatchObject({ doc: '"""一 deployment 一案例的唯一约束名; 入库侧按它认"重复入库", 故与建表同源。"""' })
expect(found[6]).toMatchObject({ doc: '"""隔着空行、赋值行尾有注释也算。"""' })
expect(found[14]).toMatchObject({ doc: '"""正在分析源 deployment."""' })
expect(found[26]).not.toHaveProperty('doc')
expect(await fieldDocs(source)).toEqual({ 17: '"""案例标题,\n 用户可编辑."""' })
})
it('只挂到紧跟着的下一条语句:两个字符串只有第一个是说明;没有值的注解、拼接的字符串也算;条件分支里的赋值不算', async () => {
const source = [
'class K:',
' a: int',
' "A" "B"',
' "又一个"',
'if DEBUG:',
' X = 1',
' """不算"""',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'class', kids: [{ at: [2, 3], kind: 'field', notes: [4] }] },
{ at: 5, kind: 'if', kids: [{ at: 6, kind: 'variable', notes: [7] }] },
])
expect(await fieldDocs(source)).toEqual({ 2: '"A" "B"' })
})
})
describe('python 函数、类的 docstring', () => {
const source = [
'class K:',
' """类说明"""',
' def run(self): # 行尾',
' # 先说一句',
' """跑一遍。',
'',
' 细节。',
' """ # 行尾',
' # 真正的注释',
' return 1',
'def g(): "单行"',
].join('\n')
it('算进函数、类头部、不单独成块,它前面的注释也在头部里;后面的注释挂到后面那条语句上', async () => {
expect(await tree(source)).toEqual([
{ at: [1, 2], kind: 'class', kids: [
{ at: [3, 8], kind: 'function', kids: [
{ at: 10, kind: 'return', notes: [9] },
] },
] },
{ at: 11, kind: 'function' },
])
})
it('进 facts.doc:是那个字符串节点;没有 docstring 的函数没有;模块开头的 docstring 照旧是注释块', async () => {
const found = await facts(source)
expect(found[1]).toMatchObject({ name: 'K', doc: '"""类说明"""' })
expect(found[3]).toMatchObject({ name: 'run', doc: '"""跑一遍。\n\n 细节。\n """' })
expect(await tree('"""模块说明"""\nclass K:\n pass')).toEqual([{ at: 1, kind: 'comment' }, { at: 2, kind: 'class', kids: [{ at: 3, kind: 'statement' }] }])
expect(found[11]).toMatchObject({ name: 'g', doc: '"单行"' })
expect(await facts('def f():\n x = 1\n """not a docstring"""')).not.toHaveProperty('1.doc')
})
})
describe('python 属性表信息(facts)', () => {
it('sample.py:import / from import、模块和类里的赋值、函数和方法', async () => {
const sample = await facts(readFixture('sample.py'))
expect(sample[3]).toEqual({ sources: ['os'], items: [{ kind: 'module', module: 'os' }] })
expect(sample[4]).toEqual({ sources: ['pathlib'], items: [{ kind: 'named', name: 'Path', alias: 'P' }] })
expect(sample[6]).toEqual({ access: 'public', declarators: [{ name: 'MAX_DEPTH', value: '8' }] })
expect(sample[10]).toEqual({
name: 'find_env_files',
doc: '"""Return every .env file under root."""',
access: 'public',
async: false,
decorators: [],
parameters: [{ name: 'root', optional: false, type: 'str' }],
returnType: 'list[str]',
})
expect(sample[17]).toEqual({ declarators: [{ name: 'found', value: 'found[:MAX_DEPTH]' }] })
expect(sample[37]).toEqual({ access: 'public', declarators: [{ name: 'label', value: '\'env\'' }] })
expect(sample[39]).toMatchObject({
name: 'summary',
access: 'public',
parameters: [{ name: 'self', optional: false }, { name: 'verbose', optional: false, type: 'bool', default: 'False' }],
returnType: 'str',
})
})
it('routes.py:future 导入、多行括号导入、带装饰器的 async def、await 赋值', async () => {
const routes = await facts(readFixture('routes.py'))
expect(routes[6]).toEqual({ sources: ['__future__'], items: [{ kind: 'named', name: 'annotations' }] })
expect(routes[19]).toEqual({
sources: ['.schemas'],
items: [
{ kind: 'named', name: 'UserAvatarUploadResponse' },
{ kind: 'named', name: 'UserDetailResponse' },
{ kind: 'named', name: 'UserUpdateRequest' },
],
})
expect(routes[29]).toEqual({
name: 'get_user',
doc: '"""获取用户详情。\n\n 管理员可查看任意用户,普通用户只能查看自己。\n """',
access: 'public',
async: true,
decorators: ['@router.get("/{user_id}", response_model=UserDetailResponse)'],
parameters: [
{ name: 'user_id', optional: false, type: 'Int4Path' },
{ name: 'db', optional: false, type: 'AsyncSession', default: 'Depends(get_db)' },
{ name: 'current_user', optional: false, type: 'User', default: 'Depends(get_current_user)' },
],
returnType: 'UserDetailResponse',
})
expect(routes[118]).toEqual({ declarators: [{ name: 'payload', value: 'await file.read()' }] })
})
it('导入:import a.b as c, d 每个模块一个来源;from . import、from m import *', async () => {
const source = [
'import os.path as osp, sys',
'from . import x',
'from m import *',
].join('\n')
expect(await facts(source)).toEqual({
1: { sources: ['os.path', 'sys'], items: [{ kind: 'module', module: 'os.path', alias: 'osp' }, { kind: 'module', module: 'sys' }] },
2: { sources: ['.'], items: [{ kind: 'named', name: 'x' }] },
3: { sources: ['m'], items: [{ kind: 'namespace' }] },
})
})
it('形参:*args / **kwargs 带前缀,* 和 / 分隔符不算参数', async () => {
const found = await facts('async def f(a, /, b: int, *args: int, c=1, d: str = \'x\', **kw) -> None:\n pass')
expect(found[1]).toEqual({
name: 'f',
access: 'public',
async: true,
decorators: [],
parameters: [
{ name: 'a', optional: false },
{ name: 'b', optional: false, type: 'int' },
{ name: '*args', optional: false, type: 'int' },
{ name: 'c', optional: false, default: '1' },
{ name: 'd', optional: false, type: 'str', default: '\'x\'' },
{ name: '**kw', optional: false },
],
returnType: 'None',
})
})
it('访问级别:单下划线开头私有、__dunder__ 公共;解构全是私有名才私有;函数体里的没有', async () => {
const source = [
'_cache = {}',
'__all__ = [\'f\']',
'a, _b = 1, 2',
'_c, _d = 1, 2',
'total: int',
'class K:',
' def __init__(self):',
' self.x = 1',
' def _helper(self):',
' def inner():',
' pass',
].join('\n')
const found = await facts(source)
expect(found[1]).toMatchObject({ access: 'private' })
expect(found[2]).toMatchObject({ access: 'public' })
expect(found[3]).toEqual({ access: 'public', declarators: [{ name: 'a, _b', value: '1, 2' }] })
expect(found[4]).toMatchObject({ access: 'private' })
expect(found[5]).toEqual({ access: 'public', declarators: [{ name: 'total', type: 'int' }] })
expect(found[7]).toMatchObject({ name: '__init__', access: 'public' })
expect(found[8]).toEqual({ declarators: [{ name: 'self.x', value: '1' }] })
expect(found[9]).toMatchObject({ name: '_helper', access: 'private' })
expect(found[10]).not.toHaveProperty('access')
})
it('声明的值照源码给,字面量的各项不在 facts 里(见 LanguageAdapter.collection)', async () => {
const found = await facts('EMPTY = {}\nITEMS = [1]\nclass K:\n labels = {"x": 1}')
expect(found[1]).toEqual({ access: 'public', declarators: [{ name: 'EMPTY', value: '{}' }] })
expect(found[2]).toEqual({ access: 'public', declarators: [{ name: 'ITEMS', value: '[1]' }] })
expect(found[4]).toEqual({ access: 'public', declarators: [{ name: 'labels', value: '{"x": 1}' }] })
})
})
describe('python 字面量的各项', () => {
const collections = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'python', source, node => adapter.collection?.(node))
it('字典:字符串键、名字键、**展开,注释不算一项;值里的字典另有自己的各项,空字典没有项', async () => {
const source = [
'_KEYS = {',
' "published": "contests:publish",',
' key: 1,',
' # 注释不算一项',
' "nested": {"a": {}},',
' **other,',
'}',
].join('\n')
expect(await collections(source)).toEqual([
{ kind: 'map', entries: [
{ kind: 'pair', key: '"published"', value: '"contests:publish"' },
{ kind: 'pair', key: 'key', value: '1' },
{ kind: 'pair', key: '"nested"', value: '{"a": {}}' },
{ kind: 'spread', value: 'other' },
] },
{ kind: 'map', entries: [{ kind: 'pair', key: '"a"', value: '{}' }] },
{ kind: 'map', entries: [] },
])
})
it('列表、元组、集合:各项照源码顺序,*x 是展开,注释不算一项;值里的字面量另有自己的各项,空列表没有项', async () => {
const source = [
'filters = [',
' Filter(field="user_id"),',
' # 注释不算一项',
' *rest,',
' (a, b),',
' {1},',
' [],',
']',
].join('\n')
expect(await collections(source)).toEqual([
{ kind: 'sequence', elements: [
{ kind: 'item', value: 'Filter(field="user_id")' },
{ kind: 'spread', value: 'rest' },
{ kind: 'item', value: '(a, b)' },
{ kind: 'item', value: '{1}' },
{ kind: 'item', value: '[]' },
] },
{ kind: 'sequence', elements: [{ kind: 'item', value: 'a' }, { kind: 'item', value: 'b' }] },
{ kind: 'sequence', elements: [{ kind: 'item', value: '1' }] },
{ kind: 'sequence', elements: [] },
])
})
it('推导式、括号里的一个式子、调用的实参列表、别的值不是', async () => {
expect(await collections('x = [a for a in b]\ny = (a)\nf(a, b)\nz = a, b')).toEqual([])
})
})
describe('python 类', () => {
it('classes.py:docstring(连同它前面的注释)算进类头部,类里的类、函数里的类照常成块', async () => {
expect(await tree(readFixture('classes.py'))).toEqual([
{ at: 1, kind: 'comment' },
{ at: 3, kind: 'import' },
{ at: [6, 10], kind: 'class', kids: [
{ at: 12, kind: 'field' },
{ at: 14, kind: 'function', kids: [{ at: 15, kind: 'variable' }] },
] },
{ at: [18, 20], kind: 'class', kids: [{ at: 21, kind: 'variable' }] },
{ at: [24, 25], kind: 'class', kids: [
{ at: 26, kind: 'class', kids: [{ at: 27, kind: 'variable' }] },
{ at: 29, kind: 'class', kids: [{ at: 30, kind: 'statement' }] },
] },
{ at: 33, kind: 'class', kids: [{ at: 34, kind: 'statement' }] },
{ at: 37, kind: 'function', kids: [
{ at: [38, 39], kind: 'class' },
{ at: 40, kind: 'return' },
] },
])
})
it('classes.py:名称、说明、访问级别(同函数的下划线规则,类里的类也按它,函数里的没有)、装饰器、父类(参数列表照写的顺序,关键字参数也算)', async () => {
const found = await facts(readFixture('classes.py'))
expect(found[6]).toEqual({
name: 'AppResponse',
doc: '"""应用响应的基类。\n\n 带上 HTTP 状态码。\n """',
access: 'public',
decorators: [],
typeParameters: [],
bases: [{ value: 'BaseModel' }],
implements: [],
})
expect(found[18]).toMatchObject({ name: 'NotFound', doc: '"""找不到资源。"""', bases: [{ value: 'AppResponse' }] })
expect(found[24]).toEqual({
name: 'Meta',
access: 'public',
decorators: ['@dataclass(frozen=True)'],
typeParameters: [],
bases: [{ value: 'Base' }, { value: 'metaclass=ABCMeta' }],
implements: [],
})
expect(found[26]).toMatchObject({ name: 'Config', access: 'public', bases: [] })
expect(found[29]).toMatchObject({ name: '_Hidden', access: 'private' })
expect(found[33]).toMatchObject({ name: '_Private', access: 'private' })
expect(found[38]).toEqual({ name: 'Local', doc: '"""局部类。"""', decorators: [], typeParameters: [], bases: [], implements: [] })
})
it('没有「抽象」这个概念;Python 3.12 的泛型参数进 typeParameters;*bases、**kw 也是父类里的一项', async () => {
expect((await facts('class G[T](Generic[T], *mixins, **kw):\n pass'))[1]).toEqual({
name: 'G',
access: 'public',
decorators: [],
typeParameters: ['T'],
bases: [{ value: 'Generic[T]' }, { value: '*mixins' }, { value: '**kw' }],
implements: [],
})
})
})
describe('python 写在表达式里的遍历(推导式)', () => {
/** 源码里每个认得出的遍历(外层在前),节点换成源码。 */
async function iterations(source: string): Promise<unknown[]> {
const tree = await runtime.parse(adapter, 'python', source)
try {
const found: IterationFacts[] = []
const visit = (node: Node): void => {
const facts = adapter.iteration?.(node)
if (facts)
found.push(facts)
for (const child of node.children) {
if (child)
visit(child)
}
}
visit(tree.rootNode)
return found.map(nodesToText)
}
finally {
tree.delete()
}
}
it('调用唯一的参数是生成器:那对括号是调用的,记在 sharedParentheses;参数不止一个时生成器自己带括号,没有', async () => {
expect(await iterations('"".join(secrets.choice(ALPHABET) for _ in range(length))')).toEqual([{
result: 'generator',
clauses: [{ kind: 'for', variables: '_', source: 'range(length)', async: false }],
value: 'secrets.choice(ALPHABET)',
sharedParentheses: { open: '(', close: ')' },
}])
const source = 'f(a, (x for x in y))'
expect(await iterations(source)).toEqual([{
result: 'generator',
clauses: [{ kind: 'for', variables: 'x', source: 'y', async: false }],
value: 'x',
}])
})
it('列表带条件、字典分出键和值、集合的变量是解构', async () => {
expect(await iterations('[u.id for u in users if u.is_active]')).toEqual([{
result: 'list',
clauses: [{ kind: 'for', variables: 'u', source: 'users', async: false }, { kind: 'if', condition: 'u.is_active' }],
value: 'u.id',
}])
expect(await iterations('{u.id: u for u in users}')).toEqual([{
result: 'dict',
clauses: [{ kind: 'for', variables: 'u', source: 'users', async: false }],
key: 'u.id',
value: 'u',
}])
expect(await iterations('{n for n, _ in pairs}')).toEqual([{
result: 'set',
clauses: [{ kind: 'for', variables: 'n, _', source: 'pairs', async: false }],
value: 'n',
}])
})
it('几节 for、if 照源码顺序;async for 标出异步', async () => {
expect(await iterations('[c for row in grid if row for c in row if c]')).toEqual([{
result: 'list',
clauses: [
{ kind: 'for', variables: 'row', source: 'grid', async: false },
{ kind: 'if', condition: 'row' },
{ kind: 'for', variables: 'c', source: 'row', async: false },
{ kind: 'if', condition: 'c' },
],
value: 'c',
}])
expect(await iterations('[v async for v in stream() if v]')).toEqual([{
result: 'list',
clauses: [{ kind: 'for', variables: 'v', source: 'stream()', async: true }, { kind: 'if', condition: 'v' }],
value: 'v',
}])
})
it('套着的遍历各自认,外层取的值就是里层整个遍历', async () => {
expect(await iterations('[[y for y in row] for row in grid]')).toEqual([
{ result: 'list', clauses: [{ kind: 'for', variables: 'row', source: 'grid', async: false }], value: '[y for y in row]' },
{ result: 'list', clauses: [{ kind: 'for', variables: 'y', source: 'row', async: false }], value: 'y' },
])
})
it('列表、字典、元组字面量和 for 语句不是', async () => {
expect(await iterations('x = [1, 2]\ny = {"a": 1}\nz = (a, b)\nfor x in y:\n pass')).toEqual([])
})
})
describe('python 调用', () => {
const calls = (source: string): Promise<unknown[]> => nodeFacts(adapter, 'python', source, node => adapter.call?.(node))
it('实参照源码顺序:按位置、按名字、*x、**x;新建对象和调用写法一样,不给 construct;点分名字取最后一节', async () => {
const source = 'user_service.update_user(\n db, # 会话\n user_id=user_id,\n *rest,\n **options,\n)'
expect(await calls(source)).toEqual([{
callee: 'user_service.update_user',
name: 'update_user',
argumentList: '(\n db, # 会话\n user_id=user_id,\n *rest,\n **options,\n)',
arguments: [
{ kind: 'positional', value: 'db' },
{ kind: 'keyword', name: 'user_id', value: 'user_id' },
{ kind: 'spread', value: 'rest' },
{ kind: 'keywordSpread', value: 'options' },
],
}])
})
it('唯一的实参是生成器表达式时就是它;被调用的不是名字时没有 name;装饰器、父类列表不是调用', async () => {
expect(await calls('sum(x for x in xs)')).toEqual([{ callee: 'sum', name: 'sum', argumentList: '(x for x in xs)', arguments: [{ kind: 'positional', value: '(x for x in xs)' }] }])
expect(await calls('handlers[k]()')).toEqual([{ callee: 'handlers[k]', argumentList: '()', arguments: [] }])
expect(await calls('@cache\nclass A(Base):\n pass')).toEqual([])
})
})
describe('python 注释挂到它讲的块上', () => {
it('同一层后面最近的那块(隔着空行也算);主体最后的挂到前面那块;主体里只有注释时挂到父块;文件最后的挂到最后一块', async () => {
const source = [
'"""模块说明。"""',
'',
'import os',
'',
'',
'# 讲 load:隔着空行',
'def load(path):',
' """读出来。"""',
' # 先检查',
' if not path:',
' return None',
' return path',
' # 函数体最后的注释',
'',
'',
'class Empty:',
' """只有说明。"""',
' # 只有注释的主体',
'',
'',
'# 文件最后的注释',
].join('\n')
expect(await tree(source)).toEqual([
{ at: 1, kind: 'comment' },
{ at: 3, kind: 'import' },
{ at: [7, 8], kind: 'function', notes: [6], kids: [
{ at: 10, kind: 'if', notes: [9], kids: [{ at: 11, kind: 'return' }] },
{ at: 12, kind: 'return', notes: [13] },
] },
{ at: [16, 17], kind: 'class', notes: [18, 21] },
])
})
it('文件开头的注释和第一块代码隔着空行是文件的说明,紧挨着就挂到它上面;#! 和写编码的那行总是文件的', async () => {
expect(await tree('# 文件的说明\n\nimport os\n')).toEqual([{ at: 1, kind: 'comment' }, { at: 3, kind: 'import' }])
expect(await tree('# 讲导入\nimport os\n')).toEqual([{ at: 2, kind: 'import', notes: [1] }])
expect(await tree('#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n# 讲导入\nimport os\n')).toEqual([
{ at: [1, 2], kind: 'comment' },
{ at: 4, kind: 'import', notes: [3] },
])
})
it('luffy 的 acquisition/repository.py:导入后面隔着两个空行的注释挂到类上,是类卡片的「注释」一行,类的编辑范围连着这几行', async () => {
const source = readFixture('acquisitionRepository.py')
const [klass] = (await tree(source)).filter(block => block.kind === 'class')
expect(klass).toMatchObject({ at: [19, 21], notes: [[13, 16]] })
})
})