From 53da9f457e1320318be0ae589e03c6b611b764e4 Mon Sep 17 00:00:00 2001 From: James Henry Date: Fri, 21 Jul 2017 13:21:03 +0100 Subject: [PATCH 1/2] WIP --- package.json | 5 ++ tests/ast-alignment/.eslintrc.yml | 2 + tests/ast-alignment/parse.js | 90 +++++++++++++++++++++++++ tests/ast-alignment/spec.js | 106 ++++++++++++++++++++++++++++++ tests/ast-alignment/utils.js | 66 +++++++++++++++++++ 5 files changed, 269 insertions(+) create mode 100644 tests/ast-alignment/.eslintrc.yml create mode 100644 tests/ast-alignment/parse.js create mode 100644 tests/ast-alignment/spec.js create mode 100644 tests/ast-alignment/utils.js diff --git a/package.json b/package.json index a5a8685..504e1eb 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,15 @@ }, "license": "BSD-2-Clause", "devDependencies": { + "babel-code-frame": "^6.22.0", + "babylon": "^7.0.0-beta.16", "eslint": "3.19.0", "eslint-config-eslint": "4.0.0", "eslint-plugin-node": "4.2.2", "eslint-release": "0.10.3", + "glob": "^7.1.2", "jest": "20.0.4", + "lodash.isplainobject": "^4.0.6", "npm-license": "0.3.3", "shelljs": "0.7.7", "shelljs-nodecli": "0.1.1", @@ -40,6 +44,7 @@ "scripts": { "test": "node Makefile.js test", "jest": "jest", + "ast-alignment-tests": "jest --config={} ./tests/ast-alignment/spec.js", "lint": "node Makefile.js lint", "release": "eslint-release", "ci-release": "eslint-ci-release", diff --git a/tests/ast-alignment/.eslintrc.yml b/tests/ast-alignment/.eslintrc.yml new file mode 100644 index 0000000..e19b2cf --- /dev/null +++ b/tests/ast-alignment/.eslintrc.yml @@ -0,0 +1,2 @@ +env: + jest: true diff --git a/tests/ast-alignment/parse.js b/tests/ast-alignment/parse.js new file mode 100644 index 0000000..2aca312 --- /dev/null +++ b/tests/ast-alignment/parse.js @@ -0,0 +1,90 @@ +"use strict"; + +const codeFrame = require("babel-code-frame"); +const parseUtils = require("./utils"); + +function createError(message, line, column) { // eslint-disable-line + // Construct an error similar to the ones thrown by Babylon. + const error = new SyntaxError(`${message} (${line}:${column})`); + error.loc = { + line, + column + }; + return error; +} + +function parseWithBabylonPluginTypescript(text) { // eslint-disable-line + const babylon = require("babylon"); + return babylon.parse(text, { + sourceType: "script", + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + ranges: true, + plugins: [ + "jsx", + "typescript", + "doExpressions", + "objectRestSpread", + "decorators", + "classProperties", + "exportExtensions", + "asyncGenerators", + "functionBind", + "functionSent", + "dynamicImport", + "numericSeparator", + "estree" + ] + }); +} + +function parseWithTypeScriptESLintParser(text) { // eslint-disable-line + const parser = require("../../parser"); + try { + return parser.parse(text, { + loc: true, + range: true, + tokens: false, + comment: false, + ecmaFeatures: { + jsx: true + } + }); + } catch (e) { + throw createError( + e.message, + e.lineNumber, + e.column + ); + } +} + +module.exports = function parse(text, opts) { + + let parseFunction; + + switch (opts.parser) { + case "typescript-eslint-parser": + parseFunction = parseWithTypeScriptESLintParser; + break; + case "babylon-plugin-typescript": + parseFunction = parseWithBabylonPluginTypescript; + break; + default: + throw new Error("Please provide a valid parser: either \"typescript-eslint-parser\" or \"babylon-plugin-typescript\""); + } + + try { + return parseUtils.normalizeNodeTypes(parseFunction(text)); + } catch (error) { + const loc = error.loc; + if (loc) { + error.codeFrame = codeFrame(text, loc.line, loc.column + 1, { + highlightCode: true + }); + error.message += `\n${error.codeFrame}`; + } + throw error; + } + +}; diff --git a/tests/ast-alignment/spec.js b/tests/ast-alignment/spec.js new file mode 100644 index 0000000..0e59a7a --- /dev/null +++ b/tests/ast-alignment/spec.js @@ -0,0 +1,106 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const glob = require("glob"); +const parse = require("./parse"); +const parseUtils = require("./utils"); + +const fixturesDirPath = path.join(__dirname, "../fixtures"); +const fixturePatternsToTest = [ + "basics/instanceof.src.js", + "basics/update-expression.src.js", + "basics/new-without-parens.src.js", + "ecma-features/arrowFunctions/**/as*.src.js" +]; + +const fixturesToTest = []; + +fixturePatternsToTest.forEach(fixturePattern => { + const matchingFixtures = glob.sync(`${fixturesDirPath}/${fixturePattern}`, {}); + matchingFixtures.forEach(filename => fixturesToTest.push(filename)); +}); + +/** + * - Babylon wraps the "Program" node in an extra "File" node, normalize this for simplicity for now... + * - Remove "start" and "end" values from Babylon nodes to reduce unimportant noise in diffs ("loc" data will still be in + * each final AST and compared). + * + * @param {Object} ast raw babylon AST + * @returns {Object} processed babylon AST + */ +function preprocessBabylonAST(ast) { + return parseUtils.omitDeep(ast.program, [ + { + key: "start", + predicate(val) { + // only return the "start" number (not the "start" object within loc) + return typeof val === "number"; + } + }, + { + key: "end", + predicate(val) { + // only return the "end" number (not the "end" object within loc) + return typeof val === "number"; + } + }, + { + key: "identifierName", + predicate() { + return true; + } + }, + { + key: "extra", + predicate() { + return true; + } + }, + { + key: "directives", + predicate() { + return true; + } + }, + { + key: "innerComments", + predicate() { + return true; + } + }, + { + key: "leadingComments", + predicate() { + return true; + } + } + ]); +} + +fixturesToTest.forEach(filename => { + + const source = fs.readFileSync(filename, "utf8").replace(/\r\n/g, "\n"); + + /** + * Parse with typescript-eslint-parser + */ + const typeScriptESLintParserAST = parse(source, { + parser: "typescript-eslint-parser" + }); + + /** + * Parse the source with babylon typescript-plugin, and perform some extra formatting steps + */ + const babylonTypeScriptPluginAST = preprocessBabylonAST(parse(source, { + parser: "babylon-plugin-typescript" + })); + + /** + * Assert the two ASTs match + */ + test(`${filename}`, () => { + expect(babylonTypeScriptPluginAST).toEqual(typeScriptESLintParserAST); + }); + +}); diff --git a/tests/ast-alignment/utils.js b/tests/ast-alignment/utils.js new file mode 100644 index 0000000..e9a52a2 --- /dev/null +++ b/tests/ast-alignment/utils.js @@ -0,0 +1,66 @@ +"use strict"; + +const isPlainObject = require("lodash.isplainobject"); + +/** + * By default, pretty-format (within Jest matchers) retains the names/types of nodes from the babylon AST, + * quick and dirty way to avoid that is to JSON.stringify and then JSON.parser the + * ASTs before comparing them with pretty-format + * + * @param {Object} ast raw AST + * @returns {Object} normalized AST + */ +function normalizeNodeTypes(ast) { + return JSON.parse(JSON.stringify(ast)); +} + +/** + * Removes the given keys from the given AST object recursively + * @param {Object} obj A JavaScript object to remove keys from + * @param {Object[]} keysToOmit Names and predicate functions use to determine what keys to omit from the final object + * @returns {Object} formatted object + */ +function omitDeep(obj, keysToOmit) { + keysToOmit = keysToOmit || []; + function shouldOmit(keyName, val) { // eslint-disable-line + if (!keysToOmit || !keysToOmit.length) { + return false; + } + for (const keyConfig of keysToOmit) { + if (keyConfig.key !== keyName) { + continue; + } + return keyConfig.predicate(val); + } + return false; + } + for (const key in obj) { + if (!obj.hasOwnProperty(key)) { + continue; + } + const val = obj[key]; + if (isPlainObject(val)) { + if (shouldOmit(key, val)) { + delete obj[key]; + break; + } + omitDeep(val, keysToOmit); + } else if (Array.isArray(val)) { + if (shouldOmit(key, val)) { + delete obj[key]; + break; + } + for (const i of val) { + omitDeep(i, keysToOmit); + } + } else if (shouldOmit(key, val)) { + delete obj[key]; + } + } + return obj; +} + +module.exports = { + normalizeNodeTypes, + omitDeep +}; From ee4c113d27a72d57a1c1a2037ca8f05d112954db Mon Sep 17 00:00:00 2001 From: James Henry Date: Fri, 21 Jul 2017 13:42:09 +0100 Subject: [PATCH 2/2] fix typo --- tests/ast-alignment/spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ast-alignment/spec.js b/tests/ast-alignment/spec.js index 0e59a7a..44bd4a0 100644 --- a/tests/ast-alignment/spec.js +++ b/tests/ast-alignment/spec.js @@ -34,14 +34,14 @@ function preprocessBabylonAST(ast) { { key: "start", predicate(val) { - // only return the "start" number (not the "start" object within loc) + // only remove the "start" number (not the "start" object within loc) return typeof val === "number"; } }, { key: "end", predicate(val) { - // only return the "end" number (not the "end" object within loc) + // only remove the "end" number (not the "end" object within loc) return typeof val === "number"; } },