✨ feat(vscode): 接口卡片的调用树认得出包了一层 fetch 的函数(authedFetch 这类),跨文件、多层、路径别名导入都算
This commit is contained in:
1 parent
78aa71ffd0
commit
cb561d23ad
11 files changed
+683
-64
No files matched your search
@@ -1,19 +1,32 @@
|
||||
/**
|
||||
* 前端发 HTTP 请求的地方的工作区索引:openapi-fetch 的 `X.GET("/path", ...)` 这类(成员名是大写的 HTTP 方法),
|
||||
* 和 `fetch("/path", { method: "POST" })`。路径要写成字符串字面量或模板字符串;接口的调用树拿它找前端调用了哪个接口(见 endpoint.ts)。
|
||||
* 前端发 HTTP 请求的地方:openapi-fetch 的 `X.GET("/path", ...)` 这类(成员名是大写的 HTTP 方法),`fetch("/path", { method: "POST" })`,
|
||||
* 和经包装函数发的:一个函数把自己收到的网址参数原样作为第一个参数交给 fetch 或另一个包装函数(可以跨文件、可以多层),它就是包装函数,
|
||||
* 调用它的地方照 fetch 的规则取网址和方法。网址要写成字符串字面量或模板字符串;接口的调用树拿它找前端调用了哪个接口(见 endpoint.ts)。
|
||||
*
|
||||
* 工作区索引按文件记下相关的调用,连同被调的名字在这份文件里指的是谁(本文件的定义、import 来源、全局的 fetch);
|
||||
* 谁是包装函数、方法是什么查的时候才从 fetch 起一层层认(见 senders),文件改了下回查就跟着变。
|
||||
*/
|
||||
|
||||
import type { IndexEntry, Node } from '../contract'
|
||||
import type { WorkspaceIndexOf } from '../workspaceIndex'
|
||||
import type { IndexedEntry, IndexFind, WorkspaceIndexOf } from '../workspaceIndex'
|
||||
import type { CallRange } from './model'
|
||||
import { argumentsOf, isFunction, named, sameNode, stringContent, unwrap } from '../rules/reactTree'
|
||||
import { importFacts } from '../languages/typescript'
|
||||
import { argumentsOf, exportedAs, isFunction, named, parametersOf, sameNode, stringContent, unwrap } from '../rules/reactTree'
|
||||
import { compareLocations } from '../workspaceIndex'
|
||||
import { moduleIds, 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 本身。
|
||||
* 不在有名字的函数里时,名字是被调用的 fetch / GET / 包装函数本身。
|
||||
*/
|
||||
export interface RequestEntry extends IndexEntry {
|
||||
/** 写的路径,模板里的 `${...}` 照写,`?`、`#` 后面的去掉 */
|
||||
@@ -24,6 +37,32 @@ export interface RequestEntry extends IndexEntry {
|
||||
caller?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引记下的一处调用。键是被调的函数定义处的名字(导入的是它导出时的名字,默认导出是 default),
|
||||
* `X.GET` 这类成员调用是点加成员名(`.GET`)。
|
||||
*/
|
||||
interface CallEntry extends IndexEntry {
|
||||
/** 被调的函数是导入的:import 后面写的模块 */
|
||||
from?: string
|
||||
/** 被调的函数定义在这份文件里:定义处名字的起点下标 */
|
||||
defined?: number
|
||||
/** 照 fetch 的规则从第二个参数读出的方法(见 methodOf);读不出、成员调用没有 */
|
||||
method?: Method
|
||||
}
|
||||
|
||||
/** 第一个参数写着网址的一处调用:名字、网址、范围、调用者同 RequestEntry。 */
|
||||
interface SendEntry extends CallEntry, Pick<RequestEntry, 'path' | 'at' | 'caller'> {}
|
||||
|
||||
/** 包装函数把自己收到的网址参数原样交给被调的函数的一处:名字是包装函数定义处的名字。 */
|
||||
interface ForwardEntry extends CallEntry {
|
||||
/** 包装函数叫什么 */
|
||||
wrapper: string
|
||||
/** 包装函数在顶层定义、导出了时,导出时叫什么(默认导出是 default) */
|
||||
exported?: string
|
||||
}
|
||||
|
||||
type IndexedCall = IndexedEntry<SendEntry | ForwardEntry>
|
||||
|
||||
/** 第一个参数写的路径:字符串字面量或模板字符串(`${...}` 照写),去掉 `?`、`#` 后面的;别的写法没有。 */
|
||||
function pathOf(node: Node | undefined): string | undefined {
|
||||
if (node?.type === 'string')
|
||||
@@ -46,37 +85,29 @@ function keyName(key: Node | null): string | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch 的方法:没有第二个参数、对象里没写 method 是 GET,method 是字面量就照它;
|
||||
* 照 fetch 的规则读第二个参数里的方法:没有第二个参数、对象里没写 method 是 GET,method 是字面量就照它;
|
||||
* method 不是字面量、第二个参数不是对象字面量、对象里带展开(可能藏着 method)都不认。
|
||||
* 包装函数转手时,fromCaller 认出它自己收到的选项(它的调用处的第二个参数):原样交出、在对象里展开它又没写 method 的,
|
||||
* 方法跟着调用处走;展开了它又写了 method 的不认。
|
||||
*/
|
||||
function fetchMethod(init: Node | undefined): string | undefined {
|
||||
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 members.some(member => member.type === 'spread_element') ? undefined : 'GET'
|
||||
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
|
||||
}
|
||||
|
||||
/** 一个调用是不是发请求:`X.GET(路径)` 这类看成员名,`fetch(路径, 选项)` 看选项里的 method。 */
|
||||
function requestOf(call: Node): { method: string, path: string, callee: Node } | undefined {
|
||||
const callee = unwrap(call.childForFieldName('function'))
|
||||
const [first, init] = argumentsOf(call)
|
||||
const path = pathOf(unwrap(first))
|
||||
if (!callee || path === undefined)
|
||||
return undefined
|
||||
if (callee.type === 'member_expression') {
|
||||
const property = callee.childForFieldName('property')
|
||||
return property && METHODS.has(property.text) ? { method: property.text, path, callee: property } : undefined
|
||||
}
|
||||
const method = callee.type === 'identifier' && callee.text === 'fetch' ? fetchMethod(unwrap(init)) : undefined
|
||||
return method ? { method, path, callee } : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数自己的名字,和编辑器的调用关系认的一样:函数声明、方法写的;箭头函数、函数表达式取赋给的 const 变量名、类字段名。
|
||||
* 匿名的、赋给 let / var 的没有。
|
||||
@@ -103,28 +134,300 @@ function callerName(node: Node): Node | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function requestsIn(root: Node): RequestEntry[] {
|
||||
return root.descendantsOfType('call_expression').flatMap((call) => {
|
||||
const request = call && requestOf(call)
|
||||
if (!request)
|
||||
// ─── 名字指的是谁 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** 名字在这份文件里指的是什么:定义的函数(定义处的名字)、导入的(模块和导出时的名字)、某个函数的第几个参数,或别的变量。 */
|
||||
type Binding
|
||||
= | { kind: 'function', name: Node }
|
||||
| { kind: 'import', from: string, name: string }
|
||||
| { 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 caller = callerName(call)
|
||||
const name = caller ?? request.callee
|
||||
return [{
|
||||
key: request.method,
|
||||
name: { startIndex: name.startIndex, endIndex: name.endIndex },
|
||||
path: request.path,
|
||||
at: { line: call.startPosition.row, character: call.startPosition.column, endLine: call.endPosition.row, endCharacter: call.endPosition.column },
|
||||
...(caller && { caller: caller.text }),
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 前端源码里发请求的地方(TSX 的语法包也读得了 .ts、.js)。 */
|
||||
export const REQUESTS_INDEX: WorkspaceIndexOf<RequestEntry> = {
|
||||
id: 'callTree.requests',
|
||||
/** 一条语句声明的名字记进 names:函数声明和赋给 const 的函数是定义的函数,import 的是导入的(只导入类型的不算),别的是变量。 */
|
||||
function declare(statement: Node | null, names: Map<string, Binding>): 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
|
||||
if (name)
|
||||
add(name.text, { kind: 'function', name })
|
||||
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<string, Binding> | undefined {
|
||||
const names = new Map<string, Binding>()
|
||||
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<number, Map<string, Binding> | 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)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 索引 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 被调的是谁:这份文件定义的函数、导入的,或全局的 fetch;参数、别的变量、别的全局函数没有。 */
|
||||
function calleeOf(callee: Node, scopes: Scopes): Pick<CallEntry, 'key' | 'from' | 'defined'> | 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<CallEntry, 'name'>, path: string): SendEntry {
|
||||
const caller = callerName(call)
|
||||
const name = caller ?? callee
|
||||
return {
|
||||
...target,
|
||||
name: { startIndex: name.startIndex, endIndex: name.endIndex },
|
||||
path,
|
||||
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)
|
||||
const path = pathOf(first)
|
||||
if (callee?.type === 'member_expression') {
|
||||
const property = callee.childForFieldName('property')
|
||||
return property && METHODS.has(property.text) && path !== undefined ? [sendEntry(call, property, { key: `.${property.text}` }, path)] : []
|
||||
}
|
||||
if (callee?.type !== 'identifier' || (path === undefined && first?.type !== 'identifier'))
|
||||
return []
|
||||
const target = calleeOf(callee, scopes)
|
||||
if (!target)
|
||||
return []
|
||||
if (path !== undefined) {
|
||||
const method = methodOf(init)
|
||||
return [sendEntry(call, callee, { ...target, ...(method && { method }) }, path)]
|
||||
}
|
||||
const binding = first && scopes.resolve(first)
|
||||
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,
|
||||
* 所以 fetch、import、`.GET(` 这类都不提的文件里不会有,不解析。
|
||||
*/
|
||||
const CALLS_INDEX: WorkspaceIndexOf<SendEntry | ForwardEntry> = {
|
||||
id: 'callTree.calls',
|
||||
languageId: 'typescriptreact',
|
||||
include: '**/*.{ts,tsx,js,jsx}',
|
||||
mentions: source => /\bfetch\s*\(|\.(?:GET|PUT|POST|DELETE|OPTIONS|HEAD|PATCH|TRACE)\s*\(/.test(source),
|
||||
entries: root => requestsIn(root),
|
||||
mentions: source => /\b(?:fetch|import)\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) : [])
|
||||
},
|
||||
}
|
||||
|
||||
// ─── 查 ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 发请求的函数(fetch、认出的包装函数)的方法;转手的几处读出的方法对不上、读不出时是 null,这样的不认。 */
|
||||
type Status = Method | null
|
||||
|
||||
function isSend(call: IndexedCall): call is IndexedEntry<SendEntry> {
|
||||
return 'path' in call.entry
|
||||
}
|
||||
|
||||
function isForward(call: IndexedCall): call is IndexedEntry<ForwardEntry> {
|
||||
return 'wrapper' in call.entry
|
||||
}
|
||||
|
||||
/** 包装函数的编号:定义在哪份文件,定义处名字的起点下标。 */
|
||||
function wrapperId(uri: string, offset: number): string {
|
||||
return `${uri}#${offset}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 fetch 起一层层认包装函数:转手给已认出的函数的各处,所在的函数就是下一层的包装函数。方法按转手的那处读:
|
||||
* 被调的写死了方法就是它,跟着调用处走的就照转手时交出的选项读;几处读出的不一样就定不下来。认出新的包装函数就接着查调它的各处,
|
||||
* 直到不再有新的。被调的名字按调用处那份文件的定义、import 来源对到包装函数,对不上、对上不止一个的不认。
|
||||
* 交回查到的各处调用,和问一处调用调的函数的方法(不是发请求的函数时没有)。
|
||||
*/
|
||||
async function senders(find: IndexFind): Promise<{ calls: IndexedCall[], statusOf: (call: IndexedCall) => Status | undefined }> {
|
||||
const resolve = await moduleResolver(find)
|
||||
const loaded = new Map<string, IndexedCall[]>()
|
||||
const wrappers = new Map<string, { uri: string, names: string[], exported?: string }>()
|
||||
let status = new Map<string, Status>()
|
||||
|
||||
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
|
||||
for (const module of resolve(location.uri, entry.from)) {
|
||||
const matched = [...wrappers].filter(([, wrapper]) => wrapper.exported === entry.key && moduleIds(wrapper.uri).includes(module))
|
||||
if (matched.length > 0)
|
||||
return matched.length === 1 ? status.get(matched[0][0]) : undefined
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
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)
|
||||
wrappers.set(wrapperId(location.uri, entry.name.startIndex), { uri: location.uri, names: [entry.wrapper, ...(entry.exported ? [entry.exported] : [])], ...(entry.exported && { exported: entry.exported }) })
|
||||
for (let changed = true; changed;) {
|
||||
const next = new Map<string, Status>()
|
||||
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 }
|
||||
}
|
||||
|
||||
/** 前端发方法是 method 的请求的各处:`X.GET` 这类、调 fetch 的、调包装函数的,照文件、位置排。 */
|
||||
export async function frontendRequests(find: IndexFind, method: string): Promise<IndexedEntry<RequestEntry>[]> {
|
||||
const { calls, statusOf } = await senders(find)
|
||||
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
|
||||
})
|
||||
return [...members.filter(isSend), ...wrapped]
|
||||
.map(({ location, entry }) => ({ location, entry: { key: method, name: entry.name, path: entry.path, at: entry.at, ...(entry.caller && { caller: entry.caller }) } }))
|
||||
.sort((a, b) => compareLocations(a.location, b.location) || a.entry.at.line - b.entry.at.line || a.entry.at.character - b.entry.at.character)
|
||||
}
|
||||
Reference in new issue
Block a user