From 84fd4f27eb537c9230196d6403aafd406e46e6e9 Mon Sep 17 00:00:00 2001 From: Konstantin Pschera Date: Wed, 19 Oct 2016 17:10:51 +0200 Subject: [PATCH 1/9] Add rule exports-last exports-last will check that all export statements are at the end of the file Closes #620 --- src/index.js | 3 ++ src/rules/exports-last.js | 39 ++++++++++++++++ tests/src/rules/exports-last.js | 82 +++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 src/rules/exports-last.js create mode 100644 tests/src/rules/exports-last.js diff --git a/src/index.js b/src/index.js index 69cbc2f5e..e5b36b8f5 100644 --- a/src/index.js +++ b/src/index.js @@ -31,6 +31,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 000000000..ad80f3a70 --- /dev/null +++ b/src/rules/exports-last.js @@ -0,0 +1,39 @@ +function isExportStatement({ type }) { + // ES Module export statements + if (type === 'ExportDefaultDeclaration' || type === 'ExportNamedDeclaration') { + return true + + // CommonJS export statements + } else if (type === 'ExpressionStatement') { + // TODO + } + + return false +} + +const rule = { + create(context) { + return { + Program({ body }) { + const lastNonExportStatement = body.reduce((acc, node, index) => { + if (isExportStatement(node)) { + return acc + } + return index + }, 0) + + body.forEach((node, index) => { + if (isExportStatement(node) && index < lastNonExportStatement) { + + context.report({ + node, + message: 'Export statements should appear at the end of the file', + }) + } + }) + }, + } + }, +} + +export default rule diff --git a/tests/src/rules/exports-last.js b/tests/src/rules/exports-last.js new file mode 100644 index 000000000..eddf2191b --- /dev/null +++ b/tests/src/rules/exports-last.js @@ -0,0 +1,82 @@ +import { test } from '../utils' + +import { RuleTester } from 'eslint' +import rule from 'rules/exports-last' + +const ruleTester = new RuleTester() + +const errors = ['Export statements should appear at the end of the file'] + +ruleTester.run('exports-last', rule, { + valid: [ + test({ + code: ` + const foo = 'bar'; + const bar = 'baz'; + `, + }), + test({ + code: ` + const foo = 'bar'; + export {foo}; + `, + }), + test({ + code: ` + const foo = 'bar'; + export default foo; + `, + }), + test({ + code: ` + const foo = 'bar'; + export default foo; + export const bar = true; + `, + }), + // test({ + // code: ` + // const foo = 'bar'; + // module.exports = foo + // `, + // }), + // test({ + // code: ` + // const foo = 'bar'; + // module.exports = foo; + // exports.bar = true + // `, + // }), + + ], + invalid: [ + test({ + code: ` + export default 'bar'; + const bar = true; + `, + errors, + }), + test({ + code: ` + export const foo = 'bar'; + const bar = true; + `, + errors, + }), + // test({ + // code: ` + // module.exports = 'bar'; + // console.log('hi'); + // `, + // errors, + // }), + // test({ + // code: ` + // exports.foo = 'bar'; + // const bar = true; + // `, + // errors, + // }), + ], +}) From 76f1b84c9dbf0b7fec33e9c78ebc03e04b6f359d Mon Sep 17 00:00:00 2001 From: Konstantin Pschera Date: Thu, 3 Nov 2016 08:00:26 +0100 Subject: [PATCH 2/9] Add documentation for exports-last --- CHANGELOG.md | 3 +++ README.md | 2 ++ docs/rules/exports-last.md | 41 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 docs/rules/exports-last.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 648e481df..f8a678c8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - Properly report [`newline-after-import`] when next line is a decorator - Fixed documentation for the default values for the [`order`] rule ([#601]) +### Added +- [`exports-last`] lints that export statements are at the end of the file ([#620] + [#632]) + ## [2.0.1] - 2016-10-06 ### Fixed - Fixed code that relied on removed dependencies. ([#604]) diff --git a/README.md b/README.md index c8c3789aa..3abbbde60 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,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`]) @@ -79,6 +80,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 000000000..9629ba9d0 --- /dev/null +++ b/docs/rules/exports-last.md @@ -0,0 +1,41 @@ +# exports-last + +This rule reports all export declaration which come 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 +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. From 0de28bd80ad98f1fb2973c2231a970f4fb19ada6 Mon Sep 17 00:00:00 2001 From: Konstantin Pschera Date: Wed, 14 Dec 2016 20:52:01 +0100 Subject: [PATCH 3/9] Remove CommonJS exports --- src/rules/exports-last.js | 4 ---- tests/src/rules/exports-last.js | 27 --------------------------- 2 files changed, 31 deletions(-) diff --git a/src/rules/exports-last.js b/src/rules/exports-last.js index ad80f3a70..ed2e03af2 100644 --- a/src/rules/exports-last.js +++ b/src/rules/exports-last.js @@ -2,10 +2,6 @@ function isExportStatement({ type }) { // ES Module export statements if (type === 'ExportDefaultDeclaration' || type === 'ExportNamedDeclaration') { return true - - // CommonJS export statements - } else if (type === 'ExpressionStatement') { - // TODO } return false diff --git a/tests/src/rules/exports-last.js b/tests/src/rules/exports-last.js index eddf2191b..2763fa7f1 100644 --- a/tests/src/rules/exports-last.js +++ b/tests/src/rules/exports-last.js @@ -34,19 +34,6 @@ ruleTester.run('exports-last', rule, { export const bar = true; `, }), - // test({ - // code: ` - // const foo = 'bar'; - // module.exports = foo - // `, - // }), - // test({ - // code: ` - // const foo = 'bar'; - // module.exports = foo; - // exports.bar = true - // `, - // }), ], invalid: [ @@ -64,19 +51,5 @@ ruleTester.run('exports-last', rule, { `, errors, }), - // test({ - // code: ` - // module.exports = 'bar'; - // console.log('hi'); - // `, - // errors, - // }), - // test({ - // code: ` - // exports.foo = 'bar'; - // const bar = true; - // `, - // errors, - // }), ], }) From 0611e2139af97b644f5898b0a8aedf051e211d8e Mon Sep 17 00:00:00 2001 From: Konstantin Pschera Date: Thu, 15 Dec 2016 16:58:31 +0100 Subject: [PATCH 4/9] Refactor exports-last rule --- src/rules/exports-last.js | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/rules/exports-last.js b/src/rules/exports-last.js index ed2e03af2..1cc033469 100644 --- a/src/rules/exports-last.js +++ b/src/rules/exports-last.js @@ -1,32 +1,24 @@ -function isExportStatement({ type }) { - // ES Module export statements - if (type === 'ExportDefaultDeclaration' || type === 'ExportNamedDeclaration') { - return true - } - - return false -} +const isExportStatement = ({ type }) => + type === 'ExportDefaultDeclaration' + || type === 'ExportNamedDeclaration' + || type === 'ExportAllDeclaration' const rule = { create(context) { return { Program({ body }) { - const lastNonExportStatement = body.reduce((acc, node, index) => { - if (isExportStatement(node)) { - return acc - } - return index - }, 0) - - body.forEach((node, index) => { - if (isExportStatement(node) && index < lastNonExportStatement) { + const firstExportStatementIndex = body.findIndex(isExportStatement) - context.report({ - node, - message: 'Export statements should appear at the end of the file', - }) - } - }) + if (firstExportStatementIndex !== -1) { + body.slice(firstExportStatementIndex).forEach((node) => { + if (!isExportStatement(node)) { + context.report({ + node, + message: 'Export statements should appear at the end of the file', + }) + } + }) + } }, } }, From 531d04dc35f6175ef5559be39e266ff2ffd4477c Mon Sep 17 00:00:00 2001 From: Konstantin Pschera Date: Thu, 15 Dec 2016 17:02:15 +0100 Subject: [PATCH 5/9] Add ES6 only hint to the docs --- docs/rules/exports-last.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/rules/exports-last.md b/docs/rules/exports-last.md index 9629ba9d0..9c7b0dd2d 100644 --- a/docs/rules/exports-last.md +++ b/docs/rules/exports-last.md @@ -2,6 +2,7 @@ This rule reports all export declaration which come before any non-export statements. + ## This will be reported ```JS @@ -39,3 +40,9 @@ 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. From baa585dc173efea1f37e1c082e3d99da322852dd Mon Sep 17 00:00:00 2001 From: Konstantin Pschera Date: Thu, 15 Dec 2016 17:20:04 +0100 Subject: [PATCH 6/9] Add more tests for exports-last --- tests/src/rules/exports-last.js | 90 ++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/tests/src/rules/exports-last.js b/tests/src/rules/exports-last.js index 2763fa7f1..4d8fab5ce 100644 --- a/tests/src/rules/exports-last.js +++ b/tests/src/rules/exports-last.js @@ -9,45 +9,107 @@ const errors = ['Export statements should appear at the end of the file'] ruleTester.run('exports-last', rule, { valid: [ + // Empty file test({ + code: '', + }), + test({ + // No exports code: ` - const foo = 'bar'; - const bar = 'baz'; + const foo = 'bar' + const bar = 'baz' `, }), test({ code: ` - const foo = 'bar'; - export {foo}; + const foo = 'bar' + export {foo} `, }), test({ code: ` - const foo = 'bar'; - export default foo; + const foo = 'bar' + export default foo `, }), + // Only exports test({ code: ` - const foo = 'bar'; - export default foo; - export const bar = true; + 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, + }), + // Named export before variable declaration + test({ + code: ` + export const foo = 'bar' + const bar = true + `, + errors, + }), + // Export all before variable declaration test({ code: ` - export default 'bar'; - const bar = true; + export * from './foo' + const bar = true `, errors, }), + // Many exports arround variable declaration test({ code: ` - export const foo = 'bar'; - const bar = true; + 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, }), From 8c0e7d62d485cb7b3a8e6d1ccb484de423c22a7c Mon Sep 17 00:00:00 2001 From: Konstantin Pschera Date: Wed, 25 Jan 2017 16:29:15 +0100 Subject: [PATCH 7/9] Refactor exports-last --- CHANGELOG.md | 7 ++----- docs/rules/exports-last.md | 4 +++- src/rules/exports-last.js | 30 +++++++++++++++++------------- tests/src/rules/exports-last.js | 19 +++++++++++++------ 4 files changed, 35 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8a678c8a..bacbc67d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## [Unreleased] ### Added - [`no-anonymous-default-export`] rule: report anonymous default exports ([#712], thanks [@duncanbeevers]). -- Add new value to [`order`]'s `newlines-between` option to allow newlines inside import groups ([#627], [#628], thanks [@giodamelio]) -- Add `count` option to the [`newline-after-import`] rule to allow configuration of number of newlines expected ([#742], thanks [@ntdb]) +- Add new value to `order`'s `newlines-between` option to allow newlines inside import groups ([#627], [#628], thanks [@giodamelio]) +- [`exports-last`] lints that export statements are at the end of the file ([#620] + [#632]) ### Changed - [`no-extraneous-dependencies`]: use `read-pkg-up` to simplify finding + loading `package.json` ([#680], thanks [@wtgtybhertgeghgtwtg]) @@ -36,9 +36,6 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - Properly report [`newline-after-import`] when next line is a decorator - Fixed documentation for the default values for the [`order`] rule ([#601]) -### Added -- [`exports-last`] lints that export statements are at the end of the file ([#620] + [#632]) - ## [2.0.1] - 2016-10-06 ### Fixed - Fixed code that relied on removed dependencies. ([#604]) diff --git a/docs/rules/exports-last.md b/docs/rules/exports-last.md index 9c7b0dd2d..22b654d2e 100644 --- a/docs/rules/exports-last.md +++ b/docs/rules/exports-last.md @@ -1,6 +1,6 @@ # exports-last -This rule reports all export declaration which come before any non-export statements. +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 @@ -26,6 +26,8 @@ const str = 'foo' ## This will not be reported ```JS +const arr = ['bar'] + export const bool = true export default bool diff --git a/src/rules/exports-last.js b/src/rules/exports-last.js index 1cc033469..91af6b421 100644 --- a/src/rules/exports-last.js +++ b/src/rules/exports-last.js @@ -1,17 +1,23 @@ -const isExportStatement = ({ type }) => - type === 'ExportDefaultDeclaration' - || type === 'ExportNamedDeclaration' - || type === 'ExportAllDeclaration' +function isNonExportStatement({ type }) { + return type !== 'ExportDefaultDeclaration' && + type !== 'ExportNamedDeclaration' && + type !== 'ExportAllDeclaration' +} -const rule = { - create(context) { +module.exports = { + create: function (context) { return { - Program({ body }) { - const firstExportStatementIndex = body.findIndex(isExportStatement) + Program: function ({ body }) { + const lastNonExportStatementIndex = body.reduce(function findLastIndex(acc, item, index) { + if (isNonExportStatement(item)) { + return index + } + return acc + }, -1) - if (firstExportStatementIndex !== -1) { - body.slice(firstExportStatementIndex).forEach((node) => { - if (!isExportStatement(node)) { + 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', @@ -23,5 +29,3 @@ const rule = { } }, } - -export default rule diff --git a/tests/src/rules/exports-last.js b/tests/src/rules/exports-last.js index 4d8fab5ce..c3c26fdfc 100644 --- a/tests/src/rules/exports-last.js +++ b/tests/src/rules/exports-last.js @@ -5,13 +5,17 @@ import rule from 'rules/exports-last' const ruleTester = new RuleTester() -const errors = ['Export statements should appear at the end of the file'] +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: '', + code: '// comment', }), test({ // No exports @@ -82,7 +86,7 @@ ruleTester.run('exports-last', rule, { export default 'bar' const bar = true `, - errors, + errors: [error('ExportDefaultDeclaration')], }), // Named export before variable declaration test({ @@ -90,7 +94,7 @@ ruleTester.run('exports-last', rule, { export const foo = 'bar' const bar = true `, - errors, + errors: [error('ExportNamedDeclaration')], }), // Export all before variable declaration test({ @@ -98,7 +102,7 @@ ruleTester.run('exports-last', rule, { export * from './foo' const bar = true `, - errors, + errors: [error('ExportAllDeclaration')], }), // Many exports arround variable declaration test({ @@ -111,7 +115,10 @@ ruleTester.run('exports-last', rule, { export const even = 'count' export const how = 'many' `, - errors, + errors: [ + error('ExportDefaultDeclaration'), + error('ExportNamedDeclaration'), + ], }), ], }) From 8d4e25edde10748ca5f2a4107fd12b22d6c982c8 Mon Sep 17 00:00:00 2001 From: Konstantin Pschera Date: Wed, 1 Mar 2017 20:20:57 +0100 Subject: [PATCH 8/9] Fix Changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bacbc67d6..f2d2c7d7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## [Unreleased] ### Added - [`no-anonymous-default-export`] rule: report anonymous default exports ([#712], thanks [@duncanbeevers]). -- Add new value to `order`'s `newlines-between` option to allow newlines inside import groups ([#627], [#628], thanks [@giodamelio]) +- Add new value to [`order`]'s `newlines-between` option to allow newlines inside import groups ([#627], [#628], thanks [@giodamelio]) +- Add `count` option to the [`newline-after-import`] rule to allow configuration of number of newlines expected ([#742], thanks [@ntdb]) - [`exports-last`] lints that export statements are at the end of the file ([#620] + [#632]) ### Changed From 1ba1c3a05251099d8422a81159bac9d72780c132 Mon Sep 17 00:00:00 2001 From: Ben Mosher Date: Sun, 27 Aug 2017 15:51:35 -0400 Subject: [PATCH 9/9] removing changelog note, will re-add --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d2c7d7c..648e481df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - [`no-anonymous-default-export`] rule: report anonymous default exports ([#712], thanks [@duncanbeevers]). - Add new value to [`order`]'s `newlines-between` option to allow newlines inside import groups ([#627], [#628], thanks [@giodamelio]) - Add `count` option to the [`newline-after-import`] rule to allow configuration of number of newlines expected ([#742], thanks [@ntdb]) -- [`exports-last`] lints that export statements are at the end of the file ([#620] + [#632]) ### Changed - [`no-extraneous-dependencies`]: use `read-pkg-up` to simplify finding + loading `package.json` ([#680], thanks [@wtgtybhertgeghgtwtg])