/** * 前端发 HTTP 请求的地方:openapi-fetch 的 `X.GET("/path", ...)` 这类(成员名是大写的 HTTP 方法),`fetch("/path", { method: "POST" })`, * 和经包装函数发的:一个函数把自己收到的网址参数原样作为第一个参数交给 fetch 或另一个包装函数(可以跨文件、可以多层),它就是包装函数, * 调用它的地方照 fetch 的规则取网址和方法。网址要写成字符串字面量或模板字符串,模板里的 `${名字}` 指向字符串常量的拼进常量的值, * 别的 `${...}` 照写,当参数段;接口的调用树拿它找前端调用了哪个接口(见 endpoint.ts)。 * * 工作区索引按文件记下相关的调用和导出的字符串常量,连同被调的名字、网址里的名字在这份文件里指的是谁(本文件的定义、import 来源、全局的 fetch); * 谁是包装函数、方法是什么查的时候才从 fetch 起一层层认(见 senders),导入的常量查的时候才取值(见 pathsOf),文件改了下回查就跟着变。 */ import type { IndexEntry, Node } from '../contract' import type { IndexedEntry, IndexFind, WorkspaceIndexOf } from '../workspaceIndex' import type { CallRange } from './model' import type { ImportedName, ModuleResolver } from './modules' import { importFacts } from '../languages/typescript' import { argumentsOf, exportedAs, isFunction, named, parametersOf, sameNode, stringContent, unwrap } from '../rules/reactTree' import { compareLocations } from '../workspaceIndex' import { exportOf, moduleResolver } from './modules' /** openapi-fetch 客户端上发请求的方法名,也是 fetch 的 method 认得的值。 */ const METHODS = new Set(['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'TRACE']) /** 方法跟着调用处走:照 fetch 的规则读调用处的第二个参数(fetch 自己就是这样)。 */ const FROM_CALLER = Symbol('fromCaller') /** 一处调用、一个发请求的函数的方法:大写的 HTTP 方法,或跟着调用处走。 */ type Method = string | typeof FROM_CALLER /** * 前端发的一个请求:键是大写的方法。名字是发请求的地方所在的最内层有名字的函数的名字(编辑器从它往上问调用者); * 不在有名字的函数里时,名字是被调用的 fetch / GET / 包装函数本身。 */ export interface RequestEntry extends IndexEntry { /** 写的路径:模板里指向字符串常量的 `${名字}` 换成常量的值,别的 `${...}` 照写;`?`、`#` 后面的,和最后一段末尾紧贴字面量的 `${...}`(查询串)去掉 */ path: string /** 发请求那个调用的范围,行和列从 0 起 */ at: CallRange /** 所在的有名字的函数叫什么;不在有名字的函数里时没有 */ caller?: string } /** * 索引记下的一处调用。键是被调的函数定义处的名字(导入的是它导出时的名字,默认导出是 default), * `X.GET` 这类成员调用是点加成员名(`.GET`)。 */ interface CallEntry extends IndexEntry { /** 被调的函数是导入的:import 后面写的模块 */ from?: string /** 被调的函数定义在这份文件里:定义处名字的起点下标 */ defined?: number /** 照 fetch 的规则从第二个参数读出的方法(见 methodOf);读不出、成员调用没有 */ method?: Method } /** * 网址的一截:照写的字,或一个 `${...}`(written 是照写的样子)。`${名字}` 的名字指向字符串常量时,constant 记着去哪取值: * 本文件定义的已经展开成几截,导入的记下模块和导出时的名字,查的时候才对到导出它的那份文件。 */ type UrlPiece = string | { written: string, constant?: UrlPiece[] | ImportedName } /** 第一个参数写着网址的一处调用:名字、范围、调用者同 RequestEntry。 */ interface SendEntry extends CallEntry, Pick { /** 写的网址,拆成几截 */ url: UrlPiece[] } /** 包装函数把自己收到的网址参数原样交给被调的函数的一处:名字是包装函数定义处的名字。 */ interface ForwardEntry extends CallEntry { /** 包装函数叫什么 */ wrapper: string /** 包装函数在顶层定义、导出了时,导出时叫什么(默认导出是 default) */ exported?: string } /** 顶层定义、导出了的字符串常量:键是导出时的名字(默认导出是 default),名字是定义处的名字。 */ interface ConstantEntry extends IndexEntry { /** 常量的值,同网址拆成几截 */ pieces: UrlPiece[] } type IndexedCall = IndexedEntry type Indexed = IndexedEntry /** 对象字面量的键名:`method`、`"method"`。 */ function keyName(key: Node | null): string | undefined { return key?.type === 'property_identifier' ? key.text : stringContent(key) } /** * 照 fetch 的规则读第二个参数里的方法:没有第二个参数、对象里没写 method 是 GET,method 是字面量就照它; * method 不是字面量、第二个参数不是对象字面量、对象里带展开(可能藏着 method)都不认。 * 包装函数转手时,fromCaller 认出它自己收到的选项(它的调用处的第二个参数):原样交出、在对象里展开它又没写 method 的, * 方法跟着调用处走;展开了它又写了 method 的不认。 */ function methodOf(init: Node | undefined, fromCaller: (node: Node | undefined) => boolean = () => false): Method | undefined { if (!init) return 'GET' if (fromCaller(init)) return FROM_CALLER if (init.type !== 'object') return undefined const members = named(init) const spreads = members.flatMap(member => member.type === 'spread_element' ? [unwrap(named(member)[0])] : []) const written = members.find(member => member.type === 'pair' ? keyName(member.childForFieldName('key')) === 'method' : member.type === 'shorthand_property_identifier' && member.text === 'method') if (!written) return spreads.length === 0 ? 'GET' : spreads.every(fromCaller) ? FROM_CALLER : undefined if (spreads.some(fromCaller)) return undefined const method = written.type === 'pair' ? stringContent(unwrap(written.childForFieldName('value')))?.toUpperCase() : undefined return method && METHODS.has(method) ? method : undefined } /** * 函数自己的名字,和编辑器的调用关系认的一样:函数声明、方法写的;箭头函数、函数表达式取赋给的 const 变量名、类字段名。 * 匿名的、赋给 let / var 的没有。 */ function functionName(fn: Node): Node | undefined { if (fn.type === 'function_declaration' || fn.type === 'generator_function_declaration' || fn.type === 'method_definition') return fn.childForFieldName('name') ?? undefined const holder = fn.parent if (!holder || !sameNode(holder.childForFieldName('value'), fn)) return undefined const name = holder.childForFieldName('name') if (holder.type === 'variable_declarator' && holder.parent?.childForFieldName('kind')?.text === 'const') return name?.type === 'identifier' ? name : undefined return holder.type === 'public_field_definition' && name?.type === 'property_identifier' ? name : undefined } /** node 所在的最内层有名字的函数的名字;匿名回调往外找,不在有名字的函数里时没有。 */ function callerName(node: Node): Node | undefined { for (let current = node.parent; current; current = current.parent) { const name = isFunction(current) ? functionName(current) : undefined if (name) return name } return undefined } // ─── 名字指的是谁 ───────────────────────────────────────────────────────────── /** 名字在这份文件里指的是什么:定义的函数(定义处的名字)、字符串常量(它的值)、导入的、某个函数的第几个参数,或别的变量。 */ type Binding = | { kind: 'function', name: Node } | { kind: 'constant', value: Node } | ({ kind: 'import' } & ImportedName) | { kind: 'parameter', fn: Node, index: number } | { kind: 'variable' } /** 参数、变量声明里写的各个名字(解构的、带默认值的、`...rest` 都算)。 */ function patternNames(pattern: Node | null | undefined): string[] { switch (pattern?.type) { case 'identifier': case 'shorthand_property_identifier_pattern': return [pattern.text] case 'object_pattern': case 'array_pattern': case 'rest_pattern': return named(pattern).flatMap(patternNames) case 'pair_pattern': return patternNames(pattern.childForFieldName('value')) case 'assignment_pattern': case 'object_assignment_pattern': return patternNames(pattern.childForFieldName('left')) default: return [] } } /** 赋给 const 的字符串常量:名字和值(字符串字面量或模板字符串,`as const` 这类包裹去掉);别的声明没有。 */ function stringConstant(declarator: Node): { name: Node, value: Node } | undefined { const name = declarator.childForFieldName('name') const value = unwrap(declarator.childForFieldName('value')) if (declarator.parent?.childForFieldName('kind')?.text !== 'const' || name?.type !== 'identifier' || (value?.type !== 'string' && value?.type !== 'template_string')) return undefined return { name, value } } /** * 一条语句声明的名字记进 names:函数声明和赋给 const 的函数是定义的函数,赋给 const 的字符串字面量、模板字符串是字符串常量, * import 的是导入的(只导入类型的不算),别的是变量。 */ function declare(statement: Node | null, names: Map): void { const add = (name: string, binding: Binding): void => { if (!names.has(name)) names.set(name, binding) } switch (statement?.type) { case 'export_statement': declare(statement.childForFieldName('declaration'), names) break case 'function_declaration': case 'generator_function_declaration': { const name = statement.childForFieldName('name') if (name) add(name.text, { kind: 'function', name }) break } case 'class_declaration': case 'abstract_class_declaration': patternNames(statement.childForFieldName('name')).forEach(name => add(name, { kind: 'variable' })) break case 'lexical_declaration': case 'variable_declaration': for (const declarator of named(statement).filter(child => child.type === 'variable_declarator')) { const value = declarator.childForFieldName('value') const name = value && isFunction(value) ? functionName(value) : undefined const constant = stringConstant(declarator) if (name) add(name.text, { kind: 'function', name }) else if (constant) add(constant.name.text, { kind: 'constant', value: constant.value }) else patternNames(declarator.childForFieldName('name')).forEach(declared => add(declared, { kind: 'variable' })) } break case 'import_statement': { const facts = importFacts(statement, true) const from = stringContent(facts.sources[0]) if (facts.typeOnly || from === undefined) break for (const item of facts.items) { if (item.kind === 'named') add((item.alias ?? item.name).text, { kind: 'import', from, name: item.name.text }) else if (item.kind === 'default') add(item.alias.text, { kind: 'import', from, name: 'default' }) else if (item.alias) add(item.alias.text, { kind: 'variable' }) } break } } } /** 这一层声明的名字:函数的参数,程序和语句块里的声明,for 头里的、catch 的参数;别的节点不声明名字。 */ function declarations(scope: Node): Map | undefined { const names = new Map() if (isFunction(scope)) { parametersOf(scope).forEach(({ name }, index) => { for (const declared of patternNames(name)) { if (!names.has(declared)) names.set(declared, name.type === 'identifier' ? { kind: 'parameter', fn: scope, index } : { kind: 'variable' }) } }) return names } switch (scope.type) { case 'program': case 'statement_block': named(scope).forEach(statement => declare(statement, names)) return names case 'for_statement': declare(scope.childForFieldName('initializer'), names) return names case 'for_in_statement': if (scope.childForFieldName('kind')) patternNames(scope.childForFieldName('left')).forEach(name => names.set(name, { kind: 'variable' })) return names case 'catch_clause': patternNames(scope.childForFieldName('parameter')).forEach(name => names.set(name, { kind: 'variable' })) return names default: return undefined } } /** 一份文件里名字指的是什么:从用到它的地方往外一层层找声明,各层声明了哪些名字按节点记下。 */ class Scopes { private readonly declared = new Map | undefined>() /** 名字指的是哪个声明;哪层都没声明(全局的)时没有。 */ resolve(name: Node): Binding | undefined { for (let scope = name.parent; scope; scope = scope.parent) { if (!this.declared.has(scope.id)) this.declared.set(scope.id, declarations(scope)) const binding = this.declared.get(scope.id)?.get(name.text) if (binding) return binding } return undefined } /** node 是 fn 的第 index 个参数(同名的、被里层的声明挡住的不算)。 */ isParameter(node: Node | null | undefined, fn: Node, index: number): boolean { const binding = node?.type === 'identifier' ? this.resolve(node) : undefined return binding?.kind === 'parameter' && binding.index === index && sameNode(binding.fn, fn) } } // ─── 索引 ───────────────────────────────────────────────────────────────────── /** * 写的网址:字符串字面量或模板字符串拆成几截,别的写法没有。模板里的 `${名字}` 指向字符串常量的记下去哪取值(见 constantOf), * outer 是正在展开的各个常量的值。 */ function urlOf(node: Node | undefined, scopes: Scopes, outer: readonly Node[] = []): UrlPiece[] | undefined { if (node?.type === 'string') return [stringContent(node) ?? ''] if (node?.type !== 'template_string') return undefined return named(node).map((part) => { if (part.type !== 'template_substitution') return part.text const constant = constantOf(named(part)[0], scopes, outer) return { written: part.text, ...(constant && { constant }) } }) } /** 本文件的字符串常量的值展开成几截:值里的 `${...}` 都得指向常量才算,绕回正在展开的常量(循环)不算。 */ function expanded(value: Node, scopes: Scopes, outer: readonly Node[]): UrlPiece[] | undefined { if (outer.some(other => sameNode(other, value))) return undefined const pieces = urlOf(value, scopes, [...outer, value]) return pieces?.every(piece => typeof piece === 'string' || piece.constant) ? pieces : undefined } /** * `${名字}` 的名字指向的字符串常量去哪取值:本文件定义的展开成几截(见 expanded),导入的是模块和导出时的名字。 * 不是名字本身的(`${cfg.prefix}`、函数调用)、被参数和别的变量挡住的、展开不了的没有。 */ function constantOf(expression: Node | undefined, scopes: Scopes, outer: readonly Node[]): UrlPiece[] | ImportedName | undefined { const binding = expression?.type === 'identifier' ? scopes.resolve(expression) : undefined if (binding?.kind === 'import') return { from: binding.from, name: binding.name } return binding?.kind === 'constant' ? expanded(binding.value, scopes, outer) : undefined } /** 顶层定义、导出了的字符串常量(别的文件的网址可能导入它),值展开不了的不记。 */ function constantsOf(root: Node, scopes: Scopes): ConstantEntry[] { return named(root).flatMap((statement) => { const declaration = statement.type === 'export_statement' ? statement.childForFieldName('declaration') : statement if (declaration?.type !== 'lexical_declaration') return [] return named(declaration).flatMap((declarator): ConstantEntry[] => { const constant = declarator.type === 'variable_declarator' ? stringConstant(declarator) : undefined const exported = constant && exportedAs(statement, constant.name.text) if (!constant || !exported) return [] const pieces = expanded(constant.value, scopes, []) return pieces ? [{ key: exported, name: { startIndex: constant.name.startIndex, endIndex: constant.name.endIndex }, pieces }] : [] }) }) } /** 被调的是谁:这份文件定义的函数、导入的,或全局的 fetch;参数、别的变量、别的全局函数没有。 */ function calleeOf(callee: Node, scopes: Scopes): Pick | undefined { const binding = scopes.resolve(callee) if (!binding) return callee.text === 'fetch' ? { key: 'fetch' } : undefined if (binding.kind === 'function') return { key: binding.name.text, defined: binding.name.startIndex } return binding.kind === 'import' ? { key: binding.name, from: binding.from } : undefined } /** 函数里给它的第一个参数重新赋过值(`url = …`、`url += …`、`url++`):交出去的就不是收到的网址了。 */ function reassigned(fn: Node, scopes: Scopes): boolean { return fn.descendantsOfType(['assignment_expression', 'augmented_assignment_expression', 'update_expression']) .some(node => !!node && scopes.isParameter(unwrap(node.childForFieldName('left') ?? node.childForFieldName('argument')), fn, 0)) } /** 顶层定义的函数导出时叫什么(见 exportedAs);不在顶层定义、没导出时没有。 */ function exportedName(fn: Node, name: Node): string | undefined { const declaration = fn.type === 'function_declaration' || fn.type === 'generator_function_declaration' ? fn : fn.parent?.parent const statement = declaration?.parent?.type === 'export_statement' ? declaration.parent : declaration return statement?.parent?.type === 'program' ? exportedAs(statement, name.text) : undefined } /** 第一个参数写着网址的一处:名字归最内层有名字的函数,不在有名字的函数里时是被调的 callee。 */ function sendEntry(call: Node, callee: Node, target: Omit, url: UrlPiece[]): SendEntry { const caller = callerName(call) const name = caller ?? callee return { ...target, name: { startIndex: name.startIndex, endIndex: name.endIndex }, url, at: { line: call.startPosition.row, character: call.startPosition.column, endLine: call.endPosition.row, endCharacter: call.endPosition.column }, ...(caller && { caller: caller.text }), } } /** * 一处调用记不记:`X.GET(网址)` 这类;第一个参数写着网址、被调的是这份文件定义的函数、导入的或 fetch; * 第一个参数是一个有名字的函数(函数声明、赋给 const 的)的第一个参数、函数里没给它重新赋过值的,记成那个函数转手。 */ function callOf(call: Node, scopes: Scopes): (SendEntry | ForwardEntry)[] { const callee = unwrap(call.childForFieldName('function')) const [first, init] = argumentsOf(call).map(unwrap) if (callee?.type === 'member_expression') { const property = callee.childForFieldName('property') const url = property && METHODS.has(property.text) ? urlOf(first, scopes) : undefined return property && url ? [sendEntry(call, property, { key: `.${property.text}` }, url)] : [] } if (callee?.type !== 'identifier') return [] const target = calleeOf(callee, scopes) if (!target) return [] const url = urlOf(first, scopes) if (url) { const method = methodOf(init) return [sendEntry(call, callee, { ...target, ...(method && { method }) }, url)] } const binding = first?.type === 'identifier' ? scopes.resolve(first) : undefined const fn = binding?.kind === 'parameter' && binding.index === 0 ? binding.fn : undefined const name = fn && functionName(fn) if (!fn || name?.type !== 'identifier' || reassigned(fn, scopes)) return [] const method = methodOf(init, node => scopes.isParameter(node, fn, 1)) const exported = exportedName(fn, name) return [{ ...target, name: { startIndex: name.startIndex, endIndex: name.endIndex }, wrapper: name.text, ...(method && { method }), ...(exported && { exported }) }] } /** * 前端源码里和发请求有关的调用,和导出的字符串常量(TSX 的语法包也读得了 .ts、.js)。包装函数一层层转手最后总落到 fetch 上, * 跨文件要靠 import,导出常量要写 export,所以 fetch、import、export、`.GET(` 这类都不提的文件里不会有,不解析。 */ const CALLS_INDEX: WorkspaceIndexOf = { id: 'callTree.calls', languageId: 'typescriptreact', include: '**/*.{ts,tsx,js,jsx}', mentions: source => /\b(?:fetch|import|export)\b|\.(?:GET|PUT|POST|DELETE|OPTIONS|HEAD|PATCH|TRACE)\s*\(/.test(source), entries: (root) => { const scopes = new Scopes() return [...root.descendantsOfType('call_expression').flatMap(call => call ? callOf(call, scopes) : []), ...constantsOf(root, scopes)] }, } // ─── 查 ─────────────────────────────────────────────────────────────────────── /** 发请求的函数(fetch、认出的包装函数)的方法;转手的几处读出的方法对不上、读不出时是 null,这样的不认。 */ type Status = Method | null function isSend(indexed: Indexed): indexed is IndexedEntry { return 'url' in indexed.entry } function isForward(indexed: Indexed): indexed is IndexedEntry { return 'wrapper' in indexed.entry } function isConstant(indexed: Indexed): indexed is IndexedEntry { return 'pieces' in indexed.entry } /** 包装函数的编号:定义在哪份文件,定义处名字的起点下标。 */ function wrapperId(uri: string, offset: number): string { return `${uri}#${offset}` } /** * 从 fetch 起一层层认包装函数:转手给已认出的函数的各处,所在的函数就是下一层的包装函数。方法按转手的那处读: * 被调的写死了方法就是它,跟着调用处走的就照转手时交出的选项读;几处读出的不一样就定不下来。认出新的包装函数就接着查调它的各处, * 直到不再有新的。被调的名字按调用处那份文件的定义、import 来源对到包装函数,对不上、对上不止一个的不认(见 exportOf)。 * 交回查到的各处,和问一处调用调的函数的方法(不是发请求的函数时没有)。 */ async function senders(find: IndexFind, resolve: ModuleResolver): Promise<{ calls: Indexed[], statusOf: (call: IndexedCall) => Status | undefined }> { const loaded = new Map() const wrappers = new Map() let status = new Map() const statusOf = ({ entry, location }: IndexedCall): Status | undefined => { if (entry.defined !== undefined) return status.get(wrapperId(location.uri, entry.defined)) if (entry.from === undefined) return entry.key === 'fetch' ? FROM_CALLER : undefined const wrapper = exportOf(resolve, location.uri, { from: entry.from, name: entry.key }, [...wrappers.values()]) return wrapper && status.get(wrapper.id) } for (let pending = ['fetch']; pending.length > 0;) { for (const name of pending) loaded.set(name, await find(CALLS_INDEX, name)) const forwards = [...loaded.values()].flat().filter(isForward) for (const { location, entry } of forwards) { const id = wrapperId(location.uri, entry.name.startIndex) wrappers.set(id, { id, uri: location.uri, names: [entry.wrapper, ...(entry.exported ? [entry.exported] : [])], ...(entry.exported && { exported: entry.exported }) }) } for (let changed = true; changed;) { const next = new Map() for (const forward of forwards) { const target = statusOf(forward) if (target === undefined) continue const own = target === FROM_CALLER ? forward.entry.method ?? null : target const id = wrapperId(forward.location.uri, forward.entry.name.startIndex) next.set(id, next.has(id) && next.get(id) !== own ? null : own) } changed = next.size !== status.size || [...next].some(([id, value]) => status.get(id) !== value) status = next } pending = [...new Set([...status.keys()].flatMap(id => wrappers.get(id)?.names ?? []))].filter(name => !loaded.has(name)) } return { calls: [...loaded.values()].flat(), statusOf } } /** * 网址拼成路径(uri 是网址所在的文件):`${名字}` 取得到常量的值就拼进去,取不到的照写;`?`、`#` 后面的去掉,照写的 `${...}` 里的不算; * 最后一段是字面量文字后面紧贴一个照写的 `${...}`(`messages${qs}`)时,它是拼上去的查询串,也去掉。 * 导入的常量和包装函数一样按 import 来源对到导出它的那处(见 exportOf),它的值里又导入的接着取;对不上、绕回正在取的(循环)取不到。 * 一份查询里同一个常量只取一次;取值时记着正在取的,所以要一处处拼,不能同时拼。 */ function pathsOf(find: IndexFind, resolve: ModuleResolver): (uri: string, url: readonly UrlPiece[]) => Promise { const values = new Map() /** 一个 `${...}` 指向的常量的值;不指向常量、取不到的没有。 */ async function valueOf(uri: string, constant: UrlPiece[] | ImportedName | undefined): Promise { if (!constant) return undefined if (Array.isArray(constant)) return joined(uri, constant) const exports = (await find(CALLS_INDEX, constant.name)).filter(isConstant).map(found => ({ uri: found.location.uri, exported: found.entry.key, found })) const target = exportOf(resolve, uri, constant, exports) if (!target) return undefined const id = `${target.uri}#${target.found.entry.name.startIndex}` if (!values.has(id)) { // 先占上:取值途中绕回这个常量就是循环,取不到 values.set(id, undefined) values.set(id, await joined(target.uri, target.found.entry.pieces)) } return values.get(id) } /** 常量的值拼起来:每个 `${...}` 都取得到才算。 */ async function joined(uri: string, pieces: readonly UrlPiece[]): Promise { let text = '' for (const piece of pieces) { const value = typeof piece === 'string' ? piece : await valueOf(uri, piece.constant) if (value === undefined) return undefined text += value } return text } return async (uri, url) => { let path = '' // 上一个照写的 `${...}` 在 path 里的结尾;末尾照写的 `${...}` 当查询串去掉时从哪截 let written = 0 let query: number | undefined for (const piece of url) { const value = typeof piece === 'string' ? piece : await valueOf(uri, piece.constant) if (value === undefined) { const segment = path.lastIndexOf('/') + 1 query = segment >= written && segment < path.length ? path.length : undefined path += typeof piece === 'string' ? piece : piece.written written = path.length continue } const end = value.search(/[?#]/) const literal = end >= 0 ? value.slice(0, end) : value path += literal if (literal) query = undefined if (end >= 0) break } return query === undefined ? path : path.slice(0, query) } } /** 前端发方法是 method 的请求的各处:`X.GET` 这类、调 fetch 的、调包装函数的,照文件、位置排。 */ export async function frontendRequests(find: IndexFind, method: string): Promise[]> { const resolve = await moduleResolver(find) const { calls, statusOf } = await senders(find, resolve) const members = await find(CALLS_INDEX, `.${method}`) const wrapped = calls.filter(isSend).filter((call) => { const target = statusOf(call) return (target === FROM_CALLER ? call.entry.method : target) === method }) const pathOf = pathsOf(find, resolve) const found: IndexedEntry[] = [] for (const { location, entry } of [...members.filter(isSend), ...wrapped]) found.push({ location, entry: { key: method, name: entry.name, path: await pathOf(location.uri, entry.url), at: entry.at, ...(entry.caller && { caller: entry.caller }) } }) return found.sort((a, b) => compareLocations(a.location, b.location) || a.entry.at.line - b.entry.at.line || a.entry.at.character - b.entry.at.character) }