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

131 lines
6.0 KiB
TypeScript

/**
* 前端发 HTTP 请求的地方的工作区索引:openapi-fetch 的 `X.GET("/path", ...)` 这类(成员名是大写的 HTTP 方法),
* 和 `fetch("/path", { method: "POST" })`。路径要写成字符串字面量或模板字符串;接口的调用树拿它找前端调用了哪个接口(见 endpoint.ts)。
*/
import type { IndexEntry, Node } from '../contract'
import type { WorkspaceIndexOf } from '../workspaceIndex'
import type { CallRange } from './model'
import { argumentsOf, isFunction, named, sameNode, stringContent, unwrap } from '../rules/reactTree'
/** openapi-fetch 客户端上发请求的方法名,也是 fetch 的 method 认得的值。 */
const METHODS = new Set(['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'TRACE'])
/**
* 前端发的一个请求:键是大写的方法。名字是发请求的地方所在的最内层有名字的函数的名字(编辑器从它往上问调用者);
* 不在有名字的函数里时,名字是被调用的 fetch / GET 本身。
*/
export interface RequestEntry extends IndexEntry {
/** 写的路径,模板里的 `${...}` 照写,`?`、`#` 后面的去掉 */
path: string
/** 发请求那个调用的范围,行和列从 0 起 */
at: CallRange
/** 所在的有名字的函数叫什么;不在有名字的函数里时没有 */
caller?: string
}
/** 第一个参数写的路径:字符串字面量或模板字符串(`${...}` 照写),去掉 `?`、`#` 后面的;别的写法没有。 */
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
}
/** 对象字面量的键名:`method`、`"method"`。 */
function keyName(key: Node | null): string | undefined {
return key?.type === 'property_identifier' ? key.text : stringContent(key)
}
/**
* fetch 的方法:没有第二个参数、对象里没写 method 是 GET,method 是字面量就照它;
* method 不是字面量、第二个参数不是对象字面量、对象里带展开(可能藏着 method)都不认。
*/
function fetchMethod(init: Node | undefined): string | undefined {
if (!init)
return 'GET'
if (init.type !== 'object')
return undefined
const members = named(init)
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'
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 的没有。
*/
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
}
function requestsIn(root: Node): RequestEntry[] {
return root.descendantsOfType('call_expression').flatMap((call) => {
const request = call && requestOf(call)
if (!request)
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',
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),
}