Files
superwork/vscode/src/codeReader/symbolLinks.test.ts
T

319 lines
18 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 } from './blockView'
import type { LanguageAdapter, SourceRange } from './contract'
import type { ReferenceSources, SymbolNode } from './symbolLinks'
import { describe, expect, it } from 'vitest'
import { createPythonAdapter } from './languages/python'
import { createTypeScriptAdapter } from './languages/typescript'
import { importsIn } from './libraryRules'
import { importedNames, outsideUses, referenceScope, resolveReference, splitReference } from './symbolLinks'
import { readFixture, runtime, WASM_DIR, withBlocks } from './testSupport'
const py = createPythonAdapter(WASM_DIR)
const ts = createTypeScriptAdapter(WASM_DIR)
function symbol(name: string, start: number, end: number, children: SymbolNode[] = []): SymbolNode {
return { name, range: { startIndex: start, endIndex: end }, selection: { startIndex: start, endIndex: start + name.length }, children }
}
/**
* 假的语言扩展:几个文件的文档符号,本文件几处名字的定义,本文件代码里用到、本文件又没定义的名字(名字 → 各处的起点),
* 语言自带的名字;记下问过哪些。
*/
function sources(
files: Record<string, SymbolNode[]>,
definitions: Record<number, { uri: string, at: number, length: number }> = {},
uses: Record<string, number[]> = {},
builtins: string[] = [],
): ReferenceSources & { asked: number[] } {
const asked: number[] = []
return {
uri: 'file:///here.py',
asked,
symbols: async uri => files[uri] ?? [],
definition: async (offset) => {
asked.push(offset)
const found = definitions[offset]
return found && { uri: found.uri, selection: { startIndex: found.at, endIndex: found.at + found.length } }
},
uses: name => (uses[name] ?? []).map(start => ({ startIndex: start, endIndex: start + name.length })),
builtin: name => builtins.includes(name),
}
}
describe('不带路径的引用:拆名字', () => {
it('拆开 Class.member:名字和成员,单个名字没有成员', () => {
expect(splitReference('Agent.owner_id')).toEqual({ head: 'Agent', members: ['owner_id'] })
expect(splitReference('USER')).toEqual({ head: 'USER', members: [] })
})
})
describe('不带路径的引用:指到哪', () => {
const imports = [{ name: 'Agent', offset: 20, range: { startIndex: 20, endIndex: 25 } }]
it('本文件定义的优先于导入的;不问语言扩展', async () => {
const found = sources({ 'file:///here.py': [symbol('Agent', 100, 200)] }, { 20: { uri: 'file:///models.py', at: 5, length: 5 } })
expect(await resolveReference('Agent', undefined, imports, found)).toEqual({ uri: 'file:///here.py', selection: { startIndex: 100, endIndex: 105 } })
expect(found.asked).toEqual([])
})
it('本文件的 Class.member 在类底下找成员;类有、成员没有就是找不到,不再去导入里找', async () => {
const here = sources({ 'file:///here.py': [symbol('Agent', 100, 200, [symbol('owner_id', 120, 140)])] }, { 20: { uri: 'file:///models.py', at: 5, length: 5 } })
expect(await resolveReference('Agent.owner_id', undefined, imports, here)).toEqual({ uri: 'file:///here.py', selection: { startIndex: 120, endIndex: 128 } })
expect(await resolveReference('Agent.missing', undefined, imports, here)).toBeUndefined()
expect(here.asked).toEqual([])
})
it('导入语句里的名字不算本文件定义的:问那个名字的定义,Class.member 在定义所在文件的那个类底下找', async () => {
const files = {
'file:///here.py': [symbol('Agent', 20, 25)],
'file:///models.py': [symbol('Other', 0, 4, [symbol('owner_id', 1, 3)]), symbol('Agent', 10, 90, [symbol('owner_id', 30, 50)])],
}
const found = sources(files, { 20: { uri: 'file:///models.py', at: 10, length: 5 } })
expect(await resolveReference('Agent', undefined, imports, found)).toEqual({ uri: 'file:///models.py', selection: { startIndex: 10, endIndex: 15 } })
expect(await resolveReference('Agent.owner_id', undefined, imports, found)).toEqual({ uri: 'file:///models.py', selection: { startIndex: 30, endIndex: 38 } })
expect(found.asked).toEqual([20, 20])
})
it('导入的是整个模块(定义落在文件开头)时成员从顶层找', async () => {
const files = { 'file:///errors.py': [symbol('NotFound', 40, 80)] }
const found = sources(files, { 7: { uri: 'file:///errors.py', at: 0, length: 0 } })
expect(await resolveReference('errors.NotFound', undefined, [{ name: 'errors', offset: 7, range: { startIndex: 7, endIndex: 13 } }], found))
.toEqual({ uri: 'file:///errors.py', selection: { startIndex: 40, endIndex: 48 } })
})
it('本文件没定义、也没导入,或语言扩展答不上:找不到', async () => {
expect(await resolveReference('Nowhere', undefined, imports, sources({}))).toBeUndefined()
expect(await resolveReference('Agent', undefined, imports, sources({}))).toBeUndefined()
})
})
describe('不带路径的引用:从被注释的声明起一层层往外找(KDoc)', () => {
const imports = [{ name: 'Agent', offset: 20, range: { startIndex: 20, endIndex: 25 } }]
/** services.py 的样子:AgentService 里套着类 Config 和几个方法,get_default_agent 里有个局部变量;顶层还有个同名的 helper。 */
const here = (): ReturnType<typeof sources> => sources({
'file:///here.py': [
symbol('Agent', 20, 25),
symbol('AgentService', 100, 900, [
symbol('Config', 120, 180, [symbol('timeout', 140, 150)]),
symbol('get_agent_admitted', 200, 300),
symbol('get_default_agent', 400, 500, [symbol('agent', 450, 460)]),
symbol('helper', 600, 700),
]),
symbol('helper', 950, 990),
],
}, { 20: { uri: 'file:///models.py', at: 10, length: 5 } })
/** 各个声明的名字在源码里的下标:注释写在它的说明里时从这儿往外找。 */
const SERVICE = 100
const DEFAULT_AGENT = 400
const HELPER = 950
const at = (uri: string, start: number, name: string): unknown => ({ uri, selection: { startIndex: start, endIndex: start + name.length } })
it('方法说明里的 [同类的另一个方法] 找得到', async () => {
expect(await resolveReference('get_agent_admitted', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 200, 'get_agent_admitted'))
})
it('类说明里的 [成员] 找得到;方法说明里的 [自己里面的名字] 也找得到', async () => {
expect(await resolveReference('get_default_agent', SERVICE, imports, here())).toEqual(at('file:///here.py', 400, 'get_default_agent'))
expect(await resolveReference('agent', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 450, 'agent'))
})
it('近的先认:方法说明里的 [helper] 是同类的方法,顶层函数说明里的是顶层函数', async () => {
expect(await resolveReference('helper', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 600, 'helper'))
expect(await resolveReference('helper', HELPER, imports, here())).toEqual(at('file:///here.py', 950, 'helper'))
})
it('顶层的、导入的照样找得到;导入语句里的名字不算本文件定义的', async () => {
const found = here()
expect(await resolveReference('AgentService', DEFAULT_AGENT, imports, found)).toEqual(at('file:///here.py', 100, 'AgentService'))
expect(await resolveReference('Agent', DEFAULT_AGENT, imports, found)).toEqual(at('file:///models.py', 10, 'Agent'))
expect(found.asked).toEqual([20])
})
it('同一个名字写在两块的说明里:方法说明里找得到同类的方法,顶层函数说明里、不在哪个声明的注释里都找不到', async () => {
expect(await resolveReference('get_agent_admitted', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 200, 'get_agent_admitted'))
expect(await resolveReference('get_agent_admitted', HELPER, imports, here())).toBeUndefined()
expect(await resolveReference('get_agent_admitted', undefined, imports, here())).toBeUndefined()
})
it('写成 Class.member 的:第一段同样一层层找,再往下找成员', async () => {
expect(await resolveReference('Config.timeout', DEFAULT_AGENT, imports, here())).toEqual(at('file:///here.py', 140, 'timeout'))
expect(await resolveReference('AgentService.get_agent_admitted', HELPER, imports, here())).toEqual(at('file:///here.py', 200, 'get_agent_admitted'))
expect(await resolveReference('Config.timeout', HELPER, imports, here())).toBeUndefined()
})
})
describe('不带路径的引用:本文件代码里用到的、语言自带的名字', () => {
const imports = [{ name: 'Agent', offset: 20, range: { startIndex: 20, endIndex: 25 } }]
const BUILTINS = 'file:///typeshed/builtins.pyi'
/** load、save 两个函数,helper 里没用到 ValueError;ValueError 在文件顶层、load 里、save 里各用了一处,Agent 是导入的、代码里也用到了。 */
const here = (definitions: Record<number, { uri: string, at: number, length: number }> = {
50: { uri: BUILTINS, at: 1000, length: 10 },
150: { uri: BUILTINS, at: 1000, length: 10 },
450: { uri: BUILTINS, at: 1000, length: 10 },
20: { uri: 'file:///models.py', at: 10, length: 5 },
}): ReturnType<typeof sources> => sources({
'file:///here.py': [symbol('Agent', 20, 25), symbol('load', 100, 300), symbol('save', 400, 600), symbol('helper', 700, 800)],
[BUILTINS]: [symbol('ValueError', 1000, 1200, [symbol('args', 1100, 1150)])],
}, definitions, { ValueError: [50, 150, 450], Agent: [20, 900] }, ['ValueError', 'str'])
const LOAD = 100
const SAVE = 400
const HELPER = 700
const valueError = { uri: BUILTINS, selection: { startIndex: 1000, endIndex: 1010 } }
it('问写在代码里的那处的定义(同 ⌘ + 点击它):先挑用在这段注释说明的那个声明里面的,没有再挑整个文件的第一处', async () => {
const inLoad = here()
expect(await resolveReference('ValueError', LOAD, imports, inLoad)).toEqual(valueError)
expect(inLoad.asked).toEqual([150])
const inSave = here()
expect(await resolveReference('ValueError', SAVE, imports, inSave)).toEqual(valueError)
expect(inSave.asked).toEqual([450])
const elsewhere = here()
expect(await resolveReference('ValueError', HELPER, imports, elsewhere)).toEqual(valueError)
expect(await resolveReference('ValueError', undefined, imports, elsewhere)).toEqual(valueError)
expect(elsewhere.asked).toEqual([50, 50])
})
it('写成 Class.member 的:在定义所在文件的那个类底下找成员,没有这个成员是找不到,不当语言自带的', async () => {
expect(await resolveReference('ValueError.args', LOAD, imports, here())).toEqual({ uri: BUILTINS, selection: { startIndex: 1100, endIndex: 1104 } })
expect(await resolveReference('ValueError.missing', LOAD, imports, here())).toBeUndefined()
})
it('本文件定义的、导入的照旧先认:导入的名字代码里也用到了,问的还是导入语句里那处', async () => {
const found = here()
expect(await resolveReference('Agent', LOAD, imports, found)).toEqual({ uri: 'file:///models.py', selection: { startIndex: 10, endIndex: 15 } })
expect(await resolveReference('load', SAVE, imports, found)).toEqual({ uri: 'file:///here.py', selection: { startIndex: 100, endIndex: 104 } })
expect(found.asked).toEqual([20])
})
it('代码里没写到的语言自带的名字是 builtin(当普通文字),Class.member 看第一段;不是语言自带的找不到', async () => {
const found = here()
expect(await resolveReference('str', LOAD, imports, found)).toBe('builtin')
expect(await resolveReference('str.join', LOAD, imports, found)).toBe('builtin')
expect(await resolveReference('Nowhere', LOAD, imports, found)).toBeUndefined()
expect(found.asked).toEqual([])
})
it('代码里写到了、语言扩展却答不上:语言自带的是 builtin,不标成找不到', async () => {
expect(await resolveReference('ValueError', LOAD, imports, here({}))).toBe('builtin')
})
})
describe('本文件代码里用到、本文件又没定义的名字(取自语言插件认的绑定)', () => {
async function usesIn(adapter: LanguageAdapter, languageId: string, source: string): Promise<Map<string, SourceRange[]>> {
const tree = await runtime.parse(adapter, languageId, source)
try {
return outsideUses(adapter.bindings!(tree.rootNode, source, languageId), source)
}
finally {
tree.delete()
}
}
const at = (source: string, text: string, name: string): SourceRange => {
const start = source.indexOf(text)
return { startIndex: start, endIndex: start + name.length }
}
it('python:内置异常、内置函数照源码顺序记下各处;注释、字符串、属性名里的不算,本文件定义的不在', async () => {
const source = [
'import os',
'',
'def load(path):',
' """读 path,不对时 @throws [ValueError]"""',
' # ValueError 也写在注释里',
' if not os.path.exists(path):',
' raise FileNotFoundError(path)',
' data = config.ValueError',
' raise ValueError(len(data))',
'',
'def check():',
' raise ValueError("ValueError")',
'',
].join('\n')
const uses = await usesIn(py, 'python', source)
expect(uses.get('ValueError')).toEqual([at(source, 'ValueError(len', 'ValueError'), at(source, 'ValueError("', 'ValueError')])
expect(uses.get('FileNotFoundError')).toEqual([at(source, 'FileNotFoundError', 'FileNotFoundError')])
expect(uses.get('len')).toEqual([at(source, 'len(', 'len')])
expect(['path', 'data', 'load', 'check'].filter(name => uses.has(name))).toEqual([])
})
it('typeScript:throw 的 Error、用到的 JSON 记下;注释、字符串、属性名里的不算,本文件定义的不在', async () => {
const source = [
'// Error 写在注释里',
'export function parse(text: string): unknown {',
' const data = JSON.parse(text)',
' if (data.Error)',
' throw new Error(\'Error\')',
' return data',
'}',
'',
].join('\n')
const uses = await usesIn(ts, 'typescript', source)
expect(uses.get('Error')).toEqual([at(source, 'Error(', 'Error')])
expect(uses.get('JSON')).toEqual([at(source, 'JSON', 'JSON')])
expect(['parse', 'text', 'data'].filter(name => uses.has(name))).toEqual([])
})
})
describe('语言插件给的语言自带的名字', () => {
it('python 的是 dir(builtins) 里公开的:内置异常、类型、函数、常量;认异常类照旧只看异常那部分', async () => {
expect(['ValueError', 'FileNotFoundError', 'str', 'dict', 'len', 'print', 'None', 'NotImplemented'].filter(name => !py.builtins?.has(name))).toEqual([])
expect(['os', 'Path', 'self', '__import__'].filter(name => py.builtins?.has(name))).toEqual([])
const exceptions = await withBlocks(py, 'python', 'class A(ValueError):\n pass\n\nclass B(str):\n pass\n', blocks => blocks.map(block => block.kind))
expect(exceptions).toEqual(['exception', 'class'])
})
it('typeScript 的是 ECMAScript 标准的全局对象,浏览器、Node 另加的不算', () => {
expect(['Error', 'TypeError', 'RangeError', 'Promise', 'Array', 'Map', 'Set', 'Object', 'JSON', 'Math'].filter(name => !ts.builtins?.has(name))).toEqual([])
expect(['window', 'document', 'process', 'Buffer'].filter(name => ts.builtins?.has(name))).toEqual([])
})
})
describe('注释写在哪块里:从哪个声明起往外找', () => {
function block(anchor: string, defines: number | undefined, children: BlockView[] = []): BlockView {
return { anchor, children, ...(defines !== undefined && { defines: { startIndex: defines, endIndex: defines + 1 } }) } as BlockView
}
const blocks = [
block('class AgentService', 100, [
block('class AgentService > function get_default_agent', 400, [
block('class AgentService > function get_default_agent > if @3fa1c2d0', undefined),
]),
]),
block('comment @0b1c2d3e', undefined),
]
it('有名字的块(声明)是它自己的名字;没名字的(注释、语句)取外层最近的有名字的块', () => {
expect(referenceScope(blocks, 'class AgentService')).toBe(100)
expect(referenceScope(blocks, 'class AgentService > function get_default_agent')).toBe(400)
expect(referenceScope(blocks, 'class AgentService > function get_default_agent > if @3fa1c2d0')).toBe(400)
})
it('顶层没名字的块、块树里没有的块:没有,只从文件顶层找', () => {
expect(referenceScope(blocks, 'comment @0b1c2d3e')).toBeUndefined()
expect(referenceScope(blocks, 'class Gone')).toBeUndefined()
})
})
describe('导入引进来的名字(取自预览已经读出的导入 facts)', () => {
it('python:from 导入问原名的位置、别名按别名认,整个模块按第一节认、起了别名问最后一节,import * 没有', async () => {
const source = 'from a.b import C, D as E\nimport os.path\nimport x.y as z\nfrom m import *\n'
const names = await withBlocks(py, 'python', source, blocks => importedNames(importsIn(blocks)))
expect(names.map(item => [item.name, source.slice(item.offset, item.offset + 1), source.slice(item.range.startIndex, item.range.endIndex)])).toEqual([
['C', 'C', 'C'],
['E', 'D', 'D as E'],
['os', 'o', 'os.path'],
['z', 'y', 'x.y as z'],
])
})
it('typeScript:具名、默认、命名空间导入', async () => {
const source = 'import d, { b as c } from \'y\'\nimport * as ns from \'z\'\n'
const names = await withBlocks(ts, 'typescript', source, blocks => importedNames(importsIn(blocks)))
expect(names.map(item => [item.name, source.slice(item.offset, item.offset + 1)])).toEqual([['d', 'd'], ['c', 'b'], ['ns', 'n']])
})
it('gate.py 导入的 require_desktop_client、AppException 都在,本文件定义的 LocalRuntimeDisabled 不在', async () => {
const names = await withBlocks(py, 'python', readFixture('localRuntimeGate.py'), blocks => importedNames(importsIn(blocks)))
expect(names.map(item => item.name)).toEqual(['annotations', 'Request', 'get_local_runtime_enabled', 'AppException', 'require_desktop_client'])
})
})