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, definitions: Record = {}, uses: Record = {}, 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 => 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 = { 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 => 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> { 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']) }) })