From 0b77e3696e803d96090914a739f5147deaac9826 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Fri, 25 Mar 2016 07:10:32 -0400 Subject: [PATCH 01/32] remove es6-* ponyfills --- CHANGELOG.md | 3 +++ package.json | 5 +---- src/core/getExports.js | 3 --- src/core/resolve.js | 3 --- src/rules/export.js | 4 ---- src/rules/namespace.js | 3 --- src/rules/no-deprecated.js | 2 -- src/rules/no-duplicates.js | 4 ---- src/rules/no-unresolved.js | 2 -- tests/src/package.js | 2 -- 10 files changed, 4 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edafaf3a5..bda1c1a8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,9 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ### Changed - Rearranged rule groups in README in preparation for more style guide rules +### Removed +- support for Node 0.10, via `es6-*` ponyfills. Using native Map/Set/Symbol. + ## [1.4.0] - 2016-03-25 ### Added - Resolver plugin interface v2: more explicit response format that more clearly covers the found-but-core-module case, where there is no path. diff --git a/package.json b/package.json index 0631b5675..d11f48011 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,4 @@ -{ +.{ "name": "eslint-plugin-import", "version": "1.10.1", "description": "Import with sanity.", @@ -68,9 +68,6 @@ "builtin-modules": "^1.1.1", "contains-path": "^0.1.0", "doctrine": "1.2.x", - "es6-map": "^0.1.3", - "es6-set": "^0.1.4", - "es6-symbol": "*", "eslint-import-resolver-node": "^0.2.0", "lodash.cond": "^4.3.0", "lodash.endswith": "^4.0.1", diff --git a/src/core/getExports.js b/src/core/getExports.js index 00a0a273b..2c0e5c562 100644 --- a/src/core/getExports.js +++ b/src/core/getExports.js @@ -1,6 +1,3 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' - import * as fs from 'fs' import { createHash } from 'crypto' diff --git a/src/core/resolve.js b/src/core/resolve.js index 1f561f948..6a9454c5a 100644 --- a/src/core/resolve.js +++ b/src/core/resolve.js @@ -1,6 +1,3 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' -import Set from 'es6-set' import assign from 'object-assign' import pkgDir from 'pkg-dir' diff --git a/src/rules/export.js b/src/rules/export.js index 9263b0ae3..bbf73225f 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,7 +1,3 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' -import Set from 'es6-set' - import ExportMap, { recursivePatternCapture } from '../core/getExports' module.exports = function (context) { diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 32ae15a07..a71023684 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -1,6 +1,3 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' - import Exports from '../core/getExports' import importDeclaration from '../importDeclaration' import declaredScope from '../core/declaredScope' diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index 882f5e48c..9cc8274ff 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,5 +1,3 @@ -import Map from 'es6-map' - import Exports from '../core/getExports' import declaredScope from '../core/declaredScope' diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index fe6afb094..5b2aa42e4 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -1,7 +1,3 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' -import Set from 'es6-set' - import resolve from '../core/resolve' function checkImports(imported, context) { diff --git a/src/rules/no-unresolved.js b/src/rules/no-unresolved.js index 8afd1613d..49db0a0a0 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -3,8 +3,6 @@ * @author Ben Mosher */ -import 'es6-symbol/implement' - import resolve from '../core/resolve' module.exports = function (context) { diff --git a/tests/src/package.js b/tests/src/package.js index 290a1f7a0..137d9c3ea 100644 --- a/tests/src/package.js +++ b/tests/src/package.js @@ -1,5 +1,3 @@ -import 'es6-symbol/implement' - var expect = require('chai').expect var path = require('path') From d1dab58f866a92a73ce58b6582c26561d729e157 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Fri, 25 Mar 2016 07:11:03 -0400 Subject: [PATCH 02/32] travis: remove Node 0.10 --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index aae667d50..504c00e82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: node_js node_js: - - 0.10 - # - 0.12 # assume 0.12 works if 0.10 does. - 4 - 6 From 693be1788c1eeacdd832ba621e8dac7bc64559aa Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Mon, 28 Mar 2016 08:26:15 -0400 Subject: [PATCH 03/32] first draft: eslint-module-utils as a separate package --- memo-parser/index.js | 6 +- package.json | 5 +- src/core/hash.js | 42 -- src/core/importType.js | 2 +- src/rules/default.js | 2 +- src/rules/export.js | 2 +- src/rules/extensions.js | 2 +- src/rules/named.js | 2 +- src/rules/namespace.js | 4 +- src/rules/no-deprecated.js | 4 +- src/rules/no-duplicates.js | 2 +- src/rules/no-named-as-default-member.js | 2 +- src/rules/no-named-as-default.js | 2 +- src/rules/no-restricted-paths.js | 2 +- src/rules/no-unresolved.js | 2 +- tests/src/core/getExports.js | 7 +- tests/src/core/parse.js | 2 +- tests/src/core/resolve.js | 2 +- src/core/getExports.js => utils/ExportMap.js | 421 ++++++++++--------- {src/core => utils}/declaredScope.js | 5 +- utils/hash.js | 59 +++ {src/core => utils}/ignore.js | 24 +- {src/core => utils}/module-require.js | 11 +- utils/package.json | 25 ++ {src/core => utils}/parse.js | 15 +- {src/core => utils}/resolve.js | 43 +- 26 files changed, 377 insertions(+), 318 deletions(-) delete mode 100644 src/core/hash.js rename src/core/getExports.js => utils/ExportMap.js (51%) rename {src/core => utils}/declaredScope.js (72%) create mode 100644 utils/hash.js rename {src/core => utils}/ignore.js (52%) rename {src/core => utils}/module-require.js (75%) create mode 100644 utils/package.json rename {src/core => utils}/parse.js (53%) rename {src/core => utils}/resolve.js (85%) diff --git a/memo-parser/index.js b/memo-parser/index.js index 6d6d3cb08..d8296ac37 100644 --- a/memo-parser/index.js +++ b/memo-parser/index.js @@ -1,8 +1,8 @@ "use strict" const crypto = require('crypto') - , moduleRequire = require('../lib/core/module-require').default - , hashObject = require('../lib/core/hash').hashObject + , moduleRequire = require('eslint-module-utils/module-require').default + , hashObject = require('eslint-module-utils/hash').hashObject const cache = new Map() @@ -22,7 +22,7 @@ exports.parse = function parse(content, options) { const keyHash = crypto.createHash('sha256') keyHash.update(content) - hashObject(keyHash, options) + hashObject(options, keyHash) const key = keyHash.digest('hex') diff --git a/package.json b/package.json index d11f48011..8c5d30917 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,4 @@ -.{ +{ "name": "eslint-plugin-import", "version": "1.10.1", "description": "Import with sanity.", @@ -51,6 +51,7 @@ "eslint": "3.x", "eslint-import-resolver-node": "file:./resolvers/node", "eslint-import-resolver-webpack": "file:./resolvers/webpack", + "eslint-module-utils": "file:./utils", "glob": "^6.0.2", "gulp": "^3.9.0", "gulp-babel": "6.1.2", @@ -73,8 +74,6 @@ "lodash.endswith": "^4.0.1", "lodash.find": "^4.3.0", "lodash.findindex": "^4.3.0", - "object-assign": "^4.0.1", - "pkg-dir": "^1.0.0", "pkg-up": "^1.0.0" } } diff --git a/src/core/hash.js b/src/core/hash.js deleted file mode 100644 index 10e8535ae..000000000 --- a/src/core/hash.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * utilities for hashing config objects. - * basically iteratively updates hash with a JSON-like format - */ - -const stringify = JSON.stringify - -export default function hashify(hash, value) { - if (value instanceof Array) { - hashArray(hash, value) - } else if (value instanceof Object) { - hashObject(hash, value) - } else { - hash.update(stringify(value) || 'undefined') - } - - return hash -} - -export function hashArray(hash, array) { - hash.update('[') - for (let i = 0; i < array.length; i++) { - hashify(hash, array[i]) - hash.update(',') - } - hash.update(']') - - return hash -} - -export function hashObject(hash, object) { - hash.update('{') - Object.keys(object).sort().forEach(key => { - hash.update(stringify(key)) - hash.update(':') - hashify(hash, object[key]) - hash.update(',') - }) - hash.update('}') - - return hash -} diff --git a/src/core/importType.js b/src/core/importType.js index 86f01bc89..ad1b5686f 100644 --- a/src/core/importType.js +++ b/src/core/importType.js @@ -2,7 +2,7 @@ import cond from 'lodash.cond' import builtinModules from 'builtin-modules' import { join } from 'path' -import resolve from './resolve' +import resolve from 'eslint-module-utils/resolve' function constant(value) { return () => value diff --git a/src/rules/default.js b/src/rules/default.js index 4aa5b965f..5fe550bc3 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -1,4 +1,4 @@ -import Exports from '../core/getExports' +import Exports from 'eslint-module-utils/ExportMap' module.exports = function (context) { diff --git a/src/rules/export.js b/src/rules/export.js index bbf73225f..f1d21b0a9 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,4 +1,4 @@ -import ExportMap, { recursivePatternCapture } from '../core/getExports' +import ExportMap, { recursivePatternCapture } from 'eslint-module-utils/ExportMap' module.exports = function (context) { const named = new Map() diff --git a/src/rules/extensions.js b/src/rules/extensions.js index f8bb86e18..a1a3c31a4 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -1,7 +1,7 @@ import path from 'path' import endsWith from 'lodash.endswith' -import resolve from '../core/resolve' +import resolve from 'eslint-module-utils/resolve' import { isBuiltIn } from '../core/importType' module.exports = function (context) { diff --git a/src/rules/named.js b/src/rules/named.js index ca3b0ebb5..ba960a2b3 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -1,5 +1,5 @@ import * as path from 'path' -import Exports from '../core/getExports' +import Exports from 'eslint-module-utils/ExportMap' module.exports = function (context) { function checkSpecifiers(key, type, node) { diff --git a/src/rules/namespace.js b/src/rules/namespace.js index a71023684..649955bce 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -1,6 +1,6 @@ -import Exports from '../core/getExports' +import Exports from 'eslint-module-utils/ExportMap' import importDeclaration from '../importDeclaration' -import declaredScope from '../core/declaredScope' +import declaredScope from 'eslint-module-utils/declaredScope' module.exports = function (context) { diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index 9cc8274ff..b0bffca08 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,5 +1,5 @@ -import Exports from '../core/getExports' -import declaredScope from '../core/declaredScope' +import Exports from 'eslint-module-utils/ExportMap' +import declaredScope from 'eslint-module-utils/declaredScope' module.exports = function (context) { const deprecated = new Map() diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 5b2aa42e4..af5747018 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -1,4 +1,4 @@ -import resolve from '../core/resolve' +import resolve from 'eslint-module-utils/resolve' function checkImports(imported, context) { for (let [module, nodes] of imported.entries()) { diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index 36ae643c2..d861f934b 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -8,7 +8,7 @@ import 'es6-symbol/implement' import Map from 'es6-map' -import Exports from '../core/getExports' +import Exports from 'eslint-module-utils/ExportMap' import importDeclaration from '../importDeclaration' //------------------------------------------------------------------------------ diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index a6a3ffd52..7786d878a 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -1,4 +1,4 @@ -import Exports from '../core/getExports' +import Exports from 'eslint-module-utils/ExportMap' import importDeclaration from '../importDeclaration' module.exports = function (context) { diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index d07ebd873..9f76b8eb3 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -1,7 +1,7 @@ import containsPath from 'contains-path' import path from 'path' -import resolve from '../core/resolve' +import resolve from 'eslint-module-utils/resolve' import isStaticRequire from '../core/staticRequire' module.exports = function noRestrictedPaths(context) { diff --git a/src/rules/no-unresolved.js b/src/rules/no-unresolved.js index 49db0a0a0..310c88419 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -3,7 +3,7 @@ * @author Ben Mosher */ -import resolve from '../core/resolve' +import resolve from 'eslint-module-utils/resolve' module.exports = function (context) { diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index c9ae6dd6d..0fe71071e 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -1,12 +1,11 @@ -import assign from 'object-assign' import { expect } from 'chai' -import ExportMap from 'core/getExports' +import ExportMap from 'eslint-module-utils/ExportMap' import * as fs from 'fs' import { getFilename } from '../utils' -describe('getExports', function () { +describe('ExportMap', function () { const fakeContext = { getFilename: getFilename, settings: {}, @@ -46,7 +45,7 @@ describe('getExports', function () { const firstAccess = ExportMap.get('./named-exports', fakeContext) expect(firstAccess).to.exist - const differentSettings = assign( + const differentSettings = Object.assign( {}, fakeContext, { parserPath: 'espree' }) diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js index 0f9ba2f64..ccb266985 100644 --- a/tests/src/core/parse.js +++ b/tests/src/core/parse.js @@ -1,6 +1,6 @@ import * as fs from 'fs' import { expect } from 'chai' -import parse from 'core/parse' +import parse from 'eslint-module-utils/parse' import { getFilename } from '../utils' diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index 95025b34d..0273dcda0 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -1,6 +1,6 @@ import { expect } from 'chai' -import resolve, { CASE_INSENSITIVE } from 'core/resolve' +import resolve, { CASE_INSENSITIVE } from 'eslint-module-utils/resolve' import * as fs from 'fs' import * as utils from '../utils' diff --git a/src/core/getExports.js b/utils/ExportMap.js similarity index 51% rename from src/core/getExports.js rename to utils/ExportMap.js index 2c0e5c562..cbd6bc3ba 100644 --- a/src/core/getExports.js +++ b/utils/ExportMap.js @@ -1,13 +1,15 @@ -import * as fs from 'fs' +"use strict" +exports.__esModule = true -import { createHash } from 'crypto' -import * as doctrine from 'doctrine' +const fs = require('fs') -import parse from './parse' -import resolve from './resolve' -import isIgnored from './ignore' +const doctrine = require('doctrine') -import { hashObject } from './hash' +const parse = require('./parse').default +const resolve = require('./resolve').default +const isIgnored = require('./ignore').default + +const hashObject = require('./hash').hashObject const exportCache = new Map() @@ -19,7 +21,7 @@ const exportCache = new Map() */ const hasExports = new RegExp('(^|[\\n;])\\s*export\\s[\\w{*]') -export default class ExportMap { +class ExportMap { constructor(path) { this.path = path this.namespace = new Map() @@ -37,190 +39,6 @@ export default class ExportMap { return size } - static get(source, context) { - - var path = resolve(source, context) - if (path == null) return null - - return ExportMap.for(path, context) - } - - static for(path, context) { - let exportMap - - const cacheKey = hashObject(createHash('sha256'), { - settings: context.settings, - parserPath: context.parserPath, - parserOptions: context.parserOptions, - path, - }).digest('hex') - - exportMap = exportCache.get(cacheKey) - - // return cached ignore - if (exportMap === null) return null - - const stats = fs.statSync(path) - if (exportMap != null) { - // date equality check - if (exportMap.mtime - stats.mtime === 0) { - return exportMap - } - // future: check content equality? - } - - const content = fs.readFileSync(path, { encoding: 'utf8' }) - - // check for and cache ignore - if (isIgnored(path, context) && !hasExports.test(content)) { - exportCache.set(cacheKey, null) - return null - } - - exportMap = ExportMap.parse(path, content, context) - exportMap.mtime = stats.mtime - - exportCache.set(cacheKey, exportMap) - return exportMap - } - - static parse(path, content, context) { - var m = new ExportMap(path) - - try { - var ast = parse(content, context) - } catch (err) { - m.errors.push(err) - return m // can't continue - } - - const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'] - const docStyleParsers = {} - docstyle.forEach(style => { - docStyleParsers[style] = availableDocStyleParsers[style] - }) - - // attempt to collect module doc - ast.comments.some(c => { - if (c.type !== 'Block') return false - try { - const doc = doctrine.parse(c.value, { unwrap: true }) - if (doc.tags.some(t => t.title === 'module')) { - m.doc = doc - return true - } - } catch (err) { /* ignore */ } - return false - }) - - const namespaces = new Map() - - function remotePath(node) { - return resolve.relative(node.source.value, path, context.settings) - } - - function resolveImport(node) { - const rp = remotePath(node) - if (rp == null) return null - return ExportMap.for(rp, context) - } - - function getNamespace(identifier) { - if (!namespaces.has(identifier.name)) return - - return function () { - return resolveImport(namespaces.get(identifier.name)) - } - } - - function addNamespace(object, identifier) { - const nsfn = getNamespace(identifier) - if (nsfn) { - Object.defineProperty(object, 'namespace', { get: nsfn }) - } - - return object - } - - - ast.body.forEach(function (n) { - - if (n.type === 'ExportDefaultDeclaration') { - const exportMeta = captureDoc(docStyleParsers, n) - if (n.declaration.type === 'Identifier') { - addNamespace(exportMeta, n.declaration) - } - m.namespace.set('default', exportMeta) - return - } - - if (n.type === 'ExportAllDeclaration') { - let remoteMap = remotePath(n) - if (remoteMap == null) return - m.dependencies.set(remoteMap, () => ExportMap.for(remoteMap, context)) - return - } - - // capture namespaces in case of later export - if (n.type === 'ImportDeclaration') { - let ns - if (n.specifiers.some(s => s.type === 'ImportNamespaceSpecifier' && (ns = s))) { - namespaces.set(ns.local.name, n) - } - return - } - - if (n.type === 'ExportNamedDeclaration'){ - // capture declaration - if (n.declaration != null) { - switch (n.declaration.type) { - case 'FunctionDeclaration': - case 'ClassDeclaration': - case 'TypeAlias': // flowtype with babel-eslint parser - m.namespace.set(n.declaration.id.name, captureDoc(docStyleParsers, n)) - break - case 'VariableDeclaration': - n.declaration.declarations.forEach((d) => - recursivePatternCapture(d.id, id => - m.namespace.set(id.name, captureDoc(docStyleParsers, d, n)))) - break - } - } - - n.specifiers.forEach((s) => { - const exportMeta = {} - let local - - switch (s.type) { - case 'ExportDefaultSpecifier': - if (!n.source) return - local = 'default' - break - case 'ExportNamespaceSpecifier': - m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { - get() { return resolveImport(n) }, - })) - return - case 'ExportSpecifier': - if (!n.source) { - m.namespace.set(s.exported.name, addNamespace(exportMeta, s.local)) - return - } - // else falls through - default: - local = s.local.name - break - } - - // todo: JSDoc - m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(n) }) - }) - } - }) - - return m - } - /** * Note that this does not check explicitly re-exported names for existence * in the base namespace, but it will expand all `export * from '...'` exports @@ -256,16 +74,18 @@ export default class ExportMap { if (this.namespace.has(name)) return { found: true, path: [this] } if (this.reexports.has(name)) { - const { local, getImport } = this.reexports.get(name) - , imported = getImport() + const reexports = this.reexports.get(name) + , imported = reexports.getImport() // if import is ignored, return explicit 'null' if (imported == null) return { found: true, path: [this] } // safeguard against cycles, only if name matches - if (imported.path === this.path && local === name) return { found: false, path: [this] } + if (imported.path === this.path && reexports.local === name) { + return { found: false, path: [this] } + } - const deep = imported.hasDeep(local) + const deep = imported.hasDeep(reexports.local) deep.path.unshift(this) return deep @@ -297,16 +117,16 @@ export default class ExportMap { if (this.namespace.has(name)) return this.namespace.get(name) if (this.reexports.has(name)) { - const { local, getImport } = this.reexports.get(name) - , imported = getImport() + const reexports = this.reexports.get(name) + , imported = reexports.getImport() // if import is ignored, return explicit 'null' if (imported == null) return null // safeguard against cycles, only if name matches - if (imported.path === this.path && local === name) return undefined + if (imported.path === this.path && reexports.local === name) return undefined - return imported.get(local) + return imported.get(reexports.local) } // default exports must be explicitly re-exported (#328) @@ -331,10 +151,10 @@ export default class ExportMap { this.namespace.forEach((v, n) => callback.call(thisArg, v, n, this)) - this.reexports.forEach(({ getImport, local }, name) => { - const reexported = getImport() + this.reexports.forEach((reexports, name) => { + const reexported = reexports.getImport() // can't look up meta for ignored re-exports (#348) - callback.call(thisArg, reexported && reexported.get(local), name, this) + callback.call(thisArg, reexported && reexported.get(reexports.local), name, this) }) this.dependencies.forEach(dep => dep().forEach((v, n) => @@ -359,8 +179,9 @@ export default class ExportMap { * @param {...[type]} nodes [description] * @return {{doc: object}} */ -function captureDoc(docStyleParsers, ...nodes) { +function captureDoc(docStyleParsers) { const metadata = {} + , nodes = Array.prototype.slice.call(arguments, 1) // 'some' short-circuits on first 'true' nodes.some(n => { @@ -431,6 +252,191 @@ function captureTomDoc(comments) { } } +ExportMap.get = function (source, context) { + const path = resolve(source, context) + if (path == null) return null + + return ExportMap.for(path, context) +} + +ExportMap.for = function (path, context) { + let exportMap + + const cacheKey = hashObject({ + settings: context.settings, + parserPath: context.parserPath, + parserOptions: context.parserOptions, + path, + }).digest('hex') + + exportMap = exportCache.get(cacheKey) + + // return cached ignore + if (exportMap === null) return null + + const stats = fs.statSync(path) + if (exportMap != null) { + // date equality check + if (exportMap.mtime - stats.mtime === 0) { + return exportMap + } + // future: check content equality? + } + + const content = fs.readFileSync(path, { encoding: 'utf8' }) + + // check for and cache ignore + if (isIgnored(path, context) && !hasExports.test(content)) { + exportCache.set(cacheKey, null) + return null + } + + exportMap = ExportMap.parse(path, content, context) + exportMap.mtime = stats.mtime + + exportCache.set(cacheKey, exportMap) + return exportMap +} + +ExportMap.parse = function (path, content, context) { + var m = new ExportMap(path) + + try { + var ast = parse(content, context) + } catch (err) { + m.errors.push(err) + return m // can't continue + } + + const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'] + const docStyleParsers = {} + docstyle.forEach(style => { + docStyleParsers[style] = availableDocStyleParsers[style] + }) + + // attempt to collect module doc + ast.comments.some(c => { + if (c.type !== 'Block') return false + try { + const doc = doctrine.parse(c.value, { unwrap: true }) + if (doc.tags.some(t => t.title === 'module')) { + m.doc = doc + return true + } + } catch (err) { /* ignore */ } + return false + }) + + const namespaces = new Map() + + function remotePath(node) { + return resolve.relative(node.source.value, path, context.settings) + } + + function resolveImport(node) { + const rp = remotePath(node) + if (rp == null) return null + return ExportMap.for(rp, context) + } + + function getNamespace(identifier) { + if (!namespaces.has(identifier.name)) return + + return function () { + return resolveImport(namespaces.get(identifier.name)) + } + } + + function addNamespace(object, identifier) { + const nsfn = getNamespace(identifier) + if (nsfn) { + Object.defineProperty(object, 'namespace', { get: nsfn }) + } + + return object + } + + + ast.body.forEach(function (n) { + + if (n.type === 'ExportDefaultDeclaration') { + const exportMeta = captureDoc(docStyleParsers, n) + if (n.declaration.type === 'Identifier') { + addNamespace(exportMeta, n.declaration) + } + m.namespace.set('default', exportMeta) + return + } + + if (n.type === 'ExportAllDeclaration') { + let remoteMap = remotePath(n) + if (remoteMap == null) return + m.dependencies.set(remoteMap, () => ExportMap.for(remoteMap, context)) + return + } + + // capture namespaces in case of later export + if (n.type === 'ImportDeclaration') { + let ns + if (n.specifiers.some(s => s.type === 'ImportNamespaceSpecifier' && (ns = s))) { + namespaces.set(ns.local.name, n) + } + return + } + + if (n.type === 'ExportNamedDeclaration'){ + // capture declaration + if (n.declaration != null) { + switch (n.declaration.type) { + case 'FunctionDeclaration': + case 'ClassDeclaration': + case 'TypeAlias': // flowtype with babel-eslint parser + m.namespace.set(n.declaration.id.name, captureDoc(docStyleParsers, n)) + break + case 'VariableDeclaration': + n.declaration.declarations.forEach((d) => + recursivePatternCapture(d.id, + id => m.namespace.set(id.name, captureDoc(docStyleParsers, d, n)))) + break + } + } + + n.specifiers.forEach((s) => { + const exportMeta = {} + let local + + switch (s.type) { + case 'ExportDefaultSpecifier': + if (!n.source) return + local = 'default' + break + case 'ExportNamespaceSpecifier': + m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { + get() { return resolveImport(n) }, + })) + return + case 'ExportSpecifier': + if (!n.source) { + m.namespace.set(s.exported.name, addNamespace(exportMeta, s.local)) + return + } + // else falls through + default: + local = s.local.name + break + } + + // todo: JSDoc + m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(n) }) + }) + } + }) + + return m +} +exports.default = ExportMap + + /** * Traverse a pattern/identifier node, calling 'callback' * for each leaf identifier. @@ -438,15 +444,15 @@ function captureTomDoc(comments) { * @param {Function} callback * @return {void} */ -export function recursivePatternCapture(pattern, callback) { +function recursivePatternCapture(pattern, callback) { switch (pattern.type) { case 'Identifier': // base case callback(pattern) break case 'ObjectPattern': - pattern.properties.forEach(({ value }) => { - recursivePatternCapture(value, callback) + pattern.properties.forEach(p => { + recursivePatternCapture(p.value, callback) }) break @@ -458,3 +464,4 @@ export function recursivePatternCapture(pattern, callback) { break } } +exports.recursivePatternCapture = recursivePatternCapture diff --git a/src/core/declaredScope.js b/utils/declaredScope.js similarity index 72% rename from src/core/declaredScope.js rename to utils/declaredScope.js index 11575f4cb..2ef3d19a9 100644 --- a/src/core/declaredScope.js +++ b/utils/declaredScope.js @@ -1,4 +1,7 @@ -export default function declaredScope(context, name) { +"use strict" +exports.__esModule = true + +exports.default = function declaredScope(context, name) { let references = context.getScope().references , i for (i = 0; i < references.length; i++) { diff --git a/utils/hash.js b/utils/hash.js new file mode 100644 index 000000000..0b946a510 --- /dev/null +++ b/utils/hash.js @@ -0,0 +1,59 @@ +/** + * utilities for hashing config objects. + * basically iteratively updates hash with a JSON-like format + */ +"use strict" +exports.__esModule = true + +const createHash = require('crypto').createHash + +const stringify = JSON.stringify + +function hashify(value, hash) { + if (!hash) hash = createHash('sha256') + + if (value instanceof Array) { + hashArray(value, hash) + } else if (value instanceof Object) { + hashObject(value, hash) + } else { + hash.update(stringify(value) || 'undefined') + } + + return hash +} +exports.default = hashify + +function hashArray(array, hash) { + if (!hash) hash = createHash('sha256') + + hash.update('[') + for (let i = 0; i < array.length; i++) { + hashify(array[i], hash) + hash.update(',') + } + hash.update(']') + + return hash +} +hashify.array = hashArray +exports.hashArray = hashArray + +function hashObject(object, hash) { + if (!hash) hash = createHash('sha256') + + hash.update("{") + Object.keys(object).sort().forEach(key => { + hash.update(stringify(key)) + hash.update(':') + hashify(object[key], hash) + hash.update(",") + }) + hash.update('}') + + return hash +} +hashify.object = hashObject +exports.hashObject = hashObject + + diff --git a/src/core/ignore.js b/utils/ignore.js similarity index 52% rename from src/core/ignore.js rename to utils/ignore.js index 50fdc76d7..efb88e151 100644 --- a/src/core/ignore.js +++ b/utils/ignore.js @@ -1,25 +1,27 @@ -import { extname } from 'path' -import Set from 'es6-set' +"use strict" +exports.__esModule = true + +const extname = require('path').extname // one-shot memoized let cachedSet, lastSettings -function validExtensions({ settings }) { - if (cachedSet && settings === lastSettings) { +function validExtensions(context) { + if (cachedSet && context.settings === lastSettings) { return cachedSet } // todo: add 'mjs'? - lastSettings = settings + lastSettings = context.settings // breaking: default to '.js' - // cachedSet = new Set(settings['import/extensions'] || [ '.js' ]) - cachedSet = 'import/extensions' in settings - ? new Set(settings['import/extensions']) + // cachedSet = new Set(context.settings['import/extensions'] || [ '.js' ]) + cachedSet = 'import/extensions' in context.settings + ? new Set(context.settings['import/extensions']) : { has: () => true } // the set of all elements return cachedSet } -export default function ignore(path, context) { +exports.default = function ignore(path, context) { // ignore node_modules by default const ignoreStrings = context.settings['import/ignore'] ? [].concat(context.settings['import/ignore']) @@ -30,8 +32,8 @@ export default function ignore(path, context) { if (ignoreStrings.length === 0) return false - for (var i = 0; i < ignoreStrings.length; i++) { - var regex = new RegExp(ignoreStrings[i]) + for (let i = 0; i < ignoreStrings.length; i++) { + const regex = new RegExp(ignoreStrings[i]) if (regex.test(path)) return true } diff --git a/src/core/module-require.js b/utils/module-require.js similarity index 75% rename from src/core/module-require.js rename to utils/module-require.js index c940c7ae4..9b387ad1a 100644 --- a/src/core/module-require.js +++ b/utils/module-require.js @@ -1,15 +1,18 @@ -import Module from 'module' -import * as path from 'path' +"use strict" +exports.__esModule = true + +const Module = require('module') +const path = require('path') // borrowed from babel-eslint function createModule(filename) { - var mod = new Module(filename) + const mod = new Module(filename) mod.filename = filename mod.paths = Module._nodeModulePaths(path.dirname(filename)) return mod } -export default function moduleRequire(p) { +exports.default = function moduleRequire(p) { try { // attempt to get espree relative to eslint const eslintPath = require.resolve('eslint') diff --git a/utils/package.json b/utils/package.json new file mode 100644 index 000000000..48dfb660a --- /dev/null +++ b/utils/package.json @@ -0,0 +1,25 @@ +{ + "name": "eslint-module-utils", + "version": "1.0.0", + "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/benmosher/eslint-plugin-import.git" + }, + "keywords": [ + "eslint-plugin-import", + "eslint", + "modules", + "esmodules" + ], + "author": "Ben Mosher ", + "license": "MIT", + "bugs": { + "url": "https://github.com/benmosher/eslint-plugin-import/issues" + }, + "homepage": "https://github.com/benmosher/eslint-plugin-import#readme" +} diff --git a/src/core/parse.js b/utils/parse.js similarity index 53% rename from src/core/parse.js rename to utils/parse.js index e30e3da13..d6d661f22 100644 --- a/src/core/parse.js +++ b/utils/parse.js @@ -1,17 +1,20 @@ -import moduleRequire from './module-require' -import assign from 'object-assign' +"use strict" +exports.__esModule = true -export default function (content, context) { +const moduleRequire = require('./module-require').default + +exports.default = function parse(content, context) { if (context == null) throw new Error('need context to parse properly') - let { parserOptions, parserPath } = context + let parserOptions = context.parserOptions + , parserPath = context.parserPath if (!parserPath) throw new Error('parserPath is required!') // hack: espree blows up with frozen options - parserOptions = assign({}, parserOptions) - parserOptions.ecmaFeatures = assign({}, parserOptions.ecmaFeatures) + parserOptions = Object.assign({}, parserOptions) + parserOptions.ecmaFeatures = Object.assign({}, parserOptions.ecmaFeatures) // always attach comments parserOptions.attachComment = true diff --git a/src/core/resolve.js b/utils/resolve.js similarity index 85% rename from src/core/resolve.js rename to utils/resolve.js index 6a9454c5a..a08191335 100644 --- a/src/core/resolve.js +++ b/utils/resolve.js @@ -1,10 +1,15 @@ -import assign from 'object-assign' -import pkgDir from 'pkg-dir' +"use strict" +exports.__esModule = true -import fs from 'fs' -import * as path from 'path' +const pkgDir = require('pkg-dir') -export const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname, 'reSOLVE.js')) +const fs = require('fs') +const path = require('path') + +const hashObject = require('./hash').hashObject + +const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname, 'reSOLVE.js')) +exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS const fileExistsCache = new Map() @@ -12,11 +17,11 @@ function cachePath(cacheKey, result) { fileExistsCache.set(cacheKey, { result, lastSeen: Date.now() }) } -function checkCache(cacheKey, { lifetime }) { +function checkCache(cacheKey, settings) { if (fileExistsCache.has(cacheKey)) { - const { result, lastSeen } = fileExistsCache.get(cacheKey) + const f = fileExistsCache.get(cacheKey) // check fresness - if (Date.now() - lastSeen < (lifetime * 1000)) return result + if (Date.now() - f.lastSeen < (settings.lifetime * 1000)) return f.result } // cache miss return undefined @@ -50,7 +55,7 @@ function fileExistsWithCaseSync(filepath, cacheSettings) { return result } -export function relative(modulePath, sourceFile, settings) { +function relative(modulePath, sourceFile, settings) { return fullResolve(modulePath, sourceFile, settings).path } @@ -60,9 +65,9 @@ function fullResolve(modulePath, sourceFile, settings) { if (coreSet != null && coreSet.has(modulePath)) return { found: true, path: null } const sourceDir = path.dirname(sourceFile) - , cacheKey = sourceDir + hashObject(settings) + modulePath + , cacheKey = sourceDir + hashObject(settings).digest('hex') + modulePath - const cacheSettings = assign({ + const cacheSettings = Object.assign({ lifetime: 30, // seconds }, settings['import/cache']) @@ -109,7 +114,9 @@ function fullResolve(modulePath, sourceFile, settings) { const resolvers = resolverReducer(configResolvers, new Map()) - for (let [name, config] of resolvers) { + for (let pair of resolvers) { + let name = pair[0] + , config = pair[1] const resolver = requireResolver(name, sourceFile) , resolved = withResolver(resolver, config) @@ -127,6 +134,7 @@ function fullResolve(modulePath, sourceFile, settings) { // cache(undefined) return { found: false } } +exports.relative = relative function resolverReducer(resolvers, map) { if (resolvers instanceof Array) { @@ -182,7 +190,7 @@ const erroredContexts = new Set() * null if package is core; * undefined if not found */ -export default function resolve(p, context) { +function resolve(p, context) { try { return relative( p , context.getFilename() @@ -199,11 +207,4 @@ export default function resolve(p, context) { } } resolve.relative = relative - - -import { createHash } from 'crypto' -function hashObject(object) { - const settingsShasum = createHash('sha1') - settingsShasum.update(JSON.stringify(object)) - return settingsShasum.digest('hex') -} +exports.default = resolve From 59999d652516758d1c67b5e8984f664690a3bc79 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Mon, 28 Mar 2016 08:40:28 -0400 Subject: [PATCH 04/32] utils: index.js --- utils/index.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 utils/index.js diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 000000000..76ced79a3 --- /dev/null +++ b/utils/index.js @@ -0,0 +1,4 @@ +// re-export defaults +exports.parse = require('./parse').default +exports.resolve = require('./resolve').default +exports.ExportMap = require('./ExportMap').default From 9fc02d4eaf5a66efd8fb544622e57f1c4c93d60b Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 29 Mar 2016 05:24:46 -0400 Subject: [PATCH 05/32] babel-preset-es2015-argon --- .babelrc | 2 +- package.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.babelrc b/.babelrc index 5427da412..df108b65e 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,4 @@ { - "presets": [ "es2015-loose" ], + "presets": [ "es2015-argon" ], "sourceMaps": "inline" } diff --git a/package.json b/package.json index 8c5d30917..7a333204b 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,7 @@ "homepage": "https://github.com/benmosher/eslint-plugin-import", "devDependencies": { "babel-eslint": "next", - "babel-preset-es2015": "^6.6.0", - "babel-preset-es2015-loose": "^7.0.0", + "babel-preset-es2015-argon": "*", "chai": "^3.4.0", "coveralls": "^2.11.4", "cross-env": "^1.0.7", From 2c14c4ca314d56dd7ce80bee607e8133e84f8323 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 29 Mar 2016 09:28:37 -0400 Subject: [PATCH 06/32] eslint-module-utils/v0.1.0 --- utils/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/package.json b/utils/package.json index 48dfb660a..a22a92fe7 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "1.0.0", + "version": "0.1.0", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "main": "index.js", "scripts": { From 96b5ef9e1a77c60627e65b2cf077b63d127a8fa3 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 29 Mar 2016 09:29:03 -0400 Subject: [PATCH 07/32] pulled out moduleVisitor from no-unresolved, to be used with eslint-plugin-git --- src/rules/no-unresolved.js | 84 +--------------------------- utils/moduleVisitor.js | 109 +++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 81 deletions(-) create mode 100644 utils/moduleVisitor.js diff --git a/src/rules/no-unresolved.js b/src/rules/no-unresolved.js index 310c88419..55c6f342a 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -4,97 +4,19 @@ */ import resolve from 'eslint-module-utils/resolve' +import moduleVisitor, { optionsSchema } from 'eslint-module-utils/moduleVisitor' module.exports = function (context) { - let ignoreRegExps = [] - if (context.options[0] != null && context.options[0].ignore != null) { - ignoreRegExps = context.options[0].ignore.map(p => new RegExp(p)) - } - function checkSourceValue(source) { - if (source == null) return - - if (ignoreRegExps.some(re => re.test(source.value))) return - if (resolve(source.value, context) === undefined) { context.report(source, 'Unable to resolve path to module \'' + source.value + '\'.') } } - // for import-y declarations - function checkSource(node) { - checkSourceValue(node.source) - } - - // for CommonJS `require` calls - // adapted from @mctep: http://git.io/v4rAu - function checkCommon(call) { - if (call.callee.type !== 'Identifier') return - if (call.callee.name !== 'require') return - if (call.arguments.length !== 1) return - - const modulePath = call.arguments[0] - if (modulePath.type !== 'Literal') return - if (typeof modulePath.value !== 'string') return - - checkSourceValue(modulePath) - } - - function checkAMD(call) { - if (call.callee.type !== 'Identifier') return - if (call.callee.name !== 'require' && - call.callee.name !== 'define') return - if (call.arguments.length !== 2) return - - const modules = call.arguments[0] - if (modules.type !== 'ArrayExpression') return - - for (let element of modules.elements) { - if (element.type !== 'Literal') continue - if (typeof element.value !== 'string') continue - - if (element.value === 'require' || - element.value === 'exports') continue // magic modules: http://git.io/vByan - - checkSourceValue(element) - } - } - - const visitors = { - 'ImportDeclaration': checkSource, - 'ExportNamedDeclaration': checkSource, - 'ExportAllDeclaration': checkSource, - } - - if (context.options[0] != null) { - const { commonjs, amd } = context.options[0] - - if (commonjs || amd) { - visitors['CallExpression'] = function (call) { - if (commonjs) checkCommon(call) - if (amd) checkAMD(call) - } - } - } + return moduleVisitor(checkSourceValue, context.options[0]) - return visitors } -module.exports.schema = [ - { - 'type': 'object', - 'properties': { - 'commonjs': { 'type': 'boolean' }, - 'amd': { 'type': 'boolean' }, - 'ignore': { - 'type': 'array', - 'minItems': 1, - 'items': { 'type': 'string' }, - 'uniqueItems': true, - }, - }, - 'additionalProperties': false, - }, -] +module.exports.schema = [ optionsSchema ] diff --git a/utils/moduleVisitor.js b/utils/moduleVisitor.js new file mode 100644 index 000000000..f222479f3 --- /dev/null +++ b/utils/moduleVisitor.js @@ -0,0 +1,109 @@ +"use strict" +exports.__esModule = true + +/** + * Returns an object of node visitors that will call + * 'visitor' with every discovered module path. + * + * todo: correct function prototype for visitor + * @param {Function(String)} visitor [description] + * @param {[type]} options [description] + * @return {object} + */ +exports.default = function visitModules(visitor, options) { + // if esmodule is not explicitly disabled, it is assumed to be enabled + options = Object.assign({ esmodule: true }, options) + + let ignoreRegExps = [] + if (options.ignore != null) { + ignoreRegExps = options.ignore.map(p => new RegExp(p)) + } + + function checkSourceValue(source) { + if (source == null) return //? + + // handle ignore + if (ignoreRegExps.some(re => re.test(source.value))) return + + // fire visitor + visitor(source) + } + + // for import-y declarations + function checkSource(node) { + checkSourceValue(node.source) + } + + // for CommonJS `require` calls + // adapted from @mctep: http://git.io/v4rAu + function checkCommon(call) { + if (call.callee.type !== 'Identifier') return + if (call.callee.name !== 'require') return + if (call.arguments.length !== 1) return + + const modulePath = call.arguments[0] + if (modulePath.type !== 'Literal') return + if (typeof modulePath.value !== 'string') return + + checkSourceValue(modulePath) + } + + function checkAMD(call) { + if (call.callee.type !== 'Identifier') return + if (call.callee.name !== 'require' && + call.callee.name !== 'define') return + if (call.arguments.length !== 2) return + + const modules = call.arguments[0] + if (modules.type !== 'ArrayExpression') return + + for (let element of modules.elements) { + if (element.type !== 'Literal') continue + if (typeof element.value !== 'string') continue + + if (element.value === 'require' || + element.value === 'exports') continue // magic modules: http://git.io/vByan + + checkSourceValue(element) + } + } + + const visitors = {} + if (options.esmodule) { + Object.assign(visitors, { + 'ImportDeclaration': checkSource, + 'ExportNamedDeclaration': checkSource, + 'ExportAllDeclaration': checkSource, + }) + } + + if (options.commonjs || options.amd) { + visitors['CallExpression'] = function (call) { + if (options.commonjs) checkCommon(call) + if (options.amd) checkAMD(call) + } + } + + return visitors +} + +/** + * json schema object for options parameter. can be used to build + * rule options schema object. + * @type {Object} + */ +exports.optionsSchema = { + 'type': 'object', + 'properties': { + 'commonjs': { 'type': 'boolean' }, + 'amd': { 'type': 'boolean' }, + 'esmodule': { 'type': 'boolean' }, + 'ignore': { + 'type': 'array', + 'minItems': 1, + 'items': { 'type': 'string' }, + 'uniqueItems': true, + }, + }, + 'additionalProperties': false, +} From 206d9246d6b73c9ebedac40c2fae22d120b9cc13 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 29 Mar 2016 10:27:50 -0400 Subject: [PATCH 08/32] eslint-module-utils/v0.1.1 --- utils/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/package.json b/utils/package.json index a22a92fe7..7eccc4c00 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "0.1.0", + "version": "0.1.1", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "main": "index.js", "scripts": { From 2c83aafd878056cd2ac3cdc0e767ca1774f3b5c1 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 5 Apr 2016 06:01:38 -0400 Subject: [PATCH 09/32] moved ExportMap back into the import plugin from utils (to avoid doctrine dep) --- {utils => src}/ExportMap.js | 10 +++++----- src/rules/default.js | 2 +- src/rules/export.js | 2 +- src/rules/named.js | 2 +- src/rules/namespace.js | 2 +- src/rules/no-deprecated.js | 2 +- src/rules/no-named-as-default-member.js | 2 +- src/rules/no-named-as-default.js | 2 +- tests/src/core/getExports.js | 2 +- utils/index.js | 4 ---- utils/package.json | 1 - 11 files changed, 13 insertions(+), 18 deletions(-) rename {utils => src}/ExportMap.js (97%) delete mode 100644 utils/index.js diff --git a/utils/ExportMap.js b/src/ExportMap.js similarity index 97% rename from utils/ExportMap.js rename to src/ExportMap.js index cbd6bc3ba..109a8cefc 100644 --- a/utils/ExportMap.js +++ b/src/ExportMap.js @@ -1,15 +1,15 @@ -"use strict" +'use strict' exports.__esModule = true const fs = require('fs') const doctrine = require('doctrine') -const parse = require('./parse').default -const resolve = require('./resolve').default -const isIgnored = require('./ignore').default +const parse = require('eslint-module-utils/parse').default +const resolve = require('eslint-module-utils/resolve').default +const isIgnored = require('eslint-module-utils/ignore').default -const hashObject = require('./hash').hashObject +const hashObject = require('eslint-module-utils/hash').hashObject const exportCache = new Map() diff --git a/src/rules/default.js b/src/rules/default.js index 5fe550bc3..4eaa70ff5 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -1,4 +1,4 @@ -import Exports from 'eslint-module-utils/ExportMap' +import Exports from '../ExportMap' module.exports = function (context) { diff --git a/src/rules/export.js b/src/rules/export.js index f1d21b0a9..61d21b86f 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,4 +1,4 @@ -import ExportMap, { recursivePatternCapture } from 'eslint-module-utils/ExportMap' +import ExportMap, { recursivePatternCapture } from '../ExportMap' module.exports = function (context) { const named = new Map() diff --git a/src/rules/named.js b/src/rules/named.js index ba960a2b3..853331752 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -1,5 +1,5 @@ import * as path from 'path' -import Exports from 'eslint-module-utils/ExportMap' +import Exports from '../ExportMap' module.exports = function (context) { function checkSpecifiers(key, type, node) { diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 649955bce..23a9f0eec 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -1,4 +1,4 @@ -import Exports from 'eslint-module-utils/ExportMap' +import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' import declaredScope from 'eslint-module-utils/declaredScope' diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index b0bffca08..fc5a959ed 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,4 +1,4 @@ -import Exports from 'eslint-module-utils/ExportMap' +import Exports from '../ExportMap' import declaredScope from 'eslint-module-utils/declaredScope' module.exports = function (context) { diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index d861f934b..89c94ea1d 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -8,7 +8,7 @@ import 'es6-symbol/implement' import Map from 'es6-map' -import Exports from 'eslint-module-utils/ExportMap' +import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' //------------------------------------------------------------------------------ diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index 7786d878a..ddee1cc5b 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -1,4 +1,4 @@ -import Exports from 'eslint-module-utils/ExportMap' +import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' module.exports = function (context) { diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 0fe71071e..254100508 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -1,5 +1,5 @@ import { expect } from 'chai' -import ExportMap from 'eslint-module-utils/ExportMap' +import ExportMap from '../../../src/ExportMap' import * as fs from 'fs' diff --git a/utils/index.js b/utils/index.js deleted file mode 100644 index 76ced79a3..000000000 --- a/utils/index.js +++ /dev/null @@ -1,4 +0,0 @@ -// re-export defaults -exports.parse = require('./parse').default -exports.resolve = require('./resolve').default -exports.ExportMap = require('./ExportMap').default diff --git a/utils/package.json b/utils/package.json index 7eccc4c00..df2e60f1a 100644 --- a/utils/package.json +++ b/utils/package.json @@ -2,7 +2,6 @@ "name": "eslint-module-utils", "version": "0.1.1", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", - "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, From cf38d7a3159216d5cbb0499e8fcc5cbd999cd25a Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 5 Apr 2016 06:02:55 -0400 Subject: [PATCH 10/32] bumped module utils to 0.2.0 --- utils/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/package.json b/utils/package.json index df2e60f1a..89398344c 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "0.1.1", + "version": "0.2.0", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" From 25968b846fd77dfc7d295cdcf7a50d25b8ece29e Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 5 Apr 2016 06:28:41 -0400 Subject: [PATCH 11/32] utils: extracted ModuleCache from resolve cache behavior --- utils/ModuleCache.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ utils/package.json | 2 +- utils/resolve.js | 34 +++++++--------------------------- 3 files changed, 52 insertions(+), 28 deletions(-) create mode 100644 utils/ModuleCache.js diff --git a/utils/ModuleCache.js b/utils/ModuleCache.js new file mode 100644 index 000000000..71b2ff215 --- /dev/null +++ b/utils/ModuleCache.js @@ -0,0 +1,44 @@ +"use strict" +exports.__esModule = true + +class ModuleCache { + constructor(map) { + this.map = map || new Map() + } + + /** + * returns value for returning inline + * @param {[type]} cacheKey [description] + * @param {[type]} result [description] + */ + set(cacheKey, result) { + this.map.set(cacheKey, { result, lastSeen: Date.now() }) + return result + } + + get(cacheKey, settings) { + if (this.map.has(cacheKey)) { + const f = this.map.get(cacheKey) + // check fresness + if (Date.now() - f.lastSeen < (settings.lifetime * 1000)) return f.result + } + // cache miss + return undefined + } + +} + +ModuleCache.getSettings = function (settings) { + const cacheSettings = Object.assign({ + lifetime: 30, // seconds + }, settings['import/cache']) + + // parse infinity + if (cacheSettings.lifetime === '∞' || cacheSettings.lifetime === 'Infinity') { + cacheSettings.lifetime = Infinity + } + + return cacheSettings +} + +exports.default = ModuleCache diff --git a/utils/package.json b/utils/package.json index 89398344c..9fff7ed25 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "0.2.0", + "version": "0.2.1", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/utils/resolve.js b/utils/resolve.js index a08191335..44ca5f6e8 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -7,25 +7,12 @@ const fs = require('fs') const path = require('path') const hashObject = require('./hash').hashObject + , ModuleCache = require('./ModuleCache').default const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname, 'reSOLVE.js')) exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS -const fileExistsCache = new Map() - -function cachePath(cacheKey, result) { - fileExistsCache.set(cacheKey, { result, lastSeen: Date.now() }) -} - -function checkCache(cacheKey, settings) { - if (fileExistsCache.has(cacheKey)) { - const f = fileExistsCache.get(cacheKey) - // check fresness - if (Date.now() - f.lastSeen < (settings.lifetime * 1000)) return f.result - } - // cache miss - return undefined -} +const fileExistsCache = new ModuleCache() // http://stackoverflow.com/a/27382838 function fileExistsWithCaseSync(filepath, cacheSettings) { @@ -37,7 +24,7 @@ function fileExistsWithCaseSync(filepath, cacheSettings) { const dir = path.dirname(filepath) - let result = checkCache(filepath, cacheSettings) + let result = fileExistsCache.get(filepath, cacheSettings) if (result != null) return result // base case @@ -51,7 +38,7 @@ function fileExistsWithCaseSync(filepath, cacheSettings) { result = fileExistsWithCaseSync(dir, cacheSettings) } } - cachePath(filepath, result) + fileExistsCache.set(filepath, result) return result } @@ -67,20 +54,13 @@ function fullResolve(modulePath, sourceFile, settings) { const sourceDir = path.dirname(sourceFile) , cacheKey = sourceDir + hashObject(settings).digest('hex') + modulePath - const cacheSettings = Object.assign({ - lifetime: 30, // seconds - }, settings['import/cache']) - - // parse infinity - if (cacheSettings.lifetime === '∞' || cacheSettings.lifetime === 'Infinity') { - cacheSettings.lifetime = Infinity - } + const cacheSettings = ModuleCache.getSettings(settings) - const cachedPath = checkCache(cacheKey, cacheSettings) + const cachedPath = fileExistsCache.get(cacheKey, cacheSettings) if (cachedPath !== undefined) return { found: true, path: cachedPath } function cache(resolvedPath) { - cachePath(cacheKey, resolvedPath) + fileExistsCache.set(cacheKey, resolvedPath) } function withResolver(resolver, config) { From 840745091f6858b751ed744eeab82fdb8d6eb45c Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 5 Apr 2016 07:16:02 -0400 Subject: [PATCH 12/32] officially declare node >=4 dependency --- package.json | 1 + utils/package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7a333204b..a5e48d748 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "eslint-plugin-import", "version": "1.10.1", "description": "Import with sanity.", + "engines": { "node": ">=4" }, "main": "lib/index.js", "directories": { "test": "tests" diff --git a/utils/package.json b/utils/package.json index 9fff7ed25..e6a939189 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,7 +1,8 @@ { "name": "eslint-module-utils", - "version": "0.2.1", + "version": "0.2.2", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", + "engines": { "node": ">=4" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, From d5c4c439baeec46470d217ece0bc4630dd9baa29 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sat, 2 Jul 2016 15:42:06 -0400 Subject: [PATCH 13/32] missed pkg-dir during rebase of core-package --- utils/package.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/utils/package.json b/utils/package.json index e6a939189..396887f4c 100644 --- a/utils/package.json +++ b/utils/package.json @@ -2,7 +2,9 @@ "name": "eslint-module-utils", "version": "0.2.2", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", - "engines": { "node": ">=4" }, + "engines": { + "node": ">=4" + }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, @@ -21,5 +23,8 @@ "bugs": { "url": "https://github.com/benmosher/eslint-plugin-import/issues" }, - "homepage": "https://github.com/benmosher/eslint-plugin-import#readme" + "homepage": "https://github.com/benmosher/eslint-plugin-import#readme", + "dependencies": { + "pkg-dir": "^1.0.0" + } } From 9d6db00b8fd18cac89c2342200187395f150bdd3 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sat, 2 Jul 2016 15:59:57 -0400 Subject: [PATCH 14/32] extra detail about no-duplicates. closes #242 --- docs/rules/no-duplicates.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/rules/no-duplicates.md b/docs/rules/no-duplicates.md index 0a7890472..86260a6ff 100644 --- a/docs/rules/no-duplicates.md +++ b/docs/rules/no-duplicates.md @@ -2,11 +2,19 @@ Reports if a resolved path is imported more than once. +ESLint core has a similar rule ([`no-duplicate-imports`](http://eslint.org/docs/rules/no-duplicate-imports)), but this version +is different in two key ways: + +1. the paths in the source code don't have to exactly match, they just have to point to the same module on the filesystem. (i.e. `./foo` and `./foo.js`) +2. this version distinguishes Flow `type` imports from standard imports. ([#334](https://github.com/benmosher/eslint-plugin-import/pull/334)) + ## Rule Details Valid: ```js import SomeDefaultClass, * as names from './mod' +// Flow `type` import from same module is fine +import type SomeType from './mod' ``` ...whereas here, both `./mod` imports will be reported: @@ -18,6 +26,9 @@ import SomeDefaultClass from './mod' import foo from './some-other-mod' import * as names from './mod' + +// will catch this too, assuming it is the same target module +import { something } from './mod.js' ``` The motivation is that this is likely a result of two developers importing different @@ -26,5 +37,7 @@ locations in the file.) This rule brings both (or n-many) to attention. ## When Not To Use It +If the core ESLint version is good enough (i.e. you're _not_ using Flow and you _are_ using [`import/extensions`](./extensions.md)), keep it and don't use this. + If you like to split up imports across lines or may need to import a default and a namespace, -you may want to disable this rule. +you may not want to enable this rule. From 12ae0a7513f1db3e897520600028a7ac49541b02 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sat, 2 Jul 2016 16:12:25 -0400 Subject: [PATCH 15/32] link to import/order (close #259) --- docs/rules/imports-first.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/rules/imports-first.md b/docs/rules/imports-first.md index c358cfa5d..0d5c762f5 100644 --- a/docs/rules/imports-first.md +++ b/docs/rules/imports-first.md @@ -1,7 +1,7 @@ # imports-first This rule reports any imports that come after non-import -statments. +statements. ## Rule Details @@ -24,6 +24,8 @@ import bar from './bar' import * as _ from 'lodash' // <- reported ``` +If you really want import type ordering, check out [`import/order`]. + Notably, `import`s are hoisted, which means the imported modules will be evaluated before any of the statements interspersed between them. Keeping all `import`s together at the top of the file may prevent surprises resulting from this part of the spec. @@ -55,6 +57,8 @@ enable this rule. ## Further Reading +- [`import/order`]: a major step up from `absolute-first` - Issue [#255] +[`import/order`]: ./order.md [#255]: https://github.com/benmosher/eslint-plugin-import/issues/255 From 633c5ba2aad7e03ba0d1e9fba7e051d5ac77e20d Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sat, 2 Jul 2016 16:19:29 -0400 Subject: [PATCH 16/32] only even try to parse `.js` imported files by default --- CHANGELOG.md | 3 ++- utils/ignore.js | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bda1c1a8d..48017874e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). ## [Unreleased] - +### Breaking +- [`import/extensions`] defaults to `['.js']`. ## [1.10.1] - 2016-07-02 ### Added diff --git a/utils/ignore.js b/utils/ignore.js index efb88e151..8ecc04503 100644 --- a/utils/ignore.js +++ b/utils/ignore.js @@ -10,14 +10,8 @@ function validExtensions(context) { return cachedSet } - // todo: add 'mjs'? lastSettings = context.settings - // breaking: default to '.js' - // cachedSet = new Set(context.settings['import/extensions'] || [ '.js' ]) - cachedSet = 'import/extensions' in context.settings - ? new Set(context.settings['import/extensions']) - : { has: () => true } // the set of all elements - + cachedSet = new Set(context.settings['import/extensions'] || [ '.js' ]) return cachedSet } From 1312560a4e079b95a4e1237eecd1dbca8264c403 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sun, 3 Jul 2016 20:22:07 -0400 Subject: [PATCH 17/32] make linklocal part of the test workflow (for utils, mostly) --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a5e48d748..16eb85caa 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,9 @@ "name": "eslint-plugin-import", "version": "1.10.1", "description": "Import with sanity.", - "engines": { "node": ">=4" }, + "engines": { + "node": ">=4" + }, "main": "lib/index.js", "directories": { "test": "tests" @@ -15,6 +17,7 @@ "scripts": { "watch": "cross-env NODE_PATH=./lib gulp watch-test", "cover": "gulp pretest && cross-env NODE_PATH=./lib istanbul cover --dir reports/coverage _mocha tests/lib/ -- --recursive -R progress", + "pretest": "linklocal", "posttest": "eslint ./src", "test": "cross-env NODE_PATH=./lib gulp test", "test-all": "npm test && for resolver in ./resolvers/*; do cd $resolver && npm test && cd ../..; done", @@ -58,6 +61,7 @@ "gulp-changed": "^1.3.0", "gulp-mocha": "^2.2.0", "istanbul": "^0.4.0", + "linklocal": "^2.6.0", "mocha": "^2.2.1", "redux": "^3.0.4", "rimraf": "2.5.2" From 4f1da9a7301bf808fae2b3507d2d1f5d2ae913e0 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sun, 3 Jul 2016 21:15:22 -0400 Subject: [PATCH 18/32] use Unambiguous JavaScript Grammar to detect/ignore non-ES-module imports. (#268 and #270) --- CHANGELOG.md | 5 ++++- README.md | 17 ++++------------- docs/rules/default.md | 12 +++++++----- docs/rules/named.md | 13 ++++++++----- docs/rules/namespace.md | 13 ++++++++----- src/ExportMap.js | 22 +++++++++++++++++++--- tests/src/rules/default.js | 13 +------------ tests/src/rules/named.js | 36 +++++++++++------------------------- tests/src/rules/namespace.js | 6 +----- utils/ignore.js | 8 ++------ utils/resolve.js | 2 +- 11 files changed, 66 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec0bebe38..689dcf56a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## [Unreleased] ### Breaking -- [`import/extensions`] defaults to `['.js']`. +- [`import/extensions` setting] defaults to `['.js']`. ([#306]) +- [`import/ignore` setting] defaults to nothing, and ambiguous modules are ignored natively. This means importing from CommonJS modules will no longer be reported by [`default`], [`named`], or [`namespace`], regardless of `import/ignore`. ([#270]) ## [1.10.1] - 2016-07-02 ### Added @@ -287,11 +288,13 @@ for info on changes for earlier releases. [#328]: https://github.com/benmosher/eslint-plugin-import/issues/328 [#317]: https://github.com/benmosher/eslint-plugin-import/issues/317 [#313]: https://github.com/benmosher/eslint-plugin-import/issues/313 +[#306]: https://github.com/benmosher/eslint-plugin-import/issues/306 [#286]: https://github.com/benmosher/eslint-plugin-import/issues/286 [#283]: https://github.com/benmosher/eslint-plugin-import/issues/283 [#281]: https://github.com/benmosher/eslint-plugin-import/issues/281 [#275]: https://github.com/benmosher/eslint-plugin-import/issues/275 [#272]: https://github.com/benmosher/eslint-plugin-import/issues/272 +[#270]: https://github.com/benmosher/eslint-plugin-import/issues/270 [#267]: https://github.com/benmosher/eslint-plugin-import/issues/267 [#266]: https://github.com/benmosher/eslint-plugin-import/issues/266 [#216]: https://github.com/benmosher/eslint-plugin-import/issues/216 diff --git a/README.md b/README.md index 34b202466..ecb40dbbd 100644 --- a/README.md +++ b/README.md @@ -199,40 +199,31 @@ You may set the following settings in your `.eslintrc`: A whitelist of file extensions that will be parsed as modules and inspected for `export`s. -This will default to `['.js']` in the next major revision of this plugin, unless -you are using the `react` shared config, in which case it is specified as `['.js', '.jsx']`. +This defaults to `['.js']`, unless you are using the `react` shared config, +in which case it is specified as `['.js', '.jsx']`. Note that this is different from (and likely a subset of) any `import/resolver` extensions settings, which may include `.json`, `.coffee`, etc. which will still factor into the `no-unresolved` rule. -Also, `import/ignore` patterns will overrule this whitelist, so `node_modules` that -end in `.js` will still be ignored by default. +Also, the following `import/ignore` patterns will overrule this whitelist. #### `import/ignore` A list of regex strings that, if matched by a path, will not report the matching module if no `export`s are found. In practice, this means rules other than [`no-unresolved`](./docs/rules/no-unresolved.md#ignore) will not report on any -`import`s with (absolute) paths matching this pattern, _unless_ `export`s were -found when parsing. This allows you to ignore `node_modules` but still properly -lint packages that define a [`jsnext:main`] in `package.json` (Redux, D3's v4 packages, etc.). +`import`s with (absolute filesystem) paths matching this pattern. `no-unresolved` has its own [`ignore`](./docs/rules/no-unresolved.md#ignore) setting. -**Note**: setting this explicitly will replace the default of `node_modules`, so you -may need to include it in your own list if you still want to ignore it. Example: - ```yaml settings: import/ignore: - - node_modules # mostly CommonJS (ignored by default) - \.coffee$ # fraught with parse errors - \.(scss|less|css)$ # can't parse unprocessed CSS modules, either ``` -[`jsnext:main`]: https://github.com/rollup/rollup/wiki/jsnext:main - #### `import/core-modules` An array of additional modules to consider as "core" modules--modules that should diff --git a/docs/rules/default.md b/docs/rules/default.md index b520dbc39..bf40278f0 100644 --- a/docs/rules/default.md +++ b/docs/rules/default.md @@ -6,12 +6,14 @@ export in the imported module. For [ES7], reports if a default is named and exported but is not found in the referenced module. -Note: for modules, the plugin will find exported names (including defaults) +Note: for packages, the plugin will find exported names from [`jsnext:main`], if present in `package.json`. -Redux's npm module includes this key, and thereby is lintable, for example. Otherwise, -the whole `node_modules` folder is ignored by default ([`import/ignore`]) as most published modules are -formatted in CommonJS, which [at time of this writing](https://github.com/benmosher/eslint-plugin-import/issues/13) -is not able to be analyzed for exports. +Redux's npm module includes this key, and thereby is lintable, for example. + +A module path that is [ignored] or not [unambiguously an ES module] will not be reported when imported. + +[ignored]: ../README.md#importignore +[unambiguously an ES module]: https://github.com/bmeck/UnambiguousJavaScriptGrammar ## Rule Details diff --git a/docs/rules/named.md b/docs/rules/named.md index 625079e27..b621310c5 100644 --- a/docs/rules/named.md +++ b/docs/rules/named.md @@ -4,11 +4,14 @@ Verifies that all named imports are part of the set of named exports in the refe For `export`, verifies that all named exports exist in the referenced module. -Note: for modules, the plugin will find exported names from [`jsnext:main`], if present in `package.json`. -Redux's npm module includes this key, and thereby is lintable, for example. Otherwise, -the whole `node_modules` folder is ignored by default ([`import/ignore`]) as most published modules are -formatted in CommonJS, which [at time of this writing](https://github.com/benmosher/eslint-plugin-import/issues/13) -is not able to be analyzed for exports. +Note: for packages, the plugin will find exported names +from [`jsnext:main`], if present in `package.json`. +Redux's npm module includes this key, and thereby is lintable, for example. + +A module path that is [ignored] or not [unambiguously an ES module] will not be reported when imported. + +[ignored]: ../README.md#importignore +[unambiguously an ES module]: https://github.com/bmeck/UnambiguousJavaScriptGrammar ## Rule Details diff --git a/docs/rules/namespace.md b/docs/rules/namespace.md index d88457f94..41cb93552 100644 --- a/docs/rules/namespace.md +++ b/docs/rules/namespace.md @@ -8,11 +8,14 @@ Also, will report for computed references (i.e. `foo["bar"]()`). Reports on assignment to a member of an imported namespace. -Note: for modules, the plugin will find exported names from [`jsnext:main`], if present in `package.json`. -Redux's npm module includes this key, and thereby is lintable, for example. Otherwise, -the whole `node_modules` folder is ignored by default ([`import/ignore`]) as most published modules are -formatted in CommonJS, which [at time of this writing](https://github.com/benmosher/eslint-plugin-import/issues/13) -is not able to be analyzed for exports. +Note: for packages, the plugin will find exported names +from [`jsnext:main`], if present in `package.json`. +Redux's npm module includes this key, and thereby is lintable, for example. + +A module path that is [ignored] or not [unambiguously an ES module] will not be reported when imported. + +[ignored]: ../README.md#importignore +[unambiguously an ES module]: https://github.com/bmeck/UnambiguousJavaScriptGrammar ## Rule Details diff --git a/src/ExportMap.js b/src/ExportMap.js index 109a8cefc..f3a12d4a0 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -14,12 +14,16 @@ const hashObject = require('eslint-module-utils/hash').hashObject const exportCache = new Map() /** - * detect exports without a full parse. + * detect possible imports/exports without a full parse. * used primarily to ignore the import/ignore setting, iif it looks like * there might be something there (i.e., jsnext:main is set). + * + * Not perfect, just a fast way to disqualify large non-ES6 modules and + * avoid a parse. * @type {RegExp} */ -const hasExports = new RegExp('(^|[\\n;])\\s*export\\s[\\w{*]') +const potentiallyUnambiguousModule = + new RegExp(`(?:^|;)\s*(?:export|import)(?:(?:\s+\w)|(?:\s*[{*]))`) class ExportMap { constructor(path) { @@ -286,18 +290,28 @@ ExportMap.for = function (path, context) { const content = fs.readFileSync(path, { encoding: 'utf8' }) // check for and cache ignore - if (isIgnored(path, context) && !hasExports.test(content)) { + if (isIgnored(path, context) && !potentiallyUnambiguousModule.test(content)) { exportCache.set(cacheKey, null) return null } exportMap = ExportMap.parse(path, content, context) + + // ambiguous modules return null + if (exportMap == null) return null + exportMap.mtime = stats.mtime exportCache.set(cacheKey, exportMap) return exportMap } + +const unambiguousNodeType = /^(Exp|Imp)ort.*Declaration$/ +function isUnambiguousModule(ast) { + return ast.body.some(node => unambiguousNodeType.test(node.type)) +} + ExportMap.parse = function (path, content, context) { var m = new ExportMap(path) @@ -308,6 +322,8 @@ ExportMap.parse = function (path, content, context) { return m // can't continue } + if (!isUnambiguousModule(ast)) return null + const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'] const docStyleParsers = {} docstyle.forEach(style => { diff --git a/tests/src/rules/default.js b/tests/src/rules/default.js index 4308d3483..11d7c18d8 100644 --- a/tests/src/rules/default.js +++ b/tests/src/rules/default.js @@ -22,8 +22,7 @@ ruleTester.run('default', rule, { // core modules always have a default test({ code: 'import crypto from "crypto";' }), - test({ code: 'import common from "./common";' - , settings: { 'import/ignore': ['common'] } }), + test({ code: 'import common from "./common";' }), // es7 export syntax test({ code: 'export bar from "./bar"' @@ -75,21 +74,11 @@ ruleTester.run('default', rule, { errors: ["Parse errors in imported module './jsx/FooES7.js': Unexpected token = (6:16)"], }), - test({ - code: 'import crypto from "./common";', - settings: { 'import/ignore': ['foo'] }, - errors: [{ message: 'No default export found in module.' - , type: 'ImportDefaultSpecifier'}]}), test({ code: 'import baz from "./named-exports";', errors: [{ message: 'No default export found in module.' , type: 'ImportDefaultSpecifier'}]}), - test({ - code: 'import bar from "./common";', - errors: [{ message: 'No default export found in module.' - , type: 'ImportDefaultSpecifier'}]}), - test({ code: "import Foo from './jsx/FooES7.js';", errors: ["Parse errors in imported module './jsx/FooES7.js': Unexpected token = (6:16)"], diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index caaed2a89..08918a341 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -40,9 +40,6 @@ ruleTester.run('named', rule, { test({ code: 'import { someThing } from "./test-module"' }), - // node_modules/a only exports 'foo', should be ignored though - test({ code: 'import { zoob } from "a"' }), - // export tests test({ code: 'export { foo } from "./bar"' }), test({ code: 'export { foo as bar } from "./bar"' }), @@ -96,23 +93,24 @@ ruleTester.run('named', rule, { settings: { 'import/ignore': ['common'] }, }), + // ignore CJS by default. always ignore ignore list + test({ code: 'import {a, b, d} from "./common"' }), + test({ + code: 'import { baz } from "./bar"', + settings: { 'import/ignore': ['bar'] }, + }), + test({ + code: 'import { common } from "./re-export-default"', + }), + ...SYNTAX_CASES, ], invalid: [ - test({ code: 'import { zoob } from "a"' - , settings: { 'import/ignore': [] } - , errors: [ error('zoob', 'a') ] }), - test({ code: 'import { somethingElse } from "./test-module"' , errors: [ error('somethingElse', './test-module') ] }), - test({code: 'import {a, b, d} from "./common"', - errors: [ error('a', './common') - , error('b', './common') - , error('d', './common') ]}), - test({code: 'import { baz } from "./bar"', errors: [error('baz', './bar')]}), @@ -126,9 +124,6 @@ ruleTester.run('named', rule, { test({code: 'import { a } from "./default-export"', errors: [error('a', './default-export')]}), - test({code: 'import { a } from "./common"', args: [2, 'es6-only'], - errors: [error('a', './common')]}), - test({code: 'import { ActionTypess } from "./qc"', errors: [error('ActionTypess', './qc')]}), @@ -201,22 +196,13 @@ ruleTester.run('named', rule, { code: 'import { baz } from "es6-module"', errors: ["baz not found in 'es6-module'"], }), - test({ - code: 'import { baz } from "./bar"', - settings: { 'import/ignore': ['bar'] }, - errors: ["baz not found in './bar'"], - }), // issue #251 test({ code: 'import { foo, bar, bap } from "./re-export-default"', errors: ["bap not found in './re-export-default'"], }), - test({ - code: 'import { common } from "./re-export-default"', - // todo: better error message - errors: ["common not found via re-export-default.js -> common.js"], - }), + // #328: * exports do not include default test({ diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index 43e0ea7d4..42a1be0e4 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -27,8 +27,7 @@ const valid = [ ecmaFeatures: { jsx: true }, }, }), - test({ code: "import * as foo from './common';" - , settings: { 'import/ignore': ['common'] } }), + test({ code: "import * as foo from './common';" }), // destructuring namespaces test({ code: 'import * as names from "./named-exports";' + @@ -91,9 +90,6 @@ const valid = [ ] const invalid = [ - test({code: "import * as foo from './common';", - errors: ["No exported names found in module './common'."]}), - test({ code: "import * as names from './named-exports'; " + ' console.log(names.c);' , errors: [error('c', 'names')] }), diff --git a/utils/ignore.js b/utils/ignore.js index 8ecc04503..33863e4f5 100644 --- a/utils/ignore.js +++ b/utils/ignore.js @@ -16,15 +16,11 @@ function validExtensions(context) { } exports.default = function ignore(path, context) { - // ignore node_modules by default - const ignoreStrings = context.settings['import/ignore'] - ? [].concat(context.settings['import/ignore']) - : ['node_modules'] - // check extension whitelist first (cheap) if (!validExtensions(context).has(extname(path))) return true - if (ignoreStrings.length === 0) return false + if (!('import/ignore' in context.settings)) return false + const ignoreStrings = context.settings['import/ignore'] for (let i = 0; i < ignoreStrings.length; i++) { const regex = new RegExp(ignoreStrings[i]) diff --git a/utils/resolve.js b/utils/resolve.js index 44ca5f6e8..19856b22d 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -180,7 +180,7 @@ function resolve(p, context) { if (!erroredContexts.has(context)) { context.report({ message: `Resolve error: ${err.message}`, - loc: { line: 1, col: 0 }, + loc: { line: 1, column: 0 }, }) erroredContexts.add(context) } From 45eab4f728916afbd1b80ceab7cb866f23a5ddf4 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sun, 3 Jul 2016 21:20:18 -0400 Subject: [PATCH 19/32] added debug log when a file is ignored (#306) --- utils/ignore.js | 7 ++++++- utils/package.json | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/utils/ignore.js b/utils/ignore.js index 33863e4f5..d92e789d6 100644 --- a/utils/ignore.js +++ b/utils/ignore.js @@ -3,6 +3,8 @@ exports.__esModule = true const extname = require('path').extname +const log = require('debug')('eslint-plugin-import:utils:ignore') + // one-shot memoized let cachedSet, lastSettings function validExtensions(context) { @@ -24,7 +26,10 @@ exports.default = function ignore(path, context) { for (let i = 0; i < ignoreStrings.length; i++) { const regex = new RegExp(ignoreStrings[i]) - if (regex.test(path)) return true + if (regex.test(path)) { + log(`ignoring ${path}, matched pattern /${ignoreStrings[i]}/`) + return true + } } return false diff --git a/utils/package.json b/utils/package.json index 396887f4c..923820d73 100644 --- a/utils/package.json +++ b/utils/package.json @@ -25,6 +25,7 @@ }, "homepage": "https://github.com/benmosher/eslint-plugin-import#readme", "dependencies": { + "debug": "2.2.0", "pkg-dir": "^1.0.0" } } From a55cd67dabae15ae6f95361e4452f46e0df69ebb Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Thu, 7 Jul 2016 06:48:55 -0400 Subject: [PATCH 20/32] recommended shared config (#402) --- CHANGELOG.md | 6 ++++++ config/react.js | 12 +++++++++++- config/recommended.js | 27 +++++++++++++++++++++++++++ src/index.js | 2 ++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 config/recommended.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 689dcf56a..47f9a86ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ This project adheres to [Semantic Versioning](http://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). ## [Unreleased] +### Added +- `recommended` shared config. Roughly `errors` and `warnings` mixed together, + with some `parserOptions` in the mix. ([#402]) +- `react` shared config: added `jsx: true` to `parserOptions.ecmaFeatures`. + ### Breaking - [`import/extensions` setting] defaults to `['.js']`. ([#306]) - [`import/ignore` setting] defaults to nothing, and ambiguous modules are ignored natively. This means importing from CommonJS modules will no longer be reported by [`default`], [`named`], or [`namespace`], regardless of `import/ignore`. ([#270]) @@ -280,6 +285,7 @@ for info on changes for earlier releases. [#157]: https://github.com/benmosher/eslint-plugin-import/pull/157 [#314]: https://github.com/benmosher/eslint-plugin-import/pull/314 +[#402]: https://github.com/benmosher/eslint-plugin-import/issues/402 [#386]: https://github.com/benmosher/eslint-plugin-import/issues/386 [#373]: https://github.com/benmosher/eslint-plugin-import/issues/373 [#370]: https://github.com/benmosher/eslint-plugin-import/issues/370 diff --git a/config/react.js b/config/react.js index c8bd7ade9..fe1b5f2ec 100644 --- a/config/react.js +++ b/config/react.js @@ -1,8 +1,18 @@ /** - * - adds `.jsx` as an extension + * Adds `.jsx` as an extension, and enables JSX parsing. + * + * Even if _you_ aren't using JSX (or .jsx) directly, if your dependencies + * define jsnext:main and have JSX internally, you may run into problems + * if you don't enable these settings at the top level. */ module.exports = { + settings: { 'import/extensions': ['.js', '.jsx'], }, + + parserOptions: { + ecmaFeatures: { jsx: true }, + }, + } diff --git a/config/recommended.js b/config/recommended.js new file mode 100644 index 000000000..a4f557ef3 --- /dev/null +++ b/config/recommended.js @@ -0,0 +1,27 @@ +/** + * The basics. + * @type {Object} + */ +module.exports = { + rules: { + // analysis/correctness + 'import/no-unresolved': 'error', + 'import/named': 'error', + 'import/namespace': 'error', + 'import/default': 'error', + 'import/export': 'error', + + // red flags (thus, warnings) + 'import/no-named-as-default': 'warn', + 'import/no-named-as-default-member': 'warn', + 'import/no-duplicates': 'warn', + }, + + // need all these for parsing dependencies (even if _your_ code doesn't need + // all of them) + parserOptions: { + sourceType: 'module', + ecmaVersion: 6, + ecmaFeatures: { experimentalObjectRestSpread: true }, + }, +} diff --git a/src/index.js b/src/index.js index 52d5668c5..2f852fe16 100644 --- a/src/index.js +++ b/src/index.js @@ -27,6 +27,8 @@ export const rules = { } export const configs = { + 'recommended': require('../config/recommended'), + 'errors': require('../config/errors'), 'warnings': require('../config/warnings'), From 7b25c1cb95ee18acc1531002fd343e1e6031f9ed Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Thu, 7 Jul 2016 07:16:13 -0400 Subject: [PATCH 21/32] extracted unambiguous JS grammar patterns/predicates to utils module (for reuse) --- src/ExportMap.js | 21 +++------------------ utils/unambiguous.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 utils/unambiguous.js diff --git a/src/ExportMap.js b/src/ExportMap.js index f3a12d4a0..4c60b1b57 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -13,17 +13,7 @@ const hashObject = require('eslint-module-utils/hash').hashObject const exportCache = new Map() -/** - * detect possible imports/exports without a full parse. - * used primarily to ignore the import/ignore setting, iif it looks like - * there might be something there (i.e., jsnext:main is set). - * - * Not perfect, just a fast way to disqualify large non-ES6 modules and - * avoid a parse. - * @type {RegExp} - */ -const potentiallyUnambiguousModule = - new RegExp(`(?:^|;)\s*(?:export|import)(?:(?:\s+\w)|(?:\s*[{*]))`) +const unambiguous = require('eslint-module-utils/unambiguous') class ExportMap { constructor(path) { @@ -290,7 +280,7 @@ ExportMap.for = function (path, context) { const content = fs.readFileSync(path, { encoding: 'utf8' }) // check for and cache ignore - if (isIgnored(path, context) && !potentiallyUnambiguousModule.test(content)) { + if (isIgnored(path, context) && !unambiguous.potentialModulePattern.test(content)) { exportCache.set(cacheKey, null) return null } @@ -307,11 +297,6 @@ ExportMap.for = function (path, context) { } -const unambiguousNodeType = /^(Exp|Imp)ort.*Declaration$/ -function isUnambiguousModule(ast) { - return ast.body.some(node => unambiguousNodeType.test(node.type)) -} - ExportMap.parse = function (path, content, context) { var m = new ExportMap(path) @@ -322,7 +307,7 @@ ExportMap.parse = function (path, content, context) { return m // can't continue } - if (!isUnambiguousModule(ast)) return null + if (!unambiguous.isModule(ast)) return null const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'] const docStyleParsers = {} diff --git a/utils/unambiguous.js b/utils/unambiguous.js new file mode 100644 index 000000000..d4830a253 --- /dev/null +++ b/utils/unambiguous.js @@ -0,0 +1,29 @@ +'use strict' +exports.__esModule = true + +/** + * detect possible imports/exports without a full parse. + * used primarily to ignore the import/ignore setting, iif it looks like + * there might be something there (i.e., jsnext:main is set). + * + * A negative test means that a file is definitely _not_ a module. + * A positive test means it _could_ be. + * + * Not perfect, just a fast way to disqualify large non-ES6 modules and + * avoid a parse. + * @type {RegExp} + */ +exports.potentialModulePattern = + new RegExp(`(?:^|;)\s*(?:export|import)(?:(?:\s+\w)|(?:\s*[{*]))`) + +// future-/Babel-proof at the expense of being a little loose +const unambiguousNodeType = /^(Exp|Imp)ort.*Declaration$/ + +/** + * Given an AST, return true if the AST unambiguously represents a module. + * @param {Program node} ast + * @return {Boolean} + */ +exports.isModule = function isUnambiguousModule(ast) { + return ast.body.some(node => unambiguousNodeType.test(node.type)) +} From 8e35cd93322efc478f205056833a17cd039e1902 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Thu, 7 Jul 2016 09:39:12 -0400 Subject: [PATCH 22/32] imports-first => first (#424) --- CHANGELOG.md | 7 ++++++- README.md | 4 ++-- docs/rules/{imports-first.md => first.md} | 2 +- src/index.js | 5 ++++- src/rules/{imports-first.js => first.js} | 2 +- tests/src/rules/{imports-first.js => first.js} | 6 +++--- 6 files changed, 17 insertions(+), 9 deletions(-) rename docs/rules/{imports-first.md => first.md} (99%) rename src/rules/{imports-first.js => first.js} (99%) rename tests/src/rules/{imports-first.js => first.js} (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f9a86ad..8bdc386b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - [`import/extensions` setting] defaults to `['.js']`. ([#306]) - [`import/ignore` setting] defaults to nothing, and ambiguous modules are ignored natively. This means importing from CommonJS modules will no longer be reported by [`default`], [`named`], or [`namespace`], regardless of `import/ignore`. ([#270]) +### Changed +- `imports-first` is renamed to [`first`]. `imports-first` alias will continue to + exist, but may be removed in a future major release. + ## [1.10.1] - 2016-07-02 ### Added - Officially support ESLint 3.x. (peerDependencies updated to `2.x - 3.x`) @@ -241,7 +245,8 @@ for info on changes for earlier releases. [`no-named-as-default-member`]: ./docs/rules/no-named-as-default-member.md [`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md [`extensions`]: ./docs/rules/extensions.md -[`imports-first`]: ./docs/rules/imports-first.md +[`first`]: ./docs/rules/first.md +[`imports-first`]: ./docs/rules/first.md [`no-nodejs-modules`]: ./docs/rules/no-nodejs-modules.md [`order`]: ./docs/rules/order.md [`named`]: ./docs/rules/named.md diff --git a/README.md b/README.md index ecb40dbbd..2b95e7ead 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a **Style guide:** -* Ensure all imports appear before other statements ([`imports-first`]) +* Ensure all imports appear before other statements ([`first`]) * Report repeated import of the same module in multiple places ([`no-duplicates`]) * Report namespace imports ([`no-namespace`]) * Ensure consistent use of file extension within the import path ([`extensions`]) @@ -61,7 +61,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Enforce a newline after import statements ([`newline-after-import`]) * Prefer a default export if module exports a single name ([`prefer-default-export`]) -[`imports-first`]: ./docs/rules/imports-first.md +[`first`]: ./docs/rules/first.md [`no-duplicates`]: ./docs/rules/no-duplicates.md [`no-namespace`]: ./docs/rules/no-namespace.md [`extensions`]: ./docs/rules/extensions.md diff --git a/docs/rules/imports-first.md b/docs/rules/first.md similarity index 99% rename from docs/rules/imports-first.md rename to docs/rules/first.md index 0d5c762f5..fe84b89f2 100644 --- a/docs/rules/imports-first.md +++ b/docs/rules/first.md @@ -1,4 +1,4 @@ -# imports-first +# first This rule reports any imports that come after non-import statements. diff --git a/src/index.js b/src/index.js index 2f852fe16..724f6d7a1 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,7 @@ export const rules = { 'no-commonjs': require('./rules/no-commonjs'), 'no-amd': require('./rules/no-amd'), 'no-duplicates': require('./rules/no-duplicates'), - 'imports-first': require('./rules/imports-first'), + 'first': require('./rules/first'), 'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'), 'no-nodejs-modules': require('./rules/no-nodejs-modules'), 'order': require('./rules/order'), @@ -24,6 +24,9 @@ export const rules = { // metadata-based 'no-deprecated': require('./rules/no-deprecated'), + + // deprecated aliases to rules + 'imports-first': require('./rules/first'), } export const configs = { diff --git a/src/rules/imports-first.js b/src/rules/first.js similarity index 99% rename from src/rules/imports-first.js rename to src/rules/first.js index 790c81b7f..8a3f37369 100644 --- a/src/rules/imports-first.js +++ b/src/rules/first.js @@ -18,7 +18,7 @@ module.exports = function (context) { } anyExpressions = true - + if (node.type === 'ImportDeclaration') { if (absoluteFirst) { if (/^\./.test(node.source.value)) { diff --git a/tests/src/rules/imports-first.js b/tests/src/rules/first.js similarity index 89% rename from tests/src/rules/imports-first.js rename to tests/src/rules/first.js index 497387269..0a7d5247e 100644 --- a/tests/src/rules/imports-first.js +++ b/tests/src/rules/first.js @@ -1,11 +1,11 @@ import { test } from '../utils' -import { linter, RuleTester } from 'eslint' +import { RuleTester } from 'eslint' const ruleTester = new RuleTester() - , rule = require('rules/imports-first') + , rule = require('rules/first') -ruleTester.run('imports-first', rule, { +ruleTester.run('first', rule, { valid: [ test({ code: "import { x } from './foo'; import { y } from './bar';\ export { x, y }" }) From 7af3d798c473bb324759cea3ed59a3db2f1a75fb Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 12 Jul 2016 20:57:29 -0400 Subject: [PATCH 23/32] re-es6'd ExportMap --- src/ExportMap.js | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/ExportMap.js b/src/ExportMap.js index 4c60b1b57..bd9f2a967 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -1,21 +1,17 @@ -'use strict' -exports.__esModule = true +import fs from 'fs' -const fs = require('fs') +import doctrine from 'doctrine' -const doctrine = require('doctrine') +import parse from 'eslint-module-utils/parse' +import resolve from 'eslint-module-utils/resolve' +import isIgnored from 'eslint-module-utils/ignore' -const parse = require('eslint-module-utils/parse').default -const resolve = require('eslint-module-utils/resolve').default -const isIgnored = require('eslint-module-utils/ignore').default - -const hashObject = require('eslint-module-utils/hash').hashObject +import { hashObject } from 'eslint-module-utils/hash' +import * as unambiguous from 'eslint-module-utils/unambiguous' const exportCache = new Map() -const unambiguous = require('eslint-module-utils/unambiguous') - -class ExportMap { +export default class ExportMap { constructor(path) { this.path = path this.namespace = new Map() @@ -435,7 +431,6 @@ ExportMap.parse = function (path, content, context) { return m } -exports.default = ExportMap /** @@ -445,7 +440,7 @@ exports.default = ExportMap * @param {Function} callback * @return {void} */ -function recursivePatternCapture(pattern, callback) { +export function recursivePatternCapture(pattern, callback) { switch (pattern.type) { case 'Identifier': // base case callback(pattern) @@ -465,4 +460,3 @@ function recursivePatternCapture(pattern, callback) { break } } -exports.recursivePatternCapture = recursivePatternCapture From c533dd213a5afad4487de2d2f64fbe1e72c4719e Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 12 Jul 2016 20:58:07 -0400 Subject: [PATCH 24/32] project: ignore 'coverage' folder --- import.sublime-project | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/import.sublime-project b/import.sublime-project index 2db05e016..9530f9d59 100644 --- a/import.sublime-project +++ b/import.sublime-project @@ -3,7 +3,7 @@ [ { "path": ".", - "folder_exclude_patterns": ["reports", "node_modules", "lib"] + "folder_exclude_patterns": ["coverage", "node_modules", "lib"] } ], "SublimeLinter": From e55263fb2929f0397f122c75c92a1f01280ca1cd Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 12 Jul 2016 21:16:19 -0400 Subject: [PATCH 25/32] first draft of `unambiguous` rule to detect ambiguous script/module (#384) --- src/index.js | 1 + src/rules/unambiguous.js | 29 ++++++++++++++++++ tests/src/rules/unambiguous.js | 56 ++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/rules/unambiguous.js create mode 100644 tests/src/rules/unambiguous.js diff --git a/src/index.js b/src/index.js index 724f6d7a1..bfa3c5c5c 100644 --- a/src/index.js +++ b/src/index.js @@ -21,6 +21,7 @@ export const rules = { 'order': require('./rules/order'), 'newline-after-import': require('./rules/newline-after-import'), 'prefer-default-export': require('./rules/prefer-default-export'), + 'unambiguous': require('./rules/unambiguous'), // metadata-based 'no-deprecated': require('./rules/no-deprecated'), diff --git a/src/rules/unambiguous.js b/src/rules/unambiguous.js new file mode 100644 index 000000000..1fb40f6a0 --- /dev/null +++ b/src/rules/unambiguous.js @@ -0,0 +1,29 @@ +/** + * @fileOverview Report modules that could parse incorrectly as scripts. + * @author Ben Mosher + */ + +import { isModule } from 'eslint-module-utils/unambiguous' + +module.exports = { + meta: {}, + + create: function (context) { + // ignore non-modules + if (context.parserOptions.sourceType !== 'module') { + return {} + } + + return { + Program: function (ast) { + if (!isModule(ast)) { + context.report({ + node: ast, + message: 'This module could be parsed as a valid script.', + }) + } + }, + } + + }, +} diff --git a/tests/src/rules/unambiguous.js b/tests/src/rules/unambiguous.js new file mode 100644 index 000000000..c1a89e829 --- /dev/null +++ b/tests/src/rules/unambiguous.js @@ -0,0 +1,56 @@ +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() + , rule = require('rules/unambiguous') + +ruleTester.run('unambiguous', rule, { + valid: [ + 'function x() {}', + '"use strict"; function y() {}', + + { + code: 'import y from "z"; function x() {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'import * as y from "z"; function x() {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'import { y } from "z"; function x() {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'import z, { y } from "z"; function x() {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'function x() {}; export {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'function x() {}; export { x }', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'function x() {}; export { y } from "z"', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'function x() {}; export * as y from "z"', + parser: 'babel-eslint', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'export function x() {}', + parserOptions: { sourceType: 'module' }, + }, + ], + invalid: [ + { + code: 'function x() {}', + parserOptions: { sourceType: 'module' }, + errors: ['This module could be parsed as a valid script.'], + }, + ], +}) From 9844c62969bf877fd5730929dce78494d4fda92d Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Wed, 20 Jul 2016 07:00:52 -0400 Subject: [PATCH 26/32] #384: `unambiguous` docs + added to `recommended` config as a warning --- README.md | 3 +++ config/recommended.js | 1 + docs/rules/unambiguous.md | 54 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 docs/rules/unambiguous.md diff --git a/README.md b/README.md index 5708c22f2..e9d1d0707 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a **Helpful warnings:** + * Report any invalid exports, i.e. re-export of the same name ([`export`]) * Report use of exported name as identifier of default export ([`no-named-as-default`]) * Report use of exported name as property of default export ([`no-named-as-default-member`]) @@ -43,10 +44,12 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a **Module systems:** +* Report potentially ambiguous parse goal (`script` vs. `module`) ([`unambiguous`]) * Report CommonJS `require` calls and `module.exports` or `exports.*`. ([`no-commonjs`]) * Report AMD `require` and `define` calls. ([`no-amd`]) * No Node.js builtin modules. ([`no-nodejs-modules`]) +[`unambiguous`]: ./docs/rules/unambiguous.md [`no-commonjs`]: ./docs/rules/no-commonjs.md [`no-amd`]: ./docs/rules/no-amd.md [`no-nodejs-modules`]: ./docs/rules/no-nodejs-modules.md diff --git a/config/recommended.js b/config/recommended.js index a4f557ef3..79561271b 100644 --- a/config/recommended.js +++ b/config/recommended.js @@ -15,6 +15,7 @@ module.exports = { 'import/no-named-as-default': 'warn', 'import/no-named-as-default-member': 'warn', 'import/no-duplicates': 'warn', + 'import/unambiguous': 'warn', }, // need all these for parsing dependencies (even if _your_ code doesn't need diff --git a/docs/rules/unambiguous.md b/docs/rules/unambiguous.md new file mode 100644 index 000000000..4a4e056d5 --- /dev/null +++ b/docs/rules/unambiguous.md @@ -0,0 +1,54 @@ +# unambiguous + +Warn if a `module` could be mistakely parsed as a `script` by a consumer leveraging +[Unambiguous JavaScript Grammar] to determine correct parsing goal. + +Will respect the [`parserOptions.sourceType`] from ESLint config, i.e. files parsed +as `script` per that setting will not be reported. + +This plugin uses [Unambiguous JavaScript Grammar] internally to decide whether +dependencies should be parsed as modules and searched for exports matching the +`import`ed names, so it may be beneficial to keep this rule on even if your application +will run in an explicit `module`-only environment. + +## Rule Details + +For files parsed as `module` by ESLint, the following are valid: + +```js +import 'foo' +function x() { return 42 } +``` + +```js +export function x() { return 42 } +``` + +```js +(function x() { return 42 })() +export {} // simple way to mark side-effects-only file as 'module' without any imports/exports +``` + +...whereas the following file would be reported: +```js +(function x() { return 42 })() +``` + +## When Not To Use It + +If your application environment will always know via [some other means](https://github.com/nodejs/node-eps/issues/13) +how to parse, regardless of syntax, you may not need this rule. + +Remember, though, that this plugin uses this strategy internally, so if you were +to `import` from a module with no `import`s or `export`s, this plugin would not +report it as it would not be clear whether it should be considered a `script` or +a `module`. + +## Further Reading + +- [Unambiguous JavaScript Grammar] +- [`parserOptions.sourceType`] +- [node-eps#13](https://github.com/nodejs/node-eps/issues/13) + +[`parserOptions.sourceType`]: http://eslint.org/docs/user-guide/configuring#specifying-parser-options +[Unambiguous JavaScript Grammar]: https://github.com/bmeck/UnambiguousJavaScriptGrammar From 3b2147822f2b0b729465c99cc30d11e5959f0173 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Wed, 20 Jul 2016 07:11:57 -0400 Subject: [PATCH 27/32] fixed(?) goof with babel presets that was negatively impacting tests --- package.json | 2 +- resolvers/webpack/.babelrc | 2 +- resolvers/webpack/package.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 53c626f35..f49f566a8 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "homepage": "https://github.com/benmosher/eslint-plugin-import", "devDependencies": { "babel-eslint": "next", - "babel-preset-es2015-argon": "*", + "babel-preset-es2015-argon": "latest", "babel-plugin-istanbul": "^1.0.3", "babel-register": "6.9.0", "chai": "^3.4.0", diff --git a/resolvers/webpack/.babelrc b/resolvers/webpack/.babelrc index 7a870ac67..eba2945b2 100644 --- a/resolvers/webpack/.babelrc +++ b/resolvers/webpack/.babelrc @@ -1 +1 @@ -{ "presets": ["es2015"] } \ No newline at end of file +{ "presets": ["es2015-argon"] } diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 0b7768dc5..b3afce206 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -40,6 +40,7 @@ "devDependencies": { "chai": "^3.4.1", "mocha": "^2.3.3", - "nyc": "^7.0.0" + "nyc": "^7.0.0", + "babel-preset-es2015-argon": "latest" } } From 814c5119026b85ba3eac16c128af8c1a77811dbe Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Thu, 21 Jul 2016 06:34:18 -0400 Subject: [PATCH 28/32] update UAJSG link --- docs/rules/unambiguous.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/unambiguous.md b/docs/rules/unambiguous.md index 4a4e056d5..52ff127df 100644 --- a/docs/rules/unambiguous.md +++ b/docs/rules/unambiguous.md @@ -51,4 +51,4 @@ a `module`. - [node-eps#13](https://github.com/nodejs/node-eps/issues/13) [`parserOptions.sourceType`]: http://eslint.org/docs/user-guide/configuring#specifying-parser-options -[Unambiguous JavaScript Grammar]: https://github.com/bmeck/UnambiguousJavaScriptGrammar +[Unambiguous JavaScript Grammar]: https://github.com/nodejs/node-eps/blob/master/002-es6-modules.md#51-determining-if-source-is-an-es-module From 88c4b940dd99eb34ce5a0b4f6d9edea12c38ff98 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Wed, 27 Jul 2016 07:44:26 -0400 Subject: [PATCH 29/32] work toward #311, still have a few issues --- src/rules/no-unresolved.js | 34 +++++++++++--- tests/src/core/resolve.js | 77 ++++++++++++++++++-------------- tests/src/rules/no-unresolved.js | 30 +++++++++++-- utils/moduleVisitor.js | 50 ++++++++++++++------- utils/resolve.js | 13 +++--- 5 files changed, 137 insertions(+), 67 deletions(-) diff --git a/src/rules/no-unresolved.js b/src/rules/no-unresolved.js index 55c6f342a..05e9d484a 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -3,15 +3,38 @@ * @author Ben Mosher */ -import resolve from 'eslint-module-utils/resolve' -import moduleVisitor, { optionsSchema } from 'eslint-module-utils/moduleVisitor' +import resolve, { CASE_SENSITIVE_FS, fileExistsWithCaseSync } from 'eslint-module-utils/resolve' +import ModuleCache from 'eslint-module-utils/ModuleCache' +import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor' -module.exports = function (context) { + + +exports.meta = { + schema: [ makeOptionsSchema({ + caseSensitive: { type: 'boolean', default: true }, + })], +} + +exports.create = function (context) { function checkSourceValue(source) { - if (resolve(source.value, context) === undefined) { + const shouldCheckCase = !CASE_SENSITIVE_FS && + (!context.options[0] || context.options[0].caseSensitive !== false) + + const resolvedPath = resolve(source.value, context) + + if (resolvedPath === undefined) { context.report(source, - 'Unable to resolve path to module \'' + source.value + '\'.') + `Unable to resolve path to module '${source.value}'.`) + } + + else if (shouldCheckCase) { + const cacheSettings = ModuleCache.getSettings(context.settings) + if (!fileExistsWithCaseSync(resolvedPath, cacheSettings)) { + context.report(source, + `Casing of ${source.value} does not match the underlying filesystem.`) + } + } } @@ -19,4 +42,3 @@ module.exports = function (context) { } -module.exports.schema = [ optionsSchema ] diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index 4fb47363a..72275e8a0 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -1,6 +1,7 @@ import { expect } from 'chai' -import resolve, { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve' +import resolve, { CASE_SENSITIVE_FS, fileExistsWithCaseSync } from 'eslint-module-utils/resolve' +import ModuleCache from 'eslint-module-utils/ModuleCache' import * as fs from 'fs' import * as utils from '../utils' @@ -26,22 +27,31 @@ describe('resolve', function () { expect(file).to.equal(utils.testFilePath('./jsx/MyCoolComponent.jsx')) }) - it('should test case sensitivity', function () { - // Note the spelling error 'MyUncoolComponent' vs 'MyUnCoolComponent' - var file = resolve( './jsx/MyUncoolComponent' - , utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] }}) - ) - - expect(file, 'path to ./jsx/MyUncoolComponent').to.be.undefined + const caseDescribe = (!CASE_SENSITIVE_FS ? describe : describe.skip) + caseDescribe('case sensitivity', function () { + let file + const testContext = utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] }}) + before('resolve', function () { + file = resolve( + // Note the case difference 'MyUncoolComponent' vs 'MyUnCoolComponent' + './jsx/MyUncoolComponent', testContext) + }) + it('resolves regardless of case', function () { + expect(file, 'path to ./jsx/MyUncoolComponent').to.exist + }) + it('detects case does not match FS', function () { + expect(fileExistsWithCaseSync(file, ModuleCache.getSettings(testContext))) + .to.be.false + }) }) - describe('case cache correctness', function () { + describe('rename cache correctness', function () { const context = utils.testContext({ 'import/cache': { 'lifetime': 1 }, }) const pairs = [ - ['./CaseyKasem.js', './CASEYKASEM.js'], + ['./CaseyKasem.js', './CASEYKASEM2.js'], ] pairs.forEach(([original, changed]) => { @@ -64,33 +74,32 @@ describe('resolve', function () { utils.testFilePath(changed), exists => done(exists ? null : new Error('new file does not exist')))) - // these tests fail on a case-sensitive file system - // because nonexistent files aren't cached - if (!CASE_SENSITIVE_FS) { - it('gets cached values within cache lifetime', function () { - // get cached values initially - expect(resolve(original, context)).to.exist - expect(resolve(changed, context)).not.to.exist - }) + it('gets cached values within cache lifetime', function () { + // get cached values initially + expect(resolve(original, context)).to.exist + }) + + it('gets updated values immediately', function () { + // get cached values initially + expect(resolve(changed, context)).to.exist + }) + + // special behavior for infinity + describe('infinite cache', function () { + this.timeout(1200) + before((done) => setTimeout(done, 1100)) + + const lifetimes = [ '∞', 'Infinity' ] + lifetimes.forEach(inf => { + const infiniteContext = utils.testContext({ + 'import/cache': { 'lifetime': inf }, + }) - // special behavior for infinity - describe('infinite cache', function () { - this.timeout(1200) - before((done) => setTimeout(done, 1100)) - - const lifetimes = [ '∞', 'Infinity' ] - lifetimes.forEach(inf => { - const infiniteContext = utils.testContext({ - 'import/cache': { 'lifetime': inf }, - }) - - it(`lifetime: ${inf} still gets cached values after ~1s`, function () { - expect(resolve(original, infiniteContext)).to.exist - expect(resolve(changed, infiniteContext)).not.to.exist - }) + it(`lifetime: ${inf} still gets cached values after ~1s`, function () { + expect(resolve(original, infiniteContext), original).to.exist }) }) - } + }) describe('finite cache', function () { this.timeout(1200) diff --git a/tests/src/rules/no-unresolved.js b/tests/src/rules/no-unresolved.js index 1389a923f..62ef13017 100644 --- a/tests/src/rules/no-unresolved.js +++ b/tests/src/rules/no-unresolved.js @@ -3,6 +3,8 @@ import * as path from 'path' import assign from 'object-assign' import { test, SYNTAX_CASES } from '../utils' +import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve' + import { RuleTester } from 'eslint' var ruleTester = new RuleTester() @@ -131,10 +133,6 @@ function runResolverTests(resolver) { , errors: ["Unable to resolve path to module './does-not-exist'."], }), - rest({ code: 'import foo from "./jsx/MyUncoolComponent.jsx"' - , errors: ["Unable to resolve path to module './jsx/MyUncoolComponent.jsx'."] }), - - // commonjs setting rest({ code: 'var bar = require("./baz")', @@ -204,6 +202,30 @@ function runResolverTests(resolver) { }), ], }) + + if (!CASE_SENSITIVE_FS) { + ruleTester.run('case sensitivity', rule, { + valid: [ + rest({ // test with explicit flag + code: 'import foo from "./jsx/MyUncoolComponent.jsx"', + options: [{ caseSensitive: false }], + }), + ], + + invalid: [ + rest({ // test default + code: 'import foo from "./jsx/MyUncoolComponent.jsx"', + errors: [`Casing of ./jsx/MyUncoolComponent.jsx does not match the underlying filesystem.`], + }), + rest({ // test with explicit flag + code: 'import foo from "./jsx/MyUncoolComponent.jsx"', + options: [{ caseSensitive: true }], + errors: [`Casing of ./jsx/MyUncoolComponent.jsx does not match the underlying filesystem.`], + }), + ], + }) + } + } ['node', 'webpack'].forEach(runResolverTests) diff --git a/utils/moduleVisitor.js b/utils/moduleVisitor.js index f222479f3..4248317b6 100644 --- a/utils/moduleVisitor.js +++ b/utils/moduleVisitor.js @@ -87,23 +87,43 @@ exports.default = function visitModules(visitor, options) { return visitors } +/** + * make an options schema for the module visitor, optionally + * adding extra fields. + + * @param {[type]} additionalProperties [description] + * @return {[type]} [description] + */ +function makeOptionsSchema(additionalProperties) { + const base = { + 'type': 'object', + 'properties': { + 'commonjs': { 'type': 'boolean' }, + 'amd': { 'type': 'boolean' }, + 'esmodule': { 'type': 'boolean' }, + 'ignore': { + 'type': 'array', + 'minItems': 1, + 'items': { 'type': 'string' }, + 'uniqueItems': true, + }, + }, + 'additionalProperties': false, + } + + if (additionalProperties){ + for (let key in additionalProperties) { + base.properties[key] = additionalProperties[key] + } + } + + return base +} +exports.makeOptionsSchema = makeOptionsSchema + /** * json schema object for options parameter. can be used to build * rule options schema object. * @type {Object} */ -exports.optionsSchema = { - 'type': 'object', - 'properties': { - 'commonjs': { 'type': 'boolean' }, - 'amd': { 'type': 'boolean' }, - 'esmodule': { 'type': 'boolean' }, - 'ignore': { - 'type': 'array', - 'minItems': 1, - 'items': { 'type': 'string' }, - 'uniqueItems': true, - }, - }, - 'additionalProperties': false, -} +exports.optionsSchema = makeOptionsSchema() diff --git a/utils/resolve.js b/utils/resolve.js index 19856b22d..1d6e164b1 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -15,24 +15,24 @@ exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS const fileExistsCache = new ModuleCache() // http://stackoverflow.com/a/27382838 -function fileExistsWithCaseSync(filepath, cacheSettings) { +exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cacheSettings) { // don't care if the FS is case-sensitive if (CASE_SENSITIVE_FS) return true // null means it resolved to a builtin if (filepath === null) return true - - const dir = path.dirname(filepath) + const parsedPath = path.parse(filepath) + , dir = parsedPath.dir let result = fileExistsCache.get(filepath, cacheSettings) if (result != null) return result // base case - if (dir === '/' || dir === '.' || /^[A-Z]:\\$/i.test(dir)) { + if (dir === '' || parsedPath.root === filepath) { result = true } else { const filenames = fs.readdirSync(dir) - if (filenames.indexOf(path.basename(filepath)) === -1) { + if (filenames.indexOf(parsedPath.base) === -1) { result = false } else { result = fileExistsWithCaseSync(dir, cacheSettings) @@ -102,9 +102,6 @@ function fullResolve(modulePath, sourceFile, settings) { if (!resolved.found) continue - // resolvers imply file existence, this double-check just ensures the case matches - if (!fileExistsWithCaseSync(resolved.path, cacheSettings)) continue - // else, counts cache(resolved.path) return resolved From 6e3b530cdab64f2ae6bd82d99b160a5394420a1a Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 23 Aug 2016 06:18:05 -0400 Subject: [PATCH 30/32] fixed broken infinite cache tests they may have always been broken (aka meaningless) in master --- tests/src/core/resolve.js | 24 +++++++++++++++++------- utils/ModuleCache.js | 5 ++++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index 72275e8a0..e8f255f34 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -50,6 +50,12 @@ describe('resolve', function () { 'import/cache': { 'lifetime': 1 }, }) + const infiniteContexts = [ '∞', 'Infinity' ].map(inf => [inf, + utils.testContext({ + 'import/cache': { 'lifetime': inf }, + })]) + + const pairs = [ ['./CaseyKasem.js', './CASEYKASEM2.js'], ] @@ -62,6 +68,13 @@ describe('resolve', function () { expect(resolve(changed, context)).not.to.exist }) + // settings are part of cache key + before('warm up infinite entries', function () { + infiniteContexts.forEach(([,c]) => { + expect(resolve(original, c)).to.exist + }) + }) + before('rename', function (done) { fs.rename( utils.testFilePath(original), @@ -86,19 +99,16 @@ describe('resolve', function () { // special behavior for infinity describe('infinite cache', function () { - this.timeout(1200) - before((done) => setTimeout(done, 1100)) + this.timeout(1500) - const lifetimes = [ '∞', 'Infinity' ] - lifetimes.forEach(inf => { - const infiniteContext = utils.testContext({ - 'import/cache': { 'lifetime': inf }, - }) + before((done) => setTimeout(done, 1100)) + infiniteContexts.forEach(([inf, infiniteContext]) => { it(`lifetime: ${inf} still gets cached values after ~1s`, function () { expect(resolve(original, infiniteContext), original).to.exist }) }) + }) describe('finite cache', function () { diff --git a/utils/ModuleCache.js b/utils/ModuleCache.js index 71b2ff215..19e6a2122 100644 --- a/utils/ModuleCache.js +++ b/utils/ModuleCache.js @@ -1,6 +1,8 @@ "use strict" exports.__esModule = true +const log = require('debug')('eslint-module-utils:ModuleCache') + class ModuleCache { constructor(map) { this.map = map || new Map() @@ -13,6 +15,7 @@ class ModuleCache { */ set(cacheKey, result) { this.map.set(cacheKey, { result, lastSeen: Date.now() }) + log('setting entry for', cacheKey) return result } @@ -21,7 +24,7 @@ class ModuleCache { const f = this.map.get(cacheKey) // check fresness if (Date.now() - f.lastSeen < (settings.lifetime * 1000)) return f.result - } + } else log('cache miss for', cacheKey) // cache miss return undefined } From d5af34e0f71e6694299616b06a85ee541d4c2217 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Tue, 23 Aug 2016 06:25:44 -0400 Subject: [PATCH 31/32] added case-insensitivity tests for default/named (#311) --- tests/src/rules/default.js | 20 ++++++++++++++++++++ tests/src/rules/named.js | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/tests/src/rules/default.js b/tests/src/rules/default.js index 11d7c18d8..ae7eca318 100644 --- a/tests/src/rules/default.js +++ b/tests/src/rules/default.js @@ -1,6 +1,8 @@ import { test, SYNTAX_CASES } from '../utils' import { RuleTester } from 'eslint' +import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve' + var ruleTester = new RuleTester() , rule = require('rules/default') @@ -65,6 +67,7 @@ ruleTester.run('default', rule, { parser: 'babel-eslint', }), + ...SYNTAX_CASES, ], @@ -114,3 +117,20 @@ ruleTester.run('default', rule, { }), ], }) + +// #311: import of mismatched case +if (!CASE_SENSITIVE_FS) { + ruleTester.run('default (path case-insensitivity)', rule, { + valid: [ + test({ + code: 'import foo from "./jsx/MyUncoolComponent.jsx"', + }), + ], + invalid: [ + test({ + code: 'import bar from "./Named-Exports"', + errors: ['No default export found in module.'], + }), + ], + }) +} diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index 08918a341..668182d47 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -1,6 +1,9 @@ import { test, SYNTAX_CASES } from '../utils' import { RuleTester } from 'eslint' +import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve' + + var ruleTester = new RuleTester() , rule = require('rules/named') @@ -211,3 +214,20 @@ ruleTester.run('named', rule, { }), ], }) + +// #311: import of mismatched case +if (!CASE_SENSITIVE_FS) { + ruleTester.run('named (path case-insensitivity)', rule, { + valid: [ + test({ + code: 'import { b } from "./Named-Exports"', + }), + ], + invalid: [ + test({ + code: 'import { foo } from "./Named-Exports"', + errors: [`foo not found in './Named-Exports'`], + }), + ], + }) +} From 20079c643be9494886a7add1a5c572dddd9e1396 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Thu, 15 Sep 2016 06:58:33 -0400 Subject: [PATCH 32/32] bump ESLint devDep to 3.x + new rule format - converted all rules using eslint-transforms tool - updated no-unresolved + namespace to match tool output (module.exports vs. exports.{meta,create}) --- package.json | 4 +- src/rules/default.js | 50 ++-- src/rules/export.js | 114 ++++----- src/rules/extensions.js | 120 +++++----- src/rules/first.js | 80 ++++--- src/rules/max-dependencies.js | 68 +++--- src/rules/named.js | 86 +++---- src/rules/namespace.js | 292 ++++++++++++------------ src/rules/newline-after-import.js | 140 ++++++------ src/rules/no-absolute-path.js | 28 ++- src/rules/no-amd.js | 36 +-- src/rules/no-commonjs.js | 73 +++--- src/rules/no-deprecated.js | 198 ++++++++-------- src/rules/no-duplicates.js | 44 ++-- src/rules/no-extraneous-dependencies.js | 74 +++--- src/rules/no-internal-modules.js | 148 ++++++------ src/rules/no-mutable-exports.js | 66 +++--- src/rules/no-named-as-default-member.js | 134 +++++------ src/rules/no-named-as-default.js | 44 ++-- src/rules/no-namespace.js | 18 +- src/rules/no-nodejs-modules.js | 32 +-- src/rules/no-restricted-paths.js | 114 ++++----- src/rules/no-unresolved.js | 44 ++-- src/rules/order.js | 138 +++++------ src/rules/prefer-default-export.js | 120 +++++----- 25 files changed, 1200 insertions(+), 1065 deletions(-) diff --git a/package.json b/package.json index a132997c4..b174c9299 100644 --- a/package.json +++ b/package.json @@ -49,13 +49,13 @@ "homepage": "https://github.com/benmosher/eslint-plugin-import", "devDependencies": { "babel-eslint": "next", - "babel-preset-es2015-argon": "latest", "babel-plugin-istanbul": "^2.0.1", + "babel-preset-es2015-argon": "latest", "babel-register": "6.9.0", "chai": "^3.4.0", "coveralls": "^2.11.4", "cross-env": "^2.0.0", - "eslint": "2.x", + "eslint": "3.x", "eslint-import-resolver-node": "file:./resolvers/node", "eslint-import-resolver-webpack": "file:./resolvers/webpack", "eslint-module-utils": "file:./utils", diff --git a/src/rules/default.js b/src/rules/default.js index 4eaa70ff5..22e5883d8 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -1,31 +1,37 @@ import Exports from '../ExportMap' -module.exports = function (context) { +module.exports = { + meta: { + docs: {}, + }, - function checkDefault(specifierType, node) { + create: function (context) { - // poor man's Array.find - let defaultSpecifier - node.specifiers.some((n) => { - if (n.type === specifierType) { - defaultSpecifier = n - return true - } - }) + function checkDefault(specifierType, node) { + + // poor man's Array.find + let defaultSpecifier + node.specifiers.some((n) => { + if (n.type === specifierType) { + defaultSpecifier = n + return true + } + }) - if (!defaultSpecifier) return - var imports = Exports.get(node.source.value, context) - if (imports == null) return + if (!defaultSpecifier) return + var imports = Exports.get(node.source.value, context) + if (imports == null) return - if (imports.errors.length) { - imports.reportErrors(context, node) - } else if (!imports.get('default')) { - context.report(defaultSpecifier, 'No default export found in module.') + if (imports.errors.length) { + imports.reportErrors(context, node) + } else if (!imports.get('default')) { + context.report(defaultSpecifier, 'No default export found in module.') + } } - } - return { - 'ImportDeclaration': checkDefault.bind(null, 'ImportDefaultSpecifier'), - 'ExportNamedDeclaration': checkDefault.bind(null, 'ExportDefaultSpecifier'), - } + return { + 'ImportDeclaration': checkDefault.bind(null, 'ImportDefaultSpecifier'), + 'ExportNamedDeclaration': checkDefault.bind(null, 'ExportDefaultSpecifier'), + } + }, } diff --git a/src/rules/export.js b/src/rules/export.js index 61d21b86f..f7f37305e 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,72 +1,78 @@ import ExportMap, { recursivePatternCapture } from '../ExportMap' -module.exports = function (context) { - const named = new Map() +module.exports = { + meta: { + docs: {}, + }, - function addNamed(name, node) { - let nodes = named.get(name) + create: function (context) { + const named = new Map() - if (nodes == null) { - nodes = new Set() - named.set(name, nodes) - } + function addNamed(name, node) { + let nodes = named.get(name) - nodes.add(node) - } + if (nodes == null) { + nodes = new Set() + named.set(name, nodes) + } - return { - 'ExportDefaultDeclaration': (node) => addNamed('default', node), + nodes.add(node) + } - 'ExportSpecifier': function (node) { - addNamed(node.exported.name, node.exported) - }, + return { + 'ExportDefaultDeclaration': (node) => addNamed('default', node), - 'ExportNamedDeclaration': function (node) { - if (node.declaration == null) return + 'ExportSpecifier': function (node) { + addNamed(node.exported.name, node.exported) + }, - if (node.declaration.id != null) { - addNamed(node.declaration.id.name, node.declaration.id) - } + 'ExportNamedDeclaration': function (node) { + if (node.declaration == null) return - if (node.declaration.declarations != null) { - for (let declaration of node.declaration.declarations) { - recursivePatternCapture(declaration.id, v => addNamed(v.name, v)) + if (node.declaration.id != null) { + addNamed(node.declaration.id.name, node.declaration.id) } - } - }, - 'ExportAllDeclaration': function (node) { - if (node.source == null) return // not sure if this is ever true + if (node.declaration.declarations != null) { + for (let declaration of node.declaration.declarations) { + recursivePatternCapture(declaration.id, v => addNamed(v.name, v)) + } + } + }, - const remoteExports = ExportMap.get(node.source.value, context) - if (remoteExports == null) return + 'ExportAllDeclaration': function (node) { + if (node.source == null) return // not sure if this is ever true - if (remoteExports.errors.length) { - remoteExports.reportErrors(context, node) - return - } - let any = false - remoteExports.forEach((v, name) => - name !== 'default' && - (any = true) && // poor man's filter - addNamed(name, node)) - - if (!any) { - context.report(node.source, - `No named exports found in module '${node.source.value}'.`) - } - }, + const remoteExports = ExportMap.get(node.source.value, context) + if (remoteExports == null) return - 'Program:exit': function () { - for (let [name, nodes] of named) { - if (nodes.size <= 1) continue + if (remoteExports.errors.length) { + remoteExports.reportErrors(context, node) + return + } + let any = false + remoteExports.forEach((v, name) => + name !== 'default' && + (any = true) && // poor man's filter + addNamed(name, node)) - for (let node of nodes) { - if (name === 'default') { - context.report(node, 'Multiple default exports.') - } else context.report(node, `Multiple exports of name '${name}'.`) + if (!any) { + context.report(node.source, + `No named exports found in module '${node.source.value}'.`) } - } - }, - } + }, + + 'Program:exit': function () { + for (let [name, nodes] of named) { + if (nodes.size <= 1) continue + + for (let node of nodes) { + if (name === 'default') { + context.report(node, 'Multiple default exports.') + } else context.report(node, `Multiple exports of name '${name}'.`) + } + } + }, + } + }, } diff --git a/src/rules/extensions.js b/src/rules/extensions.js index a1a3c31a4..eec9530ac 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -4,73 +4,79 @@ import endsWith from 'lodash.endswith' import resolve from 'eslint-module-utils/resolve' import { isBuiltIn } from '../core/importType' -module.exports = function (context) { - const configuration = context.options[0] || 'never' +module.exports = { + meta: { + docs: {}, - function isUseOfExtensionEnforced(extension) { - if (typeof configuration === 'object') { - return configuration[extension] === 'always' - } + schema: [ + { + oneOf: [ + { + enum: [ 'always', 'never' ], + }, + { + type: 'object', + patternProperties: { + '.*': { enum: [ 'always', 'never' ] }, + }, + }, + ], + }, + ], + }, - return configuration === 'always' - } + create: function (context) { + const configuration = context.options[0] || 'never' - function isResolvableWithoutExtension(file) { - const extension = path.extname(file) - const fileWithoutExtension = file.slice(0, -extension.length) - const resolvedFileWithoutExtension = resolve(fileWithoutExtension, context) + function isUseOfExtensionEnforced(extension) { + if (typeof configuration === 'object') { + return configuration[extension] === 'always' + } - return resolvedFileWithoutExtension === resolve(file, context) - } + return configuration === 'always' + } - function checkFileExtension(node) { - const { source } = node - const importPath = source.value + function isResolvableWithoutExtension(file) { + const extension = path.extname(file) + const fileWithoutExtension = file.slice(0, -extension.length) + const resolvedFileWithoutExtension = resolve(fileWithoutExtension, context) - // don't enforce anything on builtins - if (isBuiltIn(importPath, context.settings)) return + return resolvedFileWithoutExtension === resolve(file, context) + } - const resolvedPath = resolve(importPath, context) + function checkFileExtension(node) { + const { source } = node + const importPath = source.value - // get extension from resolved path, if possible. - // for unresolved, use source value. - const extension = path.extname(resolvedPath || importPath).substring(1) + // don't enforce anything on builtins + if (isBuiltIn(importPath, context.settings)) return - if (!extension || !endsWith(importPath, extension)) { - if (isUseOfExtensionEnforced(extension)) { - context.report({ - node: source, - message: - `Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPath}"`, - }) - } - } else if (extension) { - if (!isUseOfExtensionEnforced(extension) && isResolvableWithoutExtension(importPath)) { - context.report({ - node: source, - message: `Unexpected use of file extension "${extension}" for "${importPath}"`, - }) + const resolvedPath = resolve(importPath, context) + + // get extension from resolved path, if possible. + // for unresolved, use source value. + const extension = path.extname(resolvedPath || importPath).substring(1) + + if (!extension || !endsWith(importPath, extension)) { + if (isUseOfExtensionEnforced(extension)) { + context.report({ + node: source, + message: + `Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPath}"`, + }) + } + } else if (extension) { + if (!isUseOfExtensionEnforced(extension) && isResolvableWithoutExtension(importPath)) { + context.report({ + node: source, + message: `Unexpected use of file extension "${extension}" for "${importPath}"`, + }) + } } } - } - - return { - ImportDeclaration: checkFileExtension, - } -} -module.exports.schema = [ - { - oneOf: [ - { - enum: [ 'always', 'never' ], - }, - { - type: 'object', - patternProperties: { - '.*': { enum: [ 'always', 'never' ] }, - }, - }, - ], + return { + ImportDeclaration: checkFileExtension, + } }, -] +} diff --git a/src/rules/first.js b/src/rules/first.js index 8a3f37369..7642cae1d 100644 --- a/src/rules/first.js +++ b/src/rules/first.js @@ -1,45 +1,51 @@ -module.exports = function (context) { - function isPossibleDirective (node) { - return node.type === 'ExpressionStatement' && - node.expression.type === 'Literal' && - typeof node.expression.value === 'string' - } +module.exports = { + meta: { + docs: {}, + }, - return { - 'Program': function (n) { - const body = n.body - , absoluteFirst = context.options[0] === 'absolute-first' - let nonImportCount = 0 - , anyExpressions = false - , anyRelative = false - body.forEach(function (node){ - if (!anyExpressions && isPossibleDirective(node)) { - return - } + create: function (context) { + function isPossibleDirective (node) { + return node.type === 'ExpressionStatement' && + node.expression.type === 'Literal' && + typeof node.expression.value === 'string' + } - anyExpressions = true + return { + 'Program': function (n) { + const body = n.body + , absoluteFirst = context.options[0] === 'absolute-first' + let nonImportCount = 0 + , anyExpressions = false + , anyRelative = false + body.forEach(function (node){ + if (!anyExpressions && isPossibleDirective(node)) { + return + } + + anyExpressions = true - if (node.type === 'ImportDeclaration') { - if (absoluteFirst) { - if (/^\./.test(node.source.value)) { - anyRelative = true - } else if (anyRelative) { + if (node.type === 'ImportDeclaration') { + if (absoluteFirst) { + if (/^\./.test(node.source.value)) { + anyRelative = true + } else if (anyRelative) { + context.report({ + node: node.source, + message: 'Absolute imports should come before relative imports.', + }) + } + } + if (nonImportCount > 0) { context.report({ - node: node.source, - message: 'Absolute imports should come before relative imports.', + node, + message: 'Import in body of module; reorder to top.', }) } + } else { + nonImportCount++ } - if (nonImportCount > 0) { - context.report({ - node, - message: 'Import in body of module; reorder to top.', - }) - } - } else { - nonImportCount++ - } - }) - }, - } + }) + }, + } + }, } diff --git a/src/rules/max-dependencies.js b/src/rules/max-dependencies.js index 396e21e54..c28afd086 100644 --- a/src/rules/max-dependencies.js +++ b/src/rules/max-dependencies.js @@ -14,36 +14,42 @@ const countDependencies = (dependencies, lastNode, context) => { } } -module.exports = context => { - const dependencies = new Set() // keep track of dependencies - let lastNode // keep track of the last node to report on - - return { - ImportDeclaration(node) { - dependencies.add(node.source.value) - lastNode = node.source - }, - - CallExpression(node) { - if (isStaticRequire(node)) { - const [ requirePath ] = node.arguments - dependencies.add(requirePath.value) - lastNode = node - } - }, - - 'Program:exit': function () { - countDependencies(dependencies, lastNode, context) - }, - } -} +module.exports = { + meta: { + docs: {}, + + schema: [ + { + 'type': 'object', + 'properties': { + 'max': { 'type': 'number' }, + }, + 'additionalProperties': false, + }, + ], + }, -module.exports.schema = [ - { - 'type': 'object', - 'properties': { - 'max': { 'type': 'number' }, - }, - 'additionalProperties': false, + create: context => { + const dependencies = new Set() // keep track of dependencies + let lastNode // keep track of the last node to report on + + return { + ImportDeclaration(node) { + dependencies.add(node.source.value) + lastNode = node.source + }, + + CallExpression(node) { + if (isStaticRequire(node)) { + const [ requirePath ] = node.arguments + dependencies.add(requirePath.value) + lastNode = node + } + }, + + 'Program:exit': function () { + countDependencies(dependencies, lastNode, context) + }, + } }, -] +} diff --git a/src/rules/named.js b/src/rules/named.js index 853331752..d7589ebe7 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -1,54 +1,60 @@ import * as path from 'path' import Exports from '../ExportMap' -module.exports = function (context) { - function checkSpecifiers(key, type, node) { - if (node.source == null) return // local export, ignore - - if (!node.specifiers - .some(function (im) { return im.type === type })) { - return // no named imports/exports - } +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + function checkSpecifiers(key, type, node) { + if (node.source == null) return // local export, ignore + + if (!node.specifiers + .some(function (im) { return im.type === type })) { + return // no named imports/exports + } - const imports = Exports.get(node.source.value, context) - if (imports == null) return + const imports = Exports.get(node.source.value, context) + if (imports == null) return - if (imports.errors.length) { - imports.reportErrors(context, node) - return - } + if (imports.errors.length) { + imports.reportErrors(context, node) + return + } - node.specifiers.forEach(function (im) { - if (im.type !== type) return + node.specifiers.forEach(function (im) { + if (im.type !== type) return - const deepLookup = imports.hasDeep(im[key].name) + const deepLookup = imports.hasDeep(im[key].name) - if (!deepLookup.found) { - if (deepLookup.path.length > 1) { - const deepPath = deepLookup.path - .map(i => path.relative(path.dirname(context.getFilename()), i.path)) - .join(' -> ') + if (!deepLookup.found) { + if (deepLookup.path.length > 1) { + const deepPath = deepLookup.path + .map(i => path.relative(path.dirname(context.getFilename()), i.path)) + .join(' -> ') - context.report(im[key], - `${im[key].name} not found via ${deepPath}`) - } else { - context.report(im[key], - im[key].name + ' not found in \'' + node.source.value + '\'') + context.report(im[key], + `${im[key].name} not found via ${deepPath}`) + } else { + context.report(im[key], + im[key].name + ' not found in \'' + node.source.value + '\'') + } } - } - }) - } + }) + } - return { - 'ImportDeclaration': checkSpecifiers.bind( null - , 'imported' - , 'ImportSpecifier' - ), + return { + 'ImportDeclaration': checkSpecifiers.bind( null + , 'imported' + , 'ImportSpecifier' + ), - 'ExportNamedDeclaration': checkSpecifiers.bind( null - , 'local' - , 'ExportSpecifier' - ), - } + 'ExportNamedDeclaration': checkSpecifiers.bind( null + , 'local' + , 'ExportSpecifier' + ), + } + }, } diff --git a/src/rules/namespace.js b/src/rules/namespace.js index a313908b1..87824c179 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -2,49 +2,86 @@ import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' import declaredScope from 'eslint-module-utils/declaredScope' -exports.meta = { - schema: [ - { - 'type': 'object', - 'properties': { - 'allowComputed': { - 'description': - 'If `false`, will report computed (and thus, un-lintable) references ' + - 'to namespace members.', - 'type': 'boolean', - 'default': false, +module.exports = { + meta: { + schema: [ + { + 'type': 'object', + 'properties': { + 'allowComputed': { + 'description': + 'If `false`, will report computed (and thus, un-lintable) references ' + + 'to namespace members.', + 'type': 'boolean', + 'default': false, + }, }, + 'additionalProperties': false, }, - 'additionalProperties': false, - }, - ], -} + ], + }, + + create: function namespaceRule(context) { + + // read options + const { + allowComputed = false, + } = context.options[0] || {} + + const namespaces = new Map() -exports.create = function namespaceRule(context) { + function makeMessage(last, namepath) { + return `'${last.name}' not found in` + + (namepath.length > 1 ? ' deeply ' : ' ') + + `imported namespace '${namepath.join('.')}'.` + } - // read options - const { - allowComputed = false, - } = context.options[0] || {} + return { - const namespaces = new Map() + // pick up all imports at body entry time, to properly respect hoisting + 'Program': function ({ body }) { + function processBodyStatement(declaration) { + if (declaration.type !== 'ImportDeclaration') return - function makeMessage(last, namepath) { - return `'${last.name}' not found in` + - (namepath.length > 1 ? ' deeply ' : ' ') + - `imported namespace '${namepath.join('.')}'.` - } + if (declaration.specifiers.length === 0) return - return { + const imports = Exports.get(declaration.source.value, context) + if (imports == null) return null - // pick up all imports at body entry time, to properly respect hoisting - 'Program': function ({ body }) { - function processBodyStatement(declaration) { - if (declaration.type !== 'ImportDeclaration') return + if (imports.errors.length) { + imports.reportErrors(context, declaration) + return + } + + for (let specifier of declaration.specifiers) { + switch (specifier.type) { + case 'ImportNamespaceSpecifier': + if (!imports.size) { + context.report(specifier, + `No exported names found in module '${declaration.source.value}'.`) + } + namespaces.set(specifier.local.name, imports) + break + case 'ImportDefaultSpecifier': + case 'ImportSpecifier': { + const meta = imports.get( + // default to 'default' for default http://i.imgur.com/nj6qAWy.jpg + specifier.imported ? specifier.imported.name : 'default') + if (!meta || !meta.namespace) break + namespaces.set(specifier.local.name, meta.namespace) + break + } + } + } + } + body.forEach(processBodyStatement) + }, - if (declaration.specifiers.length === 0) return + // same as above, but does not add names to local map + 'ExportNamespaceSpecifier': function (namespace) { + var declaration = importDeclaration(context) - const imports = Exports.get(declaration.source.value, context) + var imports = Exports.get(declaration.source.value, context) if (imports == null) return null if (imports.errors.length) { @@ -52,133 +89,98 @@ exports.create = function namespaceRule(context) { return } - for (let specifier of declaration.specifiers) { - switch (specifier.type) { - case 'ImportNamespaceSpecifier': - if (!imports.size) { - context.report(specifier, - `No exported names found in module '${declaration.source.value}'.`) - } - namespaces.set(specifier.local.name, imports) - break - case 'ImportDefaultSpecifier': - case 'ImportSpecifier': { - const meta = imports.get( - // default to 'default' for default http://i.imgur.com/nj6qAWy.jpg - specifier.imported ? specifier.imported.name : 'default') - if (!meta || !meta.namespace) break - namespaces.set(specifier.local.name, meta.namespace) - break + if (!imports.size) { + context.report(namespace, + `No exported names found in module '${declaration.source.value}'.`) + } + }, + + // todo: check for possible redefinition + + 'MemberExpression': function (dereference) { + if (dereference.object.type !== 'Identifier') return + if (!namespaces.has(dereference.object.name)) return + + if (dereference.parent.type === 'AssignmentExpression' && + dereference.parent.left === dereference) { + context.report(dereference.parent, + `Assignment to member of namespace '${dereference.object.name}'.`) + } + + // go deep + var namespace = namespaces.get(dereference.object.name) + var namepath = [dereference.object.name] + // while property is namespace and parent is member expression, keep validating + while (namespace instanceof Exports && + dereference.type === 'MemberExpression') { + + if (dereference.computed) { + if (!allowComputed) { + context.report(dereference.property, + 'Unable to validate computed reference to imported namespace \'' + + dereference.object.name + '\'.') } + return } - } - } - body.forEach(processBodyStatement) - }, - - // same as above, but does not add names to local map - 'ExportNamespaceSpecifier': function (namespace) { - var declaration = importDeclaration(context) - - var imports = Exports.get(declaration.source.value, context) - if (imports == null) return null - - if (imports.errors.length) { - imports.reportErrors(context, declaration) - return - } - - if (!imports.size) { - context.report(namespace, - `No exported names found in module '${declaration.source.value}'.`) - } - }, - - // todo: check for possible redefinition - - 'MemberExpression': function (dereference) { - if (dereference.object.type !== 'Identifier') return - if (!namespaces.has(dereference.object.name)) return - - if (dereference.parent.type === 'AssignmentExpression' && - dereference.parent.left === dereference) { - context.report(dereference.parent, - `Assignment to member of namespace '${dereference.object.name}'.`) - } - - // go deep - var namespace = namespaces.get(dereference.object.name) - var namepath = [dereference.object.name] - // while property is namespace and parent is member expression, keep validating - while (namespace instanceof Exports && - dereference.type === 'MemberExpression') { - - if (dereference.computed) { - if (!allowComputed) { - context.report(dereference.property, - 'Unable to validate computed reference to imported namespace \'' + - dereference.object.name + '\'.') + + if (!namespace.has(dereference.property.name)) { + context.report( + dereference.property, + makeMessage(dereference.property, namepath)) + break } - return - } - if (!namespace.has(dereference.property.name)) { - context.report( - dereference.property, - makeMessage(dereference.property, namepath)) - break - } + const exported = namespace.get(dereference.property.name) + if (exported == null) return - const exported = namespace.get(dereference.property.name) - if (exported == null) return + // stash and pop + namepath.push(dereference.property.name) + namespace = exported.namespace + dereference = dereference.parent + } - // stash and pop - namepath.push(dereference.property.name) - namespace = exported.namespace - dereference = dereference.parent - } + }, - }, + 'VariableDeclarator': function ({ id, init }) { + if (init == null) return + if (init.type !== 'Identifier') return + if (!namespaces.has(init.name)) return - 'VariableDeclarator': function ({ id, init }) { - if (init == null) return - if (init.type !== 'Identifier') return - if (!namespaces.has(init.name)) return + // check for redefinition in intermediate scopes + if (declaredScope(context, init.name) !== 'module') return - // check for redefinition in intermediate scopes - if (declaredScope(context, init.name) !== 'module') return + // DFS traverse child namespaces + function testKey(pattern, namespace, path = [init.name]) { + if (!(namespace instanceof Exports)) return - // DFS traverse child namespaces - function testKey(pattern, namespace, path = [init.name]) { - if (!(namespace instanceof Exports)) return + if (pattern.type !== 'ObjectPattern') return - if (pattern.type !== 'ObjectPattern') return + for (let property of pattern.properties) { - for (let property of pattern.properties) { + if (property.key.type !== 'Identifier') { + context.report({ + node: property, + message: 'Only destructure top-level names.', + }) + continue + } - if (property.key.type !== 'Identifier') { - context.report({ - node: property, - message: 'Only destructure top-level names.', - }) - continue - } + if (!namespace.has(property.key.name)) { + context.report({ + node: property, + message: makeMessage(property.key, path), + }) + continue + } - if (!namespace.has(property.key.name)) { - context.report({ - node: property, - message: makeMessage(property.key, path), - }) - continue + path.push(property.key.name) + testKey(property.value, namespace.get(property.key.name).namespace, path) + path.pop() } - - path.push(property.key.name) - testKey(property.value, namespace.get(property.key.name).namespace, path) - path.pop() } - } - testKey(id, namespaces.get(init.name)) - }, - } + testKey(id, namespaces.get(init.name)) + }, + } + }, } diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.js index 4c429b6e9..ca0286748 100644 --- a/src/rules/newline-after-import.js +++ b/src/rules/newline-after-import.js @@ -41,86 +41,92 @@ function getLineDifference(node, nextNode) { } -module.exports = function (context) { - const scopes = [] - let scopeIndex = 0 +module.exports = { + meta: { + docs: {}, + }, - function checkForNewLine(node, nextNode, type) { - if (getLineDifference(node, nextNode) < 2) { - let column = node.loc.start.column + create: function (context) { + const scopes = [] + let scopeIndex = 0 - if (node.loc.start.line !== node.loc.end.line) { - column = 0 - } + function checkForNewLine(node, nextNode, type) { + if (getLineDifference(node, nextNode) < 2) { + let column = node.loc.start.column + + if (node.loc.start.line !== node.loc.end.line) { + column = 0 + } - context.report({ - loc: { - line: node.loc.end.line, - column, - }, - message: `Expected empty line after ${type} statement not followed by another ${type}.`, - }) + context.report({ + loc: { + line: node.loc.end.line, + column, + }, + message: `Expected empty line after ${type} statement not followed by another ${type}.`, + }) + } } - } - return { - ImportDeclaration: function (node) { - const { parent } = node - const nodePosition = parent.body.indexOf(node) - const nextNode = parent.body[nodePosition + 1] + return { + ImportDeclaration: function (node) { + const { parent } = node + const nodePosition = parent.body.indexOf(node) + const nextNode = parent.body[nodePosition + 1] - if (nextNode && nextNode.type !== 'ImportDeclaration') { - checkForNewLine(node, nextNode, 'import') - } - }, - Program: function () { - scopes.push({ scope: context.getScope(), requireCalls: [] }) - }, - CallExpression: function(node) { - const scope = context.getScope() - if (isStaticRequire(node)) { - const currentScope = scopes[scopeIndex] - - if (scope === currentScope.scope) { - currentScope.requireCalls.push(node) - } else { - scopes.push({ scope, requireCalls: [ node ] }) - scopeIndex += 1 + if (nextNode && nextNode.type !== 'ImportDeclaration') { + checkForNewLine(node, nextNode, 'import') } - } - }, - 'Program:exit': function () { - log('exit processing for', context.getFilename()) - scopes.forEach(function ({ scope, requireCalls }) { - const scopeBody = getScopeBody(scope) - - // skip non-array scopes (i.e. arrow function expressions) - if (!scopeBody || !(scopeBody instanceof Array)) { - log('invalid scope:', scopeBody) - return + }, + Program: function () { + scopes.push({ scope: context.getScope(), requireCalls: [] }) + }, + CallExpression: function(node) { + const scope = context.getScope() + if (isStaticRequire(node)) { + const currentScope = scopes[scopeIndex] + + if (scope === currentScope.scope) { + currentScope.requireCalls.push(node) + } else { + scopes.push({ scope, requireCalls: [ node ] }) + scopeIndex += 1 + } } + }, + 'Program:exit': function () { + log('exit processing for', context.getFilename()) + scopes.forEach(function ({ scope, requireCalls }) { + const scopeBody = getScopeBody(scope) + + // skip non-array scopes (i.e. arrow function expressions) + if (!scopeBody || !(scopeBody instanceof Array)) { + log('invalid scope:', scopeBody) + return + } - log('got scope:', scopeBody) + log('got scope:', scopeBody) - requireCalls.forEach(function (node, index) { - const nodePosition = findNodeIndexInScopeBody(scopeBody, node) - log('node position in scope:', nodePosition) + requireCalls.forEach(function (node, index) { + const nodePosition = findNodeIndexInScopeBody(scopeBody, node) + log('node position in scope:', nodePosition) - const statementWithRequireCall = scopeBody[nodePosition] - const nextStatement = scopeBody[nodePosition + 1] - const nextRequireCall = requireCalls[index + 1] + const statementWithRequireCall = scopeBody[nodePosition] + const nextStatement = scopeBody[nodePosition + 1] + const nextRequireCall = requireCalls[index + 1] - if (nextRequireCall && containsNodeOrEqual(statementWithRequireCall, nextRequireCall)) { - return - } + if (nextRequireCall && containsNodeOrEqual(statementWithRequireCall, nextRequireCall)) { + return + } - if (nextStatement && - (!nextRequireCall || !containsNodeOrEqual(nextStatement, nextRequireCall))) { + if (nextStatement && + (!nextRequireCall || !containsNodeOrEqual(nextStatement, nextRequireCall))) { - checkForNewLine(statementWithRequireCall, nextStatement, 'require') - } + checkForNewLine(statementWithRequireCall, nextStatement, 'require') + } + }) }) - }) - }, - } + }, + } + }, } diff --git a/src/rules/no-absolute-path.js b/src/rules/no-absolute-path.js index 08c8b80fe..33da932fd 100644 --- a/src/rules/no-absolute-path.js +++ b/src/rules/no-absolute-path.js @@ -7,15 +7,21 @@ function reportIfMissing(context, node, name) { } } -module.exports = function (context) { - return { - ImportDeclaration: function handleImports(node) { - reportIfMissing(context, node, node.source.value) - }, - CallExpression: function handleRequires(node) { - if (isStaticRequire(node)) { - reportIfMissing(context, node, node.arguments[0].value) - } - }, - } +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + return { + ImportDeclaration: function handleImports(node) { + reportIfMissing(context, node, node.source.value) + }, + CallExpression: function handleRequires(node) { + if (isStaticRequire(node)) { + reportIfMissing(context, node, node.arguments[0].value) + } + }, + } + }, } diff --git a/src/rules/no-amd.js b/src/rules/no-amd.js index 86f1c9952..6686be935 100644 --- a/src/rules/no-amd.js +++ b/src/rules/no-amd.js @@ -7,27 +7,33 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function (context) { +module.exports = { + meta: { + docs: {}, + }, - return { + create: function (context) { - 'CallExpression': function (node) { - if (context.getScope().type !== 'module') return + return { - if (node.callee.type !== 'Identifier') return - if (node.callee.name !== 'require' && - node.callee.name !== 'define') return + 'CallExpression': function (node) { + if (context.getScope().type !== 'module') return - // todo: capture define((require, module, exports) => {}) form? - if (node.arguments.length !== 2) return + if (node.callee.type !== 'Identifier') return + if (node.callee.name !== 'require' && + node.callee.name !== 'define') return - const modules = node.arguments[0] - if (modules.type !== 'ArrayExpression') return + // todo: capture define((require, module, exports) => {}) form? + if (node.arguments.length !== 2) return - // todo: check second arg type? (identifier or callback) + const modules = node.arguments[0] + if (modules.type !== 'ArrayExpression') return - context.report(node, `Expected imports instead of AMD ${node.callee.name}().`) - }, - } + // todo: check second arg type? (identifier or callback) + context.report(node, `Expected imports instead of AMD ${node.callee.name}().`) + }, + } + + }, } diff --git a/src/rules/no-commonjs.js b/src/rules/no-commonjs.js index eff79660f..62a0804f2 100644 --- a/src/rules/no-commonjs.js +++ b/src/rules/no-commonjs.js @@ -6,54 +6,59 @@ const EXPORT_MESSAGE = 'Expected "export" or "export default"' , IMPORT_MESSAGE = 'Expected "import" instead of "require()"' +function allowPrimitive(node, context) { + if (context.options.indexOf('allow-primitive-modules') < 0) return false + if (node.parent.type !== 'AssignmentExpression') return false + return (node.parent.right.type !== 'ObjectExpression') +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function (context) { +module.exports = { + meta: { + docs: {}, + }, - return { + create: function (context) { - 'MemberExpression': function (node) { + return { - // module.exports - if (node.object.name === 'module' && node.property.name === 'exports') { - if (allowPrimitive(node, context)) return - context.report({ node, message: EXPORT_MESSAGE }) - } + 'MemberExpression': function (node) { - // exports. - if (node.object.name === 'exports') { - context.report({ node, message: EXPORT_MESSAGE }) - } + // module.exports + if (node.object.name === 'module' && node.property.name === 'exports') { + if (allowPrimitive(node, context)) return + context.report({ node, message: EXPORT_MESSAGE }) + } - }, - 'CallExpression': function (call) { - if (context.getScope().type !== 'module') return + // exports. + if (node.object.name === 'exports') { + context.report({ node, message: EXPORT_MESSAGE }) + } - if (call.callee.type !== 'Identifier') return - if (call.callee.name !== 'require') return + }, + 'CallExpression': function (call) { + if (context.getScope().type !== 'module') return - if (call.arguments.length !== 1) return - var module = call.arguments[0] + if (call.callee.type !== 'Identifier') return + if (call.callee.name !== 'require') return - if (module.type !== 'Literal') return - if (typeof module.value !== 'string') return + if (call.arguments.length !== 1) return + var module = call.arguments[0] - // keeping it simple: all 1-string-arg `require` calls are reported - context.report({ - node: call.callee, - message: IMPORT_MESSAGE, - }) - }, - } + if (module.type !== 'Literal') return + if (typeof module.value !== 'string') return -} + // keeping it simple: all 1-string-arg `require` calls are reported + context.report({ + node: call.callee, + message: IMPORT_MESSAGE, + }) + }, + } - // allow non-objects as module.exports -function allowPrimitive(node, context) { - if (context.options.indexOf('allow-primitive-modules') < 0) return false - if (node.parent.type !== 'AssignmentExpression') return false - return (node.parent.right.type !== 'ObjectExpression') + }, } diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index fc5a959ed..e50c2e516 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,132 +1,138 @@ import Exports from '../ExportMap' import declaredScope from 'eslint-module-utils/declaredScope' -module.exports = function (context) { - const deprecated = new Map() - , namespaces = new Map() - - function checkSpecifiers(node) { - if (node.type !== 'ImportDeclaration') return - if (node.source == null) return // local export, ignore - - const imports = Exports.get(node.source.value, context) - if (imports == null) return +function message(deprecation) { + return 'Deprecated' + (deprecation.description ? ': ' + deprecation.description : '.') +} - let moduleDeprecation - if (imports.doc && - imports.doc.tags.some(t => t.title === 'deprecated' && (moduleDeprecation = t))) { - context.report({ node, message: message(moduleDeprecation) }) - } +function getDeprecation(metadata) { + if (!metadata || !metadata.doc) return - if (imports.errors.length) { - imports.reportErrors(context, node) - return - } + let deprecation + if (metadata.doc.tags.some(t => t.title === 'deprecated' && (deprecation = t))) { + return deprecation + } +} - node.specifiers.forEach(function (im) { - let imported, local - switch (im.type) { +module.exports = { + meta: { + docs: {}, + }, + create: function (context) { + const deprecated = new Map() + , namespaces = new Map() - case 'ImportNamespaceSpecifier':{ - if (!imports.size) return - namespaces.set(im.local.name, imports) - return - } + function checkSpecifiers(node) { + if (node.type !== 'ImportDeclaration') return + if (node.source == null) return // local export, ignore - case 'ImportDefaultSpecifier': - imported = 'default' - local = im.local.name - break + const imports = Exports.get(node.source.value, context) + if (imports == null) return - case 'ImportSpecifier': - imported = im.imported.name - local = im.local.name - break + let moduleDeprecation + if (imports.doc && + imports.doc.tags.some(t => t.title === 'deprecated' && (moduleDeprecation = t))) { + context.report({ node, message: message(moduleDeprecation) }) + } - default: return // can't handle this one + if (imports.errors.length) { + imports.reportErrors(context, node) + return } - // unknown thing can't be deprecated - const exported = imports.get(imported) - if (exported == null) return + node.specifiers.forEach(function (im) { + let imported, local + switch (im.type) { - // capture import of deep namespace - if (exported.namespace) namespaces.set(local, exported.namespace) - const deprecation = getDeprecation(imports.get(imported)) - if (!deprecation) return + case 'ImportNamespaceSpecifier':{ + if (!imports.size) return + namespaces.set(im.local.name, imports) + return + } - context.report({ node: im, message: message(deprecation) }) + case 'ImportDefaultSpecifier': + imported = 'default' + local = im.local.name + break - deprecated.set(local, deprecation) + case 'ImportSpecifier': + imported = im.imported.name + local = im.local.name + break - }) - } + default: return // can't handle this one + } - return { - 'Program': ({ body }) => body.forEach(checkSpecifiers), + // unknown thing can't be deprecated + const exported = imports.get(imported) + if (exported == null) return - 'Identifier': function (node) { - if (node.parent.type === 'MemberExpression' && node.parent.property === node) { - return // handled by MemberExpression - } + // capture import of deep namespace + if (exported.namespace) namespaces.set(local, exported.namespace) + + const deprecation = getDeprecation(imports.get(imported)) + if (!deprecation) return - // ignore specifier identifiers - if (node.parent.type.slice(0, 6) === 'Import') return + context.report({ node: im, message: message(deprecation) }) - if (!deprecated.has(node.name)) return + deprecated.set(local, deprecation) - if (declaredScope(context, node.name) !== 'module') return - context.report({ - node, - message: message(deprecated.get(node.name)), }) - }, + } - 'MemberExpression': function (dereference) { - if (dereference.object.type !== 'Identifier') return - if (!namespaces.has(dereference.object.name)) return + return { + 'Program': ({ body }) => body.forEach(checkSpecifiers), - if (declaredScope(context, dereference.object.name) !== 'module') return + 'Identifier': function (node) { + if (node.parent.type === 'MemberExpression' && node.parent.property === node) { + return // handled by MemberExpression + } - // go deep - var namespace = namespaces.get(dereference.object.name) - var namepath = [dereference.object.name] - // while property is namespace and parent is member expression, keep validating - while (namespace instanceof Exports && - dereference.type === 'MemberExpression') { + // ignore specifier identifiers + if (node.parent.type.slice(0, 6) === 'Import') return - // ignore computed parts for now - if (dereference.computed) return + if (!deprecated.has(node.name)) return - const metadata = namespace.get(dereference.property.name) + if (declaredScope(context, node.name) !== 'module') return + context.report({ + node, + message: message(deprecated.get(node.name)), + }) + }, - if (!metadata) break - const deprecation = getDeprecation(metadata) + 'MemberExpression': function (dereference) { + if (dereference.object.type !== 'Identifier') return + if (!namespaces.has(dereference.object.name)) return - if (deprecation) { - context.report({ node: dereference.property, message: message(deprecation) }) - } + if (declaredScope(context, dereference.object.name) !== 'module') return - // stash and pop - namepath.push(dereference.property.name) - namespace = metadata.namespace - dereference = dereference.parent - } - }, - } -} + // go deep + var namespace = namespaces.get(dereference.object.name) + var namepath = [dereference.object.name] + // while property is namespace and parent is member expression, keep validating + while (namespace instanceof Exports && + dereference.type === 'MemberExpression') { -function message(deprecation) { - return 'Deprecated' + (deprecation.description ? ': ' + deprecation.description : '.') -} + // ignore computed parts for now + if (dereference.computed) return -function getDeprecation(metadata) { - if (!metadata || !metadata.doc) return + const metadata = namespace.get(dereference.property.name) - let deprecation - if (metadata.doc.tags.some(t => t.title === 'deprecated' && (deprecation = t))) { - return deprecation - } + if (!metadata) break + const deprecation = getDeprecation(metadata) + + if (deprecation) { + context.report({ node: dereference.property, message: message(deprecation) }) + } + + // stash and pop + namepath.push(dereference.property.name) + namespace = metadata.namespace + dereference = dereference.parent + } + }, + } + }, } diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index af5747018..a63fc44d5 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -10,25 +10,31 @@ function checkImports(imported, context) { } } -module.exports = function (context) { - const imported = new Map() - const typesImported = new Map() - return { - 'ImportDeclaration': function (n) { - // resolved path will cover aliased duplicates - const resolvedPath = resolve(n.source.value, context) || n.source.value - const importMap = n.importKind === 'type' ? typesImported : imported +module.exports = { + meta: { + docs: {}, + }, - if (importMap.has(resolvedPath)) { - importMap.get(resolvedPath).add(n.source) - } else { - importMap.set(resolvedPath, new Set([n.source])) - } - }, + create: function (context) { + const imported = new Map() + const typesImported = new Map() + return { + 'ImportDeclaration': function (n) { + // resolved path will cover aliased duplicates + const resolvedPath = resolve(n.source.value, context) || n.source.value + const importMap = n.importKind === 'type' ? typesImported : imported - 'Program:exit': function () { - checkImports(imported, context) - checkImports(typesImported, context) - }, - } + if (importMap.has(resolvedPath)) { + importMap.get(resolvedPath).add(n.source) + } else { + importMap.set(resolvedPath, new Set([n.source])) + } + }, + + 'Program:exit': function () { + checkImports(imported, context) + checkImports(typesImported, context) + }, + } + }, } diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index 65bdb27e5..a14bb611d 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -81,42 +81,48 @@ function testConfig(config, filename) { return config.some(c => minimatch(filename, c)) } -module.exports = function (context) { - const options = context.options[0] || {} - const filename = context.getFilename() - const deps = getDependencies(context) +module.exports = { + meta: { + docs: {}, + + schema: [ + { + 'type': 'object', + 'properties': { + 'devDependencies': { 'type': ['boolean', 'array'] }, + 'optionalDependencies': { 'type': ['boolean', 'array'] }, + 'peerDependencies': { 'type': ['boolean', 'array'] }, + }, + 'additionalProperties': false, + }, + ], + }, - if (!deps) { - return {} - } + create: function (context) { + const options = context.options[0] || {} + const filename = context.getFilename() + const deps = getDependencies(context) - const depsOptions = { - allowDevDeps: testConfig(options.devDependencies, filename) !== false, - allowOptDeps: testConfig(options.optionalDependencies, filename) !== false, - allowPeerDeps: testConfig(options.peerDependencies, filename) !== false, - } + if (!deps) { + return {} + } - // todo: use module visitor from module-utils core - return { - ImportDeclaration: function (node) { - reportIfMissing(context, deps, depsOptions, node, node.source.value) - }, - CallExpression: function handleRequires(node) { - if (isStaticRequire(node)) { - reportIfMissing(context, deps, depsOptions, node, node.arguments[0].value) - } - }, - } -} + const depsOptions = { + allowDevDeps: testConfig(options.devDependencies, filename) !== false, + allowOptDeps: testConfig(options.optionalDependencies, filename) !== false, + allowPeerDeps: testConfig(options.peerDependencies, filename) !== false, + } -module.exports.schema = [ - { - 'type': 'object', - 'properties': { - 'devDependencies': { 'type': ['boolean', 'array'] }, - 'optionalDependencies': { 'type': ['boolean', 'array'] }, - 'peerDependencies': { 'type': ['boolean', 'array'] }, - }, - 'additionalProperties': false, + // todo: use module visitor from module-utils core + return { + ImportDeclaration: function (node) { + reportIfMissing(context, deps, depsOptions, node, node.source.value) + }, + CallExpression: function handleRequires(node) { + if (isStaticRequire(node)) { + reportIfMissing(context, deps, depsOptions, node, node.arguments[0].value) + } + }, + } }, -] +} diff --git a/src/rules/no-internal-modules.js b/src/rules/no-internal-modules.js index bc9669a45..0c7ede6df 100644 --- a/src/rules/no-internal-modules.js +++ b/src/rules/no-internal-modules.js @@ -5,88 +5,94 @@ import resolve from 'eslint-module-utils/resolve' import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' -module.exports = function noReachingInside(context) { - const options = context.options[0] || {} - const allowRegexps = (options.allow || []).map(p => minimatch.makeRe(p)) +module.exports = { + meta: { + docs: {}, - // test if reaching to this destination is allowed - function reachingAllowed(importPath) { - return !!find(allowRegexps, re => re.test(importPath)) - } + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + }, + ], + }, - // minimatch patterns are expected to use / path separators, like import - // statements, so normalize paths to use the same - function normalizeSep(somePath) { - return somePath.split('\\').join('/') - } + create: function noReachingInside(context) { + const options = context.options[0] || {} + const allowRegexps = (options.allow || []).map(p => minimatch.makeRe(p)) - // find a directory that is being reached into, but which shouldn't be - function isReachViolation(importPath) { - const steps = normalizeSep(importPath) - .split('/') - .reduce((acc, step) => { - if (!step || step === '.') { - return acc - } else if (step === '..') { - return acc.slice(0, -1) - } else { - return acc.concat(step) - } - }, []) + // test if reaching to this destination is allowed + function reachingAllowed(importPath) { + return !!find(allowRegexps, re => re.test(importPath)) + } + + // minimatch patterns are expected to use / path separators, like import + // statements, so normalize paths to use the same + function normalizeSep(somePath) { + return somePath.split('\\').join('/') + } - if (steps.length <= 1) return false + // find a directory that is being reached into, but which shouldn't be + function isReachViolation(importPath) { + const steps = normalizeSep(importPath) + .split('/') + .reduce((acc, step) => { + if (!step || step === '.') { + return acc + } else if (step === '..') { + return acc.slice(0, -1) + } else { + return acc.concat(step) + } + }, []) - // before trying to resolve, see if the raw import (with relative - // segments resolved) matches an allowed pattern - const justSteps = steps.join('/') - if (reachingAllowed(justSteps) || reachingAllowed(`/${justSteps}`)) return false + if (steps.length <= 1) return false - // if the import statement doesn't match directly, try to match the - // resolved path if the import is resolvable - const resolved = resolve(importPath, context) - if (!resolved || reachingAllowed(normalizeSep(resolved))) return false + // before trying to resolve, see if the raw import (with relative + // segments resolved) matches an allowed pattern + const justSteps = steps.join('/') + if (reachingAllowed(justSteps) || reachingAllowed(`/${justSteps}`)) return false - // this import was not allowed by the allowed paths, and reaches - // so it is a violation - return true - } + // if the import statement doesn't match directly, try to match the + // resolved path if the import is resolvable + const resolved = resolve(importPath, context) + if (!resolved || reachingAllowed(normalizeSep(resolved))) return false - function checkImportForReaching(importPath, node) { - const potentialViolationTypes = ['parent', 'index', 'sibling', 'external', 'internal'] - if (potentialViolationTypes.indexOf(importType(importPath, context)) !== -1 && - isReachViolation(importPath) - ) { - context.report({ - node, - message: `Reaching to "${importPath}" is not allowed.`, - }) + // this import was not allowed by the allowed paths, and reaches + // so it is a violation + return true } - } - return { - ImportDeclaration(node) { - checkImportForReaching(node.source.value, node.source) - }, - CallExpression(node) { - if (isStaticRequire(node)) { - const [ firstArgument ] = node.arguments - checkImportForReaching(firstArgument.value, firstArgument) + function checkImportForReaching(importPath, node) { + const potentialViolationTypes = ['parent', 'index', 'sibling', 'external', 'internal'] + if (potentialViolationTypes.indexOf(importType(importPath, context)) !== -1 && + isReachViolation(importPath) + ) { + context.report({ + node, + message: `Reaching to "${importPath}" is not allowed.`, + }) } - }, - } -} + } -module.exports.schema = [ - { - type: 'object', - properties: { - allow: { - type: 'array', - items: { - type: 'string', - }, + return { + ImportDeclaration(node) { + checkImportForReaching(node.source.value, node.source) }, - }, - additionalProperties: false, + CallExpression(node) { + if (isStaticRequire(node)) { + const [ firstArgument ] = node.arguments + checkImportForReaching(firstArgument.value, firstArgument) + } + }, + } }, -] +} diff --git a/src/rules/no-mutable-exports.js b/src/rules/no-mutable-exports.js index e6173bf11..8d16f8068 100644 --- a/src/rules/no-mutable-exports.js +++ b/src/rules/no-mutable-exports.js @@ -1,45 +1,51 @@ -module.exports = function (context) { - function checkDeclaration(node) { - const {kind} = node - if (kind === 'var' || kind === 'let') { - context.report(node, `Exporting mutable '${kind}' binding, use 'const' instead.`) +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + function checkDeclaration(node) { + const {kind} = node + if (kind === 'var' || kind === 'let') { + context.report(node, `Exporting mutable '${kind}' binding, use 'const' instead.`) + } } - } - function checkDeclarationsInScope({variables}, name) { - for (let variable of variables) { - if (variable.name === name) { - for (let def of variable.defs) { - if (def.type === 'Variable') { - checkDeclaration(def.parent) + function checkDeclarationsInScope({variables}, name) { + for (let variable of variables) { + if (variable.name === name) { + for (let def of variable.defs) { + if (def.type === 'Variable') { + checkDeclaration(def.parent) + } } } } } - } - function handleExportDefault(node) { - const scope = context.getScope() + function handleExportDefault(node) { + const scope = context.getScope() - if (node.declaration.name) { - checkDeclarationsInScope(scope, node.declaration.name) + if (node.declaration.name) { + checkDeclarationsInScope(scope, node.declaration.name) + } } - } - function handleExportNamed(node) { - const scope = context.getScope() + function handleExportNamed(node) { + const scope = context.getScope() - if (node.declaration) { - checkDeclaration(node.declaration) - } else if (!node.source) { - for (let specifier of node.specifiers) { - checkDeclarationsInScope(scope, specifier.local.name) + if (node.declaration) { + checkDeclaration(node.declaration) + } else if (!node.source) { + for (let specifier of node.specifiers) { + checkDeclarationsInScope(scope, specifier.local.name) + } } } - } - return { - 'ExportDefaultDeclaration': handleExportDefault, - 'ExportNamedDeclaration': handleExportNamed, - } + return { + 'ExportDefaultDeclaration': handleExportDefault, + 'ExportNamedDeclaration': handleExportNamed, + } + }, } diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index f02bfe4a5..2ffa19cff 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -11,81 +11,87 @@ import importDeclaration from '../importDeclaration' // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + }, - const fileImports = new Map() - const allPropertyLookups = new Map() + create: function(context) { - function handleImportDefault(node) { - const declaration = importDeclaration(context) - const exportMap = Exports.get(declaration.source.value, context) - if (exportMap == null) return + const fileImports = new Map() + const allPropertyLookups = new Map() - if (exportMap.errors.length) { - exportMap.reportErrors(context, declaration) - return - } + function handleImportDefault(node) { + const declaration = importDeclaration(context) + const exportMap = Exports.get(declaration.source.value, context) + if (exportMap == null) return - fileImports.set(node.local.name, { - exportMap, - sourcePath: declaration.source.value, - }) - } + if (exportMap.errors.length) { + exportMap.reportErrors(context, declaration) + return + } - function storePropertyLookup(objectName, propName, node) { - const lookups = allPropertyLookups.get(objectName) || [] - lookups.push({node, propName}) - allPropertyLookups.set(objectName, lookups) - } + fileImports.set(node.local.name, { + exportMap, + sourcePath: declaration.source.value, + }) + } + + function storePropertyLookup(objectName, propName, node) { + const lookups = allPropertyLookups.get(objectName) || [] + lookups.push({node, propName}) + allPropertyLookups.set(objectName, lookups) + } - function handlePropLookup(node) { - const objectName = node.object.name - const propName = node.property.name - storePropertyLookup(objectName, propName, node) - } + function handlePropLookup(node) { + const objectName = node.object.name + const propName = node.property.name + storePropertyLookup(objectName, propName, node) + } - function handleDestructuringAssignment(node) { - const isDestructure = ( - node.id.type === 'ObjectPattern' && - node.init != null && - node.init.type === 'Identifier' - ) - if (!isDestructure) return + function handleDestructuringAssignment(node) { + const isDestructure = ( + node.id.type === 'ObjectPattern' && + node.init != null && + node.init.type === 'Identifier' + ) + if (!isDestructure) return - const objectName = node.init.name - for (const { key } of node.id.properties) { - if (key == null) continue // true for rest properties - storePropertyLookup(objectName, key.name, key) + const objectName = node.init.name + for (const { key } of node.id.properties) { + if (key == null) continue // true for rest properties + storePropertyLookup(objectName, key.name, key) + } } - } - function handleProgramExit() { - allPropertyLookups.forEach((lookups, objectName) => { - const fileImport = fileImports.get(objectName) - if (fileImport == null) return + function handleProgramExit() { + allPropertyLookups.forEach((lookups, objectName) => { + const fileImport = fileImports.get(objectName) + if (fileImport == null) return - for (const {propName, node} of lookups) { - // the default import can have a "default" property - if (propName === 'default') continue - if (!fileImport.exportMap.namespace.has(propName)) continue + for (const {propName, node} of lookups) { + // the default import can have a "default" property + if (propName === 'default') continue + if (!fileImport.exportMap.namespace.has(propName)) continue - context.report({ - node, - message: ( - `Caution: \`${objectName}\` also has a named export ` + - `\`${propName}\`. Check if you meant to write ` + - `\`import {${propName}} from '${fileImport.sourcePath}'\` ` + - 'instead.' - ), - }) - } - }) - } + context.report({ + node, + message: ( + `Caution: \`${objectName}\` also has a named export ` + + `\`${propName}\`. Check if you meant to write ` + + `\`import {${propName}} from '${fileImport.sourcePath}'\` ` + + 'instead.' + ), + }) + } + }) + } - return { - 'ImportDefaultSpecifier': handleImportDefault, - 'MemberExpression': handlePropLookup, - 'VariableDeclarator': handleDestructuringAssignment, - 'Program:exit': handleProgramExit, - } + return { + 'ImportDefaultSpecifier': handleImportDefault, + 'MemberExpression': handlePropLookup, + 'VariableDeclarator': handleDestructuringAssignment, + 'Program:exit': handleProgramExit, + } + }, } diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index ddee1cc5b..502728cc4 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -1,29 +1,35 @@ import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' -module.exports = function (context) { - function checkDefault(nameKey, defaultSpecifier) { - var declaration = importDeclaration(context) +module.exports = { + meta: { + docs: {}, + }, - var imports = Exports.get(declaration.source.value, context) - if (imports == null) return + create: function (context) { + function checkDefault(nameKey, defaultSpecifier) { + var declaration = importDeclaration(context) - if (imports.errors.length) { - imports.reportErrors(context, declaration) - return - } + var imports = Exports.get(declaration.source.value, context) + if (imports == null) return + + if (imports.errors.length) { + imports.reportErrors(context, declaration) + return + } - if (imports.has('default') && - imports.has(defaultSpecifier[nameKey].name)) { + if (imports.has('default') && + imports.has(defaultSpecifier[nameKey].name)) { - context.report(defaultSpecifier, - 'Using exported name \'' + defaultSpecifier[nameKey].name + - '\' as identifier for default export.') + context.report(defaultSpecifier, + 'Using exported name \'' + defaultSpecifier[nameKey].name + + '\' as identifier for default export.') + } + } + return { + 'ImportDefaultSpecifier': checkDefault.bind(null, 'local'), + 'ExportDefaultSpecifier': checkDefault.bind(null, 'exported'), } - } - return { - 'ImportDefaultSpecifier': checkDefault.bind(null, 'local'), - 'ExportDefaultSpecifier': checkDefault.bind(null, 'exported'), - } + }, } diff --git a/src/rules/no-namespace.js b/src/rules/no-namespace.js index 2cd45d322..673735da0 100644 --- a/src/rules/no-namespace.js +++ b/src/rules/no-namespace.js @@ -8,10 +8,16 @@ //------------------------------------------------------------------------------ -module.exports = function (context) { - return { - 'ImportNamespaceSpecifier': function (node) { - context.report(node, `Unexpected namespace import.`) - }, - } +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + return { + 'ImportNamespaceSpecifier': function (node) { + context.report(node, `Unexpected namespace import.`) + }, + } + }, } diff --git a/src/rules/no-nodejs-modules.js b/src/rules/no-nodejs-modules.js index 9160b9e8d..262fec7dd 100644 --- a/src/rules/no-nodejs-modules.js +++ b/src/rules/no-nodejs-modules.js @@ -7,18 +7,24 @@ function reportIfMissing(context, node, allowed, name) { } } -module.exports = function (context) { - const options = context.options[0] || {} - const allowed = options.allow || [] +module.exports = { + meta: { + docs: {}, + }, - return { - ImportDeclaration: function handleImports(node) { - reportIfMissing(context, node, allowed, node.source.value) - }, - CallExpression: function handleRequires(node) { - if (isStaticRequire(node)) { - reportIfMissing(context, node, allowed, node.arguments[0].value) - } - }, - } + create: function (context) { + const options = context.options[0] || {} + const allowed = options.allow || [] + + return { + ImportDeclaration: function handleImports(node) { + reportIfMissing(context, node, allowed, node.source.value) + }, + CallExpression: function handleRequires(node) { + if (isStaticRequire(node)) { + reportIfMissing(context, node, allowed, node.arguments[0].value) + } + }, + } + }, } diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index 9f76b8eb3..0240cd764 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -4,68 +4,74 @@ import path from 'path' import resolve from 'eslint-module-utils/resolve' import isStaticRequire from '../core/staticRequire' -module.exports = function noRestrictedPaths(context) { - const options = context.options[0] || {} - const restrictedPaths = options.zones || [] - const basePath = options.basePath || process.cwd() - const currentFilename = context.getFilename() - const matchingZones = restrictedPaths.filter((zone) => { - const targetPath = path.resolve(basePath, zone.target) +module.exports = { + meta: { + docs: {}, - return containsPath(currentFilename, targetPath) - }) + schema: [ + { + type: 'object', + properties: { + zones: { + type: 'array', + minItems: 1, + items: { + type: 'object', + properties: { + target: { type: 'string' }, + from: { type: 'string' }, + }, + additionalProperties: false, + }, + }, + basePath: { type: 'string' }, + }, + additionalProperties: false, + }, + ], + }, - function checkForRestrictedImportPath(importPath, node) { - const absoluteImportPath = resolve(importPath, context) + create: function noRestrictedPaths(context) { + const options = context.options[0] || {} + const restrictedPaths = options.zones || [] + const basePath = options.basePath || process.cwd() + const currentFilename = context.getFilename() + const matchingZones = restrictedPaths.filter((zone) => { + const targetPath = path.resolve(basePath, zone.target) - if (!absoluteImportPath) { - return - } + return containsPath(currentFilename, targetPath) + }) - matchingZones.forEach((zone) => { - const absoluteFrom = path.resolve(basePath, zone.from) + function checkForRestrictedImportPath(importPath, node) { + const absoluteImportPath = resolve(importPath, context) - if (containsPath(absoluteImportPath, absoluteFrom)) { - context.report({ - node, - message: `Unexpected path "${importPath}" imported in restricted zone.`, - }) + if (!absoluteImportPath) { + return } - }) - } - return { - ImportDeclaration(node) { - checkForRestrictedImportPath(node.source.value, node.source) - }, - CallExpression(node) { - if (isStaticRequire(node)) { - const [ firstArgument ] = node.arguments + matchingZones.forEach((zone) => { + const absoluteFrom = path.resolve(basePath, zone.from) - checkForRestrictedImportPath(firstArgument.value, firstArgument) - } - }, - } -} + if (containsPath(absoluteImportPath, absoluteFrom)) { + context.report({ + node, + message: `Unexpected path "${importPath}" imported in restricted zone.`, + }) + } + }) + } -module.exports.schema = [ - { - type: 'object', - properties: { - zones: { - type: 'array', - minItems: 1, - items: { - type: 'object', - properties: { - target: { type: 'string' }, - from: { type: 'string' }, - }, - additionalProperties: false, - }, + return { + ImportDeclaration(node) { + checkForRestrictedImportPath(node.source.value, node.source) }, - basePath: { type: 'string' }, - }, - additionalProperties: false, + CallExpression(node) { + if (isStaticRequire(node)) { + const [ firstArgument ] = node.arguments + + checkForRestrictedImportPath(firstArgument.value, firstArgument) + } + }, + } }, -] +} diff --git a/src/rules/no-unresolved.js b/src/rules/no-unresolved.js index 05e9d484a..e5fe2366c 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -9,36 +9,38 @@ import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisi -exports.meta = { - schema: [ makeOptionsSchema({ - caseSensitive: { type: 'boolean', default: true }, - })], -} - -exports.create = function (context) { +module.exports = { + meta: { + schema: [ makeOptionsSchema({ + caseSensitive: { type: 'boolean', default: true }, + })], + }, - function checkSourceValue(source) { - const shouldCheckCase = !CASE_SENSITIVE_FS && - (!context.options[0] || context.options[0].caseSensitive !== false) + create: function (context) { - const resolvedPath = resolve(source.value, context) + function checkSourceValue(source) { + const shouldCheckCase = !CASE_SENSITIVE_FS && + (!context.options[0] || context.options[0].caseSensitive !== false) - if (resolvedPath === undefined) { - context.report(source, - `Unable to resolve path to module '${source.value}'.`) - } + const resolvedPath = resolve(source.value, context) - else if (shouldCheckCase) { - const cacheSettings = ModuleCache.getSettings(context.settings) - if (!fileExistsWithCaseSync(resolvedPath, cacheSettings)) { + if (resolvedPath === undefined) { context.report(source, - `Casing of ${source.value} does not match the underlying filesystem.`) + `Unable to resolve path to module '${source.value}'.`) } + else if (shouldCheckCase) { + const cacheSettings = ModuleCache.getSettings(context.settings) + if (!fileExistsWithCaseSync(resolvedPath, cacheSettings)) { + context.report(source, + `Casing of ${source.value} does not match the underlying filesystem.`) + } + + } } - } - return moduleVisitor(checkSourceValue, context.options[0]) + return moduleVisitor(checkSourceValue, context.options[0]) + }, } diff --git a/src/rules/order.js b/src/rules/order.js index ed5730ab1..0a227097c 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -145,77 +145,83 @@ function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) { }) } -module.exports = function importOrderRule (context) { - const options = context.options[0] || {} - let ranks - - try { - ranks = convertGroupsToRanks(options.groups || defaultGroups) - } catch (error) { - // Malformed configuration - return { - Program: function(node) { - context.report(node, error.message) - }, - } - } - let imported = [] - let level = 0 +module.exports = { + meta: { + docs: {}, - function incrementLevel() { - level++ - } - function decrementLevel() { - level-- - } + schema: [ + { + type: 'object', + properties: { + groups: { + type: 'array', + }, + 'newlines-between': { + enum: [ 'always', 'never' ], + }, + }, + additionalProperties: false, + }, + ], + }, - return { - ImportDeclaration: function handleImports(node) { - if (node.specifiers.length) { // Ignoring unassigned imports - const name = node.source.value - registerNode(context, node, name, 'import', ranks, imported) - } - }, - CallExpression: function handleRequires(node) { - if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) { - return - } - const name = node.arguments[0].value - registerNode(context, node, name, 'require', ranks, imported) - }, - 'Program:exit': function reportAndReset() { - makeOutOfOrderReport(context, imported) - - if ('newlines-between' in options) { - makeNewlinesBetweenReport(context, imported, options['newlines-between']) + create: function importOrderRule (context) { + const options = context.options[0] || {} + let ranks + + try { + ranks = convertGroupsToRanks(options.groups || defaultGroups) + } catch (error) { + // Malformed configuration + return { + Program: function(node) { + context.report(node, error.message) + }, } + } + let imported = [] + let level = 0 - imported = [] - }, - FunctionDeclaration: incrementLevel, - FunctionExpression: incrementLevel, - ArrowFunctionExpression: incrementLevel, - BlockStatement: incrementLevel, - ObjectExpression: incrementLevel, - 'FunctionDeclaration:exit': decrementLevel, - 'FunctionExpression:exit': decrementLevel, - 'ArrowFunctionExpression:exit': decrementLevel, - 'BlockStatement:exit': decrementLevel, - 'ObjectExpression:exit': decrementLevel, - } -} + function incrementLevel() { + level++ + } + function decrementLevel() { + level-- + } -module.exports.schema = [ - { - type: 'object', - properties: { - groups: { - type: 'array', + return { + ImportDeclaration: function handleImports(node) { + if (node.specifiers.length) { // Ignoring unassigned imports + const name = node.source.value + registerNode(context, node, name, 'import', ranks, imported) + } }, - 'newlines-between': { - enum: [ 'always', 'never' ], + CallExpression: function handleRequires(node) { + if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) { + return + } + const name = node.arguments[0].value + registerNode(context, node, name, 'require', ranks, imported) }, - }, - additionalProperties: false, + 'Program:exit': function reportAndReset() { + makeOutOfOrderReport(context, imported) + + if ('newlines-between' in options) { + makeNewlinesBetweenReport(context, imported, options['newlines-between']) + } + + imported = [] + }, + FunctionDeclaration: incrementLevel, + FunctionExpression: incrementLevel, + ArrowFunctionExpression: incrementLevel, + BlockStatement: incrementLevel, + ObjectExpression: incrementLevel, + 'FunctionDeclaration:exit': decrementLevel, + 'FunctionExpression:exit': decrementLevel, + 'ArrowFunctionExpression:exit': decrementLevel, + 'BlockStatement:exit': decrementLevel, + 'ObjectExpression:exit': decrementLevel, + } }, -] +} diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.js index 2bd4783eb..afff03341 100644 --- a/src/rules/prefer-default-export.js +++ b/src/rules/prefer-default-export.js @@ -1,63 +1,69 @@ 'use strict' -module.exports = function(context) { - let specifierExportCount = 0 - let hasDefaultExport = false - let hasStarExport = false - let namedExportNode = null - - return { - 'ExportSpecifier': function(node) { - if (node.exported.name === 'default') { - hasDefaultExport = true - } else { - specifierExportCount++ - namedExportNode = node - } - }, - - 'ExportNamedDeclaration': function(node) { - // if there are specifiers, node.declaration should be null - if (!node.declaration) return - - function captureDeclaration(identifierOrPattern) { - if (identifierOrPattern.type === 'ObjectPattern') { - // recursively capture - identifierOrPattern.properties - .forEach(function(property) { - captureDeclaration(property.value) - }) +module.exports = { + meta: { + docs: {}, + }, + + create: function(context) { + let specifierExportCount = 0 + let hasDefaultExport = false + let hasStarExport = false + let namedExportNode = null + + return { + 'ExportSpecifier': function(node) { + if (node.exported.name === 'default') { + hasDefaultExport = true } else { - // assume it's a single standard identifier specifierExportCount++ + namedExportNode = node + } + }, + + 'ExportNamedDeclaration': function(node) { + // if there are specifiers, node.declaration should be null + if (!node.declaration) return + + function captureDeclaration(identifierOrPattern) { + if (identifierOrPattern.type === 'ObjectPattern') { + // recursively capture + identifierOrPattern.properties + .forEach(function(property) { + captureDeclaration(property.value) + }) + } else { + // assume it's a single standard identifier + specifierExportCount++ + } + } + + if (node.declaration.declarations) { + node.declaration.declarations.forEach(function(declaration) { + captureDeclaration(declaration.id) + }) + } + else { + // captures 'export function foo() {}' syntax + specifierExportCount++ + } + + namedExportNode = node + }, + + 'ExportDefaultDeclaration': function() { + hasDefaultExport = true + }, + + 'ExportAllDeclaration': function() { + hasStarExport = true + }, + + 'Program:exit': function() { + if (specifierExportCount === 1 && !hasDefaultExport && !hasStarExport) { + context.report(namedExportNode, 'Prefer default export.') } - } - - if (node.declaration.declarations) { - node.declaration.declarations.forEach(function(declaration) { - captureDeclaration(declaration.id) - }) - } - else { - // captures 'export function foo() {}' syntax - specifierExportCount++ - } - - namedExportNode = node - }, - - 'ExportDefaultDeclaration': function() { - hasDefaultExport = true - }, - - 'ExportAllDeclaration': function() { - hasStarExport = true - }, - - 'Program:exit': function() { - if (specifierExportCount === 1 && !hasDefaultExport && !hasStarExport) { - context.report(namedExportNode, 'Prefer default export.') - } - }, - } + }, + } + }, }