From 3c1081667ec6730c5c55692b8962dc63a449b48a Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Wed, 26 Sep 2018 15:18:20 -0700 Subject: [PATCH 01/30] feat: use cwd to find tsconfig and build program --- lib/ast-converter.js | 3 + lib/convert.js | 9 +++ lib/tsconfig-parser.js | 67 ++++++++++++++++++++++ parser.js | 104 +++++++++++++++++++---------------- tests/ast-alignment/parse.js | 2 +- tests/lib/parse.js | 4 +- tools/test-utils.js | 2 +- 7 files changed, 140 insertions(+), 51 deletions(-) create mode 100644 lib/tsconfig-parser.js diff --git a/lib/ast-converter.js b/lib/ast-converter.js index 924a98e..626c07b 100644 --- a/lib/ast-converter.js +++ b/lib/ast-converter.js @@ -75,3 +75,6 @@ module.exports = (ast, extra) => { return estree; }; + +module.exports.tsNodeToESTreeNodeMap = convert.tsNodeToESTreeNodeMap; +module.exports.esTreeNodeToTSNodeMap = convert.esTreeNodeToTSNodeMap; diff --git a/lib/convert.js b/lib/convert.js index fc49c3d..fc4afff 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -25,6 +25,9 @@ const SyntaxKind = nodeUtils.SyntaxKind; // Public //------------------------------------------------------------------------------ +const esTreeNodeToTSNodeMap = new WeakMap(); +const tsNodeToESTreeNodeMap = new WeakMap(); + /** * Converts a TypeScript node into an ESTree node * @param {Object} config configuration options for the conversion @@ -2414,5 +2417,11 @@ module.exports = function convert(config) { deeplyCopy(); } + tsNodeToESTreeNodeMap.set(node, result); + esTreeNodeToTSNodeMap.set(result, node); + return result; }; + +module.exports.tsNodeToESTreeNodeMap = tsNodeToESTreeNodeMap; +module.exports.esTreeNodeToTSNodeMap = esTreeNodeToTSNodeMap; diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js new file mode 100644 index 0000000..00a3f7a --- /dev/null +++ b/lib/tsconfig-parser.js @@ -0,0 +1,67 @@ +"use strict"; + +const path = require("path"); +const fs = require("fs"); +const ts = require("typescript"); + +//------------------------------------------------------------------------------ +// Environment calculation +//------------------------------------------------------------------------------ + +/** + * Create object representation of TypeScript configuration + * @param {string} tsconfigPath Full path to tsconfig.json + * @returns {{options: Object, fileNames: string[]}|null} Representation of parsed tsconfig.json + */ +function parseTSConfig(tsconfigPath) { + // if no tsconfig in cwd, return + if (!fs.existsSync(tsconfigPath)) { + return undefined; + } + + // Parse tsconfig and create program + let tsconfigContents; + try { + tsconfigContents = fs.readFileSync(tsconfigPath, "utf8"); + } catch (e) { + // if can't read file, return + return undefined; + } + + const tsconfigParseResult = ts.parseJsonText( + tsconfigPath, + tsconfigContents + ); + + return ts.parseJsonConfigFileContent( + tsconfigParseResult, + ts.sys, + path.dirname(tsconfigPath), + /* existingOptions */ {}, + tsconfigPath + ); +} + +/** + * Calculate project environment using options provided by eslint + * @param {Object} options Options provided by ESLint core + * @param {string} options.cwd The current working directory for the eslint process + * @returns {ts.Program|undefined} The program defined by the tsconfig.json in the cwd, or null if something went wrong + */ +module.exports = function calculateProjectParserOptions(options) { + // if no cwd passed, return + const cwd = options.cwd || process.cwd(); + + const tsconfigPath = path.join(cwd, "tsconfig.json"); + const parsedCommandLine = parseTSConfig(tsconfigPath); + + if (parsedCommandLine === null) { + return undefined; + } + + return ts.createProgram( + parsedCommandLine.fileNames, + parsedCommandLine.options, + ts.createCompilerHost(parsedCommandLine.options, /* setParentNodes */ true) + ); +}; diff --git a/parser.js b/parser.js index 033bec8..191dd77 100644 --- a/parser.js +++ b/parser.js @@ -11,7 +11,8 @@ const astNodeTypes = require('./lib/ast-node-types'), ts = require('typescript'), convert = require('./lib/ast-converter'), - semver = require('semver'); + semver = require('semver'), + calculateProjectParserOptions = require("./lib/tsconfig-parser"); const SUPPORTED_TYPESCRIPT_VERSIONS = require('./package.json').devDependencies .typescript; @@ -40,8 +41,8 @@ function resetExtra() { strict: false, ecmaFeatures: {}, useJSXTextNode: false, - log: console.log - }; + log: console.log, + project: false } //------------------------------------------------------------------------------ @@ -107,6 +108,10 @@ function generateAST(code, options) { } else if (options.loggerFn === false) { extra.log = Function.prototype; } + + if (typeof options.project === "boolean") { + extra.project = options.project; + } } if (!isRunningSupportedTypeScriptVersion && !warnedAboutTSVersion) { @@ -124,56 +129,60 @@ function generateAST(code, options) { warnedAboutTSVersion = true; } - // Even if jsx option is set in typescript compiler, filename still has to - // contain .tsx file extension - const FILENAME = extra.ecmaFeatures.jsx ? 'estree.tsx' : 'estree.ts'; - - const compilerHost = { - fileExists() { - return true; - }, - getCanonicalFileName() { - return FILENAME; - }, - getCurrentDirectory() { - return ''; - }, - getDefaultLibFileName() { - return 'lib.d.ts'; - }, - - // TODO: Support Windows CRLF - getNewLine() { - return '\n'; - }, - getSourceFile(filename) { - return ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true); - }, - readFile() { - return null; - }, - useCaseSensitiveFileNames() { - return true; - }, - writeFile() { - return null; - } - }; + let FILENAME, program; + if (extra.project) { + FILENAME = options.filePath; + program = calculateProjectParserOptions(options); + } - const program = ts.createProgram( - [FILENAME], - { + if (program === undefined) { + // Even if jsx option is set in typescript compiler, filename still has to + // contain .tsx file extension + const FILENAME = (extra.ecmaFeatures.jsx) ? "estree.tsx" : "estree.ts"; + + const compilerHost = { + fileExists() { + return true; + }, + getCanonicalFileName() { + return FILENAME; + }, + getCurrentDirectory() { + return ""; + }, + getDefaultLibFileName() { + return "lib.d.ts"; + }, + + // TODO: Support Windows CRLF + getNewLine() { + return "\n"; + }, + getSourceFile(filename) { + return ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true); + }, + readFile() { + return null; + }, + useCaseSensitiveFileNames() { + return true; + }, + writeFile() { + return null; + } + }; + + program = ts.createProgram([FILENAME], { noResolve: true, target: ts.ScriptTarget.Latest, - jsx: extra.ecmaFeatures.jsx ? 'preserve' : undefined - }, - compilerHost - ); + jsx: extra.ecmaFeatures.jsx ? "preserve" : undefined + }, compilerHost); + } const ast = program.getSourceFile(FILENAME); extra.code = code; - return convert(ast, extra); + return { ast: convert(ast, extra), program: extra.project ? program : undefined }; } //------------------------------------------------------------------------------ @@ -183,7 +192,8 @@ function generateAST(code, options) { exports.version = require('./package.json').version; exports.parse = function parse(code, options) { - return generateAST(code, options); + const result = generateAST(code, options); + return { ast: result.ast, services: { program: result.program, esTreeNodeToTSNodeMap: convert.esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap: convert.tsNodeToESTreeNodeMap}} }; exports.AST_NODE_TYPES = astNodeTypes; diff --git a/tests/ast-alignment/parse.js b/tests/ast-alignment/parse.js index 7ff7639..6fc895f 100644 --- a/tests/ast-alignment/parse.js +++ b/tests/ast-alignment/parse.js @@ -60,7 +60,7 @@ function parseWithTypeScriptESTree(text, parserOptions) { }, parserOptions ) - ); + ).ast; } catch (e) { throw createError(e.message, e.lineNumber, e.column); } diff --git a/tests/lib/parse.js b/tests/lib/parse.js index f9f2ef5..8482c6b 100644 --- a/tests/lib/parse.js +++ b/tests/lib/parse.js @@ -22,8 +22,8 @@ const parser = require('../../parser'), describe('parse()', () => { describe('basic functionality', () => { it('should parse an empty string', () => { - expect(parser.parse('').body).toEqual([]); - expect(parser.parse('', {}).body).toEqual([]); + expect(parser.parse('').ast.body).toEqual([]); + expect(parser.parse('', {}).ast.body).toEqual([]); }); }); diff --git a/tools/test-utils.js b/tools/test-utils.js index 57fe8c7..d12f19c 100644 --- a/tools/test-utils.js +++ b/tools/test-utils.js @@ -46,7 +46,7 @@ function createSnapshotTestBlock(code, config) { * @returns {Object} the AST object */ function parse() { - const ast = parser.parse(code, config); + const ast = parser.parse(code, config).ast; return getRaw(ast); } From 1697aed8b8e47fa559a341c469e4ad0e8e21ce47 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 28 Sep 2018 15:14:04 -0700 Subject: [PATCH 02/30] fix: use text read by eslint --- lib/tsconfig-parser.js | 12 ++++++++++-- parser.js | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index 00a3f7a..5237940 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -44,11 +44,12 @@ function parseTSConfig(tsconfigPath) { /** * Calculate project environment using options provided by eslint + * @param {string} code The code being linted * @param {Object} options Options provided by ESLint core * @param {string} options.cwd The current working directory for the eslint process * @returns {ts.Program|undefined} The program defined by the tsconfig.json in the cwd, or null if something went wrong */ -module.exports = function calculateProjectParserOptions(options) { +module.exports = function calculateProjectParserOptions(code, options) { // if no cwd passed, return const cwd = options.cwd || process.cwd(); @@ -59,9 +60,16 @@ module.exports = function calculateProjectParserOptions(options) { return undefined; } + const compilerHost = ts.createCompilerHost(parsedCommandLine.options, /* setParentNodes */ true); + const oldGetSourceFile = compilerHost.getSourceFile; + compilerHost.getSourceFile = (filename, languageVersion, onError, shouldCreateNewFile) => + path.normalize(filename) === path.normalize(options.filePath) + ? ts.createSourceFile(filename, code, languageVersion, true) + : oldGetSourceFile(filename, languageVersion, onError, shouldCreateNewFile); + return ts.createProgram( parsedCommandLine.fileNames, parsedCommandLine.options, - ts.createCompilerHost(parsedCommandLine.options, /* setParentNodes */ true) + compilerHost ); }; diff --git a/parser.js b/parser.js index 191dd77..5902bc2 100644 --- a/parser.js +++ b/parser.js @@ -43,6 +43,7 @@ function resetExtra() { useJSXTextNode: false, log: console.log, project: false + } } //------------------------------------------------------------------------------ From adc6724620277525b07bf659eb66bee129f59efb Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Wed, 3 Oct 2018 14:44:56 -0700 Subject: [PATCH 03/30] fix: ensure ast maps are unique to each parse result --- lib/ast-converter.js | 18 +++++++++++++----- lib/convert.js | 17 +++++++++++++---- parser.js | 5 +++-- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/ast-converter.js b/lib/ast-converter.js index 626c07b..7e2c9a0 100644 --- a/lib/ast-converter.js +++ b/lib/ast-converter.js @@ -66,15 +66,23 @@ module.exports = (ast, extra) => { estree.tokens = nodeUtils.convertTokens(ast); } + /** + * Optionally convert and include all tokens in the AST + */ + if (extra.tokens) { + estree.tokens = nodeUtils.convertTokens(ast); + } + /** * Optionally convert and include all comments in the AST */ if (extra.comment) { - estree.comments = convertComments(ast, extra.code); + estree.comments = convertComments(ast, extra.code); } - return estree; -}; + const astMaps = convert.getASTMaps(); + convert.resetASTMaps(); -module.exports.tsNodeToESTreeNodeMap = convert.tsNodeToESTreeNodeMap; -module.exports.esTreeNodeToTSNodeMap = convert.esTreeNodeToTSNodeMap; + return { estree, astMaps }; + +}; diff --git a/lib/convert.js b/lib/convert.js index fc4afff..0c4c25d 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -25,8 +25,17 @@ const SyntaxKind = nodeUtils.SyntaxKind; // Public //------------------------------------------------------------------------------ -const esTreeNodeToTSNodeMap = new WeakMap(); -const tsNodeToESTreeNodeMap = new WeakMap(); +let esTreeNodeToTSNodeMap = new WeakMap(); +let tsNodeToESTreeNodeMap = new WeakMap(); + +function resetASTMaps() { + esTreeNodeToTSNodeMap = new WeakMap(); + tsNodeToESTreeNodeMap = new WeakMap(); +} + +function getASTMaps() { + return { esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap }; +} /** * Converts a TypeScript node into an ESTree node @@ -2423,5 +2432,5 @@ module.exports = function convert(config) { return result; }; -module.exports.tsNodeToESTreeNodeMap = tsNodeToESTreeNodeMap; -module.exports.esTreeNodeToTSNodeMap = esTreeNodeToTSNodeMap; +module.exports.getASTMaps = getASTMaps; +module.exports.resetASTMaps = resetASTMaps; diff --git a/parser.js b/parser.js index 5902bc2..e3516a5 100644 --- a/parser.js +++ b/parser.js @@ -183,7 +183,8 @@ function generateAST(code, options) { const ast = program.getSourceFile(FILENAME); extra.code = code; - return { ast: convert(ast, extra), program: extra.project ? program : undefined }; + const { estree, astMaps } = convert(ast, extra); + return { estree, program: extra.project ? program : undefined, astMaps }; } //------------------------------------------------------------------------------ @@ -194,7 +195,7 @@ exports.version = require('./package.json').version; exports.parse = function parse(code, options) { const result = generateAST(code, options); - return { ast: result.ast, services: { program: result.program, esTreeNodeToTSNodeMap: convert.esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap: convert.tsNodeToESTreeNodeMap}} + return { ast: result.estree, services: { program: result.program, esTreeNodeToTSNodeMap: result.astMaps.esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap: result.astMaps.tsNodeToESTreeNodeMap}} }; exports.AST_NODE_TYPES = astNodeTypes; From 459db21f896fc1d281c974937e7bbd215693cb70 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 4 Oct 2018 09:14:09 -0700 Subject: [PATCH 04/30] fix: handle files not included in ts project more robustly --- lib/tsconfig-parser.js | 102 ++++++++++++++++++++++------------------- parser.js | 55 ++++++++++++++-------- 2 files changed, 92 insertions(+), 65 deletions(-) diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index 5237940..e46d70f 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -1,8 +1,8 @@ -"use strict"; +'use strict'; -const path = require("path"); -const fs = require("fs"); -const ts = require("typescript"); +const path = require('path'); +const fs = require('fs'); +const ts = require('typescript'); //------------------------------------------------------------------------------ // Environment calculation @@ -14,32 +14,29 @@ const ts = require("typescript"); * @returns {{options: Object, fileNames: string[]}|null} Representation of parsed tsconfig.json */ function parseTSConfig(tsconfigPath) { - // if no tsconfig in cwd, return - if (!fs.existsSync(tsconfigPath)) { - return undefined; - } + // if no tsconfig in cwd, return + if (!fs.existsSync(tsconfigPath)) { + return undefined; + } - // Parse tsconfig and create program - let tsconfigContents; - try { - tsconfigContents = fs.readFileSync(tsconfigPath, "utf8"); - } catch (e) { - // if can't read file, return - return undefined; - } + // Parse tsconfig and create program + let tsconfigContents; + try { + tsconfigContents = fs.readFileSync(tsconfigPath, 'utf8'); + } catch (e) { + // if can't read file, return + return undefined; + } - const tsconfigParseResult = ts.parseJsonText( - tsconfigPath, - tsconfigContents - ); + const tsconfigParseResult = ts.parseJsonText(tsconfigPath, tsconfigContents); - return ts.parseJsonConfigFileContent( - tsconfigParseResult, - ts.sys, - path.dirname(tsconfigPath), - /* existingOptions */ {}, - tsconfigPath - ); + return ts.parseJsonConfigFileContent( + tsconfigParseResult, + ts.sys, + path.dirname(tsconfigPath), + /* existingOptions */ {}, + tsconfigPath + ); } /** @@ -47,29 +44,42 @@ function parseTSConfig(tsconfigPath) { * @param {string} code The code being linted * @param {Object} options Options provided by ESLint core * @param {string} options.cwd The current working directory for the eslint process - * @returns {ts.Program|undefined} The program defined by the tsconfig.json in the cwd, or null if something went wrong + * @returns {ts.Program|undefined} The program defined by the tsconfig.json in the cwd, or undefined if something went wrong */ module.exports = function calculateProjectParserOptions(code, options) { - // if no cwd passed, return - const cwd = options.cwd || process.cwd(); + // if no cwd passed, return + const cwd = options.cwd || process.cwd(); - const tsconfigPath = path.join(cwd, "tsconfig.json"); - const parsedCommandLine = parseTSConfig(tsconfigPath); + const tsconfigPath = path.join(cwd, 'tsconfig.json'); + const parsedCommandLine = parseTSConfig(tsconfigPath); - if (parsedCommandLine === null) { - return undefined; - } + if (parsedCommandLine === null) { + return undefined; + } - const compilerHost = ts.createCompilerHost(parsedCommandLine.options, /* setParentNodes */ true); - const oldGetSourceFile = compilerHost.getSourceFile; - compilerHost.getSourceFile = (filename, languageVersion, onError, shouldCreateNewFile) => - path.normalize(filename) === path.normalize(options.filePath) - ? ts.createSourceFile(filename, code, languageVersion, true) - : oldGetSourceFile(filename, languageVersion, onError, shouldCreateNewFile); + const compilerHost = ts.createCompilerHost( + parsedCommandLine.options, + /* setParentNodes */ true + ); + const oldGetSourceFile = compilerHost.getSourceFile; + compilerHost.getSourceFile = ( + filename, + languageVersion, + onError, + shouldCreateNewFile + ) => + path.normalize(filename) === path.normalize(options.filePath) + ? ts.createSourceFile(filename, code, languageVersion, true) + : oldGetSourceFile( + filename, + languageVersion, + onError, + shouldCreateNewFile + ); - return ts.createProgram( - parsedCommandLine.fileNames, - parsedCommandLine.options, - compilerHost - ); + return ts.createProgram( + parsedCommandLine.fileNames, + parsedCommandLine.options, + compilerHost + ); }; diff --git a/parser.js b/parser.js index e3516a5..506cdd8 100644 --- a/parser.js +++ b/parser.js @@ -12,7 +12,7 @@ const astNodeTypes = require('./lib/ast-node-types'), ts = require('typescript'), convert = require('./lib/ast-converter'), semver = require('semver'), - calculateProjectParserOptions = require("./lib/tsconfig-parser"); + calculateProjectParserOptions = require('./lib/tsconfig-parser'); const SUPPORTED_TYPESCRIPT_VERSIONS = require('./package.json').devDependencies .typescript; @@ -43,7 +43,7 @@ function resetExtra() { useJSXTextNode: false, log: console.log, project: false - } + }; } //------------------------------------------------------------------------------ @@ -110,7 +110,7 @@ function generateAST(code, options) { extra.log = Function.prototype; } - if (typeof options.project === "boolean") { + if (typeof options.project === 'boolean') { extra.project = options.project; } } @@ -130,16 +130,17 @@ function generateAST(code, options) { warnedAboutTSVersion = true; } - let FILENAME, program; + let program, ast; if (extra.project) { - FILENAME = options.filePath; + const FILENAME = options.filePath; program = calculateProjectParserOptions(options); + ast = program ? program.getSourceFile(FILENAME) : undefined; } - if (program === undefined) { + if (ast === undefined) { // Even if jsx option is set in typescript compiler, filename still has to // contain .tsx file extension - const FILENAME = (extra.ecmaFeatures.jsx) ? "estree.tsx" : "estree.ts"; + const FILENAME = extra.ecmaFeatures.jsx ? 'estree.tsx' : 'estree.ts'; const compilerHost = { fileExists() { @@ -149,18 +150,23 @@ function generateAST(code, options) { return FILENAME; }, getCurrentDirectory() { - return ""; + return ''; }, getDefaultLibFileName() { - return "lib.d.ts"; + return 'lib.d.ts'; }, // TODO: Support Windows CRLF getNewLine() { - return "\n"; + return '\n'; }, getSourceFile(filename) { - return ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true); + return ts.createSourceFile( + filename, + code, + ts.ScriptTarget.Latest, + true + ); }, readFile() { return null; @@ -173,14 +179,18 @@ function generateAST(code, options) { } }; - program = ts.createProgram([FILENAME], { - noResolve: true, - target: ts.ScriptTarget.Latest, - jsx: extra.ecmaFeatures.jsx ? "preserve" : undefined - }, compilerHost); - } + program = ts.createProgram( + [FILENAME], + { + noResolve: true, + target: ts.ScriptTarget.Latest, + jsx: extra.ecmaFeatures.jsx ? 'preserve' : undefined + }, + compilerHost + ); - const ast = program.getSourceFile(FILENAME); + ast = program.getSourceFile(FILENAME); + } extra.code = code; const { estree, astMaps } = convert(ast, extra); @@ -195,7 +205,14 @@ exports.version = require('./package.json').version; exports.parse = function parse(code, options) { const result = generateAST(code, options); - return { ast: result.estree, services: { program: result.program, esTreeNodeToTSNodeMap: result.astMaps.esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap: result.astMaps.tsNodeToESTreeNodeMap}} + return { + ast: result.estree, + services: { + program: result.program, + esTreeNodeToTSNodeMap: result.astMaps.esTreeNodeToTSNodeMap, + tsNodeToESTreeNodeMap: result.astMaps.tsNodeToESTreeNodeMap + } + }; }; exports.AST_NODE_TYPES = astNodeTypes; From 7f1ba19b2237bb6a11021f385792f30323034157 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 4 Oct 2018 09:18:01 -0700 Subject: [PATCH 05/30] chore: fix indentation --- lib/ast-converter.js | 5 ++--- lib/convert.js | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/ast-converter.js b/lib/ast-converter.js index 7e2c9a0..1bdb626 100644 --- a/lib/ast-converter.js +++ b/lib/ast-converter.js @@ -70,19 +70,18 @@ module.exports = (ast, extra) => { * Optionally convert and include all tokens in the AST */ if (extra.tokens) { - estree.tokens = nodeUtils.convertTokens(ast); + estree.tokens = nodeUtils.convertTokens(ast); } /** * Optionally convert and include all comments in the AST */ if (extra.comment) { - estree.comments = convertComments(ast, extra.code); + estree.comments = convertComments(ast, extra.code); } const astMaps = convert.getASTMaps(); convert.resetASTMaps(); return { estree, astMaps }; - }; diff --git a/lib/convert.js b/lib/convert.js index 0c4c25d..568e4e5 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -29,12 +29,12 @@ let esTreeNodeToTSNodeMap = new WeakMap(); let tsNodeToESTreeNodeMap = new WeakMap(); function resetASTMaps() { - esTreeNodeToTSNodeMap = new WeakMap(); - tsNodeToESTreeNodeMap = new WeakMap(); + esTreeNodeToTSNodeMap = new WeakMap(); + tsNodeToESTreeNodeMap = new WeakMap(); } function getASTMaps() { - return { esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap }; + return { esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap }; } /** From b4377a9c467f64d955a1456da139f83c4cdec3e7 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 4 Oct 2018 09:40:15 -0700 Subject: [PATCH 06/30] fix: remove duplicated code from merge --- lib/ast-converter.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/ast-converter.js b/lib/ast-converter.js index 1bdb626..661a07b 100644 --- a/lib/ast-converter.js +++ b/lib/ast-converter.js @@ -66,13 +66,6 @@ module.exports = (ast, extra) => { estree.tokens = nodeUtils.convertTokens(ast); } - /** - * Optionally convert and include all tokens in the AST - */ - if (extra.tokens) { - estree.tokens = nodeUtils.convertTokens(ast); - } - /** * Optionally convert and include all comments in the AST */ From d2d6a316a8e93e688e96cb6acbf5654796c9a00a Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 4 Oct 2018 10:46:37 -0700 Subject: [PATCH 07/30] fix: re-add missing argument --- lib/tsconfig-parser.js | 5 +++-- parser.js | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index e46d70f..2434892 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -16,7 +16,7 @@ const ts = require('typescript'); function parseTSConfig(tsconfigPath) { // if no tsconfig in cwd, return if (!fs.existsSync(tsconfigPath)) { - return undefined; + return null; } // Parse tsconfig and create program @@ -25,7 +25,7 @@ function parseTSConfig(tsconfigPath) { tsconfigContents = fs.readFileSync(tsconfigPath, 'utf8'); } catch (e) { // if can't read file, return - return undefined; + return null; } const tsconfigParseResult = ts.parseJsonText(tsconfigPath, tsconfigContents); @@ -44,6 +44,7 @@ function parseTSConfig(tsconfigPath) { * @param {string} code The code being linted * @param {Object} options Options provided by ESLint core * @param {string} options.cwd The current working directory for the eslint process + * @param {string} options.filePath The path of the file being parsed * @returns {ts.Program|undefined} The program defined by the tsconfig.json in the cwd, or undefined if something went wrong */ module.exports = function calculateProjectParserOptions(code, options) { diff --git a/parser.js b/parser.js index 506cdd8..da688b7 100644 --- a/parser.js +++ b/parser.js @@ -133,7 +133,7 @@ function generateAST(code, options) { let program, ast; if (extra.project) { const FILENAME = options.filePath; - program = calculateProjectParserOptions(options); + program = calculateProjectParserOptions(code, options); ast = program ? program.getSourceFile(FILENAME) : undefined; } @@ -152,6 +152,9 @@ function generateAST(code, options) { getCurrentDirectory() { return ''; }, + getDirectories() { + return []; + }, getDefaultLibFileName() { return 'lib.d.ts'; }, @@ -169,7 +172,7 @@ function generateAST(code, options) { ); }, readFile() { - return null; + return undefined; }, useCaseSensitiveFileNames() { return true; @@ -184,7 +187,7 @@ function generateAST(code, options) { { noResolve: true, target: ts.ScriptTarget.Latest, - jsx: extra.ecmaFeatures.jsx ? 'preserve' : undefined + jsx: extra.ecmaFeatures.jsx ? ts.JsxEmit.Preserve : undefined }, compilerHost ); From c3cc2dabf63c6d887ce0dfa5859269c2ef1054c7 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Mon, 8 Oct 2018 16:51:56 -0700 Subject: [PATCH 08/30] fix: take tsconfig paths from eslintrc rather than finding one --- lib/tsconfig-parser.js | 79 ++++++++++++++++++++++++------------------ parser.js | 36 ++++++++++++++----- 2 files changed, 72 insertions(+), 43 deletions(-) diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index 2434892..41af149 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -20,7 +20,7 @@ function parseTSConfig(tsconfigPath) { } // Parse tsconfig and create program - let tsconfigContents; + let tsconfigContents = ''; try { tsconfigContents = fs.readFileSync(tsconfigPath, 'utf8'); } catch (e) { @@ -40,47 +40,58 @@ function parseTSConfig(tsconfigPath) { } /** - * Calculate project environment using options provided by eslint + * Calculate project environments using options provided by eslint and paths from config * @param {string} code The code being linted * @param {Object} options Options provided by ESLint core * @param {string} options.cwd The current working directory for the eslint process * @param {string} options.filePath The path of the file being parsed - * @returns {ts.Program|undefined} The program defined by the tsconfig.json in the cwd, or undefined if something went wrong + * @param {Object} extra Validated parser options + * @param {string[]} extra.project Provided tsconfig paths + * @returns {ts.Program[]} The programs corresponding to the supplied tsconfig paths */ -module.exports = function calculateProjectParserOptions(code, options) { - // if no cwd passed, return +module.exports = function calculateProjectParserOptions(code, options, extra) { + const results = []; const cwd = options.cwd || process.cwd(); - const tsconfigPath = path.join(cwd, 'tsconfig.json'); - const parsedCommandLine = parseTSConfig(tsconfigPath); + for (let tsconfigPath of extra.project) { + if (!path.isAbsolute(tsconfigPath)) { + tsconfigPath = path.join(cwd, tsconfigPath); + } - if (parsedCommandLine === null) { - return undefined; - } + const parsedCommandLine = parseTSConfig(tsconfigPath); - const compilerHost = ts.createCompilerHost( - parsedCommandLine.options, - /* setParentNodes */ true - ); - const oldGetSourceFile = compilerHost.getSourceFile; - compilerHost.getSourceFile = ( - filename, - languageVersion, - onError, - shouldCreateNewFile - ) => - path.normalize(filename) === path.normalize(options.filePath) - ? ts.createSourceFile(filename, code, languageVersion, true) - : oldGetSourceFile( - filename, - languageVersion, - onError, - shouldCreateNewFile - ); + if (parsedCommandLine === null) { + continue; + } - return ts.createProgram( - parsedCommandLine.fileNames, - parsedCommandLine.options, - compilerHost - ); + const compilerHost = ts.createCompilerHost( + parsedCommandLine.options, + /* setParentNodes */ true + ); + const oldGetSourceFile = compilerHost.getSourceFile; + compilerHost.getSourceFile = ( + filename, + languageVersion, + onError, + shouldCreateNewFile + ) => + path.normalize(filename) === path.normalize(options.filePath) + ? ts.createSourceFile(filename, code, languageVersion, true) + : oldGetSourceFile( + filename, + languageVersion, + onError, + shouldCreateNewFile + ); + + results.push( + ts.createProgram( + parsedCommandLine.fileNames, + parsedCommandLine.options, + compilerHost + ) + ); + } + + return results; }; diff --git a/parser.js b/parser.js index da688b7..7c2332c 100644 --- a/parser.js +++ b/parser.js @@ -42,7 +42,7 @@ function resetExtra() { ecmaFeatures: {}, useJSXTextNode: false, log: console.log, - project: false + project: [] }; } @@ -110,7 +110,9 @@ function generateAST(code, options) { extra.log = Function.prototype; } - if (typeof options.project === 'boolean') { + if (typeof options.project === 'string') { + extra.project = [options.project]; + } else if (Array.isArray(options.project)) { extra.project = options.project; } } @@ -130,11 +132,21 @@ function generateAST(code, options) { warnedAboutTSVersion = true; } - let program, ast; - if (extra.project) { + let relevantProgram = undefined; + let ast = undefined; + const shouldProvideParserServices = extra.project && extra.project.length > 0; + + if (shouldProvideParserServices) { const FILENAME = options.filePath; - program = calculateProjectParserOptions(code, options); - ast = program ? program.getSourceFile(FILENAME) : undefined; + const programs = calculateProjectParserOptions(code, options, extra); + for (const program of programs) { + ast = program.getSourceFile(FILENAME); + + if (ast !== undefined) { + relevantProgram = program; + break; + } + } } if (ast === undefined) { @@ -182,7 +194,7 @@ function generateAST(code, options) { } }; - program = ts.createProgram( + relevantProgram = ts.createProgram( [FILENAME], { noResolve: true, @@ -192,12 +204,18 @@ function generateAST(code, options) { compilerHost ); - ast = program.getSourceFile(FILENAME); + ast = relevantProgram.getSourceFile(FILENAME); } extra.code = code; const { estree, astMaps } = convert(ast, extra); - return { estree, program: extra.project ? program : undefined, astMaps }; + return { + estree, + program: shouldProvideParserServices ? relevantProgram : undefined, + astMaps: shouldProvideParserServices + ? astMaps + : { esTreeNodeToTSNodeMap: undefined, tsNodeToESTreeNodeMap: undefined } + }; } //------------------------------------------------------------------------------ From 48216475e637c3c66afaad56e41202c98a61cbd4 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Tue, 9 Oct 2018 14:50:13 -0700 Subject: [PATCH 09/30] perf: reuse existing programs and supply outdated to create new programs --- lib/tsconfig-parser.js | 58 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index 41af149..f3056b8 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -8,12 +8,19 @@ const ts = require('typescript'); // Environment calculation //------------------------------------------------------------------------------ +/** + * Maps tsconfig paths to their corresponding file contents and resulting programs + * TODO: Have some sort of cache eviction system to prevent unbounded cache size + * @type {Map} + */ +const programCache = new Map(); + /** * Create object representation of TypeScript configuration * @param {string} tsconfigPath Full path to tsconfig.json - * @returns {{options: Object, fileNames: string[]}|null} Representation of parsed tsconfig.json + * @returns {string|null} Representation of parsed tsconfig.json */ -function parseTSConfig(tsconfigPath) { +function readTSConfigText(tsconfigPath) { // if no tsconfig in cwd, return if (!fs.existsSync(tsconfigPath)) { return null; @@ -22,14 +29,24 @@ function parseTSConfig(tsconfigPath) { // Parse tsconfig and create program let tsconfigContents = ''; try { - tsconfigContents = fs.readFileSync(tsconfigPath, 'utf8'); + return (tsconfigContents = fs.readFileSync(tsconfigPath, 'utf8')); } catch (e) { // if can't read file, return return null; } +} +/** + * Parses contents of tsconfig.json to a set of compiler options + * @param {string} tsconfigPath Full path to tsconfig.json + * @param {string} tsconfigContents Contents of tsconfig.json + * @returns {ts.ParsedCommandLine} TS compiler options + */ +function parseTSCommandLine(tsconfigPath, tsconfigContents) { const tsconfigParseResult = ts.parseJsonText(tsconfigPath, tsconfigContents); + // TODO: raise appropriate errors if bad tsconfig contents + return ts.parseJsonConfigFileContent( tsconfigParseResult, ts.sys, @@ -54,11 +71,30 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { const cwd = options.cwd || process.cwd(); for (let tsconfigPath of extra.project) { + let oldProgram = undefined; + if (!path.isAbsolute(tsconfigPath)) { tsconfigPath = path.join(cwd, tsconfigPath); } - const parsedCommandLine = parseTSConfig(tsconfigPath); + const tsconfigContents = readTSConfigText(tsconfigPath); + + if (tsconfigContents === null) { + continue; + } + const cachedProgramAndText = programCache.get(tsconfigPath); + + if (cachedProgramAndText) { + if (cachedProgramAndText.text === tsconfigContents) { + results.push(cachedProgramAndText.program); + continue; + } + oldProgram = cachedProgramAndText.program; + } + const parsedCommandLine = parseTSCommandLine( + tsconfigPath, + tsconfigContents + ); if (parsedCommandLine === null) { continue; @@ -84,13 +120,15 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { shouldCreateNewFile ); - results.push( - ts.createProgram( - parsedCommandLine.fileNames, - parsedCommandLine.options, - compilerHost - ) + const program = ts.createProgram( + parsedCommandLine.fileNames, + parsedCommandLine.options, + compilerHost, + oldProgram ); + + results.push(program); + programCache.set(tsconfigPath, { text: tsconfigContents, program }); } return results; From 9dbd1fb6354cfccaf4d8cf6df589d806f8a188ae Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Tue, 9 Oct 2018 15:12:15 -0700 Subject: [PATCH 10/30] fix: appropriately handle malformed tsconfigs --- lib/tsconfig-parser.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index f3056b8..5bf63e8 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -45,8 +45,6 @@ function readTSConfigText(tsconfigPath) { function parseTSCommandLine(tsconfigPath, tsconfigContents) { const tsconfigParseResult = ts.parseJsonText(tsconfigPath, tsconfigContents); - // TODO: raise appropriate errors if bad tsconfig contents - return ts.parseJsonConfigFileContent( tsconfigParseResult, ts.sys, @@ -80,7 +78,7 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { const tsconfigContents = readTSConfigText(tsconfigPath); if (tsconfigContents === null) { - continue; + throw new Error(`Could not read provided tsconfig.json: ${tsconfigPath}`); } const cachedProgramAndText = programCache.get(tsconfigPath); @@ -96,8 +94,8 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { tsconfigContents ); - if (parsedCommandLine === null) { - continue; + if (parsedCommandLine.errors.length > 0) { + throw new Error(`Parsing ${tsconfigPath} resulted in errors.`); } const compilerHost = ts.createCompilerHost( From 4f9608a1f7acc4c494fce4a50224db226877737b Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 11 Oct 2018 16:23:24 -0700 Subject: [PATCH 11/30] test: add test for semantic info in isolated file --- .../semanticInfo/isolated-file.src.ts | 1 + tests/fixtures/semanticInfo/tsconfig.json | 59 +++ tests/lib/__snapshots__/semanticInfo.js.snap | 355 ++++++++++++++++++ tests/lib/semanticInfo.js | 119 ++++++ tools/test-utils.js | 9 +- 5 files changed, 541 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/semanticInfo/isolated-file.src.ts create mode 100644 tests/fixtures/semanticInfo/tsconfig.json create mode 100644 tests/lib/__snapshots__/semanticInfo.js.snap create mode 100644 tests/lib/semanticInfo.js diff --git a/tests/fixtures/semanticInfo/isolated-file.src.ts b/tests/fixtures/semanticInfo/isolated-file.src.ts new file mode 100644 index 0000000..ca04667 --- /dev/null +++ b/tests/fixtures/semanticInfo/isolated-file.src.ts @@ -0,0 +1 @@ +const x = [3, 4, 5]; \ No newline at end of file diff --git a/tests/fixtures/semanticInfo/tsconfig.json b/tests/fixtures/semanticInfo/tsconfig.json new file mode 100644 index 0000000..261cdca --- /dev/null +++ b/tests/fixtures/semanticInfo/tsconfig.json @@ -0,0 +1,59 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} \ No newline at end of file diff --git a/tests/lib/__snapshots__/semanticInfo.js.snap b/tests/lib/__snapshots__/semanticInfo.js.snap new file mode 100644 index 0000000..f5342d9 --- /dev/null +++ b/tests/lib/__snapshots__/semanticInfo.js.snap @@ -0,0 +1,355 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`semanticInfo fixtures/isolated-file.src 1`] = ` +Object { + "body": Array [ + Object { + "declarations": Array [ + Object { + "id": Object { + "loc": Object { + "end": Object { + "column": 7, + "line": 1, + }, + "start": Object { + "column": 6, + "line": 1, + }, + }, + "name": "x", + "range": Array [ + 6, + 7, + ], + "type": "Identifier", + }, + "init": Object { + "elements": Array [ + Object { + "loc": Object { + "end": Object { + "column": 12, + "line": 1, + }, + "start": Object { + "column": 11, + "line": 1, + }, + }, + "range": Array [ + 11, + 12, + ], + "raw": "3", + "type": "Literal", + "value": 3, + }, + Object { + "loc": Object { + "end": Object { + "column": 15, + "line": 1, + }, + "start": Object { + "column": 14, + "line": 1, + }, + }, + "range": Array [ + 14, + 15, + ], + "raw": "4", + "type": "Literal", + "value": 4, + }, + Object { + "loc": Object { + "end": Object { + "column": 18, + "line": 1, + }, + "start": Object { + "column": 17, + "line": 1, + }, + }, + "range": Array [ + 17, + 18, + ], + "raw": "5", + "type": "Literal", + "value": 5, + }, + ], + "loc": Object { + "end": Object { + "column": 19, + "line": 1, + }, + "start": Object { + "column": 10, + "line": 1, + }, + }, + "range": Array [ + 10, + 19, + ], + "type": "ArrayExpression", + }, + "loc": Object { + "end": Object { + "column": 19, + "line": 1, + }, + "start": Object { + "column": 6, + "line": 1, + }, + }, + "range": Array [ + 6, + 19, + ], + "type": "VariableDeclarator", + }, + ], + "kind": "const", + "loc": Object { + "end": Object { + "column": 20, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 20, + ], + "type": "VariableDeclaration", + }, + ], + "loc": Object { + "end": Object { + "column": 20, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 20, + ], + "sourceType": "script", + "tokens": Array [ + Object { + "loc": Object { + "end": Object { + "column": 5, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 5, + ], + "type": "Keyword", + "value": "const", + }, + Object { + "loc": Object { + "end": Object { + "column": 7, + "line": 1, + }, + "start": Object { + "column": 6, + "line": 1, + }, + }, + "range": Array [ + 6, + 7, + ], + "type": "Identifier", + "value": "x", + }, + Object { + "loc": Object { + "end": Object { + "column": 9, + "line": 1, + }, + "start": Object { + "column": 8, + "line": 1, + }, + }, + "range": Array [ + 8, + 9, + ], + "type": "Punctuator", + "value": "=", + }, + Object { + "loc": Object { + "end": Object { + "column": 11, + "line": 1, + }, + "start": Object { + "column": 10, + "line": 1, + }, + }, + "range": Array [ + 10, + 11, + ], + "type": "Punctuator", + "value": "[", + }, + Object { + "loc": Object { + "end": Object { + "column": 12, + "line": 1, + }, + "start": Object { + "column": 11, + "line": 1, + }, + }, + "range": Array [ + 11, + 12, + ], + "type": "Numeric", + "value": "3", + }, + Object { + "loc": Object { + "end": Object { + "column": 13, + "line": 1, + }, + "start": Object { + "column": 12, + "line": 1, + }, + }, + "range": Array [ + 12, + 13, + ], + "type": "Punctuator", + "value": ",", + }, + Object { + "loc": Object { + "end": Object { + "column": 15, + "line": 1, + }, + "start": Object { + "column": 14, + "line": 1, + }, + }, + "range": Array [ + 14, + 15, + ], + "type": "Numeric", + "value": "4", + }, + Object { + "loc": Object { + "end": Object { + "column": 16, + "line": 1, + }, + "start": Object { + "column": 15, + "line": 1, + }, + }, + "range": Array [ + 15, + 16, + ], + "type": "Punctuator", + "value": ",", + }, + Object { + "loc": Object { + "end": Object { + "column": 18, + "line": 1, + }, + "start": Object { + "column": 17, + "line": 1, + }, + }, + "range": Array [ + 17, + 18, + ], + "type": "Numeric", + "value": "5", + }, + Object { + "loc": Object { + "end": Object { + "column": 19, + "line": 1, + }, + "start": Object { + "column": 18, + "line": 1, + }, + }, + "range": Array [ + 18, + 19, + ], + "type": "Punctuator", + "value": "]", + }, + Object { + "loc": Object { + "end": Object { + "column": 20, + "line": 1, + }, + "start": Object { + "column": 19, + "line": 1, + }, + }, + "range": Array [ + 19, + 20, + ], + "type": "Punctuator", + "value": ";", + }, + ], + "type": "Program", +} +`; diff --git a/tests/lib/semanticInfo.js b/tests/lib/semanticInfo.js new file mode 100644 index 0000000..e8d0977 --- /dev/null +++ b/tests/lib/semanticInfo.js @@ -0,0 +1,119 @@ +/** + * @fileoverview Tests for TypeScript-specific constructs + * @author Nicholas C. Zakas + * @author James Henry + * @copyright jQuery Foundation and other contributors, https://jquery.org/ + * MIT License + */ + +'use strict'; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const path = require('path'), + shelljs = require('shelljs'), + testUtils = require('../../tools/test-utils'), + ts = require('typescript'); + +//------------------------------------------------------------------------------ +// Setup +//------------------------------------------------------------------------------ + +const FIXTURES_DIR = './tests/fixtures/semanticInfo'; + +const testFiles = shelljs + .find(FIXTURES_DIR) + .filter(filename => filename.indexOf('.src.ts') > -1) + // strip off ".src.ts" + .map(filename => + filename.substring(FIXTURES_DIR.length - 1, filename.length - 7) + ); + +function createConfig(fileName) { + return { + loc: true, + range: true, + tokens: true, + ecmaFeatures: {}, + errorOnUnknownASTType: true, + filePath: fileName, + cwd: path.join(process.cwd(), FIXTURES_DIR), + project: './tsconfig.json' + }; +} + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe('semanticInfo', () => { + // test all AST snapshots + testFiles.forEach(filename => { + // Uncomment and fill in filename to focus on a single file + // var filename = "jsx/invalid-matching-placeholder-in-closing-tag"; + const fullFileName = `${path.resolve(FIXTURES_DIR, filename)}.src.ts`; + const code = shelljs.cat(fullFileName); + test( + `fixtures/${filename}.src`, + testUtils.createSnapshotTestBlock(code, createConfig(fullFileName)) + ); + }); + + // case-specific tests + test('isolated-file tests', () => { + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const parseResult = testUtils.parseCode( + shelljs.cat(fileName), + createConfig(fileName) + ); + + // get type checker + expect(parseResult).toHaveProperty('services.program.getTypeChecker'); + const checker = parseResult.services.program.getTypeChecker(); + + // get number node (ast shape validated by snapshot) + const arrayMember = + parseResult.ast.body[0].declarations[0].init.elements[0]; + expect(parseResult).toHaveProperty('services.esTreeNodeToTSNodeMap'); + + // get corresponding TS node + const tsArrayMember = parseResult.services.esTreeNodeToTSNodeMap.get( + arrayMember + ); + expect(tsArrayMember).toBeDefined(); + expect(tsArrayMember.kind).toBe(ts.SyntaxKind.NumericLiteral); + expect(tsArrayMember.text).toBe('3'); + + // get type of TS node + const arrayMemberType = checker.getTypeAtLocation(tsArrayMember); + expect(arrayMemberType.flags).toBe(ts.TypeFlags.NumberLiteral); + expect(arrayMemberType.value).toBe(3); + + // make sure it maps back to original ESTree node + expect(parseResult).toHaveProperty('services.tsNodeToESTreeNodeMap'); + expect(parseResult.services.tsNodeToESTreeNodeMap.get(tsArrayMember)).toBe( + arrayMember + ); + + // get bound name + const boundName = parseResult.ast.body[0].declarations[0].id; + expect(boundName.name).toBe('x'); + + const tsBoundName = parseResult.services.esTreeNodeToTSNodeMap.get( + boundName + ); + expect(tsBoundName).toBeDefined(); + + const boundNameType = checker.getTypeAtLocation(tsBoundName); + expect(boundNameType.flags).toBe(ts.TypeFlags.Object); + expect(boundNameType.objectFlags).toBe(ts.ObjectFlags.Reference); + expect(boundNameType.typeArguments).toHaveLength(1); + expect(boundNameType.typeArguments[0].flags).toBe(ts.TypeFlags.Number); + + expect(parseResult.services.tsNodeToESTreeNodeMap.get(tsBoundName)).toBe( + boundName + ); + }); +}); diff --git a/tools/test-utils.js b/tools/test-utils.js index d12f19c..ba6dfe7 100644 --- a/tools/test-utils.js +++ b/tools/test-utils.js @@ -34,12 +34,16 @@ function getRaw(ast) { ); } +function parseCode(code, config) { + return parser.parse(code, config); +} + /** * Returns a function which can be used as the callback of a Jest test() block, * and which performs an assertion on the snapshot for the given code and config. * @param {string} code The source code to parse * @param {*} config the parser configuration - * @returns {Function} callback for Jest test() block + * @returns {jest.ProvidesCallback} callback for Jest test() block */ function createSnapshotTestBlock(code, config) { /** @@ -69,5 +73,6 @@ function createSnapshotTestBlock(code, config) { module.exports = { getRaw, - createSnapshotTestBlock + createSnapshotTestBlock, + parseCode }; From d2d12b7b19f1683a23de2458bd4b1b9832914b5a Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 11 Oct 2018 17:18:00 -0700 Subject: [PATCH 12/30] test: add bad project input tests --- .../semanticInfo/badTSConfig/tsconfig.json | 60 +++++++++++++++++++ tests/lib/__snapshots__/semanticInfo.js.snap | 6 ++ tests/lib/semanticInfo.js | 27 +++++++++ 3 files changed, 93 insertions(+) create mode 100644 tests/fixtures/semanticInfo/badTSConfig/tsconfig.json diff --git a/tests/fixtures/semanticInfo/badTSConfig/tsconfig.json b/tests/fixtures/semanticInfo/badTSConfig/tsconfig.json new file mode 100644 index 0000000..b0f6ea2 --- /dev/null +++ b/tests/fixtures/semanticInfo/badTSConfig/tsconfig.json @@ -0,0 +1,60 @@ +{ + "compileOnSave": "hello", + "compilerOptions": { + /* Basic Options */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } + } \ No newline at end of file diff --git a/tests/lib/__snapshots__/semanticInfo.js.snap b/tests/lib/__snapshots__/semanticInfo.js.snap index f5342d9..9103d41 100644 --- a/tests/lib/__snapshots__/semanticInfo.js.snap +++ b/tests/lib/__snapshots__/semanticInfo.js.snap @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`semanticInfo fail to read project file 1`] = `"Could not read provided tsconfig.json: E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo"`; + exports[`semanticInfo fixtures/isolated-file.src 1`] = ` Object { "body": Array [ @@ -353,3 +355,7 @@ Object { "type": "Program", } `; + +exports[`semanticInfo malformed project file 1`] = `"Parsing E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo\\\\badTSConfig\\\\tsconfig.json resulted in errors."`; + +exports[`semanticInfo non-existent project file 1`] = `"Could not read provided tsconfig.json: E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo\\\\tsconfigs.json"`; diff --git a/tests/lib/semanticInfo.js b/tests/lib/semanticInfo.js index e8d0977..a432d52 100644 --- a/tests/lib/semanticInfo.js +++ b/tests/lib/semanticInfo.js @@ -116,4 +116,31 @@ describe('semanticInfo', () => { boundName ); }); + + test('non-existent project file', () => { + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const badConfig = createConfig(fileName); + badConfig.project = './tsconfigs.json'; + expect(() => + testUtils.parseCode(shelljs.cat(fileName), badConfig) + ).toThrowErrorMatchingSnapshot(); + }); + + test('fail to read project file', () => { + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const badConfig = createConfig(fileName); + badConfig.project = '.'; + expect(() => + testUtils.parseCode(shelljs.cat(fileName), badConfig) + ).toThrowErrorMatchingSnapshot(); + }); + + test('malformed project file', () => { + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const badConfig = createConfig(fileName); + badConfig.project = './badTSConfig/tsconfig.json'; + expect(() => + testUtils.parseCode(shelljs.cat(fileName), badConfig) + ).toThrowErrorMatchingSnapshot(); + }); }); From 460953dd6e3119c3312ac47f0f719c297a844288 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 12 Oct 2018 11:25:44 -0700 Subject: [PATCH 13/30] test: add test validating cross-file type resolution --- lib/tsconfig-parser.js | 4 +- .../fixtures/semanticInfo/export-file.src.ts | 1 + .../fixtures/semanticInfo/import-file.src.ts | 2 + tests/lib/__snapshots__/semanticInfo.js.snap | 776 ++++++++++++++++++ tests/lib/semanticInfo.js | 60 +- 5 files changed, 829 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/semanticInfo/export-file.src.ts create mode 100644 tests/fixtures/semanticInfo/import-file.src.ts diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index 5bf63e8..c582189 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -26,10 +26,8 @@ function readTSConfigText(tsconfigPath) { return null; } - // Parse tsconfig and create program - let tsconfigContents = ''; try { - return (tsconfigContents = fs.readFileSync(tsconfigPath, 'utf8')); + return fs.readFileSync(tsconfigPath, 'utf8'); } catch (e) { // if can't read file, return return null; diff --git a/tests/fixtures/semanticInfo/export-file.src.ts b/tests/fixtures/semanticInfo/export-file.src.ts new file mode 100644 index 0000000..8bb4cb8 --- /dev/null +++ b/tests/fixtures/semanticInfo/export-file.src.ts @@ -0,0 +1 @@ +export default [3, 4, 5]; \ No newline at end of file diff --git a/tests/fixtures/semanticInfo/import-file.src.ts b/tests/fixtures/semanticInfo/import-file.src.ts new file mode 100644 index 0000000..da5d202 --- /dev/null +++ b/tests/fixtures/semanticInfo/import-file.src.ts @@ -0,0 +1,2 @@ +import arr from "./export-file.src"; +arr.push(6, 7); \ No newline at end of file diff --git a/tests/lib/__snapshots__/semanticInfo.js.snap b/tests/lib/__snapshots__/semanticInfo.js.snap index 9103d41..095e949 100644 --- a/tests/lib/__snapshots__/semanticInfo.js.snap +++ b/tests/lib/__snapshots__/semanticInfo.js.snap @@ -2,6 +2,782 @@ exports[`semanticInfo fail to read project file 1`] = `"Could not read provided tsconfig.json: E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo"`; +exports[`semanticInfo fixtures/export-file.src 1`] = ` +Object { + "body": Array [ + Object { + "declaration": Object { + "elements": Array [ + Object { + "loc": Object { + "end": Object { + "column": 17, + "line": 1, + }, + "start": Object { + "column": 16, + "line": 1, + }, + }, + "range": Array [ + 16, + 17, + ], + "raw": "3", + "type": "Literal", + "value": 3, + }, + Object { + "loc": Object { + "end": Object { + "column": 20, + "line": 1, + }, + "start": Object { + "column": 19, + "line": 1, + }, + }, + "range": Array [ + 19, + 20, + ], + "raw": "4", + "type": "Literal", + "value": 4, + }, + Object { + "loc": Object { + "end": Object { + "column": 23, + "line": 1, + }, + "start": Object { + "column": 22, + "line": 1, + }, + }, + "range": Array [ + 22, + 23, + ], + "raw": "5", + "type": "Literal", + "value": 5, + }, + ], + "loc": Object { + "end": Object { + "column": 24, + "line": 1, + }, + "start": Object { + "column": 15, + "line": 1, + }, + }, + "range": Array [ + 15, + 24, + ], + "type": "ArrayExpression", + }, + "loc": Object { + "end": Object { + "column": 25, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 25, + ], + "type": "ExportDefaultDeclaration", + }, + ], + "loc": Object { + "end": Object { + "column": 25, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 25, + ], + "sourceType": "module", + "tokens": Array [ + Object { + "loc": Object { + "end": Object { + "column": 6, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 6, + ], + "type": "Keyword", + "value": "export", + }, + Object { + "loc": Object { + "end": Object { + "column": 14, + "line": 1, + }, + "start": Object { + "column": 7, + "line": 1, + }, + }, + "range": Array [ + 7, + 14, + ], + "type": "Keyword", + "value": "default", + }, + Object { + "loc": Object { + "end": Object { + "column": 16, + "line": 1, + }, + "start": Object { + "column": 15, + "line": 1, + }, + }, + "range": Array [ + 15, + 16, + ], + "type": "Punctuator", + "value": "[", + }, + Object { + "loc": Object { + "end": Object { + "column": 17, + "line": 1, + }, + "start": Object { + "column": 16, + "line": 1, + }, + }, + "range": Array [ + 16, + 17, + ], + "type": "Numeric", + "value": "3", + }, + Object { + "loc": Object { + "end": Object { + "column": 18, + "line": 1, + }, + "start": Object { + "column": 17, + "line": 1, + }, + }, + "range": Array [ + 17, + 18, + ], + "type": "Punctuator", + "value": ",", + }, + Object { + "loc": Object { + "end": Object { + "column": 20, + "line": 1, + }, + "start": Object { + "column": 19, + "line": 1, + }, + }, + "range": Array [ + 19, + 20, + ], + "type": "Numeric", + "value": "4", + }, + Object { + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 20, + "line": 1, + }, + }, + "range": Array [ + 20, + 21, + ], + "type": "Punctuator", + "value": ",", + }, + Object { + "loc": Object { + "end": Object { + "column": 23, + "line": 1, + }, + "start": Object { + "column": 22, + "line": 1, + }, + }, + "range": Array [ + 22, + 23, + ], + "type": "Numeric", + "value": "5", + }, + Object { + "loc": Object { + "end": Object { + "column": 24, + "line": 1, + }, + "start": Object { + "column": 23, + "line": 1, + }, + }, + "range": Array [ + 23, + 24, + ], + "type": "Punctuator", + "value": "]", + }, + Object { + "loc": Object { + "end": Object { + "column": 25, + "line": 1, + }, + "start": Object { + "column": 24, + "line": 1, + }, + }, + "range": Array [ + 24, + 25, + ], + "type": "Punctuator", + "value": ";", + }, + ], + "type": "Program", +} +`; + +exports[`semanticInfo fixtures/import-file.src 1`] = ` +Object { + "body": Array [ + Object { + "loc": Object { + "end": Object { + "column": 36, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 36, + ], + "source": Object { + "loc": Object { + "end": Object { + "column": 35, + "line": 1, + }, + "start": Object { + "column": 16, + "line": 1, + }, + }, + "range": Array [ + 16, + 35, + ], + "raw": "\\"./export-file.src\\"", + "type": "Literal", + "value": "./export-file.src", + }, + "specifiers": Array [ + Object { + "loc": Object { + "end": Object { + "column": 10, + "line": 1, + }, + "start": Object { + "column": 7, + "line": 1, + }, + }, + "local": Object { + "loc": Object { + "end": Object { + "column": 10, + "line": 1, + }, + "start": Object { + "column": 7, + "line": 1, + }, + }, + "name": "arr", + "range": Array [ + 7, + 10, + ], + "type": "Identifier", + }, + "range": Array [ + 7, + 10, + ], + "type": "ImportDefaultSpecifier", + }, + ], + "type": "ImportDeclaration", + }, + Object { + "expression": Object { + "arguments": Array [ + Object { + "loc": Object { + "end": Object { + "column": 10, + "line": 2, + }, + "start": Object { + "column": 9, + "line": 2, + }, + }, + "range": Array [ + 46, + 47, + ], + "raw": "6", + "type": "Literal", + "value": 6, + }, + Object { + "loc": Object { + "end": Object { + "column": 13, + "line": 2, + }, + "start": Object { + "column": 12, + "line": 2, + }, + }, + "range": Array [ + 49, + 50, + ], + "raw": "7", + "type": "Literal", + "value": 7, + }, + ], + "callee": Object { + "computed": false, + "loc": Object { + "end": Object { + "column": 8, + "line": 2, + }, + "start": Object { + "column": 0, + "line": 2, + }, + }, + "object": Object { + "loc": Object { + "end": Object { + "column": 3, + "line": 2, + }, + "start": Object { + "column": 0, + "line": 2, + }, + }, + "name": "arr", + "range": Array [ + 37, + 40, + ], + "type": "Identifier", + }, + "property": Object { + "loc": Object { + "end": Object { + "column": 8, + "line": 2, + }, + "start": Object { + "column": 4, + "line": 2, + }, + }, + "name": "push", + "range": Array [ + 41, + 45, + ], + "type": "Identifier", + }, + "range": Array [ + 37, + 45, + ], + "type": "MemberExpression", + }, + "loc": Object { + "end": Object { + "column": 14, + "line": 2, + }, + "start": Object { + "column": 0, + "line": 2, + }, + }, + "range": Array [ + 37, + 51, + ], + "type": "CallExpression", + }, + "loc": Object { + "end": Object { + "column": 15, + "line": 2, + }, + "start": Object { + "column": 0, + "line": 2, + }, + }, + "range": Array [ + 37, + 52, + ], + "type": "ExpressionStatement", + }, + ], + "loc": Object { + "end": Object { + "column": 15, + "line": 2, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 52, + ], + "sourceType": "module", + "tokens": Array [ + Object { + "loc": Object { + "end": Object { + "column": 6, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 6, + ], + "type": "Keyword", + "value": "import", + }, + Object { + "loc": Object { + "end": Object { + "column": 10, + "line": 1, + }, + "start": Object { + "column": 7, + "line": 1, + }, + }, + "range": Array [ + 7, + 10, + ], + "type": "Identifier", + "value": "arr", + }, + Object { + "loc": Object { + "end": Object { + "column": 15, + "line": 1, + }, + "start": Object { + "column": 11, + "line": 1, + }, + }, + "range": Array [ + 11, + 15, + ], + "type": "Identifier", + "value": "from", + }, + Object { + "loc": Object { + "end": Object { + "column": 35, + "line": 1, + }, + "start": Object { + "column": 16, + "line": 1, + }, + }, + "range": Array [ + 16, + 35, + ], + "type": "String", + "value": "\\"./export-file.src\\"", + }, + Object { + "loc": Object { + "end": Object { + "column": 36, + "line": 1, + }, + "start": Object { + "column": 35, + "line": 1, + }, + }, + "range": Array [ + 35, + 36, + ], + "type": "Punctuator", + "value": ";", + }, + Object { + "loc": Object { + "end": Object { + "column": 3, + "line": 2, + }, + "start": Object { + "column": 0, + "line": 2, + }, + }, + "range": Array [ + 37, + 40, + ], + "type": "Identifier", + "value": "arr", + }, + Object { + "loc": Object { + "end": Object { + "column": 4, + "line": 2, + }, + "start": Object { + "column": 3, + "line": 2, + }, + }, + "range": Array [ + 40, + 41, + ], + "type": "Punctuator", + "value": ".", + }, + Object { + "loc": Object { + "end": Object { + "column": 8, + "line": 2, + }, + "start": Object { + "column": 4, + "line": 2, + }, + }, + "range": Array [ + 41, + 45, + ], + "type": "Identifier", + "value": "push", + }, + Object { + "loc": Object { + "end": Object { + "column": 9, + "line": 2, + }, + "start": Object { + "column": 8, + "line": 2, + }, + }, + "range": Array [ + 45, + 46, + ], + "type": "Punctuator", + "value": "(", + }, + Object { + "loc": Object { + "end": Object { + "column": 10, + "line": 2, + }, + "start": Object { + "column": 9, + "line": 2, + }, + }, + "range": Array [ + 46, + 47, + ], + "type": "Numeric", + "value": "6", + }, + Object { + "loc": Object { + "end": Object { + "column": 11, + "line": 2, + }, + "start": Object { + "column": 10, + "line": 2, + }, + }, + "range": Array [ + 47, + 48, + ], + "type": "Punctuator", + "value": ",", + }, + Object { + "loc": Object { + "end": Object { + "column": 13, + "line": 2, + }, + "start": Object { + "column": 12, + "line": 2, + }, + }, + "range": Array [ + 49, + 50, + ], + "type": "Numeric", + "value": "7", + }, + Object { + "loc": Object { + "end": Object { + "column": 14, + "line": 2, + }, + "start": Object { + "column": 13, + "line": 2, + }, + }, + "range": Array [ + 50, + 51, + ], + "type": "Punctuator", + "value": ")", + }, + Object { + "loc": Object { + "end": Object { + "column": 15, + "line": 2, + }, + "start": Object { + "column": 14, + "line": 2, + }, + }, + "range": Array [ + 51, + 52, + ], + "type": "Punctuator", + "value": ";", + }, + ], + "type": "Program", +} +`; + exports[`semanticInfo fixtures/isolated-file.src 1`] = ` Object { "body": Array [ diff --git a/tests/lib/semanticInfo.js b/tests/lib/semanticInfo.js index a432d52..a0566fc 100644 --- a/tests/lib/semanticInfo.js +++ b/tests/lib/semanticInfo.js @@ -31,7 +31,7 @@ const testFiles = shelljs filename.substring(FIXTURES_DIR.length - 1, filename.length - 7) ); -function createConfig(fileName) { +function createOptions(fileName) { return { loc: true, range: true, @@ -57,7 +57,7 @@ describe('semanticInfo', () => { const code = shelljs.cat(fullFileName); test( `fixtures/${filename}.src`, - testUtils.createSnapshotTestBlock(code, createConfig(fullFileName)) + testUtils.createSnapshotTestBlock(code, createOptions(fullFileName)) ); }); @@ -66,7 +66,7 @@ describe('semanticInfo', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const parseResult = testUtils.parseCode( shelljs.cat(fileName), - createConfig(fileName) + createOptions(fileName) ); // get type checker @@ -106,20 +106,43 @@ describe('semanticInfo', () => { ); expect(tsBoundName).toBeDefined(); - const boundNameType = checker.getTypeAtLocation(tsBoundName); - expect(boundNameType.flags).toBe(ts.TypeFlags.Object); - expect(boundNameType.objectFlags).toBe(ts.ObjectFlags.Reference); - expect(boundNameType.typeArguments).toHaveLength(1); - expect(boundNameType.typeArguments[0].flags).toBe(ts.TypeFlags.Number); + checkNumberArrayType(checker, tsBoundName); expect(parseResult.services.tsNodeToESTreeNodeMap.get(tsBoundName)).toBe( boundName ); }); + test('imported-file tests', () => { + const fileName = path.resolve(FIXTURES_DIR, 'import-file.src.ts'); + const parseResult = testUtils.parseCode( + shelljs.cat(fileName), + createOptions(fileName) + ); + + // get type checker + expect(parseResult).toHaveProperty('services.program.getTypeChecker'); + const checker = parseResult.services.program.getTypeChecker(); + + // get array node (ast shape validated by snapshot) + const arrayBoundName = parseResult.ast.body[1].expression.callee.object; + expect(arrayBoundName.name).toBe('arr'); + + expect(parseResult).toHaveProperty('services.esTreeNodeToTSNodeMap'); + const tsArrayBoundName = parseResult.services.esTreeNodeToTSNodeMap.get( + arrayBoundName + ); + expect(tsArrayBoundName).toBeDefined(); + checkNumberArrayType(checker, tsArrayBoundName); + + expect( + parseResult.services.tsNodeToESTreeNodeMap.get(tsArrayBoundName) + ).toBe(arrayBoundName); + }); + test('non-existent project file', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); - const badConfig = createConfig(fileName); + const badConfig = createOptions(fileName); badConfig.project = './tsconfigs.json'; expect(() => testUtils.parseCode(shelljs.cat(fileName), badConfig) @@ -128,7 +151,7 @@ describe('semanticInfo', () => { test('fail to read project file', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); - const badConfig = createConfig(fileName); + const badConfig = createOptions(fileName); badConfig.project = '.'; expect(() => testUtils.parseCode(shelljs.cat(fileName), badConfig) @@ -137,10 +160,25 @@ describe('semanticInfo', () => { test('malformed project file', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); - const badConfig = createConfig(fileName); + const badConfig = createOptions(fileName); badConfig.project = './badTSConfig/tsconfig.json'; expect(() => testUtils.parseCode(shelljs.cat(fileName), badConfig) ).toThrowErrorMatchingSnapshot(); }); }); + +/** + * Verifies that the type of a TS node is number[] as expected + * @param {ts.TypeChecker} checker + * @param {ts.Node} tsNode + */ +function checkNumberArrayType(checker, tsNode) { + const nodeType = /** @type {ts.ObjectType & ts.TypeReference} */ (checker.getTypeAtLocation( + tsNode + )); + expect(nodeType.flags).toBe(ts.TypeFlags.Object); + expect(nodeType.objectFlags).toBe(ts.ObjectFlags.Reference); + expect(nodeType.typeArguments).toHaveLength(1); + expect(nodeType.typeArguments[0].flags).toBe(ts.TypeFlags.Number); +} From 2315459737b4d6c0b6ee0a3622ff993d0123f8f0 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 12 Oct 2018 11:26:52 -0700 Subject: [PATCH 14/30] chore: add comment to test --- tests/lib/semanticInfo.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/lib/semanticInfo.js b/tests/lib/semanticInfo.js index a0566fc..1c0f319 100644 --- a/tests/lib/semanticInfo.js +++ b/tests/lib/semanticInfo.js @@ -125,6 +125,7 @@ describe('semanticInfo', () => { const checker = parseResult.services.program.getTypeChecker(); // get array node (ast shape validated by snapshot) + // node is defined in other file than the parsed one const arrayBoundName = parseResult.ast.body[1].expression.callee.object; expect(arrayBoundName.name).toBe('arr'); From ade71f2121a18b73a163099a001b0a05ba7cdadb Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 12 Oct 2018 11:30:31 -0700 Subject: [PATCH 15/30] chore: limit badtsconfig for readability --- .../semanticInfo/badTSConfig/tsconfig.json | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/tests/fixtures/semanticInfo/badTSConfig/tsconfig.json b/tests/fixtures/semanticInfo/badTSConfig/tsconfig.json index b0f6ea2..134439a 100644 --- a/tests/fixtures/semanticInfo/badTSConfig/tsconfig.json +++ b/tests/fixtures/semanticInfo/badTSConfig/tsconfig.json @@ -1,60 +1,9 @@ { "compileOnSave": "hello", "compilerOptions": { - /* Basic Options */ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ } } \ No newline at end of file From b3c407f7759ad7015a58f532157f9c33d420b7cc Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Fri, 12 Oct 2018 11:32:47 -0700 Subject: [PATCH 16/30] chore: update test header --- tests/lib/semanticInfo.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/lib/semanticInfo.js b/tests/lib/semanticInfo.js index 1c0f319..1406c13 100644 --- a/tests/lib/semanticInfo.js +++ b/tests/lib/semanticInfo.js @@ -1,9 +1,6 @@ /** - * @fileoverview Tests for TypeScript-specific constructs - * @author Nicholas C. Zakas - * @author James Henry - * @copyright jQuery Foundation and other contributors, https://jquery.org/ - * MIT License + * @fileoverview Tests for semantic information + * @author Benjamin Lichtman */ 'use strict'; From 47c9b1cda206b042518bfd4eddb00dd2d66da4f0 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Wed, 17 Oct 2018 14:04:26 -0700 Subject: [PATCH 17/30] perf: use TS watch API to track changes to programs --- lib/convert.js | 62 +++++---- lib/node-utils.js | 78 ++++++++++-- lib/tsconfig-parser.js | 120 ++++++------------ .../fixtures/semanticInfo/badTSConfig/app.ts | 0 tests/lib/__snapshots__/semanticInfo.js.snap | 6 +- 5 files changed, 145 insertions(+), 121 deletions(-) create mode 100644 tests/fixtures/semanticInfo/badTSConfig/app.ts diff --git a/lib/convert.js b/lib/convert.js index 568e4e5..5150841 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -65,7 +65,7 @@ module.exports = function convert(config) { */ let result = { type: '', - range: [node.getStart(), node.end], + range: [node.getStart(ast), node.end], loc: nodeUtils.getLoc(node, ast) }; @@ -144,7 +144,7 @@ module.exports = function convert(config) { if (nodeUtils.isTypeKeyword(typeArgument.kind)) { return { type: AST_NODE_TYPES[`TS${SyntaxKind[typeArgument.kind]}`], - range: [typeArgument.getStart(), typeArgument.getEnd()], + range: [typeArgument.getStart(ast), typeArgument.getEnd()], loc: nodeUtils.getLoc(typeArgument, ast) }; } @@ -158,7 +158,7 @@ module.exports = function convert(config) { } return { type: AST_NODE_TYPES.TSTypeReference, - range: [typeArgument.getStart(), typeArgument.getEnd()], + range: [typeArgument.getStart(ast), typeArgument.getEnd()], loc: nodeUtils.getLoc(typeArgument, ast), typeName: convertChild(typeArgument.typeName || typeArgument), typeParameters: typeArgument.typeArguments @@ -211,7 +211,7 @@ module.exports = function convert(config) { return { type: AST_NODE_TYPES.TSTypeParameter, - range: [typeParameter.getStart(), typeParameter.getEnd()], + range: [typeParameter.getStart(ast), typeParameter.getEnd()], loc: nodeUtils.getLoc(typeParameter, ast), name, constraint, @@ -278,7 +278,7 @@ module.exports = function convert(config) { const expression = convertChild(decorator.expression); return { type: AST_NODE_TYPES.Decorator, - range: [decorator.getStart(), decorator.end], + range: [decorator.getStart(ast), decorator.end], loc: nodeUtils.getLoc(decorator, ast), expression }; @@ -489,7 +489,11 @@ module.exports = function convert(config) { }); result.range[1] = node.endOfFileToken.end; - result.loc = nodeUtils.getLocFor(node.getStart(), result.range[1], ast); + result.loc = nodeUtils.getLocFor( + node.getStart(ast), + result.range[1], + ast + ); break; case SyntaxKind.Block: @@ -949,11 +953,12 @@ module.exports = function convert(config) { return false; } return nodeUtils.getTextForTokenKind(token.kind) === '('; - } + }, + ast ); const methodLoc = ast.getLineAndCharacterOfPosition( - openingParen.getStart() + openingParen.getStart(ast) ), nodeIsMethod = node.kind === SyntaxKind.MethodDeclaration, method = { @@ -1084,10 +1089,10 @@ module.exports = function convert(config) { }; const constructorIdentifierLocStart = ast.getLineAndCharacterOfPosition( - firstConstructorToken.getStart() + firstConstructorToken.getStart(ast) ), constructorIdentifierLocEnd = ast.getLineAndCharacterOfPosition( - firstConstructorToken.getEnd() + firstConstructorToken.getEnd(ast) ), constructorIsComputed = !!node.name && nodeUtils.isComputedProperty(node.name); @@ -1099,7 +1104,10 @@ module.exports = function convert(config) { type: AST_NODE_TYPES.Literal, value: 'constructor', raw: node.name.getText(), - range: [firstConstructorToken.getStart(), firstConstructorToken.end], + range: [ + firstConstructorToken.getStart(ast), + firstConstructorToken.end + ], loc: { start: { line: constructorIdentifierLocStart.line + 1, @@ -1115,7 +1123,10 @@ module.exports = function convert(config) { constructorKey = { type: AST_NODE_TYPES.Identifier, name: 'constructor', - range: [firstConstructorToken.getStart(), firstConstructorToken.end], + range: [ + firstConstructorToken.getStart(ast), + firstConstructorToken.end + ], loc: { start: { line: constructorIdentifierLocStart.line + 1, @@ -1248,9 +1259,9 @@ module.exports = function convert(config) { type: AST_NODE_TYPES.AssignmentPattern, left: convertChild(node.name), right: convertChild(node.initializer), - range: [node.name.getStart(), node.initializer.end], + range: [node.name.getStart(ast), node.initializer.end], loc: nodeUtils.getLocFor( - node.name.getStart(), + node.name.getStart(ast), node.initializer.end, ast ) @@ -1307,7 +1318,7 @@ module.exports = function convert(config) { { type: AST_NODE_TYPES.TemplateElement, value: { - raw: ast.text.slice(node.getStart() + 1, node.end - 1), + raw: ast.text.slice(node.getStart(ast) + 1, node.end - 1), cooked: node.text }, tail: true, @@ -1350,7 +1361,10 @@ module.exports = function convert(config) { Object.assign(result, { type: AST_NODE_TYPES.TemplateElement, value: { - raw: ast.text.slice(node.getStart() + 1, node.end - (tail ? 1 : 2)), + raw: ast.text.slice( + node.getStart(ast) + 1, + node.end - (tail ? 1 : 2) + ), cooked: node.text }, tail @@ -1441,7 +1455,7 @@ module.exports = function convert(config) { if (node.modifiers) { return { type: AST_NODE_TYPES.TSParameterProperty, - range: [node.getStart(), node.end], + range: [node.getStart(ast), node.end], loc: nodeUtils.getLoc(node, ast), accessibility: nodeUtils.getTSNodeAccessibility(node) || undefined, readonly: @@ -1538,8 +1552,8 @@ module.exports = function convert(config) { body: [], // TODO: Fix location info - range: [openBrace.getStart(), result.range[1]], - loc: nodeUtils.getLocFor(openBrace.getStart(), node.end, ast) + range: [openBrace.getStart(ast), result.range[1]], + loc: nodeUtils.getLocFor(openBrace.getStart(ast), node.end, ast) }, superClass: superClass && superClass.types[0] @@ -2157,7 +2171,7 @@ module.exports = function convert(config) { type: AST_NODE_TYPES.VariableDeclarator, id: convertChild(node.name), init: convertChild(node.type), - range: [node.name.getStart(), node.end] + range: [node.name.getStart(ast), node.end] }; typeAliasDeclarator.loc = nodeUtils.getLocFor( @@ -2314,8 +2328,12 @@ module.exports = function convert(config) { const interfaceBody = { type: AST_NODE_TYPES.TSInterfaceBody, body: node.members.map(member => convertChild(member)), - range: [interfaceOpenBrace.getStart(), result.range[1]], - loc: nodeUtils.getLocFor(interfaceOpenBrace.getStart(), node.end, ast) + range: [interfaceOpenBrace.getStart(ast), result.range[1]], + loc: nodeUtils.getLocFor( + interfaceOpenBrace.getStart(ast), + node.end, + ast + ) }; Object.assign(result, { diff --git a/lib/node-utils.js b/lib/node-utils.js index 5f83a67..59e5d18 100644 --- a/lib/node-utils.js +++ b/lib/node-utils.js @@ -295,7 +295,7 @@ function getLocFor(start, end, ast) { * @returns {Object} the loc data */ function getLoc(nodeOrToken, ast) { - return getLocFor(nodeOrToken.getStart(), nodeOrToken.end, ast); + return getLocFor(nodeOrToken.getStart(ast), nodeOrToken.end, ast); } /** @@ -407,13 +407,28 @@ function hasStaticModifierFlag(node) { * Finds the next token based on the previous one and its parent * @param {TSToken} previousToken The previous TSToken * @param {TSNode} parent The parent TSNode + * @param {ts.SourceFileLike} ast The TS AST * @returns {TSToken} the next TSToken */ -function findNextToken(previousToken, parent) { - /** - * TODO: Remove dependency on private TypeScript method - */ - return ts.findNextToken(previousToken, parent); +function findNextToken(previousToken, parent, ast) { + return find(parent); + + function find(n) { + if (ts.isToken(n) && n.pos === previousToken.end) { + // this is token that starts at the end of previous token - return it + return n; + } + return firstDefined(n.getChildren(ast), child => { + const shouldDiveInChildNode = + // previous token is enclosed somewhere in the child + (child.pos <= previousToken.pos && child.end > previousToken.end) || + // previous token ends exactly at the beginning of child + child.pos === previousToken.end; + return shouldDiveInChildNode && nodeHasTokens(child, ast) + ? find(child) + : undefined; + }); + } } /** @@ -421,14 +436,15 @@ function findNextToken(previousToken, parent) { * @param {TSToken} previousToken The previous TSToken * @param {TSNode} parent The parent TSNode * @param {Function} predicate The predicate function to apply to each checked token + * @param {ts.SourceFileLike} ast The TS AST * @returns {TSToken|undefined} a matching TSToken */ -function findFirstMatchingToken(previousToken, parent, predicate) { +function findFirstMatchingToken(previousToken, parent, predicate, ast) { while (previousToken) { if (predicate(previousToken)) { return previousToken; } - previousToken = findNextToken(previousToken, parent); + previousToken = findNextToken(previousToken, parent, ast); } return undefined; } @@ -536,9 +552,9 @@ function fixExports(node, result, ast) { lastModifier = node.modifiers[node.modifiers.length - 1], declarationIsDefault = nextModifier && nextModifier.kind === SyntaxKind.DefaultKeyword, - varToken = findNextToken(lastModifier, ast); + varToken = findNextToken(lastModifier, ast, ast); - result.range[0] = varToken.getStart(); + result.range[0] = varToken.getStart(ast); result.loc = getLocFor(result.range[0], result.range[1], ast); const declarationType = declarationIsDefault @@ -548,8 +564,8 @@ function fixExports(node, result, ast) { const newResult = { type: declarationType, declaration: result, - range: [exportKeyword.getStart(), result.range[1]], - loc: getLocFor(exportKeyword.getStart(), result.range[1], ast) + range: [exportKeyword.getStart(ast), result.range[1]], + loc: getLocFor(exportKeyword.getStart(ast), result.range[1], ast) }; if (!declarationIsDefault) { @@ -680,7 +696,7 @@ function convertToken(token, ast) { const start = token.kind === SyntaxKind.JsxText ? token.getFullStart() - : token.getStart(), + : token.getStart(ast), end = token.getEnd(), value = ast.text.slice(start, end), newToken = { @@ -725,7 +741,7 @@ function convertTokens(ast) { result.push(converted); } } else { - node.getChildren().forEach(walk); + node.getChildren(ast).forEach(walk); } } walk(ast); @@ -779,3 +795,37 @@ function createError(ast, start, message) { message }; } + +/** + * @param {ts.Node} n the TSNode + * @param {ts.SourceFileLike} ast the TS AST + */ +function nodeHasTokens(n, ast) { + // If we have a token or node that has a non-zero width, it must have tokens. + // Note: getWidth() does not take trivia into account. + return n.kind === SyntaxKind.EndOfFileToken + ? !!n.jsDoc + : n.getWidth(ast) !== 0; +} + +/** + * Like `forEach`, but suitable for use with numbers and strings (which may be falsy). + * @template T + * @template U + * @param {ReadonlyArray|undefined} array + * @param {(element: T, index: number) => (U|undefined)} callback + * @returns {U|undefined} + */ +function firstDefined(array, callback) { + if (array === undefined) { + return undefined; + } + + for (let i = 0; i < array.length; i++) { + const result = callback(array[i], i); + if (result !== undefined) { + return result; + } + } + return undefined; +} diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index c582189..5d37073 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -9,49 +9,24 @@ const ts = require('typescript'); //------------------------------------------------------------------------------ /** - * Maps tsconfig paths to their corresponding file contents and resulting programs - * TODO: Have some sort of cache eviction system to prevent unbounded cache size - * @type {Map} + * Maps tsconfig paths to their corresponding file contents and resulting watches + * @type {Map>} */ -const programCache = new Map(); +const knownWatchProgramMap = new Map(); /** - * Create object representation of TypeScript configuration - * @param {string} tsconfigPath Full path to tsconfig.json - * @returns {string|null} Representation of parsed tsconfig.json + * Appropriately report issues found when reading a config file + * @param {ts.Diagnostic} diagnostic The diagnostic raised when creating a program + * @returns {void} */ -function readTSConfigText(tsconfigPath) { - // if no tsconfig in cwd, return - if (!fs.existsSync(tsconfigPath)) { - return null; - } - - try { - return fs.readFileSync(tsconfigPath, 'utf8'); - } catch (e) { - // if can't read file, return - return null; - } -} - -/** - * Parses contents of tsconfig.json to a set of compiler options - * @param {string} tsconfigPath Full path to tsconfig.json - * @param {string} tsconfigContents Contents of tsconfig.json - * @returns {ts.ParsedCommandLine} TS compiler options - */ -function parseTSCommandLine(tsconfigPath, tsconfigContents) { - const tsconfigParseResult = ts.parseJsonText(tsconfigPath, tsconfigContents); - - return ts.parseJsonConfigFileContent( - tsconfigParseResult, - ts.sys, - path.dirname(tsconfigPath), - /* existingOptions */ {}, - tsconfigPath +function diagnosticReporter(diagnostic) { + throw new Error( + ts.flattenDiagnosticMessageText(diagnostic.messageText, ts.sys.newLine) ); } +const noopFileWatcher = { close: () => {} }; + /** * Calculate project environments using options provided by eslint and paths from config * @param {string} code The code being linted @@ -67,64 +42,45 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { const cwd = options.cwd || process.cwd(); for (let tsconfigPath of extra.project) { - let oldProgram = undefined; - if (!path.isAbsolute(tsconfigPath)) { tsconfigPath = path.join(cwd, tsconfigPath); } - const tsconfigContents = readTSConfigText(tsconfigPath); + const existingWatch = knownWatchProgramMap.get(tsconfigPath); - if (tsconfigContents === null) { - throw new Error(`Could not read provided tsconfig.json: ${tsconfigPath}`); + if (typeof existingWatch !== 'undefined') { + results.push(existingWatch.getProgram().getProgram()); + continue; } - const cachedProgramAndText = programCache.get(tsconfigPath); - if (cachedProgramAndText) { - if (cachedProgramAndText.text === tsconfigContents) { - results.push(cachedProgramAndText.program); - continue; - } - oldProgram = cachedProgramAndText.program; - } - const parsedCommandLine = parseTSCommandLine( + const watchCompilerHost = ts.createWatchCompilerHost( tsconfigPath, - tsconfigContents + /*optionsToExtend*/ undefined, + ts.sys, + ts.createSemanticDiagnosticsBuilderProgram, + diagnosticReporter, + /*reportWatchStatus*/ () => {} ); - - if (parsedCommandLine.errors.length > 0) { - throw new Error(`Parsing ${tsconfigPath} resulted in errors.`); + const oldReadFile = watchCompilerHost.readFile; + watchCompilerHost.readFile = (filePath, encoding) => + path.normalize(filePath) === path.normalize(options.filePath) + ? code + : oldReadFile(filePath, encoding); + watchCompilerHost.onUnRecoverableConfigFileDiagnostic = diagnosticReporter; + watchCompilerHost.afterProgramCreate = undefined; + watchCompilerHost.watchFile = () => noopFileWatcher; + watchCompilerHost.watchDirectory = () => noopFileWatcher; + + const programWatch = ts.createWatchProgram(watchCompilerHost); + const program = programWatch.getProgram().getProgram(); + + const configFileDiagnostics = program.getConfigFileParsingDiagnostics(); + if (configFileDiagnostics.length > 0) { + diagnosticReporter(configFileDiagnostics[0]); } - const compilerHost = ts.createCompilerHost( - parsedCommandLine.options, - /* setParentNodes */ true - ); - const oldGetSourceFile = compilerHost.getSourceFile; - compilerHost.getSourceFile = ( - filename, - languageVersion, - onError, - shouldCreateNewFile - ) => - path.normalize(filename) === path.normalize(options.filePath) - ? ts.createSourceFile(filename, code, languageVersion, true) - : oldGetSourceFile( - filename, - languageVersion, - onError, - shouldCreateNewFile - ); - - const program = ts.createProgram( - parsedCommandLine.fileNames, - parsedCommandLine.options, - compilerHost, - oldProgram - ); - + knownWatchProgramMap.set(tsconfigPath, programWatch); results.push(program); - programCache.set(tsconfigPath, { text: tsconfigContents, program }); } return results; diff --git a/tests/fixtures/semanticInfo/badTSConfig/app.ts b/tests/fixtures/semanticInfo/badTSConfig/app.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/lib/__snapshots__/semanticInfo.js.snap b/tests/lib/__snapshots__/semanticInfo.js.snap index 095e949..e0301c3 100644 --- a/tests/lib/__snapshots__/semanticInfo.js.snap +++ b/tests/lib/__snapshots__/semanticInfo.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`semanticInfo fail to read project file 1`] = `"Could not read provided tsconfig.json: E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo"`; +exports[`semanticInfo fail to read project file 1`] = `"File 'E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo' not found."`; exports[`semanticInfo fixtures/export-file.src 1`] = ` Object { @@ -1132,6 +1132,6 @@ Object { } `; -exports[`semanticInfo malformed project file 1`] = `"Parsing E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo\\\\badTSConfig\\\\tsconfig.json resulted in errors."`; +exports[`semanticInfo malformed project file 1`] = `"Compiler option 'compileOnSave' requires a value of type boolean."`; -exports[`semanticInfo non-existent project file 1`] = `"Could not read provided tsconfig.json: E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo\\\\tsconfigs.json"`; +exports[`semanticInfo non-existent project file 1`] = `"File 'E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo\\\\tsconfigs.json' not found."`; From fb5470c5ccca01321697eb3e93a9d48eac204d70 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 18 Oct 2018 14:34:04 -0700 Subject: [PATCH 18/30] fix: ensure changes to the linted file are detected --- lib/tsconfig-parser.js | 58 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index 5d37073..7c542a5 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -14,6 +14,22 @@ const ts = require('typescript'); */ const knownWatchProgramMap = new Map(); +/** + * Maps file paths to their set of corresponding watch callbacks + * There may be more than one per file if a file is shared between projects + * @type {Map} + */ +const watchCallbackTrackingMap = new Map(); + +/** + * Holds information about the file currently being linted + * @type {{code: string, filePath: string}} + */ +const currentLintOperationState = { + code: '', + filePath: '' +}; + /** * Appropriately report issues found when reading a config file * @param {ts.Diagnostic} diagnostic The diagnostic raised when creating a program @@ -41,7 +57,19 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { const results = []; const cwd = options.cwd || process.cwd(); + // preserve reference to code and file being linted + currentLintOperationState.code = code; + currentLintOperationState.filePath = options.filePath; + + // Update file version if necessary + // TODO: only update when necessary, currently marks as changed on every lint + const watchCallback = watchCallbackTrackingMap.get(options.filePath); + if (typeof watchCallback !== 'undefined') { + watchCallback(options.filePath, ts.FileWatcherEventKind.Changed); + } + for (let tsconfigPath of extra.project) { + // if absolute paths aren't provided, make relative to cwd if (!path.isAbsolute(tsconfigPath)) { tsconfigPath = path.join(cwd, tsconfigPath); } @@ -49,10 +77,12 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { const existingWatch = knownWatchProgramMap.get(tsconfigPath); if (typeof existingWatch !== 'undefined') { + // get new program (updated if necessary) results.push(existingWatch.getProgram().getProgram()); continue; } + // create compiler host const watchCompilerHost = ts.createWatchCompilerHost( tsconfigPath, /*optionsToExtend*/ undefined, @@ -61,24 +91,46 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { diagnosticReporter, /*reportWatchStatus*/ () => {} ); + + // ensure readFile reads the code being linted instead of the copy on disk const oldReadFile = watchCompilerHost.readFile; watchCompilerHost.readFile = (filePath, encoding) => - path.normalize(filePath) === path.normalize(options.filePath) - ? code + path.normalize(filePath) === + path.normalize(currentLintOperationState.filePath) + ? currentLintOperationState.code : oldReadFile(filePath, encoding); + + // ensure process reports error on failure instead of exiting process immediately watchCompilerHost.onUnRecoverableConfigFileDiagnostic = diagnosticReporter; + + // ensure process doesn't emit programs watchCompilerHost.afterProgramCreate = undefined; - watchCompilerHost.watchFile = () => noopFileWatcher; + + // register callbacks to trigger program updates without using fileWatchers + watchCompilerHost.watchFile = (fileName, callback) => { + const normalizedFileName = path.normalize(fileName); + watchCallbackTrackingMap.set(normalizedFileName, callback); + return { + close: () => { + watchCallbackTrackingMap.delete(normalizedFileName); + } + }; + }; + + // ensure fileWatchers aren't created for directories watchCompilerHost.watchDirectory = () => noopFileWatcher; + // create program const programWatch = ts.createWatchProgram(watchCompilerHost); const program = programWatch.getProgram().getProgram(); + // report error if there are any errors in the config file const configFileDiagnostics = program.getConfigFileParsingDiagnostics(); if (configFileDiagnostics.length > 0) { diagnosticReporter(configFileDiagnostics[0]); } + // cache watch program and return current program knownWatchProgramMap.set(tsconfigPath, programWatch); results.push(program); } From f27a444f6c1cf084959927adf43c1096b93f23ff Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 18 Oct 2018 14:39:46 -0700 Subject: [PATCH 19/30] fix: improve project option validation --- parser.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/parser.js b/parser.js index 7c2332c..96e2e2d 100644 --- a/parser.js +++ b/parser.js @@ -112,7 +112,10 @@ function generateAST(code, options) { if (typeof options.project === 'string') { extra.project = [options.project]; - } else if (Array.isArray(options.project)) { + } else if ( + Array.isArray(options.project) && + options.project.every(projectPath => typeof projectPath === 'string') + ) { extra.project = options.project; } } From 234fe43a4825547d2ac98696652662b7f3a15c76 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 18 Oct 2018 14:44:44 -0700 Subject: [PATCH 20/30] chore: remove unnecessary comments from tsconfig in tests --- tests/fixtures/semanticInfo/tsconfig.json | 59 ++--------------------- 1 file changed, 4 insertions(+), 55 deletions(-) diff --git a/tests/fixtures/semanticInfo/tsconfig.json b/tests/fixtures/semanticInfo/tsconfig.json index 261cdca..3caa872 100644 --- a/tests/fixtures/semanticInfo/tsconfig.json +++ b/tests/fixtures/semanticInfo/tsconfig.json @@ -1,59 +1,8 @@ { "compilerOptions": { - /* Basic Options */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "target": "es5", + "module": "commonjs", + "strict": true, + "esModuleInterop": true } } \ No newline at end of file From 01a312b97c991c9c5d4c958901526fcf7b93ee57 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Mon, 22 Oct 2018 10:00:55 -0700 Subject: [PATCH 21/30] fix: report config file errors whenever the program updates --- lib/tsconfig-parser.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index 7c542a5..a8feb06 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -104,7 +104,18 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { watchCompilerHost.onUnRecoverableConfigFileDiagnostic = diagnosticReporter; // ensure process doesn't emit programs - watchCompilerHost.afterProgramCreate = undefined; + watchCompilerHost.afterProgramCreate = program => { + // report error if there are any errors in the config file + const configFileDiagnostics = program + .getConfigFileParsingDiagnostics() + .filter( + diag => + diag.category === ts.DiagnosticCategory.Error && diag.code !== 18003 + ); + if (configFileDiagnostics.length > 0) { + diagnosticReporter(configFileDiagnostics[0]); + } + }; // register callbacks to trigger program updates without using fileWatchers watchCompilerHost.watchFile = (fileName, callback) => { @@ -124,12 +135,6 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { const programWatch = ts.createWatchProgram(watchCompilerHost); const program = programWatch.getProgram().getProgram(); - // report error if there are any errors in the config file - const configFileDiagnostics = program.getConfigFileParsingDiagnostics(); - if (configFileDiagnostics.length > 0) { - diagnosticReporter(configFileDiagnostics[0]); - } - // cache watch program and return current program knownWatchProgramMap.set(tsconfigPath, programWatch); results.push(program); From 8e60d5f8e98ddb706726c895126ba88130249402 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Mon, 29 Oct 2018 10:50:37 -0700 Subject: [PATCH 22/30] fix: add sourcefile to more operations --- lib/convert.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/convert.js b/lib/convert.js index 5150841..a2a8789 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -132,7 +132,11 @@ module.exports = function convert(config) { typeArgumentsParent.kind === SyntaxKind.TypeReference) ) { const lastTypeArgument = typeArguments[typeArguments.length - 1]; - const greaterThanToken = nodeUtils.findNextToken(lastTypeArgument, ast); + const greaterThanToken = nodeUtils.findNextToken( + lastTypeArgument, + ast, + ast + ); end = greaterThanToken.end; } } @@ -178,7 +182,11 @@ module.exports = function convert(config) { const firstTypeParameter = typeParameters[0]; const lastTypeParameter = typeParameters[typeParameters.length - 1]; - const greaterThanToken = nodeUtils.findNextToken(lastTypeParameter, ast); + const greaterThanToken = nodeUtils.findNextToken( + lastTypeParameter, + ast, + ast + ); return { type: AST_NODE_TYPES.TSTypeParameterDeclaration, @@ -1065,7 +1073,7 @@ module.exports = function convert(config) { node ), firstConstructorToken = constructorIsStatic - ? nodeUtils.findNextToken(node.getFirstToken(), ast) + ? nodeUtils.findNextToken(node.getFirstToken(), ast, ast) : node.getFirstToken(), constructorLoc = ast.getLineAndCharacterOfPosition( node.parameters.pos - 1 @@ -1488,7 +1496,7 @@ module.exports = function convert(config) { node.typeParameters[node.typeParameters.length - 1]; if (!lastClassToken || lastTypeParameter.pos > lastClassToken.pos) { - lastClassToken = nodeUtils.findNextToken(lastTypeParameter, ast); + lastClassToken = nodeUtils.findNextToken(lastTypeParameter, ast, ast); } result.typeParameters = convertTSTypeParametersToTypeParametersDeclaration( node.typeParameters @@ -1512,14 +1520,14 @@ module.exports = function convert(config) { const lastModifier = node.modifiers[node.modifiers.length - 1]; if (!lastClassToken || lastModifier.pos > lastClassToken.pos) { - lastClassToken = nodeUtils.findNextToken(lastModifier, ast); + lastClassToken = nodeUtils.findNextToken(lastModifier, ast, ast); } } else if (!lastClassToken) { // no name lastClassToken = node.getFirstToken(); } - const openBrace = nodeUtils.findNextToken(lastClassToken, ast); + const openBrace = nodeUtils.findNextToken(lastClassToken, ast, ast); const superClass = heritageClauses.find( clause => clause.token === SyntaxKind.ExtendsKeyword ); @@ -2307,6 +2315,7 @@ module.exports = function convert(config) { ) { interfaceLastClassToken = nodeUtils.findNextToken( interfaceLastTypeParameter, + ast, ast ); } @@ -2322,6 +2331,7 @@ module.exports = function convert(config) { ); const interfaceOpenBrace = nodeUtils.findNextToken( interfaceLastClassToken, + ast, ast ); From 8a46d080d66426f67ff6e41ab5386e907be0f32a Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Mon, 29 Oct 2018 10:58:55 -0700 Subject: [PATCH 23/30] refactor: only pass projects to project option handling --- lib/tsconfig-parser.js | 11 +++++++---- parser.js | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/tsconfig-parser.js b/lib/tsconfig-parser.js index a8feb06..21951fb 100644 --- a/lib/tsconfig-parser.js +++ b/lib/tsconfig-parser.js @@ -49,11 +49,14 @@ const noopFileWatcher = { close: () => {} }; * @param {Object} options Options provided by ESLint core * @param {string} options.cwd The current working directory for the eslint process * @param {string} options.filePath The path of the file being parsed - * @param {Object} extra Validated parser options - * @param {string[]} extra.project Provided tsconfig paths + * @param {string[]} projects Provided tsconfig paths * @returns {ts.Program[]} The programs corresponding to the supplied tsconfig paths */ -module.exports = function calculateProjectParserOptions(code, options, extra) { +module.exports = function calculateProjectParserOptions( + code, + options, + projects +) { const results = []; const cwd = options.cwd || process.cwd(); @@ -68,7 +71,7 @@ module.exports = function calculateProjectParserOptions(code, options, extra) { watchCallback(options.filePath, ts.FileWatcherEventKind.Changed); } - for (let tsconfigPath of extra.project) { + for (let tsconfigPath of projects) { // if absolute paths aren't provided, make relative to cwd if (!path.isAbsolute(tsconfigPath)) { tsconfigPath = path.join(cwd, tsconfigPath); diff --git a/parser.js b/parser.js index 96e2e2d..859ee88 100644 --- a/parser.js +++ b/parser.js @@ -141,7 +141,11 @@ function generateAST(code, options) { if (shouldProvideParserServices) { const FILENAME = options.filePath; - const programs = calculateProjectParserOptions(code, options, extra); + const programs = calculateProjectParserOptions( + code, + options, + extra.project + ); for (const program of programs) { ast = program.getSourceFile(FILENAME); From 285cbd1871f15bf5bd85e4362d9c47611328b69f Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Mon, 29 Oct 2018 11:25:17 -0700 Subject: [PATCH 24/30] refactor: break up program creation for readability --- parser.js | 175 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 97 insertions(+), 78 deletions(-) diff --git a/parser.js b/parser.js index 859ee88..b407c1e 100644 --- a/parser.js +++ b/parser.js @@ -46,6 +46,97 @@ function resetExtra() { }; } +/** + * @param {string} code The code of the file being linted + * @param {Object} options The config object + * @returns {{ast: ts.SourceFile, program: ts.Program} | undefined} If found, returns the source file corresponding to the code and the containing program + */ +function getASTFromProject(code, options) { + for (const program of calculateProjectParserOptions( + code, + options, + extra.project + )) { + const ast = program.getSourceFile(options.filePath); + + if (ast) { + return { ast, program }; + } + } +} + +/** + * @param {string} code The code of the file being linted + * @returns {{ast: ts.SourceFile, program: ts.Program}} Returns a new source file and program corresponding to the linted code + */ +function createNewProgram(code) { + // Even if jsx option is set in typescript compiler, filename still has to + // contain .tsx file extension + const FILENAME = extra.ecmaFeatures.jsx ? 'estree.tsx' : 'estree.ts'; + + const compilerHost = { + fileExists() { + return true; + }, + getCanonicalFileName() { + return FILENAME; + }, + getCurrentDirectory() { + return ''; + }, + getDirectories() { + return []; + }, + getDefaultLibFileName() { + return 'lib.d.ts'; + }, + + // TODO: Support Windows CRLF + getNewLine() { + return '\n'; + }, + getSourceFile(filename) { + return ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true); + }, + readFile() { + return undefined; + }, + useCaseSensitiveFileNames() { + return true; + }, + writeFile() { + return null; + } + }; + + const program = ts.createProgram( + [FILENAME], + { + noResolve: true, + target: ts.ScriptTarget.Latest, + jsx: extra.ecmaFeatures.jsx ? ts.JsxEmit.Preserve : undefined + }, + compilerHost + ); + + const ast = /** @type {ts.SourceFile} */ (program.getSourceFile(FILENAME)); + + return { ast, program }; +} + +/** + * @param {string} code The code of the file being linted + * @param {Object} options The config object + * @param {boolean} shouldProvideParserServices True iff the program should be attempted to be calculated from provided tsconfig files + * @returns {{ast: ts.SourceFile, program: ts.Program}} Returns a source file and program corresponding to the linted code + */ +function getProgramAndAST(code, options, shouldProvideParserServices) { + return ( + (shouldProvideParserServices && getASTFromProject(code, options)) || + createNewProgram(code) + ); +} + //------------------------------------------------------------------------------ // Parser //------------------------------------------------------------------------------ @@ -135,90 +226,18 @@ function generateAST(code, options) { warnedAboutTSVersion = true; } - let relevantProgram = undefined; - let ast = undefined; const shouldProvideParserServices = extra.project && extra.project.length > 0; - - if (shouldProvideParserServices) { - const FILENAME = options.filePath; - const programs = calculateProjectParserOptions( - code, - options, - extra.project - ); - for (const program of programs) { - ast = program.getSourceFile(FILENAME); - - if (ast !== undefined) { - relevantProgram = program; - break; - } - } - } - - if (ast === undefined) { - // Even if jsx option is set in typescript compiler, filename still has to - // contain .tsx file extension - const FILENAME = extra.ecmaFeatures.jsx ? 'estree.tsx' : 'estree.ts'; - - const compilerHost = { - fileExists() { - return true; - }, - getCanonicalFileName() { - return FILENAME; - }, - getCurrentDirectory() { - return ''; - }, - getDirectories() { - return []; - }, - getDefaultLibFileName() { - return 'lib.d.ts'; - }, - - // TODO: Support Windows CRLF - getNewLine() { - return '\n'; - }, - getSourceFile(filename) { - return ts.createSourceFile( - filename, - code, - ts.ScriptTarget.Latest, - true - ); - }, - readFile() { - return undefined; - }, - useCaseSensitiveFileNames() { - return true; - }, - writeFile() { - return null; - } - }; - - relevantProgram = ts.createProgram( - [FILENAME], - { - noResolve: true, - target: ts.ScriptTarget.Latest, - jsx: extra.ecmaFeatures.jsx ? ts.JsxEmit.Preserve : undefined - }, - compilerHost - ); - - ast = relevantProgram.getSourceFile(FILENAME); - } + const { ast, program } = getProgramAndAST( + code, + options, + shouldProvideParserServices + ); extra.code = code; const { estree, astMaps } = convert(ast, extra); return { estree, - program: shouldProvideParserServices ? relevantProgram : undefined, + program: shouldProvideParserServices ? program : undefined, astMaps: shouldProvideParserServices ? astMaps : { esTreeNodeToTSNodeMap: undefined, tsNodeToESTreeNodeMap: undefined } From 7e546578f34760a4b67e2bb5015ccd0873eca06f Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Mon, 29 Oct 2018 11:33:12 -0700 Subject: [PATCH 25/30] refactor: add comment and use util function --- lib/node-utils.js | 4 +++- parser.js | 19 ++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/node-utils.js b/lib/node-utils.js index 59e5d18..5adda29 100644 --- a/lib/node-utils.js +++ b/lib/node-utils.js @@ -166,7 +166,8 @@ module.exports = { isTypeKeyword, isComment, isJSDocComment, - createError + createError, + firstDefined }; /** @@ -405,6 +406,7 @@ function hasStaticModifierFlag(node) { /** * Finds the next token based on the previous one and its parent + * Had to copy this from TS instead of using TS's version because theirs doesn't pass the ast to getChildren * @param {TSToken} previousToken The previous TSToken * @param {TSNode} parent The parent TSNode * @param {ts.SourceFileLike} ast The TS AST diff --git a/parser.js b/parser.js index b407c1e..6134308 100644 --- a/parser.js +++ b/parser.js @@ -12,7 +12,8 @@ const astNodeTypes = require('./lib/ast-node-types'), ts = require('typescript'), convert = require('./lib/ast-converter'), semver = require('semver'), - calculateProjectParserOptions = require('./lib/tsconfig-parser'); + calculateProjectParserOptions = require('./lib/tsconfig-parser'), + util = require('./lib/node-utils'); const SUPPORTED_TYPESCRIPT_VERSIONS = require('./package.json').devDependencies .typescript; @@ -52,17 +53,13 @@ function resetExtra() { * @returns {{ast: ts.SourceFile, program: ts.Program} | undefined} If found, returns the source file corresponding to the code and the containing program */ function getASTFromProject(code, options) { - for (const program of calculateProjectParserOptions( - code, - options, - extra.project - )) { - const ast = program.getSourceFile(options.filePath); - - if (ast) { - return { ast, program }; + return util.firstDefined( + calculateProjectParserOptions(code, options, extra.project), + currentProgram => { + const ast = currentProgram.getSourceFile(options.filePath); + return ast && { ast, program: currentProgram }; } - } + ); } /** From d4435ea4a4919669f7686987e4bf2d061b97a7b0 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Mon, 5 Nov 2018 11:26:53 -0800 Subject: [PATCH 26/30] fix: don't try to convert symbols --- lib/convert.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/convert.js b/lib/convert.js index a2a8789..7899fa1 100644 --- a/lib/convert.js +++ b/lib/convert.js @@ -362,7 +362,12 @@ module.exports = function convert(config) { } else { if (Array.isArray(node[key])) { result[key] = node[key].map(convertChild); - } else if (node[key] && typeof node[key] === 'object') { + } else if ( + node[key] && + typeof node[key] === 'object' && + node[key].kind + ) { + // need to check node[key].kind to ensure we don't try to convert a symbol result[key] = convertChild(node[key]); } else { result[key] = node[key]; From 89787c45655aeb7e2695f22c42e9a8e5e4b246f9 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Mon, 5 Nov 2018 15:15:48 -0800 Subject: [PATCH 27/30] test: update semanticInfo baseline to be path-agnostic --- tests/lib/__snapshots__/semanticInfo.ts.snap | 4 ---- tests/lib/semanticInfo.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/lib/__snapshots__/semanticInfo.ts.snap b/tests/lib/__snapshots__/semanticInfo.ts.snap index 4e1b249..5af1b41 100644 --- a/tests/lib/__snapshots__/semanticInfo.ts.snap +++ b/tests/lib/__snapshots__/semanticInfo.ts.snap @@ -1,7 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`semanticInfo fail to read project file 1`] = `"File 'E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo' not found."`; - exports[`semanticInfo fixtures/export-file.src 1`] = ` Object { "body": Array [ @@ -1136,5 +1134,3 @@ Object { `; exports[`semanticInfo malformed project file 1`] = `"Compiler option 'compileOnSave' requires a value of type boolean."`; - -exports[`semanticInfo non-existent project file 1`] = `"File 'E:\\\\typescript-estree\\\\tests\\\\fixtures\\\\semanticInfo\\\\tsconfigs.json' not found."`; diff --git a/tests/lib/semanticInfo.ts b/tests/lib/semanticInfo.ts index 436b76f..3e50bce 100644 --- a/tests/lib/semanticInfo.ts +++ b/tests/lib/semanticInfo.ts @@ -147,18 +147,18 @@ describe('semanticInfo', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = './tsconfigs.json'; - expect(() => - parseCode(shelljs.cat(fileName), badConfig) - ).toThrowErrorMatchingSnapshot(); + expect(() => parseCode(shelljs.cat(fileName), badConfig)).toThrowError( + /File .+tsconfigs\.json' not found/ + ); }); test('fail to read project file', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = '.'; - expect(() => - parseCode(shelljs.cat(fileName), badConfig) - ).toThrowErrorMatchingSnapshot(); + expect(() => parseCode(shelljs.cat(fileName), badConfig)).toThrowError( + /File .+semanticInfo' not found/ + ); }); test('malformed project file', () => { From 5d6708afc24001bf2efc2c4984ad9e1e7c5ae119 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 8 Nov 2018 20:53:18 -0800 Subject: [PATCH 28/30] refactor: rename tsconfig root directory option and respond to cr --- src/node-utils.ts | 2 +- src/parser.ts | 30 +++++++++++++--------------- src/temp-types-based-on-js-source.ts | 5 +++-- src/tsconfig-parser.ts | 29 +++++++++++++-------------- tests/lib/semanticInfo.ts | 3 +-- 5 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/node-utils.ts b/src/node-utils.ts index 0d5e8dd..873db9c 100644 --- a/src/node-utils.ts +++ b/src/node-utils.ts @@ -410,7 +410,7 @@ function hasStaticModifierFlag(node: ts.Node): boolean { * Had to copy this from TS instead of using TS's version because theirs doesn't pass the ast to getChildren * @param {ts.Token} previousToken The previous TSToken * @param {ts.Node} parent The parent TSNode - * @param {ts.SourceFileLike} ast The TS AST + * @param {ts.SourceFile} ast The TS AST * @returns {ts.Token} the next TSToken */ function findNextToken( diff --git a/src/parser.ts b/src/parser.ts index fe6b538..0b353ff 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -12,7 +12,7 @@ import convert from './ast-converter'; import util from './node-utils'; import { Extra, ParserOptions } from './temp-types-based-on-js-source'; -import packageJSON from '../package.json'; +const packageJSON: typeof import('../package.json') = require('../package.json'); const SUPPORTED_TYPESCRIPT_VERSIONS = packageJSON.devDependencies.typescript; const ACTIVE_TYPESCRIPT_VERSION = ts.version; @@ -39,9 +39,10 @@ function resetExtra(): void { jsx: false, useJSXTextNode: false, log: console.log, - project: [], + projects: [], errorOnUnknownASTType: false, - code: '' + code: '', + tsconfigRootDir: process.cwd() }; } @@ -52,7 +53,7 @@ function resetExtra(): void { */ function getASTFromProject(code: string, options: ParserOptions) { return util.firstDefined( - calculateProjectParserOptions(code, options, extra.project), + calculateProjectParserOptions(code, options.filePath, extra), (currentProgram: ts.Program) => { const ast = currentProgram.getSourceFile(options.filePath); return ast && { ast, program: currentProgram }; @@ -114,7 +115,7 @@ function createNewProgram(code: string) { compilerHost ); - const ast = /** @type {ts.SourceFile} */ (program.getSourceFile(FILENAME)); + const ast = program.getSourceFile(FILENAME)!; return { ast, program }; } @@ -172,14 +173,6 @@ function generateAST(code: string, options: ParserOptions): any { extra.jsx = true; } - if ( - typeof options.ecmaFeatures === 'object' && - typeof options.ecmaFeatures.jsx === 'boolean' && - options.ecmaFeatures.jsx - ) { - extra.jsx = true; - } - /** * Allow the user to cause the parser to error if it encounters an unknown AST Node Type * (used in testing). @@ -205,12 +198,16 @@ function generateAST(code: string, options: ParserOptions): any { } if (typeof options.project === 'string') { - extra.project = [options.project]; + extra.projects = [options.project]; } else if ( Array.isArray(options.project) && options.project.every(projectPath => typeof projectPath === 'string') ) { - extra.project = options.project; + extra.projects = options.project; + } + + if (typeof options.tsconfigRootDir === 'string') { + extra.tsconfigRootDir = options.tsconfigRootDir; } } @@ -229,7 +226,8 @@ function generateAST(code: string, options: ParserOptions): any { warnedAboutTSVersion = true; } - const shouldProvideParserServices = extra.project && extra.project.length > 0; + const shouldProvideParserServices = + extra.projects && extra.projects.length > 0; const { ast, program } = getProgramAndAST( code, options, diff --git a/src/temp-types-based-on-js-source.ts b/src/temp-types-based-on-js-source.ts index 4531042..f1afe55 100644 --- a/src/temp-types-based-on-js-source.ts +++ b/src/temp-types-based-on-js-source.ts @@ -66,7 +66,8 @@ export interface Extra { strict: boolean; jsx: boolean; log: Function; - project: string[]; + projects: string[]; + tsconfigRootDir: string; } export interface ParserOptions { @@ -75,10 +76,10 @@ export interface ParserOptions { tokens: boolean; comment: boolean; jsx: boolean; - ecmaFeatures: { jsx: boolean }; errorOnUnknownASTType: boolean; useJSXTextNode: boolean; loggerFn: Function | false; project: string | string[]; filePath: string; + tsconfigRootDir: string; } diff --git a/src/tsconfig-parser.ts b/src/tsconfig-parser.ts index 0441ea4..63d472f 100644 --- a/src/tsconfig-parser.ts +++ b/src/tsconfig-parser.ts @@ -2,7 +2,7 @@ import path from 'path'; import ts from 'typescript'; -import { ParserOptions } from './temp-types-based-on-js-source'; +import { Extra } from './temp-types-based-on-js-source'; //------------------------------------------------------------------------------ // Environment calculation @@ -47,37 +47,36 @@ function diagnosticReporter(diagnostic: ts.Diagnostic): void { const noopFileWatcher = { close: () => {} }; /** - * Calculate project environments using options provided by eslint and paths from config + * Calculate project environments using options provided by consumer and paths from config * @param {string} code The code being linted - * @param {Object} options Options provided by ESLint core - * @param {string} options.cwd The current working directory for the eslint process - * @param {string} options.filePath The path of the file being parsed - * @param {string[]} projects Provided tsconfig paths + * @param {string} filePath The path of the file being parsed + * @param {string} extra.tsconfigRootDir The root directory for relative tsconfig paths + * @param {string[]} extra.project Provided tsconfig paths * @returns {ts.Program[]} The programs corresponding to the supplied tsconfig paths */ export default function calculateProjectParserOptions( code: string, - options: ParserOptions & { cwd?: string }, - projects: string[] + filePath: string, + extra: Extra ): ts.Program[] { const results = []; - const cwd = options.cwd || process.cwd(); + const tsconfigRootDir = extra.tsconfigRootDir; // preserve reference to code and file being linted currentLintOperationState.code = code; - currentLintOperationState.filePath = options.filePath; + currentLintOperationState.filePath = filePath; // Update file version if necessary // TODO: only update when necessary, currently marks as changed on every lint - const watchCallback = watchCallbackTrackingMap.get(options.filePath); + const watchCallback = watchCallbackTrackingMap.get(filePath); if (typeof watchCallback !== 'undefined') { - watchCallback(options.filePath, ts.FileWatcherEventKind.Changed); + watchCallback(filePath, ts.FileWatcherEventKind.Changed); } - for (let tsconfigPath of projects) { - // if absolute paths aren't provided, make relative to cwd + for (let tsconfigPath of extra.projects) { + // if absolute paths aren't provided, make relative to tsconfigRootDir if (!path.isAbsolute(tsconfigPath)) { - tsconfigPath = path.join(cwd, tsconfigPath); + tsconfigPath = path.join(tsconfigRootDir, tsconfigPath); } const existingWatch = knownWatchProgramMap.get(tsconfigPath); diff --git a/tests/lib/semanticInfo.ts b/tests/lib/semanticInfo.ts index 3e50bce..44610a5 100644 --- a/tests/lib/semanticInfo.ts +++ b/tests/lib/semanticInfo.ts @@ -35,12 +35,11 @@ function createOptions(fileName: string): ParserOptions & { cwd?: string } { range: true, tokens: true, comment: true, - ecmaFeatures: { jsx: false }, jsx: false, useJSXTextNode: false, errorOnUnknownASTType: true, filePath: fileName, - cwd: path.join(process.cwd(), FIXTURES_DIR), + tsconfigRootDir: path.join(process.cwd(), FIXTURES_DIR), project: './tsconfig.json', loggerFn: false }; From 3982abf74ee7b48715393f18d057e2d6b7618523 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Thu, 8 Nov 2018 21:01:18 -0800 Subject: [PATCH 29/30] fix: undo resolveJsonModule to avoid changing dist structure --- src/parser.ts | 2 +- tsconfig.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 0b353ff..3436437 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -12,7 +12,7 @@ import convert from './ast-converter'; import util from './node-utils'; import { Extra, ParserOptions } from './temp-types-based-on-js-source'; -const packageJSON: typeof import('../package.json') = require('../package.json'); +const packageJSON = require('../package.json'); const SUPPORTED_TYPESCRIPT_VERSIONS = packageJSON.devDependencies.typescript; const ACTIVE_TYPESCRIPT_VERSION = ts.version; diff --git a/tsconfig.json b/tsconfig.json index 6db20b8..207c6dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,8 +5,7 @@ "declaration": true, "outDir": "./dist", "strict": true, - "esModuleInterop": true, - "resolveJsonModule": true + "esModuleInterop": true }, "include": ["src"] } From 792ee887fcc977f9b02b985f9e916cb87022afc6 Mon Sep 17 00:00:00 2001 From: Benjamin Lichtman Date: Wed, 14 Nov 2018 10:37:45 -0800 Subject: [PATCH 30/30] refactor: move generating services to separate top-level function --- src/ast-converter.ts | 21 +++++++++++++++------ src/convert.ts | 33 ++++++++++++++++++++++++--------- src/parser.ts | 17 +++++++++++++---- tests/ast-alignment/parse.ts | 2 +- tests/lib/parse.ts | 4 ++-- tests/lib/semanticInfo.ts | 29 ++++++++++++++++++----------- tools/test-utils.ts | 17 +++++++++++++---- 7 files changed, 86 insertions(+), 37 deletions(-) diff --git a/src/ast-converter.ts b/src/ast-converter.ts index d3bbd42..04df089 100644 --- a/src/ast-converter.ts +++ b/src/ast-converter.ts @@ -8,6 +8,7 @@ import convert, { getASTMaps, resetASTMaps } from './convert'; import { convertComments } from './convert-comments'; import nodeUtils from './node-utils'; +import ts from 'typescript'; import { Extra } from './temp-types-based-on-js-source'; /** @@ -23,13 +24,17 @@ function convertError(error: any) { ); } -export default (ast: any, extra: Extra) => { +export default ( + ast: ts.SourceFile, + extra: Extra, + shouldProvideParserServices: boolean +) => { /** * The TypeScript compiler produced fundamental parse errors when parsing the * source. */ - if (ast.parseDiagnostics.length) { - throw convertError(ast.parseDiagnostics[0]); + if ((ast as any).parseDiagnostics.length) { + throw convertError((ast as any).parseDiagnostics[0]); } /** @@ -41,7 +46,8 @@ export default (ast: any, extra: Extra) => { ast, additionalOptions: { errorOnUnknownASTType: extra.errorOnUnknownASTType || false, - useJSXTextNode: extra.useJSXTextNode || false + useJSXTextNode: extra.useJSXTextNode || false, + shouldProvideParserServices } }); @@ -59,8 +65,11 @@ export default (ast: any, extra: Extra) => { estree.comments = convertComments(ast, extra.code); } - const astMaps = getASTMaps(); - resetASTMaps(); + let astMaps = undefined; + if (shouldProvideParserServices) { + astMaps = getASTMaps(); + resetASTMaps(); + } return { estree, astMaps }; }; diff --git a/src/convert.ts b/src/convert.ts index 6ee9389..ce612fd 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -24,6 +24,19 @@ export function getASTMaps() { return { esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap }; } +interface ConvertAdditionalOptions { + errorOnUnknownASTType: boolean; + useJSXTextNode: boolean; + shouldProvideParserServices: boolean; +} + +interface ConvertConfig { + node: ts.Node; + parent?: ts.Node | null; + ast: ts.SourceFile; + additionalOptions: ConvertAdditionalOptions; +} + /** * Converts a TypeScript node into an ESTree node * @param {Object} config configuration options for the conversion @@ -33,7 +46,7 @@ export function getASTMaps() { * @param {Object} config.additionalOptions additional options for the conversion * @returns {ESTreeNode|null} the converted ESTreeNode */ -export default function convert(config: any): ESTreeNode | null { +export default function convert(config: ConvertConfig): ESTreeNode | null { const node = config.node as ts.Node; const parent = config.parent; const ast = config.ast; @@ -895,7 +908,7 @@ export default function convert(config: any): ESTreeNode | null { } case SyntaxKind.ComputedPropertyName: - if (parent.kind === SyntaxKind.ObjectLiteralExpression) { + if (parent!.kind === SyntaxKind.ObjectLiteralExpression) { Object.assign(result, { type: AST_NODE_TYPES.Property, key: convertChild((node as any).name), @@ -1000,7 +1013,7 @@ export default function convert(config: any): ESTreeNode | null { (method as any).returnType = convertTypeAnnotation((node as any).type); } - if (parent.kind === SyntaxKind.ObjectLiteralExpression) { + if (parent!.kind === SyntaxKind.ObjectLiteralExpression) { (method as any).params = (node as any).parameters.map(convertChild); Object.assign(result, { @@ -1233,7 +1246,7 @@ export default function convert(config: any): ESTreeNode | null { break; case SyntaxKind.BindingElement: - if (parent.kind === SyntaxKind.ArrayBindingPattern) { + if (parent!.kind === SyntaxKind.ArrayBindingPattern) { const arrayItem = convert({ node: (node as any).name, parent, @@ -1255,7 +1268,7 @@ export default function convert(config: any): ESTreeNode | null { } else { return arrayItem; } - } else if (parent.kind === SyntaxKind.ObjectBindingPattern) { + } else if (parent!.kind === SyntaxKind.ObjectBindingPattern) { if ((node as any).dotDotDotToken) { Object.assign(result, { type: AST_NODE_TYPES.RestElement, @@ -1875,7 +1888,7 @@ export default function convert(config: any): ESTreeNode | null { break; case SyntaxKind.PropertyAccessExpression: - if (nodeUtils.isJSXToken(parent)) { + if (nodeUtils.isJSXToken(parent!)) { const jsxMemberExpression = { type: AST_NODE_TYPES.MemberExpression, object: convertChild((node as any).expression), @@ -1974,7 +1987,7 @@ export default function convert(config: any): ESTreeNode | null { type: AST_NODE_TYPES.Literal, raw: ast.text.slice((result as any).range[0], (result as any).range[1]) }); - if (parent.name && parent.name === node) { + if ((parent as any).name && (parent as any).name === node) { (result as any).value = (node as any).text; } else { (result as any).value = nodeUtils.unescapeStringLiteralText( @@ -2524,8 +2537,10 @@ export default function convert(config: any): ESTreeNode | null { deeplyCopy(); } - tsNodeToESTreeNodeMap.set(node, result); - esTreeNodeToTSNodeMap.set(result, node); + if (additionalOptions.shouldProvideParserServices) { + tsNodeToESTreeNodeMap.set(node, result); + esTreeNodeToTSNodeMap.set(result, node); + } return result as any; } diff --git a/src/parser.ts b/src/parser.ts index 3436437..1a48475 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -144,10 +144,15 @@ function getProgramAndAST( /** * Parses the given source code to produce a valid AST * @param {string} code TypeScript code + * @param {boolean} shouldGenerateServices Flag determining whether to generate ast maps and program or not * @param {ParserOptions} options configuration object for the parser * @returns {Object} the AST */ -function generateAST(code: string, options: ParserOptions): any { +function generateAST( + code: string, + options: ParserOptions, + shouldGenerateServices = false +): any { const toString = String; if (typeof code !== 'string' && !((code as any) instanceof String)) { @@ -227,7 +232,7 @@ function generateAST(code: string, options: ParserOptions): any { } const shouldProvideParserServices = - extra.projects && extra.projects.length > 0; + shouldGenerateServices && extra.projects && extra.projects.length > 0; const { ast, program } = getProgramAndAST( code, options, @@ -235,7 +240,7 @@ function generateAST(code: string, options: ParserOptions): any { ); extra.code = code; - const { estree, astMaps } = convert(ast, extra); + const { estree, astMaps } = convert(ast, extra, shouldProvideParserServices); return { estree, program: shouldProvideParserServices ? program : undefined, @@ -255,7 +260,11 @@ export { version }; const version = packageJSON.version; export function parse(code: string, options: ParserOptions) { - const result = generateAST(code, options); + return generateAST(code, options).estree; +} + +export function parseAndGenerateServices(code: string, options: ParserOptions) { + const result = generateAST(code, options, /*shouldGenerateServices*/ true); return { ast: result.estree, services: { diff --git a/tests/ast-alignment/parse.ts b/tests/ast-alignment/parse.ts index 231799b..d875556 100644 --- a/tests/ast-alignment/parse.ts +++ b/tests/ast-alignment/parse.ts @@ -57,7 +57,7 @@ function parseWithTypeScriptESTree( jsx: true }, parserOptions - ) as any).ast; + ) as any); } catch (e) { throw createError(e.message, e.lineNumber, e.column); } diff --git a/tests/lib/parse.ts b/tests/lib/parse.ts index 6a293fc..cbb9beb 100644 --- a/tests/lib/parse.ts +++ b/tests/lib/parse.ts @@ -16,8 +16,8 @@ import { createSnapshotTestBlock } from '../../tools/test-utils'; describe('parse()', () => { describe('basic functionality', () => { it('should parse an empty string', () => { - expect((parser as any).parse('').ast.body).toEqual([]); - expect(parser.parse('', {} as any).ast.body).toEqual([]); + expect((parser as any).parse('').body).toEqual([]); + expect(parser.parse('', {} as any).body).toEqual([]); }); }); diff --git a/tests/lib/semanticInfo.ts b/tests/lib/semanticInfo.ts index 44610a5..5dafc92 100644 --- a/tests/lib/semanticInfo.ts +++ b/tests/lib/semanticInfo.ts @@ -11,7 +11,10 @@ import path from 'path'; import shelljs from 'shelljs'; -import { parseCode, createSnapshotTestBlock } from '../../tools/test-utils'; +import { + parseCodeAndGenerateServices, + createSnapshotTestBlock +} from '../../tools/test-utils'; import ts from 'typescript'; import { ParserOptions } from '../../src/temp-types-based-on-js-source'; @@ -58,14 +61,18 @@ describe('semanticInfo', () => { const code = shelljs.cat(fullFileName); test( `fixtures/${filename}.src`, - createSnapshotTestBlock(code, createOptions(fullFileName)) + createSnapshotTestBlock( + code, + createOptions(fullFileName), + /*generateServices*/ true + ) ); }); // case-specific tests test('isolated-file tests', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); - const parseResult = parseCode( + const parseResult = parseCodeAndGenerateServices( shelljs.cat(fileName), createOptions(fileName) ); @@ -116,7 +123,7 @@ describe('semanticInfo', () => { test('imported-file tests', () => { const fileName = path.resolve(FIXTURES_DIR, 'import-file.src.ts'); - const parseResult = parseCode( + const parseResult = parseCodeAndGenerateServices( shelljs.cat(fileName), createOptions(fileName) ); @@ -146,18 +153,18 @@ describe('semanticInfo', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = './tsconfigs.json'; - expect(() => parseCode(shelljs.cat(fileName), badConfig)).toThrowError( - /File .+tsconfigs\.json' not found/ - ); + expect(() => + parseCodeAndGenerateServices(shelljs.cat(fileName), badConfig) + ).toThrowError(/File .+tsconfigs\.json' not found/); }); test('fail to read project file', () => { const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); const badConfig = createOptions(fileName); badConfig.project = '.'; - expect(() => parseCode(shelljs.cat(fileName), badConfig)).toThrowError( - /File .+semanticInfo' not found/ - ); + expect(() => + parseCodeAndGenerateServices(shelljs.cat(fileName), badConfig) + ).toThrowError(/File .+semanticInfo' not found/); }); test('malformed project file', () => { @@ -165,7 +172,7 @@ describe('semanticInfo', () => { const badConfig = createOptions(fileName); badConfig.project = './badTSConfig/tsconfig.json'; expect(() => - parseCode(shelljs.cat(fileName), badConfig) + parseCodeAndGenerateServices(shelljs.cat(fileName), badConfig) ).toThrowErrorMatchingSnapshot(); }); }); diff --git a/tools/test-utils.ts b/tools/test-utils.ts index 6af353a..d167d49 100644 --- a/tools/test-utils.ts +++ b/tools/test-utils.ts @@ -24,8 +24,11 @@ export function getRaw(ast: any) { ); } -export function parseCode(code: string, config: ParserOptions) { - return parser.parse(code, config); +export function parseCodeAndGenerateServices( + code: string, + config: ParserOptions +) { + return parser.parseAndGenerateServices(code, config); } /** @@ -35,12 +38,18 @@ export function parseCode(code: string, config: ParserOptions) { * @param {ParserOptions} config the parser configuration * @returns {jest.ProvidesCallback} callback for Jest it() block */ -export function createSnapshotTestBlock(code: string, config: ParserOptions) { +export function createSnapshotTestBlock( + code: string, + config: ParserOptions, + generateServices?: true +) { /** * @returns {Object} the AST object */ function parse() { - const ast = parser.parse(code, config).ast; + const ast = generateServices + ? parser.parseAndGenerateServices(code, config).ast + : parser.parse(code, config); return getRaw(ast); }