From 498b25a698deb98f8960290c596da75dc85207d8 Mon Sep 17 00:00:00 2001 From: Michael Blaszczyk Date: Thu, 12 Nov 2020 17:13:36 +1100 Subject: [PATCH] [Fix] `import/no-cycle`: fix perf regression Fixes #1943. --- src/ExportMap.js | 27 ++++++++++--- .../typescript-export-assign-property.ts | 3 ++ tests/src/core/getExports.js | 40 ++++++++++++++++++- 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 tests/files/typescript-export-assign-property.ts diff --git a/src/ExportMap.js b/src/ExportMap.js index 9ffe7ac8d6..215f3de716 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -22,6 +22,7 @@ let parseConfigFileTextToJson; const log = debug('eslint-plugin-import:ExportMap'); const exportCache = new Map(); +const tsConfigCache = new Map(); export default class ExportMap { constructor(path) { @@ -438,9 +439,11 @@ ExportMap.parse = function (path, content, context) { const source = makeSourceCode(content, ast); - function isEsModuleInterop() { + function readTsConfig() { const tsConfigInfo = tsConfigLoader({ - cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), + cwd: + (context.parserOptions && context.parserOptions.tsconfigRootDir) || + process.cwd(), getEnv: (key) => process.env[key], }); try { @@ -450,12 +453,26 @@ ExportMap.parse = function (path, content, context) { // this is because projects not using TypeScript won't have typescript installed ({ parseConfigFileTextToJson } = require('typescript')); } - const tsConfig = parseConfigFileTextToJson(tsConfigInfo.tsConfigPath, jsonText).config; - return tsConfig.compilerOptions.esModuleInterop; + return parseConfigFileTextToJson(tsConfigInfo.tsConfigPath, jsonText).config; } } catch (e) { - return false; + // Catch any errors } + + return null; + } + + function isEsModuleInterop() { + const cacheKey = hashObject({ + tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, + }).digest('hex'); + let tsConfig = tsConfigCache.get(cacheKey); + if (typeof tsConfig === 'undefined') { + tsConfig = readTsConfig(); + tsConfigCache.set(cacheKey, tsConfig); + } + + return tsConfig !== null ? tsConfig.compilerOptions.esModuleInterop : false; } ast.body.forEach(function (n) { diff --git a/tests/files/typescript-export-assign-property.ts b/tests/files/typescript-export-assign-property.ts new file mode 100644 index 0000000000..8dc2b9981e --- /dev/null +++ b/tests/files/typescript-export-assign-property.ts @@ -0,0 +1,3 @@ +const AnalyticsNode = { Analytics: {} }; + +export = AnalyticsNode.Analytics; diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 523b0ef4cf..5684d7cdf6 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -1,6 +1,8 @@ import { expect } from 'chai'; import semver from 'semver'; +import sinon from 'sinon'; import eslintPkg from 'eslint/package.json'; +import * as tsConfigLoader from 'tsconfig-paths/lib/tsconfig-loader'; import ExportMap from '../../../src/ExportMap'; import * as fs from 'fs'; @@ -51,7 +53,8 @@ describe('ExportMap', function () { const differentSettings = Object.assign( {}, fakeContext, - { parserPath: 'espree' }); + { parserPath: 'espree' }, + ); expect(ExportMap.get('./named-exports', differentSettings)) .to.exist.and @@ -358,8 +361,12 @@ describe('ExportMap', function () { let imports; before('load imports', function () { this.timeout(20000); // takes a long time :shrug: + sinon.spy(tsConfigLoader, 'tsConfigLoader'); imports = ExportMap.get('./typescript.ts', context); }); + after('clear spies', function () { + tsConfigLoader.tsConfigLoader.restore(); + }); it('returns something for a TypeScript file', function () { expect(imports).to.exist; @@ -388,9 +395,38 @@ describe('ExportMap', function () { it('has exported abstract class', function () { expect(imports.has('Bar')).to.be.true; }); + + it('should cache tsconfig until tsconfigRootDir parser option changes', function () { + const customContext = Object.assign( + {}, + context, + { + parserOptions: { + tsconfigRootDir: null, + }, + }, + ); + expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(0); + ExportMap.parse('./baz.ts', 'export const baz = 5', customContext); + expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1); + ExportMap.parse('./baz.ts', 'export const baz = 5', customContext); + expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1); + + const differentContext = Object.assign( + {}, + context, + { + parserOptions: { + tsconfigRootDir: process.cwd(), + }, + }, + ); + + ExportMap.parse('./baz.ts', 'export const baz = 5', differentContext); + expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(2); + }); }); }); - }); // todo: move to utils