diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae731d8a2..a0e1583c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,15 @@ This project adheres to [Semantic Versioning](http://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). ## [Unreleased] +### Added +- [`exports-last`] rule ([#620] + [#632], thanks [@k15a]) +### Changed +- Case-sensitivity checking ignores working directory and ancestors. ([#720] + [#858], thanks [@laysent]) ## [2.7.0] - 2017-07-06 ### Changed -- [`no-absolute-path`] picks up speed boost, optional AMD support ([#843], thansk [@jseminck]) +- [`no-absolute-path`] picks up speed boost, optional AMD support ([#843], thanks [@jseminck]) ## [2.6.1] - 2017-06-29 ### Fixed @@ -419,10 +423,12 @@ for info on changes for earlier releases. [`no-unassigned-import`]: ./docs/rules/no-unassigned-import.md [`unambiguous`]: ./docs/rules/unambiguous.md [`no-anonymous-default-export`]: ./docs/rules/no-anonymous-default-export.md +[`exports-last`]: ./docs/rules/exports-last.md [`no-self-import`]: ./docs/rules/no-self-import.md [`memo-parser`]: ./memo-parser/README.md +[#858]: https://github.com/benmosher/eslint-plugin-import/pull/858 [#843]: https://github.com/benmosher/eslint-plugin-import/pull/843 [#871]: https://github.com/benmosher/eslint-plugin-import/pull/871 [#742]: https://github.com/benmosher/eslint-plugin-import/pull/742 @@ -434,6 +440,7 @@ for info on changes for earlier releases. [#680]: https://github.com/benmosher/eslint-plugin-import/pull/680 [#654]: https://github.com/benmosher/eslint-plugin-import/pull/654 [#639]: https://github.com/benmosher/eslint-plugin-import/pull/639 +[#632]: https://github.com/benmosher/eslint-plugin-import/pull/632 [#630]: https://github.com/benmosher/eslint-plugin-import/pull/630 [#628]: https://github.com/benmosher/eslint-plugin-import/pull/628 [#596]: https://github.com/benmosher/eslint-plugin-import/pull/596 @@ -488,11 +495,13 @@ for info on changes for earlier releases. [#863]: https://github.com/benmosher/eslint-plugin-import/issues/863 [#839]: https://github.com/benmosher/eslint-plugin-import/issues/839 +[#720]: https://github.com/benmosher/eslint-plugin-import/issues/720 [#686]: https://github.com/benmosher/eslint-plugin-import/issues/686 [#671]: https://github.com/benmosher/eslint-plugin-import/issues/671 [#660]: https://github.com/benmosher/eslint-plugin-import/issues/660 [#653]: https://github.com/benmosher/eslint-plugin-import/issues/653 [#627]: https://github.com/benmosher/eslint-plugin-import/issues/627 +[#620]: https://github.com/benmosher/eslint-plugin-import/issues/620 [#609]: https://github.com/benmosher/eslint-plugin-import/issues/609 [#604]: https://github.com/benmosher/eslint-plugin-import/issues/604 [#602]: https://github.com/benmosher/eslint-plugin-import/issues/602 @@ -638,3 +647,5 @@ for info on changes for earlier releases. [@eelyafi]: https://github.com/eelyafi [@mastilver]: https://github.com/mastilver [@jseminck]: https://github.com/jseminck +[@laysent]: https://github.com/laysent +[@k15a]: https://github.com/k15a diff --git a/README.md b/README.md index 47f28fd0f2..66ca535f00 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a **Style guide:** * Ensure all imports appear before other statements ([`first`]) +* Ensure all exports appear after other statements ([`exports-last`]) * 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`]) @@ -81,6 +82,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Forbid anonymous values as default exports ([`no-anonymous-default-export`]) [`first`]: ./docs/rules/first.md +[`exports-last`]: ./docs/rules/exports-last.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/exports-last.md b/docs/rules/exports-last.md new file mode 100644 index 0000000000..22b654d2ea --- /dev/null +++ b/docs/rules/exports-last.md @@ -0,0 +1,50 @@ +# exports-last + +This rule enforces that all exports are declared at the bottom of the file. This rule will report any export declarations that comes before any non-export statements. + + +## This will be reported + +```JS + +const bool = true + +export default bool + +const str = 'foo' + +``` + +```JS + +export const bool = true + +const str = 'foo' + +``` + +## This will not be reported + +```JS +const arr = ['bar'] + +export const bool = true + +export default bool + +export function func() { + console.log('Hello World 🌍') +} + +export const str = 'foo' +``` + +## When Not To Use It + +If you don't mind exports being sprinkled throughout a file, you may not want to enable this rule. + +#### ES6 exports only + +The exports-last rule is currently only working on ES6 exports. You may not want to enable this rule if you're using CommonJS exports. + +If you need CommonJS support feel free to open an issue or create a PR. diff --git a/src/core/importType.js b/src/core/importType.js index 905b826133..d663a1a87c 100644 --- a/src/core/importType.js +++ b/src/core/importType.js @@ -8,13 +8,23 @@ function constant(value) { return () => value } +function baseModule(name) { + if (isScoped(name)) { + const [scope, pkg] = name.split('/') + return `${scope}/${pkg}` + } + const [pkg] = name.split('/') + return pkg +} + export function isAbsolute(name) { return name.indexOf('/') === 0 } export function isBuiltIn(name, settings) { + const base = baseModule(name) const extras = (settings && settings['import/core-modules']) || [] - return builtinModules.indexOf(name) !== -1 || extras.indexOf(name) > -1 + return builtinModules.indexOf(base) !== -1 || extras.indexOf(base) > -1 } function isExternalPath(path, name, settings) { diff --git a/src/index.js b/src/index.js index 1c414cce8f..03a8ebd268 100644 --- a/src/index.js +++ b/src/index.js @@ -32,6 +32,9 @@ export const rules = { 'unambiguous': require('./rules/unambiguous'), 'no-unassigned-import': require('./rules/no-unassigned-import'), + // export + 'exports-last': require('./rules/exports-last'), + // metadata-based 'no-deprecated': require('./rules/no-deprecated'), diff --git a/src/rules/exports-last.js b/src/rules/exports-last.js new file mode 100644 index 0000000000..91af6b421d --- /dev/null +++ b/src/rules/exports-last.js @@ -0,0 +1,31 @@ +function isNonExportStatement({ type }) { + return type !== 'ExportDefaultDeclaration' && + type !== 'ExportNamedDeclaration' && + type !== 'ExportAllDeclaration' +} + +module.exports = { + create: function (context) { + return { + Program: function ({ body }) { + const lastNonExportStatementIndex = body.reduce(function findLastIndex(acc, item, index) { + if (isNonExportStatement(item)) { + return index + } + return acc + }, -1) + + if (lastNonExportStatementIndex !== -1) { + body.slice(0, lastNonExportStatementIndex).forEach(function checkNonExport(node) { + if (!isNonExportStatement(node)) { + context.report({ + node, + message: 'Export statements should appear at the end of the file', + }) + } + }) + } + }, + } + }, +} diff --git a/tests/src/core/importType.js b/tests/src/core/importType.js index dedf43d0d2..abf9b95228 100644 --- a/tests/src/core/importType.js +++ b/tests/src/core/importType.js @@ -72,9 +72,21 @@ describe('importType(name)', function () { it("should return 'builtin' for additional core modules", function() { // without extra config, should be marked external expect(importType('electron', context)).to.equal('external') + expect(importType('@org/foobar', context)).to.equal('external') const electronContext = testContext({ 'import/core-modules': ['electron'] }) expect(importType('electron', electronContext)).to.equal('builtin') + + const scopedContext = testContext({ 'import/core-modules': ['@org/foobar'] }) + expect(importType('@org/foobar', scopedContext)).to.equal('builtin') + }) + + it("should return 'builtin' for resources inside additional core modules", function() { + const electronContext = testContext({ 'import/core-modules': ['electron'] }) + expect(importType('electron/some/path/to/resource.json', electronContext)).to.equal('builtin') + + const scopedContext = testContext({ 'import/core-modules': ['@org/foobar'] }) + expect(importType('@org/foobar/some/path/to/resource.json', scopedContext)).to.equal('builtin') }) it("should return 'external' for module from 'node_modules' with default config", function() { diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index bfff7935ca..307befcde0 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -3,6 +3,7 @@ import { expect } from 'chai' import resolve, { CASE_SENSITIVE_FS, fileExistsWithCaseSync } from 'eslint-module-utils/resolve' import ModuleCache from 'eslint-module-utils/ModuleCache' +import * as path from 'path' import * as fs from 'fs' import * as utils from '../utils' @@ -133,6 +134,11 @@ describe('resolve', function () { expect(fileExistsWithCaseSync(file, ModuleCache.getSettings(testContext))) .to.be.false }) + it('detecting case does not include parent folder path (issue #720)', function () { + const f = path.join(process.cwd().toUpperCase(), './tests/files/jsx/MyUnCoolComponent.jsx') + expect(fileExistsWithCaseSync(f, ModuleCache.getSettings(testContext), true)) + .to.be.true + }) }) describe('rename cache correctness', function () { diff --git a/tests/src/rules/exports-last.js b/tests/src/rules/exports-last.js new file mode 100644 index 0000000000..c3c26fdfc7 --- /dev/null +++ b/tests/src/rules/exports-last.js @@ -0,0 +1,124 @@ +import { test } from '../utils' + +import { RuleTester } from 'eslint' +import rule from 'rules/exports-last' + +const ruleTester = new RuleTester() + +const error = type => ({ + ruleId: 'exports-last', + message: 'Export statements should appear at the end of the file', + type +}); + +ruleTester.run('exports-last', rule, { + valid: [ + // Empty file + test({ + code: '// comment', + }), + test({ + // No exports + code: ` + const foo = 'bar' + const bar = 'baz' + `, + }), + test({ + code: ` + const foo = 'bar' + export {foo} + `, + }), + test({ + code: ` + const foo = 'bar' + export default foo + `, + }), + // Only exports + test({ + code: ` + export default foo + export const bar = true + `, + }), + test({ + code: ` + const foo = 'bar' + export default foo + export const bar = true + `, + }), + // Multiline export + test({ + code: ` + const foo = 'bar' + export default function foo () { + const very = 'multiline' + } + export const bar = true + `, + }), + // Many exports + test({ + code: ` + const foo = 'bar' + export default foo + export const so = 'many' + export const exports = ':)' + export const i = 'cant' + export const even = 'count' + export const how = 'many' + `, + }), + // Export all + test({ + code: ` + export * from './foo' + `, + }), + ], + invalid: [ + // Default export before variable declaration + test({ + code: ` + export default 'bar' + const bar = true + `, + errors: [error('ExportDefaultDeclaration')], + }), + // Named export before variable declaration + test({ + code: ` + export const foo = 'bar' + const bar = true + `, + errors: [error('ExportNamedDeclaration')], + }), + // Export all before variable declaration + test({ + code: ` + export * from './foo' + const bar = true + `, + errors: [error('ExportAllDeclaration')], + }), + // Many exports arround variable declaration + test({ + code: ` + export default 'such foo many bar' + export const so = 'many' + const foo = 'bar' + export const exports = ':)' + export const i = 'cant' + export const even = 'count' + export const how = 'many' + `, + errors: [ + error('ExportDefaultDeclaration'), + error('ExportNamedDeclaration'), + ], + }), + ], +}) diff --git a/utils/resolve.js b/utils/resolve.js index 1d6e164b15..8193e77314 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -21,6 +21,7 @@ exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cache // null means it resolved to a builtin if (filepath === null) return true + if (filepath.toLowerCase() === process.cwd().toLowerCase()) return true const parsedPath = path.parse(filepath) , dir = parsedPath.dir