Closed yqs112358 closed 2 months ago
感觉涉及到复杂类型时,难以进行翻译 例如Typescript中的交叉、联合类型、keyof、typeof、条件类型、类型推断,难以在python中表示
当然,仅提供API的类型时,可能涉及不到复杂的类型定义,可以考虑忽略复杂类型翻译
假定这个API建模手动完成,或许考虑选取一个与特定语言无关的形式来提供这个内容。 这样子,对与一些复杂类型,我们可以采取类似下面这个形式。同时,这么做或许可以更易于解析和处理
[class]
docs = '''
文档
'''
# 选择toml举例是因为它的多行字符串功能会文档内嵌提供不小的便利
[class.methodsA]
# 一个带有复杂类型声明的方法
docs = '''
文档内容
'''
type.ts = ""
type.py = ""
type.lua = ""
# 把这些字符分别替换给对应语言
[class.methodsB]
# 一个简单的类型声明
docs='''
文档内容
'''
type=""
# 这里或许应该采取别的形式,而非字符串,但是我暂时没有进一步的思考
一个额外的思考,有没有那个语言从声明文件生成的文档可以直接满足ls项目文档的需求,他们通常会提供一些极有价值的功能 (虽然似乎如果需要,我们可以在自己实现文档生成的时候自己实现那些功能
另外一个问题,我们有多大可能自动化的从源码生成API类型声明。
像OneLang、Haxe、Progsbase这些工具似乎都不能同时支持所需的三种语言 简单的试用过后感觉对接口定义的翻译不是很友好(该点存疑,若有错误请懂行的大佬指正)
或许可以用json或yml等形式,直接编写一个自定义的语法树 该树类似AST但只包含主要需要的定义,这些定义大多有高度概括性,或者只是一种大概的模糊定义 具体的代码生成则由对应目标语言的翻译器实现
对于本项目来说,一些语言特性与复杂类型可能用不到,因此翻译器只需要实现主要语法翻译即可 同时因为这个语法树的概括性,对于文档生成有一定帮助
像OneLang、Haxe、Progsbase这些工具似乎都不能同时支持所需的三种语言 简单的试用过后感觉对接口定义的翻译不是很友好(该点存疑,若有错误请懂行的大佬指正)
或许可以用json或yml等形式,直接编写一个自定义的语法树 该树类似AST但只包含主要需要的定义,这些定义大多有高度概括性,或者只是一种大概的模糊定义 具体的代码生成则由对应目标语言的翻译器实现
对于本项目来说,一些语言特性与复杂类型可能用不到,因此翻译器只需要实现主要语法翻译即可 同时因为这个语法树的概括性,对于文档生成有一定帮助
这里的概括性是对于不同目标语言来说的,例如语言1和语言2中的类型分别是T1和T2,语法树中则可以使用T表示。目标语言翻译器在翻译时自动转换
写了一个小demo,尝试在typescript的定义文件中加入一些规则,来统一生成多种语言的类型定义文件。
demo地址:https://github.com/superx101/TypeLoom
这个demo把typescript的AST转换为了一个自定义的简单AST,并解析这个AST实现目标代码生成 目前仅实现了d.ts解析,和d.ts生成。 在解析时可以顺便生成一份键值对文件,用于国际化。解析AST时则可以读取这个键值对文件替换成其他自然语言。
用一个样例可以快速解释该demo的内容: 首先手动建模一份定义文件,如下:
// 使用特定的类型名称来定义基本类型
// 目前解析器中会忽略所有的类型别名定义,这里仅仅为了编译器不报错,类型取值随意
declare type dint = number;
declare type dfloat = number;
declare type dvoid = void;
declare type dstring = string;
declare type dany = any;
declare type dbool = boolean;
/**
* 在生成文档、注释时忽略这个命名空间
* @diable
*/
declare namespace exampleNamespace1 {
/**
* 在生成文档、注释时忽略这个接口
* @disable
*/
interface Interface1 {
/**
* 为属性添加一段描述。
* 这个标签默认开启
* @text
*/
i1p1: dint;
}
/**
* 标记为弃用
* @deprecated
*/
interface Interface2 {
/**
* @deprecated
*
* 为属性添加一个示例
* @example
*/
i2p1: dint;
/**
* 在生成注释时,为每个函数属性都添加注释
* @params
*
* 同理,添加返回值注释
* @returns
*/
i2p2(p1: dint, p2: dstring): dint;
}
class Class1 {
// 如果函数没有注释,则按照默认标签处理,这里也会加上 params 和 returns
constructor(p1: dint, p2: dbool);
/**
* 在标签后面添加false,显式的禁用标签。若不指明true/false则默认true
* @returns false
*/
c1m1(): dint;
/**
* 同理,显式开关标签
* @params false
* @returns true
*/
c1m2(p1: dint, p2: dstring): dint;
/**
* text是个可重复标签,可多次使用
* @text
* @params
* @returns
* @text
*/
c2m3(p1: dint, p2: dstring): dint;
}
}
使用demo中的DTSParser,把文件解析为AST。AST的结构可见: https://github.com/superx101/TypeLoom/blob/main/example/1-getting-started/output/ast-json.json 为了方便理解,把一些AST中的枚举替换为了字符串
同时让demo对AST遍历生成一个语言文件,这部分功能demo中已封装好。语言文件如下:
{
"source-d-ts.example-namespace1.interface1.i1p1.text0": "",
"source-d-ts.example-namespace1.interface1.i1p1.text1": "",
"source-d-ts.example-namespace1.interface2.i2p1.text0": "",
"source-d-ts.example-namespace1.interface2.i2p1.deprecated": "",
"source-d-ts.example-namespace1.interface2.i2p1.example.ts": "",
"source-d-ts.example-namespace1.interface2.i2p2.text0": "",
"source-d-ts.example-namespace1.interface2.i2p2.param.p1": "",
"source-d-ts.example-namespace1.interface2.i2p2.param.p2": "",
"source-d-ts.example-namespace1.interface2.i2p2.returns": "",
"source-d-ts.example-namespace1.interface2.text0": "",
"source-d-ts.example-namespace1.interface2.deprecated": "",
"source-d-ts.example-namespace1.class1.constructor.text0": "",
"source-d-ts.example-namespace1.class1.constructor.param.p1": "",
"source-d-ts.example-namespace1.class1.constructor.param.p2": "",
"source-d-ts.example-namespace1.class1.c1m1.text0": "",
"source-d-ts.example-namespace1.class1.c1m1.returns": "",
"source-d-ts.example-namespace1.class1.c1m2.text0": "",
"source-d-ts.example-namespace1.class1.c1m2.param.p1": "",
"source-d-ts.example-namespace1.class1.c1m2.param.p2": "",
"source-d-ts.example-namespace1.class1.c1m2.returns": "",
"source-d-ts.example-namespace1.class1.c2m3.text0": "",
"source-d-ts.example-namespace1.class1.c2m3.param.p1": "",
"source-d-ts.example-namespace1.class1.c2m3.param.p2": "",
"source-d-ts.example-namespace1.class1.c2m3.returns": "",
"source-d-ts.example-namespace1.class1.c2m3.text1": "",
"source-d-ts.example-namespace1.class1.c2m3.text2": "",
"source-d-ts.example-namespace1.class1.text0": "",
"source-d-ts.example-namespace1.text0": "",
"source-d-ts.example-namespace1.diable": ""
}
最后让目标代码生成器,装入AST和这个语言文件,得到目标代码的d.ts文件。这里因为没有为语言文件填入内容,得到的代码中打印的是语言文件的键。在实际使用中也可以先输出一个带key的代码,在语言文件中对照翻译后再生成翻译后的代码。
当前demo中的目标代码生成器,会把所有涉及到的d.ts文件自动打包为一个d.ts
代码如下:
// file: source-d-ts
//这里是一个错误,标签disable拼写错误。demo中没有定义的标签依然可以被当做一个普通标签输出
/**
* source-d-ts.example-namespace1.text0
* @diable source-d-ts.example-namespace1.diable
*/
declare namespace exampleNamespace1 {
interface Interface1 {
/**
* source-d-ts.example-namespace1.interface1.i1p1.text0
* source-d-ts.example-namespace1.interface1.i1p1.text1
*/
i1p1: number;
}
/**
* source-d-ts.example-namespace1.interface2.text0
* @deprecated source-d-ts.example-namespace1.interface2.deprecated
*/
interface Interface2 {
/**
* source-d-ts.example-namespace1.interface2.i2p1.text0
* @deprecated source-d-ts.example-namespace1.interface2.i2p1.deprecated
* @example source-d-ts.example-namespace1.interface2.i2p1.example.ts
*/
i2p1: number;
/**
* source-d-ts.example-namespace1.interface2.i2p2.text0
* @param p1 source-d-ts.example-namespace1.interface2.i2p2.param.p1
* @param p2 source-d-ts.example-namespace1.interface2.i2p2.param.p2
* @returns source-d-ts.example-namespace1.interface2.i2p2.returns
*/
i2p2(p1: number, p2: string): number;
}
/**
* source-d-ts.example-namespace1.class1.text0
*/
class Class1 {
/**
* source-d-ts.example-namespace1.class1.constructor.text0
* @param p1 source-d-ts.example-namespace1.class1.constructor.param.p1
* @param p2 source-d-ts.example-namespace1.class1.constructor.param.p2
*/
constructor(p1: number, p2: boolean);
/**
* source-d-ts.example-namespace1.class1.c1m1.text0
* @returns source-d-ts.example-namespace1.class1.c1m1.returns
*/
c1m1(): number;
/**
* source-d-ts.example-namespace1.class1.c1m2.text0
* @param p1 source-d-ts.example-namespace1.class1.c1m2.param.p1
* @param p2 source-d-ts.example-namespace1.class1.c1m2.param.p2
* @returns source-d-ts.example-namespace1.class1.c1m2.returns
*/
c1m2(p1: number, p2: string): number;
/**
* source-d-ts.example-namespace1.class1.c2m3.text0
* @param p1 source-d-ts.example-namespace1.class1.c2m3.param.p1
* @param p2 source-d-ts.example-namespace1.class1.c2m3.param.p2
* @returns source-d-ts.example-namespace1.class1.c2m3.returns
* source-d-ts.example-namespace1.class1.c2m3.text1
* source-d-ts.example-namespace1.class1.c2m3.text2
*/
c2m3(p1: number, p2: string): number;
}
}
更加详细的示例请见:https://github.com/superx101/TypeLoom/tree/main/example/1-getting-started
该demo中还支持了简单的泛型定义、简单的泛型约束、继承、实现接口,而泛型推断不打算支持,因为部分语言没有ts这样的类型能力,且对于API定义来说,用不到泛型推断
LLSE在引擎层采用跨语言统一接口设计,但是不同语言后端的补全库需要单独手动维护(js/lua/py),工作量较大且容易出现遗漏。另外文档编写和修改也较为麻烦。
设想: 使用某种统一接口定义语言(如TypeScript接口)对所有API进行建模,然后写程序解析AST并翻译到各目标语言的补全文件。以后变动API时,除了修改代码之外,只需要对接口文件进行修改,并执行程序更新补全库即可(可由github actions自动化)
另外如果可能的话,在接口文件中嵌入文本段,并用于自动化生成文档,以降低文档维护工作量