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/.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 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/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 0631b5675..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" @@ -43,14 +44,14 @@ "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", "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", @@ -68,16 +69,11 @@ "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", "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/getExports.js b/src/ExportMap.js similarity index 51% rename from src/core/getExports.js rename to src/ExportMap.js index 00a0a273b..109a8cefc 100644 --- a/src/core/getExports.js +++ b/src/ExportMap.js @@ -1,16 +1,15 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' +'use strict' +exports.__esModule = true -import * as fs from 'fs' +const fs = require('fs') -import { createHash } from 'crypto' -import * as doctrine from 'doctrine' +const doctrine = require('doctrine') -import parse from './parse' -import resolve from './resolve' -import isIgnored from './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 -import { hashObject } from './hash' +const hashObject = require('eslint-module-utils/hash').hashObject const exportCache = new Map() @@ -22,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() @@ -40,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 @@ -259,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 @@ -300,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) @@ -334,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) => @@ -362,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 => { @@ -434,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. @@ -441,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 @@ -461,3 +464,4 @@ export function recursivePatternCapture(pattern, callback) { break } } +exports.recursivePatternCapture = recursivePatternCapture 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..4eaa70ff5 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -1,4 +1,4 @@ -import Exports from '../core/getExports' +import Exports from '../ExportMap' module.exports = function (context) { diff --git a/src/rules/export.js b/src/rules/export.js index 9263b0ae3..61d21b86f 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,8 +1,4 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' -import Set from 'es6-set' - -import ExportMap, { recursivePatternCapture } from '../core/getExports' +import ExportMap, { recursivePatternCapture } from '../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..853331752 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 '../ExportMap' module.exports = function (context) { function checkSpecifiers(key, type, node) { diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 32ae15a07..23a9f0eec 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -1,9 +1,6 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' - -import Exports from '../core/getExports' +import Exports from '../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 882f5e48c..fc5a959ed 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,7 +1,5 @@ -import Map from 'es6-map' - -import Exports from '../core/getExports' -import declaredScope from '../core/declaredScope' +import Exports from '../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 fe6afb094..af5747018 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -1,8 +1,4 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' -import Set from 'es6-set' - -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..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 '../core/getExports' +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 a6a3ffd52..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 '../core/getExports' +import Exports from '../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 8afd1613d..55c6f342a 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -3,100 +3,20 @@ * @author Ben Mosher */ -import 'es6-symbol/implement' - -import resolve from '../core/resolve' +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/tests/src/core/getExports.js b/tests/src/core/getExports.js index c9ae6dd6d..254100508 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 '../../../src/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/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') 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/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/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, +} diff --git a/utils/package.json b/utils/package.json new file mode 100644 index 000000000..e6a939189 --- /dev/null +++ b/utils/package.json @@ -0,0 +1,25 @@ +{ + "name": "eslint-module-utils", + "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" + }, + "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 73% rename from src/core/resolve.js rename to utils/resolve.js index 1f561f948..44ca5f6e8 100644 --- a/src/core/resolve.js +++ b/utils/resolve.js @@ -1,29 +1,18 @@ -import 'es6-symbol/implement' -import Map from 'es6-map' -import Set from 'es6-set' -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 fileExistsCache = new Map() +const hashObject = require('./hash').hashObject + , ModuleCache = require('./ModuleCache').default -function cachePath(cacheKey, result) { - fileExistsCache.set(cacheKey, { result, lastSeen: Date.now() }) -} +const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname, 'reSOLVE.js')) +exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS -function checkCache(cacheKey, { lifetime }) { - if (fileExistsCache.has(cacheKey)) { - const { result, lastSeen } = fileExistsCache.get(cacheKey) - // check fresness - if (Date.now() - lastSeen < (lifetime * 1000)) return result - } - // cache miss - return undefined -} +const fileExistsCache = new ModuleCache() // http://stackoverflow.com/a/27382838 function fileExistsWithCaseSync(filepath, cacheSettings) { @@ -35,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 @@ -49,11 +38,11 @@ function fileExistsWithCaseSync(filepath, cacheSettings) { result = fileExistsWithCaseSync(dir, cacheSettings) } } - cachePath(filepath, result) + fileExistsCache.set(filepath, result) return result } -export function relative(modulePath, sourceFile, settings) { +function relative(modulePath, sourceFile, settings) { return fullResolve(modulePath, sourceFile, settings).path } @@ -63,22 +52,15 @@ 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 - - const cacheSettings = assign({ - lifetime: 30, // seconds - }, settings['import/cache']) + , cacheKey = sourceDir + hashObject(settings).digest('hex') + modulePath - // 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) { @@ -112,7 +94,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) @@ -130,6 +114,7 @@ function fullResolve(modulePath, sourceFile, settings) { // cache(undefined) return { found: false } } +exports.relative = relative function resolverReducer(resolvers, map) { if (resolvers instanceof Array) { @@ -185,7 +170,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() @@ -202,11 +187,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