131 lines
6.0 KiB
TypeScript
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),
|
|
}
|