/** * 前端发 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 = { 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), }