Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exports-last #632

Merged
merged 9 commits into from
Aug 27, 2017
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we lost one of the changelog entries - let's fix that :-)


### Changed
- [`no-extraneous-dependencies`]: use `read-pkg-up` to simplify finding + loading `package.json` ([#680], thanks [@wtgtybhertgeghgtwtg])
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`])
Expand All @@ -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
Expand Down
50 changes: 50 additions & 0 deletions docs/rules/exports-last.md
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a simple non-exported variable declaration up top, to show that that is allowed


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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to indicate that this is only for the export statements using the export keyword, and not for CommonJS module.exports/exports


#### 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.
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),

Expand Down
31 changes: 31 additions & 0 deletions src/rules/exports-last.js
Original file line number Diff line number Diff line change
@@ -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)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this blank line (sorry about the nitpicking 😅)

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',
})
}
})
}
},
}
},
}
124 changes: 124 additions & 0 deletions tests/src/rules/exports-last.js
Original file line number Diff line number Diff line change
@@ -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'),
],
}),
],
})