Skip to content
This repository was archived by the owner on Jan 19, 2019. It is now read-only.

Commit 5e22fac

Browse files
authored
Chore: AST alignment testing against Babylon (#342)
1 parent 34aaa71 commit 5e22fac

File tree

5 files changed

+269
-0
lines changed

5 files changed

+269
-0
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@
1818
},
1919
"license": "BSD-2-Clause",
2020
"devDependencies": {
21+
"babel-code-frame": "^6.22.0",
22+
"babylon": "^7.0.0-beta.16",
2123
"eslint": "3.19.0",
2224
"eslint-config-eslint": "4.0.0",
2325
"eslint-plugin-node": "4.2.2",
2426
"eslint-release": "0.10.3",
27+
"glob": "^7.1.2",
2528
"jest": "20.0.4",
29+
"lodash.isplainobject": "^4.0.6",
2630
"npm-license": "0.3.3",
2731
"shelljs": "0.7.7",
2832
"shelljs-nodecli": "0.1.1",
@@ -40,6 +44,7 @@
4044
"scripts": {
4145
"test": "node Makefile.js test",
4246
"jest": "jest",
47+
"ast-alignment-tests": "jest --config={} ./tests/ast-alignment/spec.js",
4348
"lint": "node Makefile.js lint",
4449
"release": "eslint-release",
4550
"ci-release": "eslint-ci-release",

tests/ast-alignment/.eslintrc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
env:
2+
jest: true

tests/ast-alignment/parse.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"use strict";
2+
3+
const codeFrame = require("babel-code-frame");
4+
const parseUtils = require("./utils");
5+
6+
function createError(message, line, column) { // eslint-disable-line
7+
// Construct an error similar to the ones thrown by Babylon.
8+
const error = new SyntaxError(`${message} (${line}:${column})`);
9+
error.loc = {
10+
line,
11+
column
12+
};
13+
return error;
14+
}
15+
16+
function parseWithBabylonPluginTypescript(text) { // eslint-disable-line
17+
const babylon = require("babylon");
18+
return babylon.parse(text, {
19+
sourceType: "script",
20+
allowImportExportEverywhere: true,
21+
allowReturnOutsideFunction: true,
22+
ranges: true,
23+
plugins: [
24+
"jsx",
25+
"typescript",
26+
"doExpressions",
27+
"objectRestSpread",
28+
"decorators",
29+
"classProperties",
30+
"exportExtensions",
31+
"asyncGenerators",
32+
"functionBind",
33+
"functionSent",
34+
"dynamicImport",
35+
"numericSeparator",
36+
"estree"
37+
]
38+
});
39+
}
40+
41+
function parseWithTypeScriptESLintParser(text) { // eslint-disable-line
42+
const parser = require("../../parser");
43+
try {
44+
return parser.parse(text, {
45+
loc: true,
46+
range: true,
47+
tokens: false,
48+
comment: false,
49+
ecmaFeatures: {
50+
jsx: true
51+
}
52+
});
53+
} catch (e) {
54+
throw createError(
55+
e.message,
56+
e.lineNumber,
57+
e.column
58+
);
59+
}
60+
}
61+
62+
module.exports = function parse(text, opts) {
63+
64+
let parseFunction;
65+
66+
switch (opts.parser) {
67+
case "typescript-eslint-parser":
68+
parseFunction = parseWithTypeScriptESLintParser;
69+
break;
70+
case "babylon-plugin-typescript":
71+
parseFunction = parseWithBabylonPluginTypescript;
72+
break;
73+
default:
74+
throw new Error("Please provide a valid parser: either \"typescript-eslint-parser\" or \"babylon-plugin-typescript\"");
75+
}
76+
77+
try {
78+
return parseUtils.normalizeNodeTypes(parseFunction(text));
79+
} catch (error) {
80+
const loc = error.loc;
81+
if (loc) {
82+
error.codeFrame = codeFrame(text, loc.line, loc.column + 1, {
83+
highlightCode: true
84+
});
85+
error.message += `\n${error.codeFrame}`;
86+
}
87+
throw error;
88+
}
89+
90+
};

tests/ast-alignment/spec.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"use strict";
2+
3+
const fs = require("fs");
4+
const path = require("path");
5+
const glob = require("glob");
6+
const parse = require("./parse");
7+
const parseUtils = require("./utils");
8+
9+
const fixturesDirPath = path.join(__dirname, "../fixtures");
10+
const fixturePatternsToTest = [
11+
"basics/instanceof.src.js",
12+
"basics/update-expression.src.js",
13+
"basics/new-without-parens.src.js",
14+
"ecma-features/arrowFunctions/**/as*.src.js"
15+
];
16+
17+
const fixturesToTest = [];
18+
19+
fixturePatternsToTest.forEach(fixturePattern => {
20+
const matchingFixtures = glob.sync(`${fixturesDirPath}/${fixturePattern}`, {});
21+
matchingFixtures.forEach(filename => fixturesToTest.push(filename));
22+
});
23+
24+
/**
25+
* - Babylon wraps the "Program" node in an extra "File" node, normalize this for simplicity for now...
26+
* - Remove "start" and "end" values from Babylon nodes to reduce unimportant noise in diffs ("loc" data will still be in
27+
* each final AST and compared).
28+
*
29+
* @param {Object} ast raw babylon AST
30+
* @returns {Object} processed babylon AST
31+
*/
32+
function preprocessBabylonAST(ast) {
33+
return parseUtils.omitDeep(ast.program, [
34+
{
35+
key: "start",
36+
predicate(val) {
37+
// only remove the "start" number (not the "start" object within loc)
38+
return typeof val === "number";
39+
}
40+
},
41+
{
42+
key: "end",
43+
predicate(val) {
44+
// only remove the "end" number (not the "end" object within loc)
45+
return typeof val === "number";
46+
}
47+
},
48+
{
49+
key: "identifierName",
50+
predicate() {
51+
return true;
52+
}
53+
},
54+
{
55+
key: "extra",
56+
predicate() {
57+
return true;
58+
}
59+
},
60+
{
61+
key: "directives",
62+
predicate() {
63+
return true;
64+
}
65+
},
66+
{
67+
key: "innerComments",
68+
predicate() {
69+
return true;
70+
}
71+
},
72+
{
73+
key: "leadingComments",
74+
predicate() {
75+
return true;
76+
}
77+
}
78+
]);
79+
}
80+
81+
fixturesToTest.forEach(filename => {
82+
83+
const source = fs.readFileSync(filename, "utf8").replace(/\r\n/g, "\n");
84+
85+
/**
86+
* Parse with typescript-eslint-parser
87+
*/
88+
const typeScriptESLintParserAST = parse(source, {
89+
parser: "typescript-eslint-parser"
90+
});
91+
92+
/**
93+
* Parse the source with babylon typescript-plugin, and perform some extra formatting steps
94+
*/
95+
const babylonTypeScriptPluginAST = preprocessBabylonAST(parse(source, {
96+
parser: "babylon-plugin-typescript"
97+
}));
98+
99+
/**
100+
* Assert the two ASTs match
101+
*/
102+
test(`${filename}`, () => {
103+
expect(babylonTypeScriptPluginAST).toEqual(typeScriptESLintParserAST);
104+
});
105+
106+
});

tests/ast-alignment/utils.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use strict";
2+
3+
const isPlainObject = require("lodash.isplainobject");
4+
5+
/**
6+
* By default, pretty-format (within Jest matchers) retains the names/types of nodes from the babylon AST,
7+
* quick and dirty way to avoid that is to JSON.stringify and then JSON.parser the
8+
* ASTs before comparing them with pretty-format
9+
*
10+
* @param {Object} ast raw AST
11+
* @returns {Object} normalized AST
12+
*/
13+
function normalizeNodeTypes(ast) {
14+
return JSON.parse(JSON.stringify(ast));
15+
}
16+
17+
/**
18+
* Removes the given keys from the given AST object recursively
19+
* @param {Object} obj A JavaScript object to remove keys from
20+
* @param {Object[]} keysToOmit Names and predicate functions use to determine what keys to omit from the final object
21+
* @returns {Object} formatted object
22+
*/
23+
function omitDeep(obj, keysToOmit) {
24+
keysToOmit = keysToOmit || [];
25+
function shouldOmit(keyName, val) { // eslint-disable-line
26+
if (!keysToOmit || !keysToOmit.length) {
27+
return false;
28+
}
29+
for (const keyConfig of keysToOmit) {
30+
if (keyConfig.key !== keyName) {
31+
continue;
32+
}
33+
return keyConfig.predicate(val);
34+
}
35+
return false;
36+
}
37+
for (const key in obj) {
38+
if (!obj.hasOwnProperty(key)) {
39+
continue;
40+
}
41+
const val = obj[key];
42+
if (isPlainObject(val)) {
43+
if (shouldOmit(key, val)) {
44+
delete obj[key];
45+
break;
46+
}
47+
omitDeep(val, keysToOmit);
48+
} else if (Array.isArray(val)) {
49+
if (shouldOmit(key, val)) {
50+
delete obj[key];
51+
break;
52+
}
53+
for (const i of val) {
54+
omitDeep(i, keysToOmit);
55+
}
56+
} else if (shouldOmit(key, val)) {
57+
delete obj[key];
58+
}
59+
}
60+
return obj;
61+
}
62+
63+
module.exports = {
64+
normalizeNodeTypes,
65+
omitDeep
66+
};

0 commit comments

Comments
 (0)