Fun with ASTs and Uglify

Pop quiz, hot shot! You got some unit-tests. You want to know whether it's written in QUnit or Jasmine. What do you do? What do you do?

Get a Javascript parser... Uglify! Install it for node

npm install uglify-js

Read in the file and parse it to get an AST

var parser = require('uglify-js').parser
  , fs = require('fs')
  , filename = process.argv[0]
var ast = parser.parse(String(fs.readFileSync(filename)))

Print out the AST

console.log(JSON.stringify(ast))

Look at the AST for an example QUnit test file

[
  "toplevel",
  [
    [
      "stat",
      [
        "call",
        [
          "name",
          "test"
        ],
        [
          [
            "string",
            "validate phone #"
          ],
          [
            "function",
            null,
            [],
            [
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "ok"
                  ],
                  [
                    [
                      "call",
                      [
                        "name",
                        "validatePhone"
                      ],
                      [
                        [
                          "string",
                          "770 123 4567"
                        ]
                      ]
                    ],
                    [
                      "string",
                      "basic"
                    ]
                  ]
                ]
              ]
            ]
          ]
        ]
      ]
    ],
    [
      "stat",
      [
        "call",
        [
          "name",
          "test"
        ],
        [
          [
            "string",
            "no non-numbers"
          ],
          [
            "function",
            null,
            [],
            [
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "equal"
                  ],
                  [
                    [
                      "name",
                      "false"
                    ],
                    [
                      "call",
                      [
                        "name",
                        "validatePhone"
                      ],
                      [
                        [
                          "string",
                          "a"
                        ]
                      ]
                    ]
                  ]
                ]
              ],
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "equal"
                  ],
                  [
                    [
                      "name",
                      "false"
                    ],
                    [
                      "call",
                      [
                        "name",
                        "validatePhone"
                      ],
                      [
                        [
                          "string",
                          "?"
                        ]
                      ]
                    ]
                  ]
                ]
              ]
            ]
          ]
        ]
      ]
    ],
    [
      "stat",
      [
        "call",
        [
          "name",
          "test"
        ],
        [
          [
            "string",
            "must be in right grouping"
          ],
          [
            "function",
            null,
            [],
            [
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "equal"
                  ],
                  [
                    [
                      "name",
                      "false"
                    ],
                    [
                      "call",
                      [
                        "name",
                        "validatePhone"
                      ],
                      [
                        [
                          "string",
                          " 3 9 3"
                        ]
                      ]
                    ]
                  ]
                ]
              ],
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "equal"
                  ],
                  [
                    [
                      "name",
                      "false"
                    ],
                    [
                      "call",
                      [
                        "name",
                        "validatePhone"
                      ],
                      [
                        [
                          "string",
                          "12 23 2345 3 thuno"
                        ]
                      ]
                    ]
                  ]
                ]
              ]
            ]
          ]
        ]
      ]
    ],
    [
      "stat",
      [
        "call",
        [
          "name",
          "test"
        ],
        [
          [
            "string",
            "dashes are ok"
          ],
          [
            "function",
            null,
            [],
            [
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "ok"
                  ],
                  [
                    [
                      "call",
                      [
                        "name",
                        "validatePhone"
                      ],
                      [
                        [
                          "string",
                          "770-123-4567e"
                        ]
                      ]
                    ]
                  ]
                ]
              ]
            ]
          ]
        ]
      ]
    ]
  ]
]

Do the same for an example Jasmine test file

[
  "toplevel",
  [
    [
      "stat",
      [
        "call",
        [
          "name",
          "describe"
        ],
        [
          [
            "string",
            "hello"
          ],
          [
            "function",
            null,
            [],
            [
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "it"
                  ],
                  [
                    [
                      "string",
                      "should say hello"
                    ],
                    [
                      "function",
                      null,
                      [],
                      [
                        [
                          "stat",
                          [
                            "call",
                            [
                              "dot",
                              [
                                "call",
                                [
                                  "name",
                                  "expect"
                                ],
                                [
                                  [
                                    "call",
                                    [
                                      "name",
                                      "hello"
                                    ],
                                    []
                                  ]
                                ]
                              ],
                              "toBe"
                            ],
                            [
                              [
                                "string",
                                "hello world"
                              ]
                            ]
                          ]
                        ]
                      ]
                    ]
                  ]
                ]
              ],
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "it"
                  ],
                  [
                    [
                      "string",
                      "should not say not hello"
                    ],
                    [
                      "function",
                      null,
                      [],
                      [
                        [
                          "stat",
                          [
                            "call",
                            [
                              "dot",
                              [
                                "call",
                                [
                                  "name",
                                  "expect"
                                ],
                                [
                                  [
                                    "call",
                                    [
                                      "name",
                                      "hello"
                                    ],
                                    []
                                  ]
                                ]
                              ],
                              "toNotBe"
                            ],
                            [
                              [
                                "string",
                                "not hello world"
                              ]
                            ]
                          ]
                        ]
                      ]
                    ]
                  ]
                ]
              ],
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "it"
                  ],
                  [
                    [
                      "string",
                      "should throw exception"
                    ],
                    [
                      "function",
                      null,
                      [],
                      [
                        [
                          "throw",
                          [
                            "new",
                            [
                              "name",
                              "Error"
                            ],
                            [
                              [
                                "string",
                                "Crap"
                              ]
                            ]
                          ]
                        ]
                      ]
                    ]
                  ]
                ]
              ],
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "it"
                  ],
                  [
                    [
                      "string",
                      "should throw another exception"
                    ],
                    [
                      "function",
                      null,
                      [],
                      [
                        [
                          "throw",
                          [
                            "new",
                            [
                              "name",
                              "Error"
                            ],
                            [
                              [
                                "string",
                                "Blah"
                              ]
                            ]
                          ]
                        ]
                      ]
                    ]
                  ]
                ]
              ],
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "it"
                  ],
                  [
                    [
                      "string",
                      "should throw even more exceptions"
                    ],
                    [
                      "function",
                      null,
                      [],
                      [
                        [
                          "throw",
                          [
                            "new",
                            [
                              "name",
                              "Error"
                            ],
                            [
                              [
                                "string",
                                "Foobar"
                              ]
                            ]
                          ]
                        ]
                      ]
                    ]
                  ]
                ]
              ],
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "it"
                  ],
                  [
                    [
                      "string",
                      "should be firefox"
                    ],
                    [
                      "function",
                      null,
                      [],
                      [
                        [
                          "stat",
                          [
                            "call",
                            [
                              "dot",
                              [
                                "call",
                                [
                                  "name",
                                  "expect"
                                ],
                                [
                                  [
                                    "dot",
                                    [
                                      "name",
                                      "navigator"
                                    ],
                                    "userAgent"
                                  ]
                                ]
                              ],
                              "toMatch"
                            ],
                            [
                              [
                                "regexp",
                                "Firefox",
                                ""
                              ]
                            ]
                          ]
                        ]
                      ]
                    ]
                  ]
                ]
              ],
              [
                "stat",
                [
                  "call",
                  [
                    "name",
                    "it"
                  ],
                  [
                    [
                      "string",
                      "should do async"
                    ],
                    [
                      "function",
                      null,
                      [],
                      [
                        [
                          "stat",
                          [
                            "call",
                            [
                              "name",
                              "waits"
                            ],
                            [
                              [
                                "num",
                                1000
                              ]
                            ]
                          ]
                        ]
                      ]
                    ]
                  ]
                ]
              ]
            ]
          ]
        ]
      ]
    ]
  ]
]

List some common patterns for the AST for the QUnit example

"call",
[
  "name",
  "test"
],
[
  [
    "string",

,

"call",
[
  "name",
  "ok"
],

and

"call",
[
  "name",
  "equal"
],

List some common patterns for the AST for the Jasmine example

"call",
[
  "name",
  "describe"
],
[
  [
    "string",

,

"call",
[
  "name",
  "it"
],
[
  [
    "string",

and

"call",
[
  "name",
  "expect"
],

Traverse the ASTs and match against these patterns and count the occurances of all patterns, grouping by framework.

So, first a function to traverse the AST

function traverse(node, visit){
    visit(node)
    if (node instanceof Array){
        for (var i = 0, len = node.length; i < len; i++)
            traverse(node[i], visit)
    }
}

Write the pattern detectors as functions

var patterns = {
    jasmine: [
        function(node){
            return node[0] === 'call' &&
                node[1][0] === 'name' &&
                node[1][1] === 'describe' &&
                node[2][0][0] === 'string'
        },
        function(node){
            return node[0] === 'call' &&
                node[1][0] === 'name' &&
                node[1][1] === 'it' &&
                node[2][0][0] === 'string'
        },
        function(node){
            return node[0] === 'call' &&
                node[1][0] === 'name' &&
                node[1][1] === 'expect'
        }
    ],
    qunit: [
        function(node){
            return node[0] === 'call' &&
                node[1][0] === 'name' &&
                node[1][1] === 'test' &&
                node[2][0][0] === 'string'
        },
        function(node){
            return node[0] === 'call' &&
                node[1][0] === 'name' &&
                node[1][1] === 'ok'
        },
        function(node){
            return node[0] === 'call' &&
                node[1][0] === 'name' &&
                node[1][1] === 'equal'
        }
    ]
}

Keep a tally

var tally = {
    jasmine: 0,
    qunit: 0
}

Now actually traverse the AST, try to match the patterns and count the number of hits

function patternMatches(node, test){
    try{
        return test(node)
    }catch(e){
        return false
    }
}

function detectPatterns(node){
    for (var framework in patterns){
        patterns[framework].forEach(function(pat, idx){
            if (patternMatches(node, pat)){
                tally[framework]++
            }
        })
    }
}

traverse(ast, detectPatterns)

How many hits did we get for QUnit vs Jasmine?

for (var key in tally){
    console.log(tally[key] + ' points for ' + key + '.')
}

Is that your final answer?

console.log('Tests were written in ' + (tally.qunit > tally.jasmine ?
    'QUnit': 'Jasmine') + '.')

Sample output

airportyh$ node detect.js tests.js
0 points for jasmine.
10 points for qunit.
Tests were written in QUnit.
airportyh$ node detect.js specs.js
11 points for jasmine.
0 points for qunit.
Tests were written in Jasmine.

Full script at this gist.

blog comments powered by Disqus