Files
superwork/vscode/src/codeReader/callTree/requests.ts
T

577 lines
29 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.
/**
* 前端发 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<RequestEntry, 'at' | 'caller'> {
/** 写的网址,拆成几截 */
url: UrlPiece[]
}
/** 包装函数把自己收到的网址参数原样交给被调的函数的一处:名字是包装函数定义处的名字。 */
interface ForwardEntry extends CallEntry {
/** 包装函数叫什么 */
wrapper: string
/** 包装函数在顶层定义、导出了时,导出时叫什么(默认导出是 default) */
exported?: string
}
/** 顶层定义、导出了的字符串常量:键是导出时的名字(默认导出是 default),名字是定义处的名字。 */
interface ConstantEntry extends IndexEntry {
/** 常量的值,同网址拆成几截 */
pieces: UrlPiece[]
}
type IndexedCall = IndexedEntry<SendEntry | ForwardEntry>
type Indexed = IndexedEntry<SendEntry | ForwardEntry | ConstantEntry>
/** 对象字面量的键名:`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<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
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<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)
}
}
// ─── 索引 ─────────────────────────────────────────────────────────────────────
/**
* 写的网址:字符串字面量或模板字符串拆成几截,别的写法没有。模板里的 `${名字}` 指向字符串常量的记下去哪取值(见 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<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'>, 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<SendEntry | ForwardEntry | ConstantEntry> = {
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<SendEntry> {
return 'url' in indexed.entry
}
function isForward(indexed: Indexed): indexed is IndexedEntry<ForwardEntry> {
return 'wrapper' in indexed.entry
}
function isConstant(indexed: Indexed): indexed is IndexedEntry<ConstantEntry> {
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<string, Indexed[]>()
const wrappers = new Map<string, { id: 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
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<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 }
}
/**
* 网址拼成路径(uri 是网址所在的文件):`${名字}` 取得到常量的值就拼进去,取不到的照写;`?`、`#` 后面的去掉,照写的 `${...}` 里的不算;
* 最后一段是字面量文字后面紧贴一个照写的 `${...}`(`messages${qs}`)时,它是拼上去的查询串,也去掉。
* 导入的常量和包装函数一样按 import 来源对到导出它的那处(见 exportOf),它的值里又导入的接着取;对不上、绕回正在取的(循环)取不到。
* 一份查询里同一个常量只取一次;取值时记着正在取的,所以要一处处拼,不能同时拼。
*/
function pathsOf(find: IndexFind, resolve: ModuleResolver): (uri: string, url: readonly UrlPiece[]) => Promise<string> {
const values = new Map<string, string | undefined>()
/** 一个 `${...}` 指向的常量的值;不指向常量、取不到的没有。 */
async function valueOf(uri: string, constant: UrlPiece[] | ImportedName | undefined): Promise<string | undefined> {
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<string | undefined> {
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<IndexedEntry<RequestEntry>[]> {
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<RequestEntry>[] = []
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)
}