diff --git a/src/ExportMap.js b/src/ExportMap.js index 66b212a21..9954b098d 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -266,14 +266,14 @@ function captureTomDoc(comments) { } } -ExportMap.get = function (source, context) { +ExportMap.get = function (source, context, options) { const path = resolve(source, context) if (path == null) return null - return ExportMap.for(childContext(path, context)) + return ExportMap.for(childContext(path, context), options) } -ExportMap.for = function (context) { +ExportMap.for = function (context, options = {}) { const { path } = context const cacheKey = hashObject(context).digest('hex') @@ -300,14 +300,14 @@ ExportMap.for = function (context) { const content = fs.readFileSync(path, { encoding: 'utf8' }) // check for and cache ignore - if (isIgnored(path, context) || !unambiguous.test(content)) { + if (isIgnored(path, context) || (!options.useCommonjsExports && !unambiguous.test(content))) { log('ignored path due to unambiguous regex or ignore settings:', path) exportCache.set(cacheKey, null) return null } log('cache miss', cacheKey, 'for path', path) - exportMap = ExportMap.parse(path, content, context) + exportMap = ExportMap.parse(path, content, context, options) // ambiguous modules return null if (exportMap == null) return null @@ -319,7 +319,9 @@ ExportMap.for = function (context) { } -ExportMap.parse = function (path, content, context) { +ExportMap.parse = function (path, content, context, options = {}) { + log('using commonjs exports:', options.useCommonjsExports) + var m = new ExportMap(path) try { @@ -330,7 +332,7 @@ ExportMap.parse = function (path, content, context) { return m // can't continue } - if (!unambiguous.isModule(ast)) return null + if (!options.useCommonjsExports && !unambiguous.isModule(ast)) return null const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'] const docStyleParsers = {} @@ -362,7 +364,7 @@ ExportMap.parse = function (path, content, context) { function resolveImport(value) { const rp = remotePath(value) if (rp == null) return null - return ExportMap.for(childContext(rp, context)) + return ExportMap.for(childContext(rp, context), options) } function getNamespace(identifier) { @@ -390,7 +392,7 @@ ExportMap.parse = function (path, content, context) { const existing = m.imports.get(p) if (existing != null) return existing.getter - const getter = () => ExportMap.for(childContext(p, context)) + const getter = () => ExportMap.for(childContext(p, context), options) m.imports.set(p, { getter, source: { // capturing actual node reference holds full AST in memory! @@ -401,8 +403,118 @@ ExportMap.parse = function (path, content, context) { return getter } + // for saving all commonjs exports + let moduleExports = {} + + // for if module exports has been declared directly (exports/module.exports = ...) + let moduleExportsMain = null + + function parseModuleExportsObjectExpression(node) { + moduleExportsMain = true + moduleExports = {} + node.properties.forEach( + function(property) { + const keyType = property.key.type + + if (keyType === 'Identifier') { + const keyName = property.key.name + moduleExports[keyName] = property.value + } + else if (keyType === 'Literal') { + const keyName = property.key.value + moduleExports[keyName] = property.value + } + } + ) + } + + function handleModuleExports() { + let isEsModule = false + const esModule = moduleExports.__esModule + if (esModule && esModule.type === 'Literal' && esModule.value) { + // for interopRequireDefault calls + } + + Object.getOwnPropertyNames(moduleExports).forEach(function (propertyName) { + m.namespace.set(propertyName) + }) + + if (!isEsModule && moduleExportsMain && !options.noInterop) { + // recognizes default for import statements + m.namespace.set('default') + } + } ast.body.forEach(function (n) { + if (options.useCommonjsExports) { + if (n.type === 'ExpressionStatement') { + if (n.expression.type === 'AssignmentExpression') { + const left = n.expression.left + const right = n.expression.right + + // exports/module.exports = ... + if (isCommonjsExportsObject(left)) { + moduleExportsMain = true + + // exports/module.exports = {...} + if (right.type === 'ObjectExpression') { + parseModuleExportsObjectExpression(right) + } + } + else if (left.type === 'MemberExpression' + && isCommonjsExportsObject(left.object)) { + // (exports/module.exports). = ... + if (left.property.type === 'Identifier') { + const keyName = left.property.name + moduleExports[keyName] = right + } + // (exports/module.exports).[""] = ... + else if (left.property.type === 'Literal') { + const keyName = left.property.value + moduleExports[keyName] = right + } + } + else return + } + // Object.defineProperty((exports/module.exports), , {value: }) + else if (n.expression.type === 'CallExpression') { + const call = n.expression + + const callee = call.callee + if (callee.type !== 'MemberExpression') return + if (callee.object.type !== 'Identifier' || callee.object.name !== 'Object') return + if (callee.property.type !== 'Identifier' || callee.property.name !== 'defineProperty') return + + if (call.arguments.length !== 3) return + if (!isCommonjsExportsObject(call.arguments[0])) return + if (call.arguments[1].type !== 'Literal') return + if (call.arguments[2].type !== 'ObjectExpression') return + + call.arguments[2].properties.forEach(function (defineProperty) { + if (defineProperty.type !== 'Property') return + + if (defineProperty.key.type === 'Literal' + && defineProperty.key.value === 'value') { + // {'value': } + Object.defineProperty( + moduleExports, + call.arguments[1].value, + defineProperty.value + ) + } + else if (defineProperty.key.type === 'Identifier' + && defineProperty.key.name === 'value') { + // {value: } + Object.defineProperty( + moduleExports, + call.arguments[1].value, + defineProperty.value + ) + } + }) + } + } + } if (n.type === 'ExportDefaultDeclaration') { const exportMeta = captureDoc(docStyleParsers, n) @@ -483,6 +595,8 @@ ExportMap.parse = function (path, content, context) { } }) + if (options.useCommonjsExports) handleModuleExports() + return m } @@ -527,3 +641,25 @@ function childContext(path, context) { path, } } + +/** + * Check if a given node is exports, module.exports, or module['exports'] + * @param {node} node + * @return {boolean} + */ +function isCommonjsExportsObject(node) { + // exports + if (node.type === 'Identifier' && node.name === 'exports') return true + + if (node.type !== 'MemberExpression') return false + + if (node.object.type === 'Identifier' && node.object.name === 'module') { + // module.exports + if (node.property.type === 'Identifier' && node.property.name === 'exports') return true + + // module['exports'] + if (node.property.type === 'Literal' && node.property.value === 'exports') return true + } + + return false +} diff --git a/src/rules/named.js b/src/rules/named.js index 57e4f1d9e..1edcd4a3b 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -7,9 +7,31 @@ module.exports = { docs: { url: docsUrl('named'), }, + schema : [{ + type: 'object', + properties: { + commonjs: { + oneOf: [ + { type: 'boolean' }, + { + type: 'object', + properties: { + require: { type: 'boolean' }, + exports: { type: 'boolean' }, + }, + }, + ], + }, + }, + additionalProperties: false, + }], }, create: function (context) { + const options = context.options[0] || {} + const { commonjs = {} } = options + const useCommonjsExports = typeof commonjs === 'boolean' ? commonjs : commonjs.exports + function checkSpecifiers(key, type, node) { // ignore local exports and type imports if (node.source == null || node.importKind === 'type') return @@ -19,7 +41,12 @@ module.exports = { return // no named imports/exports } - const imports = Exports.get(node.source.value, context) + const exportsOptions = { + useCommonjsExports, + noInterop: false, // this should only be true when using require() calls + } + + const imports = Exports.get(node.source.value, context, exportsOptions) if (imports == null) return if (imports.errors.length) { diff --git a/tests/files/module-exports-direct.js b/tests/files/module-exports-direct.js new file mode 100644 index 000000000..b55d834ab --- /dev/null +++ b/tests/files/module-exports-direct.js @@ -0,0 +1,5 @@ +exports = { + a: 0, + 1: 1, + 'c': 2, +} diff --git a/tests/files/module-exports-number.js b/tests/files/module-exports-number.js new file mode 100644 index 000000000..caedade49 --- /dev/null +++ b/tests/files/module-exports-number.js @@ -0,0 +1 @@ +module.exports = 5 diff --git a/tests/files/named-default-export-es5.js b/tests/files/named-default-export-es5.js new file mode 100644 index 000000000..e8042f19d --- /dev/null +++ b/tests/files/named-default-export-es5.js @@ -0,0 +1,5 @@ +Object.defineProperty(exports, '__esModule', { value: true }) + +const foo = 'foo' + +exports.default = foo diff --git a/tests/files/named-exports-es5.js b/tests/files/named-exports-es5.js new file mode 100644 index 000000000..04571436b --- /dev/null +++ b/tests/files/named-exports-es5.js @@ -0,0 +1,15 @@ +Object.defineProperty(exports, '__esModule', {value: true}) + +exports.destructuredProp = {} + +module.exports.arrayKeyProp = null + +exports.deepSparseElement = [] + +exports.a = 1 +exports.b = 2 + +const c = 3 +module.exports.d = c + +exports.ExportedClass = class {} diff --git a/tests/files/re-export-default-es5.js b/tests/files/re-export-default-es5.js new file mode 100644 index 000000000..de3e3b437 --- /dev/null +++ b/tests/files/re-export-default-es5.js @@ -0,0 +1,46 @@ +/* compiled with babel */ +'use strict' + +Object.defineProperty(exports, '__esModule', { + value: true, +}) + +var _defaultExport = require('./default-export') + +Object.defineProperty(exports, 'bar', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_defaultExport).default + }, +}) + +var _namedDefaultExport = require('./named-default-export') + +Object.defineProperty(exports, 'foo', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_namedDefaultExport).default + }, +}) + +var _common = require('./common') + +Object.defineProperty(exports, 'common', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_common).default + }, +}) + +var _t = require('./t') + +Object.defineProperty(exports, 't', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_t).default + }, +}) + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj } } + +exports.baz = 'baz? really?' diff --git a/tests/files/re-export-names-es5.js b/tests/files/re-export-names-es5.js new file mode 100644 index 000000000..c80073a13 --- /dev/null +++ b/tests/files/re-export-names-es5.js @@ -0,0 +1,8 @@ +const namedExports = require('./named-exports-es5') + +module.exports = { + __esModule: true, + foo: namedExports.a, + bar: namedExports.b, + baz: 'will it blend?', +} diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index cb1a5b843..c0f952be1 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -33,6 +33,14 @@ ruleTester.run('named', rule, { '// eslint-disable-line named' }), test({ code: 'import { foo, bar } from "./re-export-names"' }), + test({ + code: 'import { foo, bar } from "./re-export-names-es5"', + options: [{ commonjs: { exports: true }}], + }), + test({ + code: 'import { foo, bar } from "./re-export-names-es5"', + options: [{ commonjs: true }], + }), test({ code: 'import { foo, bar } from "./common"' , settings: { 'import/ignore': ['common'] } }), @@ -65,6 +73,11 @@ ruleTester.run('named', rule, { test({ code: 'import { destructuredProp } from "./named-exports"' }), test({ code: 'import { arrayKeyProp } from "./named-exports"' }), test({ code: 'import { deepProp } from "./named-exports"' }), + test({ code: 'import { deepProp, deepSparseElement } from "./named-exports-es5"' }), + test({ + code: 'import { deepProp, deepSparseElement } from "./named-exports-es5"', + options: [{ commonjs: { exports: true } }], + }), test({ code: 'import { deepSparseElement } from "./named-exports"' }), // should ignore imported flow types, even if they don’t exist @@ -154,6 +167,10 @@ ruleTester.run('named', rule, { test({ code: '/*jsnext*/ import { createStore } from "redux"', }), + test({ + code: 'import { createStore } from "redux/lib/index"', + options: [{ commonjs: true }], + }), // ignore is ignored if exports are found test({ code: 'import { foo } from "es6-module"' }), @@ -178,6 +195,17 @@ ruleTester.run('named', rule, { code: 'import { common } from "./re-export-default"', }), + // direct module exports cases + test({ + code: 'import { a, c } from "./module-exports-direct"', + options: [{ commonjs: true }], + }), + + test({ + code: 'import { default as n } from "./module-exports-number"', + options: [{ commonjs: true }], + }), + ...SYNTAX_CASES, ], @@ -207,10 +235,16 @@ ruleTester.run('named', rule, { test({ code: 'import { a } from "./re-export-names"', - options: [2, 'es6-only'], + options: [{ commonjs: true }], errors: [error('a', './re-export-names')], }), + test({ + code: 'import { a } from "./re-export-names-es5"', + options: [{ commonjs: true }], + errors: [error('a', './re-export-names-es5')], + }), + // export tests test({ code: 'export { bar } from "./bar"', @@ -282,11 +316,23 @@ ruleTester.run('named', rule, { settings: { 'import/ignore': [] }, errors: ["createSnorlax not found in 'redux'"], }), + test({ + code: 'import { createSnorlax } from "redux/lib/index"', + settings: { 'import/ignore': [] }, + options: [{ commonjs: true }], + errors: ["createSnorlax not found in 'redux/lib/index'"], + }), // should work without ignore test({ code: '/*jsnext*/ import { createSnorlax } from "redux"', errors: ["createSnorlax not found in 'redux'"], }), + test({ + code: 'import { createSnorlax } from "redux/lib/index"', + options: [{ commonjs: { exports: true } }], + errors: ["createSnorlax not found in 'redux/lib/index'"], + }), + // ignore is ignored if exports are found test({ @@ -306,6 +352,19 @@ ruleTester.run('named', rule, { code: 'import { default as barDefault } from "./re-export"', errors: [`default not found in './re-export'`], }), + + // direct module.exports assignment + test({ + code: 'import { b } from "./module-exports-direct"', + options: [{ commonjs: true }], + errors: [ error('b', './module-exports-direct') ], + }), + + test({ + code: 'import { noExports } from "./module-exports-number"', + options: [{ commonjs: true }], + errors: [ error('noExports', './module-exports-number') ], + }), ], })