Path: blob/main/tests/src/core/getExports.js
829 views
import { expect } from 'chai';1import semver from 'semver';2import sinon from 'sinon';3import eslintPkg from 'eslint/package.json';4import * as tsConfigLoader from 'tsconfig-paths/lib/tsconfig-loader';5import ExportMap from '../../../src/ExportMap';67import * as fs from 'fs';89import { getFilename } from '../utils';10import { test as testUnambiguous } from 'eslint-module-utils/unambiguous';1112describe('ExportMap', function () {13const fakeContext = Object.assign(14semver.satisfies(eslintPkg.version, '>= 7.28') ? {15getFilename() { throw new Error('Should call getPhysicalFilename() instead of getFilename()'); },16getPhysicalFilename: getFilename,17} : {18getFilename,19},20{21settings: {},22parserPath: 'babel-eslint',23},24);2526it('handles ExportAllDeclaration', function () {27let imports;28expect(function () {29imports = ExportMap.get('./export-all', fakeContext);30}).not.to.throw(Error);3132expect(imports).to.exist;33expect(imports.has('foo')).to.be.true;3435});3637it('returns a cached copy on subsequent requests', function () {38expect(ExportMap.get('./named-exports', fakeContext))39.to.exist.and.equal(ExportMap.get('./named-exports', fakeContext));40});4142it('does not return a cached copy after modification', (done) => {43const firstAccess = ExportMap.get('./mutator', fakeContext);44expect(firstAccess).to.exist;4546// mutate (update modified time)47const newDate = new Date();48fs.utimes(getFilename('mutator.js'), newDate, newDate, (error) => {49expect(error).not.to.exist;50expect(ExportMap.get('./mutator', fakeContext)).not.to.equal(firstAccess);51done();52});53});5455it('does not return a cached copy with different settings', () => {56const firstAccess = ExportMap.get('./named-exports', fakeContext);57expect(firstAccess).to.exist;5859const differentSettings = Object.assign(60{},61fakeContext,62{ parserPath: 'espree' },63);6465expect(ExportMap.get('./named-exports', differentSettings))66.to.exist.and67.not.to.equal(firstAccess);68});6970it('does not throw for a missing file', function () {71let imports;72expect(function () {73imports = ExportMap.get('./does-not-exist', fakeContext);74}).not.to.throw(Error);7576expect(imports).not.to.exist;7778});7980it('exports explicit names for a missing file in exports', function () {81let imports;82expect(function () {83imports = ExportMap.get('./exports-missing', fakeContext);84}).not.to.throw(Error);8586expect(imports).to.exist;87expect(imports.has('bar')).to.be.true;8889});9091it('finds exports for an ES7 module with babel-eslint', function () {92const path = getFilename('jsx/FooES7.js');93const contents = fs.readFileSync(path, { encoding: 'utf8' });94const imports = ExportMap.parse(95path,96contents,97{ parserPath: 'babel-eslint', settings: {} },98);99100expect(imports, 'imports').to.exist;101expect(imports.errors).to.be.empty;102expect(imports.get('default'), 'default export').to.exist;103expect(imports.has('Bar')).to.be.true;104});105106context('deprecation metadata', function () {107108function jsdocTests(parseContext, lineEnding) {109context('deprecated imports', function () {110let imports;111before('parse file', function () {112const path = getFilename('deprecated.js');113const contents = fs.readFileSync(path, { encoding: 'utf8' }).replace(/[\r]\n/g, lineEnding);114imports = ExportMap.parse(path, contents, parseContext);115116// sanity checks117expect(imports.errors).to.be.empty;118});119120it('works with named imports.', function () {121expect(imports.has('fn')).to.be.true;122123expect(imports.get('fn'))124.to.have.nested.property('doc.tags[0].title', 'deprecated');125expect(imports.get('fn'))126.to.have.nested.property('doc.tags[0].description', 'please use \'x\' instead.');127});128129it('works with default imports.', function () {130expect(imports.has('default')).to.be.true;131const importMeta = imports.get('default');132133expect(importMeta).to.have.nested.property('doc.tags[0].title', 'deprecated');134expect(importMeta).to.have.nested.property('doc.tags[0].description', 'this is awful, use NotAsBadClass.');135});136137it('works with variables.', function () {138expect(imports.has('MY_TERRIBLE_ACTION')).to.be.true;139const importMeta = imports.get('MY_TERRIBLE_ACTION');140141expect(importMeta).to.have.nested.property(142'doc.tags[0].title', 'deprecated');143expect(importMeta).to.have.nested.property(144'doc.tags[0].description', 'please stop sending/handling this action type.');145});146147context('multi-line variables', function () {148it('works for the first one', function () {149expect(imports.has('CHAIN_A')).to.be.true;150const importMeta = imports.get('CHAIN_A');151152expect(importMeta).to.have.nested.property(153'doc.tags[0].title', 'deprecated');154expect(importMeta).to.have.nested.property(155'doc.tags[0].description', 'this chain is awful');156});157it('works for the second one', function () {158expect(imports.has('CHAIN_B')).to.be.true;159const importMeta = imports.get('CHAIN_B');160161expect(importMeta).to.have.nested.property(162'doc.tags[0].title', 'deprecated');163expect(importMeta).to.have.nested.property(164'doc.tags[0].description', 'so awful');165});166it('works for the third one, etc.', function () {167expect(imports.has('CHAIN_C')).to.be.true;168const importMeta = imports.get('CHAIN_C');169170expect(importMeta).to.have.nested.property(171'doc.tags[0].title', 'deprecated');172expect(importMeta).to.have.nested.property(173'doc.tags[0].description', 'still terrible');174});175});176});177178context('full module', function () {179let imports;180before('parse file', function () {181const path = getFilename('deprecated-file.js');182const contents = fs.readFileSync(path, { encoding: 'utf8' });183imports = ExportMap.parse(path, contents, parseContext);184185// sanity checks186expect(imports.errors).to.be.empty;187});188189it('has JSDoc metadata', function () {190expect(imports.doc).to.exist;191});192});193}194195context('default parser', function () {196jsdocTests({197parserPath: 'espree',198parserOptions: {199ecmaVersion: 2015,200sourceType: 'module',201attachComment: true,202},203settings: {},204}, '\n');205jsdocTests({206parserPath: 'espree',207parserOptions: {208ecmaVersion: 2015,209sourceType: 'module',210attachComment: true,211},212settings: {},213}, '\r\n');214});215216context('babel-eslint', function () {217jsdocTests({218parserPath: 'babel-eslint',219parserOptions: {220ecmaVersion: 2015,221sourceType: 'module',222attachComment: true,223},224settings: {},225}, '\n');226jsdocTests({227parserPath: 'babel-eslint',228parserOptions: {229ecmaVersion: 2015,230sourceType: 'module',231attachComment: true,232},233settings: {},234}, '\r\n');235});236});237238context('exported static namespaces', function () {239const espreeContext = { parserPath: 'espree', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, settings: {} };240const babelContext = { parserPath: 'babel-eslint', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, settings: {} };241242it('works with espree & traditional namespace exports', function () {243const path = getFilename('deep/a.js');244const contents = fs.readFileSync(path, { encoding: 'utf8' });245const a = ExportMap.parse(path, contents, espreeContext);246expect(a.errors).to.be.empty;247expect(a.get('b').namespace).to.exist;248expect(a.get('b').namespace.has('c')).to.be.true;249});250251it('captures namespace exported as default', function () {252const path = getFilename('deep/default.js');253const contents = fs.readFileSync(path, { encoding: 'utf8' });254const def = ExportMap.parse(path, contents, espreeContext);255expect(def.errors).to.be.empty;256expect(def.get('default').namespace).to.exist;257expect(def.get('default').namespace.has('c')).to.be.true;258});259260it('works with babel-eslint & ES7 namespace exports', function () {261const path = getFilename('deep-es7/a.js');262const contents = fs.readFileSync(path, { encoding: 'utf8' });263const a = ExportMap.parse(path, contents, babelContext);264expect(a.errors).to.be.empty;265expect(a.get('b').namespace).to.exist;266expect(a.get('b').namespace.has('c')).to.be.true;267});268});269270context('deep namespace caching', function () {271const espreeContext = { parserPath: 'espree', parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, settings: {} };272let a;273before('sanity check and prime cache', function (done) {274// first version275fs.writeFileSync(getFilename('deep/cache-2.js'),276fs.readFileSync(getFilename('deep/cache-2a.js')));277278const path = getFilename('deep/cache-1.js');279const contents = fs.readFileSync(path, { encoding: 'utf8' });280a = ExportMap.parse(path, contents, espreeContext);281expect(a.errors).to.be.empty;282283expect(a.get('b').namespace).to.exist;284expect(a.get('b').namespace.has('c')).to.be.true;285286// wait ~1s, cache check is 1s resolution287setTimeout(function reup() {288fs.unlinkSync(getFilename('deep/cache-2.js'));289// swap in a new file and touch it290fs.writeFileSync(getFilename('deep/cache-2.js'),291fs.readFileSync(getFilename('deep/cache-2b.js')));292done();293}, 1100);294});295296it('works', function () {297expect(a.get('b').namespace.has('c')).to.be.false;298});299300after('remove test file', (done) => fs.unlink(getFilename('deep/cache-2.js'), done));301});302303context('Map API', function () {304context('#size', function () {305306it('counts the names', () => expect(ExportMap.get('./named-exports', fakeContext))307.to.have.property('size', 12));308309it('includes exported namespace size', () => expect(ExportMap.get('./export-all', fakeContext))310.to.have.property('size', 1));311312});313});314315context('issue #210: self-reference', function () {316it(`doesn't crash`, function () {317expect(() => ExportMap.get('./narcissist', fakeContext)).not.to.throw(Error);318});319it(`'has' circular reference`, function () {320expect(ExportMap.get('./narcissist', fakeContext))321.to.exist.and.satisfy(m => m.has('soGreat'));322});323it(`can 'get' circular reference`, function () {324expect(ExportMap.get('./narcissist', fakeContext))325.to.exist.and.satisfy(m => m.get('soGreat') != null);326});327});328329context('issue #478: never parse non-whitelist extensions', function () {330const context = Object.assign({}, fakeContext,331{ settings: { 'import/extensions': ['.js'] } });332333let imports;334before('load imports', function () {335imports = ExportMap.get('./typescript.ts', context);336});337338it('returns nothing for a TypeScript file', function () {339expect(imports).not.to.exist;340});341342});343344context('alternate parsers', function () {345346const configs = [347// ['string form', { 'typescript-eslint-parser': '.ts' }],348];349350if (semver.satisfies(eslintPkg.version, '>5')) {351configs.push(['array form', { '@typescript-eslint/parser': ['.ts', '.tsx'] }]);352}353354if (semver.satisfies(eslintPkg.version, '<6')) {355configs.push(['array form', { 'typescript-eslint-parser': ['.ts', '.tsx'] }]);356}357358configs.forEach(([description, parserConfig]) => {359360describe(description, function () {361const context = Object.assign({}, fakeContext,362{ settings: {363'import/extensions': ['.js'],364'import/parsers': parserConfig,365} });366367let imports;368before('load imports', function () {369this.timeout(20000); // takes a long time :shrug:370sinon.spy(tsConfigLoader, 'tsConfigLoader');371imports = ExportMap.get('./typescript.ts', context);372});373after('clear spies', function () {374tsConfigLoader.tsConfigLoader.restore();375});376377it('returns something for a TypeScript file', function () {378expect(imports).to.exist;379});380381it('has no parse errors', function () {382expect(imports).property('errors').to.be.empty;383});384385it('has exported function', function () {386expect(imports.has('getFoo')).to.be.true;387});388389it('has exported typedef', function () {390expect(imports.has('MyType')).to.be.true;391});392393it('has exported enum', function () {394expect(imports.has('MyEnum')).to.be.true;395});396397it('has exported interface', function () {398expect(imports.has('Foo')).to.be.true;399});400401it('has exported abstract class', function () {402expect(imports.has('Bar')).to.be.true;403});404405it('should cache tsconfig until tsconfigRootDir parser option changes', function () {406const customContext = Object.assign(407{},408context,409{410parserOptions: {411tsconfigRootDir: null,412},413},414);415expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(0);416ExportMap.parse('./baz.ts', 'export const baz = 5', customContext);417expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1);418ExportMap.parse('./baz.ts', 'export const baz = 5', customContext);419expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1);420421const differentContext = Object.assign(422{},423context,424{425parserOptions: {426tsconfigRootDir: process.cwd(),427},428},429);430431ExportMap.parse('./baz.ts', 'export const baz = 5', differentContext);432expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(2);433});434});435});436});437438// todo: move to utils439describe('unambiguous regex', function () {440const testFiles = [441['deep/b.js', true],442['bar.js', true],443['deep-es7/b.js', true],444['common.js', false],445];446447for (const [testFile, expectedRegexResult] of testFiles) {448it(`works for ${testFile} (${expectedRegexResult})`, function () {449const content = fs.readFileSync('./tests/files/' + testFile, 'utf8');450expect(testUnambiguous(content)).to.equal(expectedRegexResult);451});452}453});454});455456457