✨ feat(vscode): 接口卡片找前端调用时,网址模板里拼的字符串常量(${API_PREFIX} 这类)换成它的值再对接口,本文件定义的、import 进来的、常量引用常量的都算
This commit is contained in:
1 parent
5426c50cd9
commit
50672c70d1
3 files changed
+324
-61
No files matched your search
@@ -1,19 +1,21 @@
|
||||
/**
|
||||
* 前端发 HTTP 请求的地方:openapi-fetch 的 `X.GET("/path", ...)` 这类(成员名是大写的 HTTP 方法),`fetch("/path", { method: "POST" })`,
|
||||
* 和经包装函数发的:一个函数把自己收到的网址参数原样作为第一个参数交给 fetch 或另一个包装函数(可以跨文件、可以多层),它就是包装函数,
|
||||
* 调用它的地方照 fetch 的规则取网址和方法。网址要写成字符串字面量或模板字符串;接口的调用树拿它找前端调用了哪个接口(见 endpoint.ts)。
|
||||
* 调用它的地方照 fetch 的规则取网址和方法。网址要写成字符串字面量或模板字符串,模板里的 `${名字}` 指向字符串常量的拼进常量的值,
|
||||
* 别的 `${...}` 照写,当参数段;接口的调用树拿它找前端调用了哪个接口(见 endpoint.ts)。
|
||||
*
|
||||
* 工作区索引按文件记下相关的调用,连同被调的名字在这份文件里指的是谁(本文件的定义、import 来源、全局的 fetch);
|
||||
* 谁是包装函数、方法是什么查的时候才从 fetch 起一层层认(见 senders),文件改了下回查就跟着变。
|
||||
* 工作区索引按文件记下相关的调用和导出的字符串常量,连同被调的名字、网址里的名字在这份文件里指的是谁(本文件的定义、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 { moduleIds, moduleResolver } from './modules'
|
||||
import { exportOf, moduleResolver } from './modules'
|
||||
|
||||
/** openapi-fetch 客户端上发请求的方法名,也是 fetch 的 method 认得的值。 */
|
||||
const METHODS = new Set(['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'TRACE'])
|
||||
@@ -29,7 +31,7 @@ type Method = string | typeof FROM_CALLER
|
||||
* 不在有名字的函数里时,名字是被调用的 fetch / GET / 包装函数本身。
|
||||
*/
|
||||
export interface RequestEntry extends IndexEntry {
|
||||
/** 写的路径,模板里的 `${...}` 照写,`?`、`#` 后面的去掉 */
|
||||
/** 写的路径:模板里指向字符串常量的 `${名字}` 换成常量的值,别的 `${...}` 照写,`?`、`#` 后面的去掉 */
|
||||
path: string
|
||||
/** 发请求那个调用的范围,行和列从 0 起 */
|
||||
at: CallRange
|
||||
@@ -50,8 +52,17 @@ interface CallEntry extends IndexEntry {
|
||||
method?: Method
|
||||
}
|
||||
|
||||
/** 第一个参数写着网址的一处调用:名字、网址、范围、调用者同 RequestEntry。 */
|
||||
interface SendEntry extends CallEntry, Pick<RequestEntry, 'path' | 'at' | 'caller'> {}
|
||||
/**
|
||||
* 网址的一截:照写的字,或一个 `${...}`(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 {
|
||||
@@ -61,23 +72,15 @@ interface ForwardEntry extends CallEntry {
|
||||
exported?: string
|
||||
}
|
||||
|
||||
/** 顶层定义、导出了的字符串常量:键是导出时的名字(默认导出是 default),名字是定义处的名字。 */
|
||||
interface ConstantEntry extends IndexEntry {
|
||||
/** 常量的值,同网址拆成几截 */
|
||||
pieces: UrlPiece[]
|
||||
}
|
||||
|
||||
type IndexedCall = IndexedEntry<SendEntry | ForwardEntry>
|
||||
|
||||
/** 第一个参数写的路径:字符串字面量或模板字符串(`${...}` 照写),去掉 `?`、`#` 后面的;别的写法没有。 */
|
||||
function pathOf(node: Node | undefined): string | undefined {
|
||||
if (node?.type === 'string')
|
||||
return stringContent(node)?.split(/[?#]/)[0]
|
||||
if (node?.type !== 'template_string')
|
||||
return undefined
|
||||
let path = ''
|
||||
for (const part of named(node)) {
|
||||
const end = part.type === 'template_substitution' ? -1 : part.text.search(/[?#]/)
|
||||
if (end >= 0)
|
||||
return path + part.text.slice(0, end)
|
||||
path += part.text
|
||||
}
|
||||
return path
|
||||
}
|
||||
type Indexed = IndexedEntry<SendEntry | ForwardEntry | ConstantEntry>
|
||||
|
||||
/** 对象字面量的键名:`method`、`"method"`。 */
|
||||
function keyName(key: Node | null): string | undefined {
|
||||
@@ -136,10 +139,11 @@ function callerName(node: Node): Node | undefined {
|
||||
|
||||
// ─── 名字指的是谁 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** 名字在这份文件里指的是什么:定义的函数(定义处的名字)、导入的(模块和导出时的名字)、某个函数的第几个参数,或别的变量。 */
|
||||
/** 名字在这份文件里指的是什么:定义的函数(定义处的名字)、字符串常量(它的值)、导入的、某个函数的第几个参数,或别的变量。 */
|
||||
type Binding
|
||||
= | { kind: 'function', name: Node }
|
||||
| { kind: 'import', from: string, name: string }
|
||||
| { kind: 'constant', value: Node }
|
||||
| ({ kind: 'import' } & ImportedName)
|
||||
| { kind: 'parameter', fn: Node, index: number }
|
||||
| { kind: 'variable' }
|
||||
|
||||
@@ -163,7 +167,19 @@ function patternNames(pattern: Node | null | undefined): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
/** 一条语句声明的名字记进 names:函数声明和赋给 const 的函数是定义的函数,import 的是导入的(只导入类型的不算),别的是变量。 */
|
||||
/** 赋给 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))
|
||||
@@ -189,8 +205,11 @@ function declare(statement: Node | null, names: Map<string, Binding>): void {
|
||||
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' }))
|
||||
}
|
||||
@@ -270,6 +289,59 @@ class Scopes {
|
||||
|
||||
// ─── 索引 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 写的网址:字符串字面量或模板字符串拆成几截,别的写法没有。模板里的 `${名字}` 指向字符串常量的记下去哪取值(见 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)
|
||||
@@ -294,13 +366,13 @@ function exportedName(fn: Node, name: Node): string | undefined {
|
||||
}
|
||||
|
||||
/** 第一个参数写着网址的一处:名字归最内层有名字的函数,不在有名字的函数里时是被调的 callee。 */
|
||||
function sendEntry(call: Node, callee: Node, target: Omit<CallEntry, 'name'>, path: string): SendEntry {
|
||||
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 },
|
||||
path,
|
||||
url,
|
||||
at: { line: call.startPosition.row, character: call.startPosition.column, endLine: call.endPosition.row, endCharacter: call.endPosition.column },
|
||||
...(caller && { caller: caller.text }),
|
||||
}
|
||||
@@ -313,21 +385,22 @@ function sendEntry(call: Node, callee: Node, target: Omit<CallEntry, 'name'>, pa
|
||||
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)] : []
|
||||
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' || (path === undefined && first?.type !== 'identifier'))
|
||||
if (callee?.type !== 'identifier')
|
||||
return []
|
||||
const target = calleeOf(callee, scopes)
|
||||
if (!target)
|
||||
return []
|
||||
if (path !== undefined) {
|
||||
const url = urlOf(first, scopes)
|
||||
if (url) {
|
||||
const method = methodOf(init)
|
||||
return [sendEntry(call, callee, { ...target, ...(method && { method }) }, path)]
|
||||
return [sendEntry(call, callee, { ...target, ...(method && { method }) }, url)]
|
||||
}
|
||||
const binding = first && scopes.resolve(first)
|
||||
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))
|
||||
@@ -338,17 +411,17 @@ function callOf(call: Node, scopes: Scopes): (SendEntry | ForwardEntry)[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端源码里和发请求有关的调用(TSX 的语法包也读得了 .ts、.js)。包装函数一层层转手最后总落到 fetch 上,跨文件要靠 import,
|
||||
* 所以 fetch、import、`.GET(` 这类都不提的文件里不会有,不解析。
|
||||
* 前端源码里和发请求有关的调用,和导出的字符串常量(TSX 的语法包也读得了 .ts、.js)。包装函数一层层转手最后总落到 fetch 上,
|
||||
* 跨文件要靠 import,导出常量要写 export,所以 fetch、import、export、`.GET(` 这类都不提的文件里不会有,不解析。
|
||||
*/
|
||||
const CALLS_INDEX: WorkspaceIndexOf<SendEntry | ForwardEntry> = {
|
||||
const CALLS_INDEX: WorkspaceIndexOf<SendEntry | ForwardEntry | ConstantEntry> = {
|
||||
id: 'callTree.calls',
|
||||
languageId: 'typescriptreact',
|
||||
include: '**/*.{ts,tsx,js,jsx}',
|
||||
mentions: source => /\b(?:fetch|import)\b|\.(?:GET|PUT|POST|DELETE|OPTIONS|HEAD|PATCH|TRACE)\s*\(/.test(source),
|
||||
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) : [])
|
||||
return [...root.descendantsOfType('call_expression').flatMap(call => call ? callOf(call, scopes) : []), ...constantsOf(root, scopes)]
|
||||
},
|
||||
}
|
||||
|
||||
@@ -357,12 +430,16 @@ const CALLS_INDEX: WorkspaceIndexOf<SendEntry | ForwardEntry> = {
|
||||
/** 发请求的函数(fetch、认出的包装函数)的方法;转手的几处读出的方法对不上、读不出时是 null,这样的不认。 */
|
||||
type Status = Method | null
|
||||
|
||||
function isSend(call: IndexedCall): call is IndexedEntry<SendEntry> {
|
||||
return 'path' in call.entry
|
||||
function isSend(indexed: Indexed): indexed is IndexedEntry<SendEntry> {
|
||||
return 'url' in indexed.entry
|
||||
}
|
||||
|
||||
function isForward(call: IndexedCall): call is IndexedEntry<ForwardEntry> {
|
||||
return 'wrapper' in call.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
|
||||
}
|
||||
|
||||
/** 包装函数的编号:定义在哪份文件,定义处名字的起点下标。 */
|
||||
@@ -373,13 +450,12 @@ function wrapperId(uri: string, offset: number): string {
|
||||
/**
|
||||
* 从 fetch 起一层层认包装函数:转手给已认出的函数的各处,所在的函数就是下一层的包装函数。方法按转手的那处读:
|
||||
* 被调的写死了方法就是它,跟着调用处走的就照转手时交出的选项读;几处读出的不一样就定不下来。认出新的包装函数就接着查调它的各处,
|
||||
* 直到不再有新的。被调的名字按调用处那份文件的定义、import 来源对到包装函数,对不上、对上不止一个的不认。
|
||||
* 交回查到的各处调用,和问一处调用调的函数的方法(不是发请求的函数时没有)。
|
||||
* 直到不再有新的。被调的名字按调用处那份文件的定义、import 来源对到包装函数,对不上、对上不止一个的不认(见 exportOf)。
|
||||
* 交回查到的各处,和问一处调用调的函数的方法(不是发请求的函数时没有)。
|
||||
*/
|
||||
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 }>()
|
||||
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 => {
|
||||
@@ -387,20 +463,18 @@ async function senders(find: IndexFind): Promise<{ calls: IndexedCall[], statusO
|
||||
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
|
||||
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)
|
||||
wrappers.set(wrapperId(location.uri, entry.name.startIndex), { uri: location.uri, names: [entry.wrapper, ...(entry.exported ? [entry.exported] : [])], ...(entry.exported && { exported: entry.exported }) })
|
||||
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) {
|
||||
@@ -419,15 +493,74 @@ async function senders(find: IndexFind): Promise<{ calls: IndexedCall[], statusO
|
||||
return { calls: [...loaded.values()].flat(), statusOf }
|
||||
}
|
||||
|
||||
/**
|
||||
* 网址拼成路径(uri 是网址所在的文件):`${名字}` 取得到常量的值就拼进去,取不到的照写;`?`、`#` 后面的去掉,照写的 `${...}` 里的不算。
|
||||
* 导入的常量和包装函数一样按 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 = ''
|
||||
for (const piece of url) {
|
||||
const value = typeof piece === 'string' ? piece : await valueOf(uri, piece.constant)
|
||||
if (value === undefined) {
|
||||
path += typeof piece === 'string' ? piece : piece.written
|
||||
continue
|
||||
}
|
||||
const end = value.search(/[?#]/)
|
||||
if (end >= 0)
|
||||
return path + value.slice(0, end)
|
||||
path += value
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
/** 前端发方法是 method 的请求的各处:`X.GET` 这类、调 fetch 的、调包装函数的,照文件、位置排。 */
|
||||
export async function frontendRequests(find: IndexFind, method: string): Promise<IndexedEntry<RequestEntry>[]> {
|
||||
const { calls, statusOf } = await senders(find)
|
||||
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
|
||||
})
|
||||
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)
|
||||
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)
|
||||
}
|
||||
Reference in new issue
Block a user