From daf38ac98001d31b8ac078aad7f8a74dd30ed364 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 12 Sep 2016 00:40:20 -0700 Subject: [PATCH 1/2] [New] `extensions`: accept both a string, and an object to override it. Fixes #390. --- docs/rules/extensions.md | 4 ++- src/rules/extensions.js | 61 ++++++++++++++++++++++------------- tests/src/rules/extensions.js | 53 ++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 23 deletions(-) diff --git a/docs/rules/extensions.md b/docs/rules/extensions.md index 4fe70eea9..6ca33bb44 100644 --- a/docs/rules/extensions.md +++ b/docs/rules/extensions.md @@ -6,10 +6,12 @@ In order to provide a consistent use of file extensions across your code base, t ## Rule Details -This rule has one option which could be either a string or an object. If it is `"never"` (the default value) the rule forbids the use for any extension. If `"always"` then the rule enforces the use of extensions for all import statements. +This rule either takes one string option, one object option, or a string and an object option. If it is the string `"never"` (the default value), then the rule forbids the use for any extension. If it is the string `"always"`, then the rule enforces the use of extensions for all import statements. By providing an object you can configure each extension separately, so for example `{ "js": "always", "json": "never" }` would always enforce the use of the `.js` extension but never allow the use of the `.json` extension. +By providing both a string and an object, the string will set the default setting for all extensions, and the object can be used to set granular overrides for specific extensions. For example, `[, "never", { "svg": "always" }]` would require that all extensions are omitted, except for "svg". + ### Exception When disallowing the use of certain extensions this rule makes an exception and allows the use of extension when the file would not be resolvable without extension. diff --git a/src/rules/extensions.js b/src/rules/extensions.js index f8bb86e18..e4acd188b 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -4,15 +4,19 @@ import endsWith from 'lodash.endswith' import resolve from '../core/resolve' import { isBuiltIn } from '../core/importType' +const has = Object.prototype.hasOwnProperty; + module.exports = function (context) { const configuration = context.options[0] || 'never' + const defaultConfig = typeof configuration === 'string' ? configuration : null + const modifiers = typeof configuration === 'object' ? configuration : context.options[1] || {} - function isUseOfExtensionEnforced(extension) { - if (typeof configuration === 'object') { - return configuration[extension] === 'always' - } + function isUseOfExtensionRequired(extension) { + return (has.call(modifiers, extension) || defaultConfig) === 'always' + } - return configuration === 'always' + function isUseOfExtensionForbidden(extension) { + return (has.call(modifiers, extension) || defaultConfig) === 'never' } function isResolvableWithoutExtension(file) { @@ -37,7 +41,7 @@ module.exports = function (context) { const extension = path.extname(resolvedPath || importPath).substring(1) if (!extension || !endsWith(importPath, extension)) { - if (isUseOfExtensionEnforced(extension)) { + if (isUseOfExtensionRequired(extension) && !isUseOfExtensionForbidden(extension)) { context.report({ node: source, message: @@ -45,7 +49,7 @@ module.exports = function (context) { }) } } else if (extension) { - if (!isUseOfExtensionEnforced(extension) && isResolvableWithoutExtension(importPath)) { + if (isUseOfExtensionForbidden(extension) && isResolvableWithoutExtension(importPath)) { context.report({ node: source, message: `Unexpected use of file extension "${extension}" for "${importPath}"`, @@ -59,18 +63,31 @@ module.exports = function (context) { } } -module.exports.schema = [ - { - oneOf: [ - { - enum: [ 'always', 'never' ], - }, - { - type: 'object', - patternProperties: { - '.*': { enum: [ 'always', 'never' ] }, - }, - }, - ], - }, -] +const enumValues = { enum: [ 'always', 'never' ] } +const patternProperties = { + type: 'object', + patternProperties: { '.*': enumValues }, +} + +module.exports.schema = { + anyOf: [ + { + type: 'array', + items: [enumValues], + additionalItems: false, + }, + { + type: 'array', + items: [patternProperties], + additionalItems: false, + }, + { + type: 'array', + items: [ + enumValues, + patternProperties, + ], + additionalItems: false, + }, + ], +} diff --git a/tests/src/rules/extensions.js b/tests/src/rules/extensions.js index 74f8c1473..dfc58bfec 100644 --- a/tests/src/rules/extensions.js +++ b/tests/src/rules/extensions.js @@ -33,6 +33,25 @@ ruleTester.run('extensions', rule, { settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, }), + test({ + code: [ + 'import lib from "./bar"', + 'import lib from "./bar.json"', + 'import lib from "./bar.hbs"', + ].join('\n'), + options: [ 'always', { js: 'never', jsx: 'never' } ], + settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json', '.hbs' ] } }, + }), + + test({ + code: [ + 'import lib from "./bar.js"', + 'import lib from "./package"', + ].join('\n'), + options: [ 'never', { js: 'always', json: 'never' } ], + settings: { 'import/resolve': { 'extensions': [ '.js', '.json' ] } }, + }), + // unresolved (#271/#295) test({ code: 'import path from "path"' }), test({ code: 'import path from "path"', options: [ 'never' ] }), @@ -124,6 +143,40 @@ ruleTester.run('extensions', rule, { ], }), + test({ + code: [ + 'import lib from "./bar.js"', + 'import lib from "./bar.json"', + 'import lib from "./bar"', + ].join('\n'), + options: [ 'always', { json: 'always', js: 'never', jsx: 'never' } ], + settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, + errors: [ + { + message: 'Unexpected use of file extension "js" for "./bar.js"', + line: 1, + column: 17, + }, + ], + }), + + test({ + code: [ + 'import lib from "./bar.js"', + 'import lib from "./bar.json"', + 'import lib from "./bar"', + ].join('\n'), + options: [ 'never', { json: 'always', js: 'never', jsx: 'never' } ], + settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, + errors: [ + { + message: 'Unexpected use of file extension "js" for "./bar.js"', + line: 1, + column: 17, + }, + ], + }), + // unresolved (#271/#295) test({ code: 'import thing from "./fake-file.js"', From f507ff35ff01946d8e06f0af4001c12a24592522 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Fri, 16 Sep 2016 02:13:24 -0700 Subject: [PATCH 2/2] Cache extension settings using `has`. --- package.json | 1 + resolvers/webpack/index.js | 3 ++- src/rules/extensions.js | 15 ++++++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d49f36cb3..0d02e57bd 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "es6-map": "^0.1.3", "es6-set": "^0.1.4", "eslint-import-resolver-node": "^0.2.0", + "has": "^1.0.1", "lodash.cond": "^4.3.0", "lodash.endswith": "^4.0.1", "lodash.find": "^4.3.0", diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index d463695a0..535ebf2b7 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -10,6 +10,7 @@ var findRoot = require('find-root') , assign = require('object-assign') , resolve = require('resolve') , semver = require('semver') + , has = require('has') var log = require('debug')('eslint-plugin-import:resolver:webpack') @@ -265,7 +266,7 @@ function findExternal(source, externals, context) { // else, vanilla object for (var key in externals) { - if (!externals.hasOwnProperty(key)) continue + if (!has(externals, key)) continue if (source === key) return true } return false diff --git a/src/rules/extensions.js b/src/rules/extensions.js index e4acd188b..1522ffe7c 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -1,22 +1,27 @@ import path from 'path' import endsWith from 'lodash.endswith' +import has from 'has' +import assign from 'object-assign' import resolve from '../core/resolve' import { isBuiltIn } from '../core/importType' -const has = Object.prototype.hasOwnProperty; - module.exports = function (context) { const configuration = context.options[0] || 'never' const defaultConfig = typeof configuration === 'string' ? configuration : null - const modifiers = typeof configuration === 'object' ? configuration : context.options[1] || {} + const modifiers = assign( + {}, + typeof configuration === 'object' ? configuration : context.options[1] + ) function isUseOfExtensionRequired(extension) { - return (has.call(modifiers, extension) || defaultConfig) === 'always' + if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig } + return modifiers[extension] === 'always' } function isUseOfExtensionForbidden(extension) { - return (has.call(modifiers, extension) || defaultConfig) === 'never' + if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig } + return modifiers[extension] === 'never' } function isResolvableWithoutExtension(file) {