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

No relative parent imports rule #1093

Merged
merged 11 commits into from May 17, 2018
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -4,7 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).

## [Unreleased]

- Add [`no-relative-parent-imports`] rule: disallow relative imports from parent directories.

## [2.12.0] - 2018-05-17
### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -26,6 +26,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
* Forbid a module from importing itself ([`no-self-import`])
* Forbid a module from importing a module with a dependency path back to itself ([`no-cycle`])
* Prevent unnecessary path segments in import and require statements ([`no-useless-path-segments`])
* Forbid importing modules from parent directories ([`no-relative-parent-imports`])

[`no-unresolved`]: ./docs/rules/no-unresolved.md
[`named`]: ./docs/rules/named.md
Expand All @@ -39,6 +40,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
[`no-self-import`]: ./docs/rules/no-self-import.md
[`no-cycle`]: ./docs/rules/no-cycle.md
[`no-useless-path-segments`]: ./docs/rules/no-useless-path-segments.md
[`no-relative-parent-imports`]: ./docs/rules/no-relative-parent-imports.md

### Helpful warnings

Expand Down
120 changes: 120 additions & 0 deletions docs/rules/no-relative-parent-imports.md
@@ -0,0 +1,120 @@
# no-relative-parent-imports

Use this rule to prevent imports to folders in relative parent paths.

This rule is useful for enforcing tree-like folder structures instead of complex graph-like folder structures. While this restriction might be a departure from Node's default resolution style, it can lead large, complex codebases to be easier to maintain. If you've ever had debates over "where to put files" this rule is for you.

To fix violations of this rule there are three general strategies. Given this example:

```
numbers
└── three.js
add.js
```

```js
// ./add.js
export default function (numbers) {
return numbers.reduce((sum, n) => sum + n, 0);
}

// ./numbers/three.js
import add from '../add'; // violates import/no-relative-parent-imports

export default function three() {
return add([1, 2]);
}
```

You can,

1. Move the file to be in a sibling folder (or higher) of the dependency.

`three.js` could be be in the same folder as `add.js`:

```
three.js
add.js
```

or since `add` doesn't have any imports, it could be in it's own directory (namespace):

```
math
└── add.js
three.js
```

2. Pass the dependency as an argument at runtime (dependency injection)

```js
// three.js
export default function three(add) {
return add([1, 2]);
}

// somewhere else when you use `three.js`:
import add from './add';
import three from './numbers/three';
console.log(three(add));
```

3. Make the dependency a package so it's globally available to all files in your project:

```js
import add from 'add'; // from https://www.npmjs.com/package/add
export default function three() {
return add([1,2]);
}
```

These are (respectively) static, dynamic & global solutions to graph-like dependency resolution.

### Examples

Given the following folder structure:

```
my-project
├── lib
│ ├── a.js
│ └── b.js
└── main.js
```

And the .eslintrc file:
```
{
...
"rules": {
"import/no-relative-parent-imports": "error"
}
}
```

The following patterns are considered problems:

```js
/**
* in my-project/lib/a.js
*/

import bar from '../main'; // Import parent file using a relative path
```

The following patterns are NOT considered problems:

```js
/**
* in my-project/main.js
*/

import foo from 'foo'; // Import package using module path
import a from './lib/a'; // Import child file using relative path

/**
* in my-project/lib/a.js
*/

import b from './b'; // Import sibling file using relative path
```
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -10,6 +10,7 @@ export const rules = {
'no-restricted-paths': require('./rules/no-restricted-paths'),
'no-internal-modules': require('./rules/no-internal-modules'),
'group-exports': require('./rules/group-exports'),
'no-relative-parent-imports': require('./rules/no-relative-parent-imports'),

'no-self-import': require('./rules/no-self-import'),
'no-cycle': require('./rules/no-cycle'),
Expand Down
34 changes: 34 additions & 0 deletions src/rules/no-relative-parent-imports.js
@@ -0,0 +1,34 @@
import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor'
import docsUrl from '../docsUrl'
import { basename } from 'path'

import importType from '../core/importType'

module.exports = {
meta: {
docs: {
url: docsUrl('no-relative-parent-imports'),
},
schema: [makeOptionsSchema()],
},

create: function noRelativePackages(context) {
const myPath = context.getFilename()
if (myPath === '<text>') return {} // can't cycle-check a non-file

function checkSourceValue(sourceNode) {
const depPath = sourceNode.value
if (importType(depPath, context) === 'parent') {
context.report({
node: sourceNode,
message: 'Relative imports from parent directories are not allowed. ' +
`Please either pass what you're importing through at runtime ` +
`(dependency injection), move \`${basename(myPath)}\` to same ` +
`directory as \`${depPath}\` or consider making \`${depPath}\` a package.`,
})
}
}

return moduleVisitor(checkSourceValue, context.options[0])
},
}
73 changes: 73 additions & 0 deletions tests/src/rules/no-relative-parent-imports.js
@@ -0,0 +1,73 @@
import { RuleTester } from 'eslint'
import rule from 'rules/no-relative-parent-imports'
import { test as _test, testFilePath } from '../utils'

const test = def => _test(Object.assign(def, {
filename: testFilePath('./internal-modules/plugins/plugin2/index.js'),
parser: 'babel-eslint',
}))

const ruleTester = new RuleTester()

ruleTester.run('no-relative-parent-imports', rule, {
valid: [
test({
code: 'import foo from "./internal.js"',
}),
test({
code: 'import foo from "./app/index.js"',
}),
test({
code: 'import foo from "package"',
}),
test({
code: 'require("./internal.js")',
options: [{ commonjs: true }],
}),
test({
code: 'require("./app/index.js")',
options: [{ commonjs: true }],
}),
test({
code: 'require("package")',
options: [{ commonjs: true }],
}),
test({
code: 'import("./internal.js")',
}),
test({
code: 'import("./app/index.js")',
}),
test({
code: 'import("package")',
}),
],

invalid: [
test({
code: 'import foo from "../plugin.js"',
errors: [ {
message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `../plugin.js` or consider making `../plugin.js` a package.',
line: 1,
column: 17,
} ],
}),
test({
code: 'require("../plugin.js")',
options: [{ commonjs: true }],
errors: [ {
message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `../plugin.js` or consider making `../plugin.js` a package.',
line: 1,
column: 9,
} ],
}),
test({
code: 'import("../plugin.js")',
errors: [ {
message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `../plugin.js` or consider making `../plugin.js` a package.',
line: 1,
column: 8,
} ],
}),
],
})
13 changes: 13 additions & 0 deletions utils/moduleVisitor.js
Expand Up @@ -34,6 +34,18 @@ exports.default = function visitModules(visitor, options) {
checkSourceValue(node.source, node)
}

// for esmodule dynamic `import()` calls
function checkImportCall(node) {
if (node.callee.type !== 'Import') return
if (node.arguments.length !== 1) return

const modulePath = node.arguments[0]
if (modulePath.type !== 'Literal') return
if (typeof modulePath.value !== 'string') return

checkSourceValue(modulePath, node)
}

// for CommonJS `require` calls
// adapted from @mctep: http://git.io/v4rAu
function checkCommon(call) {
Expand Down Expand Up @@ -74,6 +86,7 @@ exports.default = function visitModules(visitor, options) {
'ImportDeclaration': checkSource,
'ExportNamedDeclaration': checkSource,
'ExportAllDeclaration': checkSource,
'CallExpression': checkImportCall,
})
}

Expand Down