使用 LLM 自动化生成 d.ts
背景
在项目里面,有很多旧的全局的方法是没有任何注释、参数类型的,但却异常稳定,有类型推断和注释在目前开发中已经是必不可少了,但因为有上千个这样的旧的全局方法,可以考虑 AI 生成,然后人工审核来降低开发成本。
方案权衡
方案 | 优点 | 缺点 |
---|---|---|
AST + LLM 生成 d.ts | 没有中间结果,不需要做额外的操作 | 生成结果不稳定,若要重复利用比 json 解析麻烦 |
AST + LLM 生成 JSON | 利用 json 存储中间结果,可以重复利用。 | 中间 json 解析和生成 d.ts 需要额外的步骤 |
AST + LLM 生成 JSON + 修改源代码 JSDoc | 跳转到函数定义时能直观看出类型和描述 | 需要额外的库操作,例如 magic-string |
经过时间成本和技术成本的考虑,采用 AST + LLM 生成 JSON 方案。
流程设计
提取全局代码
一般来说,全局方法是 window.XXX 或者 (NameSpace).XXX 的格式,我们需要先解析 js 文件,把它全部提取到内存数组里。这里我们采用 ast-grep 这个包来提取代码。
import { js } from '@ast-grep/napi';
export async function makeDts(source: string) {
const ast = js.parse(source);
const root = ast.root();
const functions = root.findAll({
rule: {
pattern: '$A.$B = function($$$ARGS) { $$$ }',
},
})
const allFunctionsText = functions.map(node => node.text())
for(let func of allFunctionsText) {
// TODO call LLM
}
}
/**
*
* @param {string} filepath
*/
export function makeDtsFormFile(filepath: string = '') {
// 1. get file content from filepath
const fileContent = fs.readFileSync(filepath, 'utf-8');
return makeDts(fileContent);
}
调用 LLM 获取 d.ts 返回内容
这里采用文心一言来作为例子
interface Message {
role: string
content: string,
name?: string
}
export interface CompletionOptions {
messages: Message[]
stream?: boolean
}
export interface BotAPI {
createCompletions: (option: CompletionOptions) => Promise<any>
}
/**
*
* 文心一言接口
*
*/
import fetch from 'node-fetch'
// @ts-ignore
import 'dotenv/config'
import { BotAPI, CompletionOptions } from './bot'
/**
* 使用 AK,SK 生成鉴权签名(Access Token)
* @return string 鉴权签名信息(Access Token)
*/
let accessToken: string = ''
async function getAccessToken(): Promise<string> {
if (accessToken) {
return accessToken
}
const AK = process.env.ERNIE_BOT_AK
const SK = process.env.ERNIE_BOT_SK
const url = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + AK + '&client_secret=' + SK
const response = await fetch(url, {
method: 'post',
})
const data = await response.json() as { access_token: string }
if (data.access_token) {
accessToken = data?.access_token
}
return accessToken
}
export const ernie: BotAPI = {
createCompletions: async function (option: CompletionOptions): Promise<any> {
const accessToken = await getAccessToken()
const url = 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant?access_token=' + accessToken
try {
const response = await fetch(url, {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: option.messages,
})
})
const data = await response.json() as { result: string }
return data.result
} catch (error) {
console.error(error)
return Promise.reject(error)
}
}
}
import { BotAPI } from "./bots/bot";
/**
*
* @param {string} source
* @returns
*/
export async function askForDTS(source: string = '', botAPI: BotAPI) {
const cot = `
作为一名前端工程师的 AI 辅助,我的任务是将提取 JS 源代码中的函数类型描述,生成 json 格式的文本
示例1:
Q: 请返回以下 JS 函数的类型描述 json 数据格式的文本, 不需要返回其他信息
源代码如下:
Site.vote = function (voteId, moduleId) {
const $module = $(moduleId);
$module.find('#vote' + voteId).click();
}
A: 我一步一步地分析了这个函数,它具体描述的是根据 moduleId 和 voteId 发起投票事件,接受两个参数,第一个参数是 voteId,类型是 string 或者 number,第二个参数是 moduleId,类型是 string 或者 number,返回值是 void
所以我返回的结果为:
{
"function": "Site.vote",
"description": "根据 moduleId 和 voteId 发起投票事件",
"signature": {
"parameters": [
{
"name": "voteId",
"type": "string | number",
"description": "投票 id"
},
{
"name": "moduleId",
"type": "string | number",
"description": "模块 id"
}
],
"return_type": "void",
}
}
`
const prompt = `
请返回类型描述的 json 数据格式的文本, 不需要返回其他信息
源代码如下:
${source}
Attention!!! 结果返回应该是一个 json 格式的文本
`
try {
const completion = await botAPI.createCompletions({
messages: [
{ role: 'system', content: cot },
{ role: "user", content: prompt }
],
...BotOptions
});
return completion
} catch (error) {
throw error
}
}
根据 JSON 生成 d.ts
import fs from 'fs'
// @ts-ignore
import { outputFileSync } from 'fs-extra/esm'
import path from 'path';
import { js } from '@ast-grep/napi';
import MagicString from 'magic-string';
/**
* get a json string and output to file
* @param json
* @param filePath
*/
export function outputJson(json: string, filePath: string) {
try {
outputFileSync(filePath, json, 'utf-8');
} catch (error) {
console.error(error)
}
}
interface Parameter {
name: string
type: string
description: string
}
interface FunctionDTS {
function: string
description: string
signature: {
parameters: Array<Parameter>
return_type: string
return_description: string
}
}
/**
*
* @param {string} typesRoot
*
*/
export function jsonToDTS(typesRoot: string) {
const jsonPaths = findDtsInTypesRoot(typesRoot) // 这里是文本操作找到所有的 json 文件
for(let jsonPath of jsonPaths) {
const json = fs.readFileSync(jsonPath, 'utf-8');
const dts = transformJsonToDTS(json)
if (dts !== '') {
// write file to ${outputPath}/${jsonPath name}.d.ts
outputFileSync(jsonPath.replace('.dts.json', '.d.ts'), dts, 'utf-8')
}
}
}
function transformJsonToDTS(json: string): string {
try {
const functionDTS = JSON.parse(json) as FunctionDTS[]
const namespaces: Record<string, FunctionDTS[]> = {}
let dtsOutput= ''
/**
* create namespace
*/
functionDTS.forEach((dts) => {
const functionName = dts.function
// get namespace
const parts = functionName.split('.')
const [namespaceName, name] = parts
if (!name) return
dts.function = name
if (namespaceName in namespaces) {
namespaces[namespaceName].push(dts)
} else {
namespaces[namespaceName] = [dts]
}
})
for(let namespace in namespaces) {
const functions: FunctionDTS[] = namespaces[namespace]
dtsOutput += `
declare namespace ${namespace} {
${functions.map(transformDTS).join('\n')}
}
`
}
return dtsOutput
} catch (error) {
console.error(error, `json: ${json.slice(0, 100)}`)
return ''
}
}
/**
* transform to
*
* 注释
* declare function xxx(xxx: xxx): xxx
*/
function transformDTS(dTSObject: FunctionDTS): string {
const { function: functionName, signature } = dTSObject
const sign = signature ?? { return_type: '', return_description: '' }
const { return_type, return_description } = sign
const functionSignature = transformSignature(sign)
const functionParamDesc = transformParamDesc(sign)
return `
/**
* ${dTSObject.description}
${functionParamDesc}
* @returns {${normalizeType(return_type)}} ${return_description}
*/
function ${functionName}(${functionSignature}): ${normalizeType(return_type)}
`
}
function transformSignature(signature: FunctionDTS['signature']): string {
const { parameters = [] } = signature
return parameters.map((parameter) => {
const { name, type } = parameter
return `${name}: ${normalizeType(type)}`
}).join(', ')
}
function transformParamDesc(signature: FunctionDTS['signature']): string {
const { parameters = [] } = signature
return parameters.map((parameter) => {
const { name, description } = parameter
return ` * @param ${name} ${description}`
}).join('\n')
}
function normalizeType(type: string): string {
if (!type) return 'any'
const transforms: Record<string, string> = {
'Void': 'void',
'Number': 'number',
'String': 'string',
'Boolean': 'boolean'
}
return transforms[type] ?? type
}
生成代码如下

辅助生成 d.ts 后,可以看出体验会好很多
