Javascript類型推斷(1) - 獲取token和類型

Javascript類型推斷(1) - 獲取token和類型

js類型推斷的三種思路

第一種思路是用傳統(tǒng)的編譯類的方法的烁,推斷是沒啥好辦法褐耳,但是可以用來驗證。
第二種思路是利用對象的屬性或方法的調(diào)用來推斷渴庆,JSNice就是這樣做的。
第三種思路比較先進(jìn)雅镊,充分利用到越來越流行的Typescript襟雷,通過學(xué)習(xí)Typescript生成的javascript進(jìn)行監(jiān)督學(xué)習(xí)。這種思路是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下的同學(xué)使用,我用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選項去掉了总寒,因為有幾個工程是比較大的扶歪,需要下載一段時間,還是有個進(jìn)度條看起來比較舒服摄闸。

在data/repo-SHAs.txt中善镰,記錄了github上托管的一些工程,和當(dāng)時作者們所用的分支的SHA值年枕。
我們看下前4行的內(nèi)容:

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

下載完成后炫欺,將data/Repos目錄復(fù)制一份到data/Repos-cleaned下面。后面的處理要寫數(shù)據(jù)都是在data/Repos-cleaned下面完成熏兄。

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

讀取token信息

下一步品洛,我們調(diào)用CleanRepos.js來讀這些工程中的類型信息,然后將其寫到.ttokens文件中霍弹。如果是用戶用注釋方式描述的類型信息毫别,則寫到.ttokens.pure文件中

遍歷ts和js文件

我們來讀代碼,第一段只是做個目錄遍歷典格,找到每一個工程后岛宦,調(diào)用traverse函數(shù)來處理。
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番官,如果有了庐完。就去調(diào)用extractAlignedSequences去進(jìn)行進(jìn)一步的處理。

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

extractAlignedSequences中徘熔,首先去調(diào)用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;
}

創(chuàng)建Type Checker

然后我們調(diào)用typescript去創(chuàng)建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;

最后調(diào)用到對于token的處理邏輯extractTokens:

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

在extractTokens中,有兩類標(biāo)記是暫不進(jìn)行處理的酷师,一類是空白符讶凉,一類是模板:

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;
        }

先看個例子

進(jìn)入正式的邏輯之前山孔,我們先看個例子增加一下感性認(rèn)識懂讯。

我們先寫一個一句話的typescript:

let a = 1;

這里面是5個token: let, a, = , 1, ;.

對應(yīng)的.ttokens為:

O $number$ O O O

再來一個字符串的例子:

let s = "Hello";

對應(yīng)的.ttokens為:

O $string$ O O O

我們再看一個打印日志的例子:

console.log(s);

上面是6個token,

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

log函數(shù)值得說一下饱须,因為它的類型其實是:(message?: any, ...optionalParams: any[]) => void
返回void是代碼中進(jìn)行了處理域醇。

我們再來看一個面向?qū)ο蟮睦樱?/p>

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

我們看下token和對應(yīng)的類型:

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部分和類型就基本上講清楚了减宣,細(xì)節(jié)可以對照完整代碼再看一下。

參考文獻(xiàn)

  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.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末玩荠,一起剝皮案震驚了整個濱河市漆腌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阶冈,老刑警劉巖闷尿,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異女坑,居然都是意外死亡填具,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劳景,“玉大人誉简,你說我怎么就攤上這事∶斯悖” “怎么了闷串?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長筋量。 經(jīng)常有香客問我烹吵,道長,這世上最難降的妖魔是什么毛甲? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任年叮,我火速辦了婚禮,結(jié)果婚禮上玻募,老公的妹妹穿的比我還像新娘。我一直安慰自己一姿,他們只是感情好七咧,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著叮叹,像睡著了一般艾栋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蛉顽,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天蝗砾,我揣著相機(jī)與錄音,去河邊找鬼携冤。 笑死悼粮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的曾棕。 我是一名探鬼主播扣猫,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼翘地!你這毒婦竟也來了申尤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤衙耕,失蹤者是張志新(化名)和其女友劉穎昧穿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體橙喘,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡时鸵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了渴杆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寥枝。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡宪塔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出囊拜,到底是詐尸還是另有隱情某筐,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布冠跷,位于F島的核電站南誊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蜜托。R本人自食惡果不足惜抄囚,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望橄务。 院中可真熱鬧幔托,春花似錦、人聲如沸蜂挪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棠涮。三九已至谬哀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間严肪,已是汗流浹背史煎。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留驳糯,地道東北人篇梭。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像结窘,于是被迫代替她去往敵國和親很洋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容