Javascript类型推断(1) - 获取token和类型

Javascript类型推断(1) - 获取token和类型

js类型推断的三种思路

第一种思路是用传统的编译类的方法,推断是没啥好办法,但是可以用来验证。
第二种思路是利用对象的属性或方法的调用来推断,JSNice就是这样做的。
第三种思路比较先进,充分利用到越来越流行的Typescript,通过学习Typescript生成的javascript进行监督学习。这种思路是Vincent J. Hellendoorn,Christian Bird,Earl T. Barr,Miltiadis Allamanis的论文《Deep Learning Type Inference》中提到的。

下载素材

既然是要落地了,我们就边做边说。
首先下载代码:git clone https://github.com/DeepTyper/DeepTyper.git

然后我们要下载一些Typescript工程做素材。作者们提供了一个cloner.sh。
为了方便Windows下的同学使用,我用Python将其改写一下:

import os
with open("./repo-SHAs.txt", "r") as fr:
    for line in fr:
        cmd1 = line.split()
        clone_cmd = 'git clone https://github.com/'+cmd1[0]+' ./Repos/'+cmd1[0]
        os.system(clone_cmd)
        checkout_cmd = 'git -C ./Repos/'+cmd1[0]+' reset --hard '+cmd1[1]
        os.system(checkout_cmd)

针对原作有一处修改是将git clone中的q选项去掉了,因为有几个工程是比较大的,需要下载一段时间,还是有个进度条看起来比较舒服。

在data/repo-SHAs.txt中,记录了github上托管的一些工程,和当时作者们所用的分支的SHA值。
我们看下前4行的内容:

0xProject/0x.js e25cc301fddbc67f793ca0eb0f7635cdb9147a71
0xProject/contracts d80460d94daf8725b0017ff40c81f02a9a8f7f89
1backend/1backend 29869b6b160feb764b5a4f9f1984a9d1db0bed80
2fd/graphdoc a5bbc7b601975b00ec83b781a6afe6014ebe171b

下载完成后,将data/Repos目录复制一份到data/Repos-cleaned下面。后面的处理要写数据都是在data/Repos-cleaned下面完成。

let repos = "data/Repos"
let cleaned = "data/Repos-cleaned";

读取token信息

下一步,我们调用CleanRepos.js来读这些工程中的类型信息,然后将其写到.ttokens文件中。如果是用户用注释方式描述的类型信息,则写到.ttokens.pure文件中

遍历ts和js文件

我们来读代码,第一段只是做个目录遍历,找到每一个工程后,调用traverse函数来处理。
org是一级目录,project是二级工程。
以“0xProject/0x.js”为例,0xProject就是org,而0x.js是project。
最后,由repos, org, project三级拼出每一个工程的完整的路径:

for (let org of fs.readdirSync( repos)) {
    for (let project of fs.readdirSync(repos + "/" + org)) {
        // This project stalls forever
        if (org == "SAP") continue
        let dir = repos + "/" + org + "/" + project;
        traverse(dir);
    }
}

我们知道,如果是ts工程,在根目录下会有一个tsconfig.json。所以我们到一个工程之后,首先去查找是否有tsconfig.json,如果有了。就去调用extractAlignedSequences去进行进一步的处理。

function traverse(dir) {
    var children = fs.readdirSync(dir);
    if (children.find(value => value == "tsconfig.json")) {
        print("Config in: " + dir);
        extractAlignedSequences(dir);
    }

extractAlignedSequences中,首先去调用walkSync从inputDirectory获取文件列表:

function extractAlignedSequences(inputDirectory) {
    const keywords = ["async", "await", "break", "continue", "class", "extends", "constructor", "super", "extends", "const", "let", "var", "debugger", "delete", "do", "while", "export", "import", "for", "each", "in", "of", "function", "return", "get", "set", "if", "else", "instanceof", "typeof", "null", "undefined", "switch", "case", "default", "this", "true", "false", "try", "catch", "finally", "void", "yield", "any", "boolean", "null", "never", "number", "string", "symbol", "undefined", "void", "as", "is", "enum", "type", "interface", "abstract", "implements", "static", "readonly", "private", "protected", "public", "declare", "module", "namespace", "require", "from", "of", "package"];
    let files = [];
    walkSync(inputDirectory, files);

walkSync的功能也非常简单,就是将非.git目录中小于1M的js和ts文件返回回来:

function walkSync(dir, filelist) {
    var fs = fs || require('fs'), files = fs.readdirSync(dir);
    filelist = filelist || [];
    files.forEach(function (file) {
        let fullPath = path.join(dir, file);
        print('fullPath=',fullPath)
        try {
            if (fs.statSync(fullPath).isDirectory()) {
                if (file != ".git")
                    filelist = walkSync(dir + '/' + file, filelist);
            }
            else if (file.endsWith('.js') || file.endsWith('.ts')) {
                if (fs.statSync(fullPath).size < 1*1000*1000)
                    filelist.push(fullPath);
            }
        }
        catch (e) {
            console.error("Error processing " + file);
        }
    });
    return filelist;
}

创建Type Checker

然后我们调用typescript去创建program和checker:

    let program = ts.createProgram(files, { target: ts.ScriptTarget.Latest, module: ts.ModuleKind.CommonJS, checkJs: true, allowJs: true });
    let checker = null;
    try {
        checker = program.getTypeChecker();
    }
    catch (err) {
        return null;
    }

下面我们还要将d.ts过滤掉:

    for (const sourceFile of program.getSourceFiles()) {
        let filename = sourceFile.getSourceFile().fileName;
        if (filename.endsWith('.d.ts'))
            continue;
        try {
            let relativePath = path.relative(inputDirectory, filename);
            if (relativePath.startsWith(".."))
                continue;

最后调用到对于token的处理逻辑extractTokens:

            let memS = [];
            let memT = [];
            let memP = [];
            extractTokens(sourceFile, checker, memS, memT, memP);

在extractTokens中,有两类标记是暂不进行处理的,一类是空白符,一类是模板:

var removableLexicalKinds = [
    ts.SyntaxKind.EndOfFileToken,
    ts.SyntaxKind.NewLineTrivia,
    ts.SyntaxKind.WhitespaceTrivia
];
var templateKinds = [
    ts.SyntaxKind.TemplateHead,
    ts.SyntaxKind.TemplateMiddle,
    ts.SyntaxKind.TemplateSpan,
    ts.SyntaxKind.TemplateTail,
    ts.SyntaxKind.TemplateExpression,
    ts.SyntaxKind.TaggedTemplateExpression,
    ts.SyntaxKind.FirstTemplateToken,
    ts.SyntaxKind.LastTemplateToken,
    ts.SyntaxKind.TemplateMiddle
];

如果是空白符和JSDoc,则continue:

function extractTokens(tree, checker, memS, memT, memP) {
    var justPopped = false;
    for (var i in tree.getChildren()) {
        //print('i='+i);
        var ix = parseInt(i);
        var child = tree.getChildren()[ix];
        if (removableLexicalKinds.indexOf(child.kind) != -1 ||
            ts.SyntaxKind[child.kind].indexOf("JSDoc") != -1) {
            continue;
        }

先看个例子

进入正式的逻辑之前,我们先看个例子增加一下感性认识。

我们先写一个一句话的typescript:

let a = 1;

这里面是5个token: let, a, = , 1, ;.

对应的.ttokens为:

O $number$ O O O

再来一个字符串的例子:

let s = "Hello";

对应的.ttokens为:

O $string$ O O O

我们再看一个打印日志的例子:

console.log(s);

上面是6个token,

$Console$ O $void$ O $string$ O O

log函数值得说一下,因为它的类型其实是:(message?: any, ...optionalParams: any[]) => void
返回void是代码中进行了处理。

我们再来看一个面向对象的例子:

class Test{
    public value : number;
    constructor(v: number){
        this.value = v;
    }
}
let t = new Test(1);

我们看下token和对应的类型:

class Test { public value : number ; constructor ( v ) { this . value = v ; } } let t = new Test ( 1 ) ;
O $any$ O O $number$ O O O O O $number$ O O O O $number$ O $number$ O O O O $Test$ O O $any$ O O O O

Test本身的类型是typeof Test。而在保存到.ttokens中时,按any做处理。

对照了例子后,下面的代码就容易理解了:

        if (child.getChildCount() == 0) {
            var source = child.getText();
            var target = "O";
            switch (child.kind) {
                case ts.SyntaxKind.Identifier:
                    try {
                        let symbol = checker.getSymbolAtLocation(child);
                        if (!symbol) {
                            target = "$any$"
                            break;
                        }
                        let type = checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol, child));
                        if (checker.isUnknownSymbol(symbol) || type.startsWith("typeof"))
                            target = "$any$";
                        else if (type.startsWith("\""))
                            target = "O";
                        else if (type.match("[0-9]+"))
                            target = "O";
                        else
                            target = '$' + type + '$';
                        break;
                    }
                    catch (e) {
                        console.error(e);
                     }
                    break;

到这里,获取token部分和类型就基本上讲清楚了,细节可以对照完整代码再看一下。

参考文献

  1. Hellendoorn, V. J., Bird, C., Barr, E. T., & Allamanis, M. (2018, October). Deep learning type inference. In Proceedings of the 2018 26th ACM Joint Meeting on European Software Engineering Conference and Symposium on the Foundations of Software Engineering (pp. 152-162). ACM.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容