前几天公司的一个前端大佬用js写了一个库,基于通义千问的api将公司某项目中的js文件给配置上了d.ts。
然后我想着这玩意儿用rust写一个应该挺舒服,毕竟是工具。
代码:
不过我们这里是基于api来实现的,所以我们实际要去到的网站是:如何使用通义千问API_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
我们要请求它的接口,第一步就是要有请求的权限,官方要求请求得加上DashScope API-KEY:如何开通DashScope并创建API-KEY_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
这里就不展示创建流程了,官方的文档已经说的很清楚了。
我们这里没有特殊要求,所以直接用最简单的大语言模型就好:通义千问模型简介_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
我们要的接口:POST https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation
然后请求的时候把API-KEY传入header里即可
我们是基于接口请求来实现而不是SDK(问题是也没有呀,虽然也可以通过interface的方式去接入Python的SDK),所以我们这里需要用到:
我们先搞一个请求的demo,确保流程跑的通
}
然后我们在根目录创建一个test
文件夹,往里放一个jump.js
(不要问为啥叫这名字,从公司项目里随便找了个文件copy过来的。。里面代码我删掉了,毕竟公司的资产)
然后我们cargo run
把多余的话去掉:
不是很对,这说明我们的prompt
没写好,不过我们现在的目的不是这个,而是实现请求并且有反馈。目前我们做到了这点。
前面我们确认了这种方案是走得通的,那么我们就基于这个方案做一下拓展,将它变成一个工具:
js
,只针对js
文件里export
的函数(包括箭头函数)typescript
类型d.ts
文件input_path
,一个要存放处理后文件的路径output_path
,其中input_path
可以是文件或者文件夹。两个路径需要兼顾相对路径和绝对路径。还需要有个deep♂
的参数用来自定义是否需要递归子文件夹export function
和export const xx = () => {}
这两种,所以我们需要对源码进行过滤,这里就需要将js解析成 AST
那么就先简单分析到这里。
根据上面我们分析之后的几点,我们先完成代码的架构
InputArgs
,用来获取三个参数和转换成绝对路径read
,用来获取文件的名字和内容,转换成File
结构compile
,即将js
源码转换成AST
,并且从里面拿到符合要求的源码ask
,用来访问通义千问write
,将访问得到的内容转换成对应的文件整理一下:
说一下为啥要这么拆分,因为这么做我们才好做单元测试
我们在src
下创建一个args
的文件夹和一个mod.rs
,我们直接把代码放在这个mod.rs
(别忘了引入到main.rs
中)就好,当然,如果你还想细分,也可以再搞其他的文件。
首先我们来设计下存放输入数据的结构和方法:
init
作为入口get_input
作为数据获取的主要部分absoluted_path
将代码转换成绝对路径然后我们先来实现get_input
的部分:
这一部分我们需要获取三个参数input_path
、output_path
和deep
,这里我们就需要借助stdout和stdin,来等待用户输入的内容
path
中path
末尾可能存在的\n
get_input
主体没啥好说的,最后一个deep
参数写起来有些怪,如果你有更好的方案,可以评论区说下~
在开始实现下一个方法之前,我们需要设计下测试用例,并做单元测试(后面我就不写那么详细的测试用例了,麻烦,而且占篇幅挺大的,其实就是懒,不想写):
对应的输出(不要在意\r\n
为啥不一起处理):
然后是get_input
的:
对应的输出:
ok,没问题,我们来实现第二个方法absouluted_path
,即将内容转换成绝对路径
代码稍微长了些,因为我们需要判断这个路径是否是绝对路径,如果不是,我们需要转换成绝对路径,另外我们还需要确认路径是否存在,如果不存在,那么我们还需要帮他创建这个文件路径(当然你也可以直接报错不执行后续的)
其它没啥好说的了,测试用例就不写了,前面说过原因了。
然后我们来实现InputArgs
最后一个方法:这个方法简单,只需要组装前面两个方法即可
这个方法没啥好说的
那么到这,获取参数这一块的就完成了。
最后我们可以搞一个整体的测试用例:
这里使用了tempfile,可以创建临时文件夹
我们在src
下创建一个file_io
文件夹,里面创建read
、write
和mod.rs
(另外两个文件记得在mod.rs
中导出)三个文件。
我们现在暂时不涉及write
,我们先来实现read
的逻辑。我们需要读取文件内容、文件的名字,另外如果输入的目标地址是一个文件夹,并且deep
为true
,那么这个时候我们就需要深度递归这个文件夹,并且记录这个过程中子文件夹的名字和路径。
这一点有些复杂,需要用到递归的逻辑,另外子文件夹的路径我们可以使用字符串拼接,然后传递给下次递归,这样可以保证路径是对的,相当于维护一个单调栈,不过是字符串的形式。
我们先设计下输出的数据的结构
name
:文件名字content
:文件内容relative_name
:如果deep
为true
的话,那么我们还需要记录文件的相对路径,这样在生成文件的时候也能保证相对位置是正确的。然后就是实现读取的部分,我直接贴代码了,递归相信大家都知道怎么写:
我们这里还单独记录了每个子文件夹的路径,这么做是为了保证输出文件在创建的过程中可以正常创建,因为最终都是绝对路径。
然后就没啥好说的了,接着也是简单的一个测试用例
然后我们把原本test/jump.js
文件迁移到新建的test/haha/jump.js
就不写target_input
了,直接肉眼看下是否正常:
肉眼peek正常
这部分稍微复杂一些,因为swc,这个包迭代非常非常频繁,半年前的代码现在已经不能用了(官方自己的example也有的没有更新),然后文档里面不介绍内容,需要一堆子包配合,所以比较复杂。
我们在src
下创建一个compile
文件夹,里面创建parse.rs
、transform.rs
、gen.rs
、mod.rs
四个文件。
在开始写代码之前,我们需要引入几个crate
:
要注意包的版本,因为更新很频繁!
我们先处理parse
:
这里swc
依赖了一个包:scoped-tls,这个包提供了(旧)标准库的scoped_thread_local!
(当前标准库的文档中没看到,只有一个thread_local!, 它用来声明一个拥有其内容的线程本地存储密钥。)的功能,可以让我们自己去impl
它。
GLOBALS
:来看下相关的源码:
也就是给GLOBALS
创建一个ScopedKey,有点类似前端localStorage
的用法。现在GLOBALS
有一个全局的唯一本地线程秘钥和空间。
ScopedKey
之后拥有的方法,它接收一个参数和一个闭包,参数会在闭包运行的过程中存在,可以调用with拿到。我们这里用不到这个数据,我们用到只是因为swc
官方要求的。。。Lrc
://!
Lrc is an alias of either Rc or Arc.
SourceMap
:span
(节点最小单位),作为span
的中间器(interner
)。这里面存储的span
都只是指针([BytePos])
指向数据的位置。简单地说,我们可以通过它拿到节点的信息。Handler
:这个就不多说了(相关的比如EmitterWriter
也不多说了,会用即可),用来处理、收集错误的。new_source_file
:顾名思义,创建一个source_file
,一个source_map
可以存储多个source_file
。Lexer
:官方没有给这货和Capturing
任何的注释,但是看名字就知道是一个词法分析器,看它的配置项比如target
支持设置不同的ES
版本,比如ES5、ES6
也可以推测出。我们暂时就不去深入了解了。Parser
:解析器get_source_file
:顾名思义,就是用来获取文件对象信息的,src
即是源码,是一个Arc<String>
。注意我们不能使用我们自己的源码,它内部应该是处理过的,我们后面根据节点的start
和end
是根据它这个处理过的源码进行定位的,所以如果我们用自己的源码容易出现偏移。其实我也不太了解怎么用,官方没有文档介绍怎么用,迭代又非常频繁导致别人的demo
很容易过期,跟着过期攻略
+ 官方example
自己组装的。。。以后有机会可以来分析下源码。
然后我们随便搞个测试用例:
输出结果:
可以看到和babel
的AST
类似,毕竟解析逻辑是一致的。
然后我们来到transform
阶段,我们需要的是export function
和export const xx = () => {}
,所以我们需要写一个visitor
用来visit
所有的节点:
然后我们可以实现官方提供的Visit in swc_ecma_visit对象,它内部支持实现对不同类型节点的visit
。这一点和babel
的类似:
export
节点的,然后我们再根据它进行分析:这里的逻辑就多说了,相信大家都看得懂。
visitor
写完了,我们准备来实现接入visit
的过程:
HELPERS
:和Globals
一样,我们需要这个HELPERS
包裹,这样才能处理js
中比如async
这种语法。resolver
:看下官方的描述:何时运行
解析器需要“干净”的 ast。您可以通过解析或通过 删除 AST 节点中的所有语法上下文。
它有什么作用
首先,所有作用域(fn、block)都有自己的 SyntaxContext。 Resolver 访问模块中的所有标识符,并在作用域中查找绑定标识。这些标识符现在具有范围 (fn, block) 的 SyntaxContext。执行此操作时,解析程序会尝试将普通标识符(无卫生(
hygiene
)信息)解析为对范围标识符的引用。如果解析器找到合适的变量,则标识符引用将与变量具有相同的上下文。
简单地说我们可以通过它去修改节点,类似babel
里的transform
阶段。
preset_env
:这个我也不清楚干啥的,后面这段transformer
的应该是不需要的,我是参考的过期攻略。不管怎么样,代码是写完了,照例搞一个测试用例:
对应输出结果:
可以看到正确的拿到了我们的函数。
最后还有个gen.rs
,这个实际上不是ast
阶段的,只是为了好看我给放到这里了。。
哦,忘了说了,因为官方api
每次请求都是有token
长度限制的,所以我对每个文件还进行了二次拆分,具体限制可以自行看官方文档:
通义千问如何计量计费和查看账单_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
你可以通过这个接口去确认是否超了:
Token计算API详情_模型服务灵积(DashScope)-阿里云帮助中心 (aliyun.com)
接下来就是请求阶段了,这块是我耗时最久的,不过原因则是因为通义千问官方的api
的QPM
(每分钟最多请求)< 200(实际更少),所以我这里要调整每秒的请求频率,很麻烦。
我们同样在src
下创建一个ask
文件夹,里面放client.rs
和mod.rs
。
代码没啥好说的,我直接贴出来了:
}
因为大模型有时候话很多
,所以我只能是去捕捉````typescript`里的代码,注意这里用到了regex的包。
我这里没写测试用例,主要是忘了。
不过没关系,到这里我们就基本差最后一步了,可以直接在main
中组装。
我这里就不跑了,你们可以自行运行下
没啥好说的,直接上代码:
还记得前面read
时收集的子文件夹相对路径集合么?我们在这里先根据这个集合创建相对文件夹,然后再创建文件,这样就不会报错了。
这里简单搞一个测试用例:
对应输出:
ok
然后我们直接运行,不搞测试用例了。(记得先设置你的API-KEY
)
对应输出:
这就完成了,随便找个文件看下:color.d.ts
表现比较正常(当然,有时候也很抽风)
最后我们运行cargo build --release
生成最终产物exe
文件,这样就拿到立即执行文件了。
中间漏了一些代码,具体可以去github
上看下,这里就不多说了。
这个工具比较简单,中间的一些过程可以抽离变成一个通用的结构,这样以后写一些工具就可以灵活变通了。