Skip to content

Commit

Permalink
Merge branch 'no-mutable-exports': merges + closes #290
Browse files Browse the repository at this point in the history
  • Loading branch information
benmosher committed May 5, 2016
2 parents e2e52a3 + 24e2c9d commit 940b294
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
- [`newline-after-import`], new rule. ([#245], thanks [@singles])
- Added an `optionalDependencies` option to [`no-extraneous-dependencies`] to allow/forbid optional dependencies ([#266], thanks [@jfmengels]).
- Added `newlines-between` option to [`order`] rule ([#298], thanks [@singles])
- add [`no-mutable-exports`] rule ([#290], thanks [@josh])

### Fixed
- [`extensions`]: fallback to source path for extension enforcement if imported
Expand Down Expand Up @@ -194,9 +195,11 @@ for info on changes for earlier releases.
[`order`]: ./docs/rules/order.md
[`named`]: ./docs/rules/named.md
[`newline-after-import`]: ./docs/rules/newline-after-import.md
[`no-mutable-exports`]: ./docs/rules/no-mutable-exports.md

[#298]: https://github.com/benmosher/eslint-plugin-import/pull/298
[#296]: https://github.com/benmosher/eslint-plugin-import/pull/296
[#290]: https://github.com/benmosher/eslint-plugin-import/pull/290
[#289]: https://github.com/benmosher/eslint-plugin-import/pull/289
[#288]: https://github.com/benmosher/eslint-plugin-import/pull/288
[#287]: https://github.com/benmosher/eslint-plugin-import/pull/287
Expand Down Expand Up @@ -263,3 +266,4 @@ for info on changes for earlier releases.
[@taion]: https://github.com/taion
[@strawbrary]: https://github.com/strawbrary
[@SimenB]: https://github.com/SimenB
[@josh]: https://github.com/josh
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
[`no-named-as-default-member`]: ./docs/rules/no-named-as-default-member.md
[`no-deprecated`]: ./docs/rules/no-deprecated.md
[`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md
[`no-mutable-exports`]: ./docs/rules/no-mutable-exports.md

**Module systems:**

Expand Down
52 changes: 52 additions & 0 deletions docs/rules/no-mutable-exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# no-mutable-exports

Forbids the use of mutable exports with `var` or `let`.

## Rule Details

Valid:

```js
export const count = 1
export function getCount() {}
export class Counter {}
```

...whereas here exports will be reported:

```js
export let count = 2
export var count = 3

let count = 4
export { count } // reported here
```

## Functions/Classes

Note that exported function/class declaration identifiers may be reassigned,
but are not flagged by this rule at this time. They may be in the future, if a
reassignment is detected, i.e.

```js
// possible future behavior!
export class Counter {} // reported here: exported class is reassigned on line [x].
Counter = KitchenSink // not reported here unless you enable no-class-assign

// this pre-declaration reassignment is valid on account of function hoisting
getCount = function getDuke() {} // not reported here without no-func-assign
export function getCount() {} // reported here: exported function is reassigned on line [x].
```

To prevent general reassignment of these identifiers, exported or not, you may
want to enable the following core ESLint rules:

- [no-func-assign]
- [no-class-assign]

[no-func-assign]: http://eslint.org/docs/rules/no-func-assign
[no-class-assign]: http://eslint.org/docs/rules/no-class-assign

## When Not To Use It

If your environment correctly implements mutable export bindings.
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const rules = {
'namespace': require('./rules/namespace'),
'no-namespace': require('./rules/no-namespace'),
'export': require('./rules/export'),
'no-mutable-exports': require('./rules/no-mutable-exports'),
'extensions': require('./rules/extensions'),

'no-named-as-default': require('./rules/no-named-as-default'),
Expand Down
45 changes: 45 additions & 0 deletions src/rules/no-mutable-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
module.exports = function (context) {
function checkDeclaration(node) {
const {kind} = node
if (kind === 'var' || kind === 'let') {
context.report(node, `Exporting mutable '${kind}' binding, use 'const' instead.`)
}
}

function checkDeclarationsInScope({variables}, name) {
for (let variable of variables) {
if (variable.name === name) {
for (let def of variable.defs) {
if (def.type === 'Variable') {
checkDeclaration(def.parent)
}
}
}
}
}

function handleExportDefault(node) {
const scope = context.getScope()

if (node.declaration.name) {
checkDeclarationsInScope(scope, node.declaration.name)
}
}

function handleExportNamed(node) {
const scope = context.getScope()

if (node.declaration) {
checkDeclaration(node.declaration)
} else {
for (let specifier of node.specifiers) {
checkDeclarationsInScope(scope, specifier.local.name)
}
}
}

return {
'ExportDefaultDeclaration': handleExportDefault,
'ExportNamedDeclaration': handleExportNamed,
}
}
62 changes: 62 additions & 0 deletions tests/src/rules/no-mutable-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {test} from '../utils'
import {RuleTester} from 'eslint'
import rule from 'rules/no-mutable-exports'

const ruleTester = new RuleTester()

ruleTester.run('no-mutable-exports', rule, {
valid: [
test({ code: 'export const count = 1'}),
test({ code: 'export function getCount() {}'}),
test({ code: 'export class Counter {}'}),
test({ code: 'export default count = 1'}),
test({ code: 'export default function getCount() {}'}),
test({ code: 'export default class Counter {}'}),
test({ code: 'const count = 1\nexport { count }'}),
test({ code: 'const count = 1\nexport { count as counter }'}),
test({ code: 'const count = 1\nexport default count'}),
test({ code: 'const count = 1\nexport { count as default }'}),
test({ code: 'function getCount() {}\nexport { getCount }'}),
test({ code: 'function getCount() {}\nexport { getCount as getCounter }'}),
test({ code: 'function getCount() {}\nexport default getCount'}),
test({ code: 'function getCount() {}\nexport { getCount as default }'}),
test({ code: 'class Counter {}\nexport { Counter }'}),
test({ code: 'class Counter {}\nexport { Counter as Count }'}),
test({ code: 'class Counter {}\nexport default Counter'}),
test({ code: 'class Counter {}\nexport { Counter as default }'}),
],
invalid: [
test({
code: 'export let count = 1',
errors: ['Exporting mutable \'let\' binding, use \'const\' instead.'],
}),
test({
code: 'export var count = 1',
errors: ['Exporting mutable \'var\' binding, use \'const\' instead.'],
}),
test({
code: 'let count = 1\nexport { count }',
errors: ['Exporting mutable \'let\' binding, use \'const\' instead.'],
}),
test({
code: 'var count = 1\nexport { count }',
errors: ['Exporting mutable \'var\' binding, use \'const\' instead.'],
}),
test({
code: 'let count = 1\nexport { count as counter }',
errors: ['Exporting mutable \'let\' binding, use \'const\' instead.'],
}),
test({
code: 'var count = 1\nexport { count as counter }',
errors: ['Exporting mutable \'var\' binding, use \'const\' instead.'],
}),
test({
code: 'let count = 1\nexport default count',
errors: ['Exporting mutable \'let\' binding, use \'const\' instead.'],
}),
test({
code: 'var count = 1\nexport default count',
errors: ['Exporting mutable \'var\' binding, use \'const\' instead.'],
}),
],
})

0 comments on commit 940b294

Please sign in to comment.