-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add no-shallow-imports rule #2408
base: main
Are you sure you want to change the base?
Changes from all commits
385fb30
22217de
84e23da
75bc227
7ce4d2b
6a75b85
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,64 @@ | ||||||
# import/no-shallow-imports | ||||||
|
||||||
Prevent the use of shallow (barrel) imports. | ||||||
|
||||||
|
||||||
## Options | ||||||
|
||||||
There's one option available: | ||||||
* `allow`: an array of minimatch/glob patterns that match paths which allow shallow importing (exempt from the rule) | ||||||
|
||||||
### Example rule (barebones) | ||||||
|
||||||
```js | ||||||
"import/no-shallow-imports": "error" | ||||||
``` | ||||||
|
||||||
### Example rule (with option) | ||||||
|
||||||
```js | ||||||
"import/no-shallow-imports": ["error", { | ||||||
"allow": ["**/index.test.*"] | ||||||
}] | ||||||
``` | ||||||
|
||||||
## Examples | ||||||
|
||||||
Given the following structure, along with the example rule (with option) above.... | ||||||
|
||||||
``` | ||||||
my-project | ||||||
├── dir1 | ||||||
│ └── example1.js | ||||||
│ └── index.js | ||||||
└── dir2 | ||||||
│ └── example2.js | ||||||
│ └── index.js | ||||||
└── index.js | ||||||
└── index.test.js | ||||||
``` | ||||||
|
||||||
|
||||||
### Examples of **incorrect** code for this rule: | ||||||
|
||||||
```js | ||||||
// dir1/example1.js | ||||||
|
||||||
import example2 from '..'; // imports from /index.js | ||||||
import example2 from '../dir2'; // imports from /dir2/index.js | ||||||
import example2 from '../dir2/index'; // same as above but explicit | ||||||
``` | ||||||
Comment on lines
+47
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are these incorrect? What if |
||||||
|
||||||
### Examples of **correct** code for this rule: | ||||||
|
||||||
```js | ||||||
// dir1/example1.js | ||||||
|
||||||
import example2 from '../dir2/example2'; // deep import :D | ||||||
``` | ||||||
|
||||||
```js | ||||||
// index.test.js | ||||||
|
||||||
import * as index from './index'; // allowed | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not necessarily, but the docs shouldn't suggest it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree it shouldn't suggest it, but I think it is important for the docs to show that the pattern is still allowed with this rule - as users may assume that it is disallowed or vice versa. Maybe the docs should show it as correct but discouraged / not recommended?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, that's a reasonable compromise. |
||||||
``` |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,104 @@ | ||||||||||
import fs from 'fs'; | ||||||||||
import minimatch from 'minimatch'; | ||||||||||
import docsUrl from '../docsUrl'; | ||||||||||
|
||||||||||
module.exports = { | ||||||||||
meta: { | ||||||||||
type: 'problem', | ||||||||||
docs: { | ||||||||||
description: 'Prevent the use of shallow imports', | ||||||||||
recommended: true, | ||||||||||
url: docsUrl('no-shallow-imports'), | ||||||||||
}, | ||||||||||
schema: [ | ||||||||||
{ | ||||||||||
oneOf: [ | ||||||||||
{ | ||||||||||
type: 'object', | ||||||||||
properties: { | ||||||||||
allow: { | ||||||||||
type: 'array', | ||||||||||
items: { | ||||||||||
type: 'string', | ||||||||||
}, | ||||||||||
}, | ||||||||||
}, | ||||||||||
additionalProperties: false, | ||||||||||
}, | ||||||||||
], | ||||||||||
}, | ||||||||||
], | ||||||||||
}, | ||||||||||
create: context => { | ||||||||||
return { | ||||||||||
ImportDeclaration: node => maybeReportShallowImport(node, context), | ||||||||||
}; | ||||||||||
}, | ||||||||||
}; | ||||||||||
|
||||||||||
const maybeReportShallowImport = (node, context) => { | ||||||||||
const filenamePath = context.getFilename(); | ||||||||||
|
||||||||||
const options = context.options[0] || {}; | ||||||||||
const allowRegexps = (options.allow || []).map(p => minimatch.makeRe(p)); | ||||||||||
|
||||||||||
const doesFileMatchAllowRegexps = allowRegexps.some(regex => regex.test(filenamePath)); | ||||||||||
|
||||||||||
// If the file does not match the regexps and is a shallow import, then report it | ||||||||||
if (!doesFileMatchAllowRegexps && isShallowImport(node, context)) { | ||||||||||
return context.report({ | ||||||||||
node, | ||||||||||
message: 'Please deep import!', | ||||||||||
}); | ||||||||||
} | ||||||||||
}; | ||||||||||
|
||||||||||
const isCalledIndex = filename => filename.slice(0, 5) === 'index'; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Think this might be slightly better because otherwise: isCalledIndex('index-lorem-ipsum.tsx') will be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ljharb
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, that sounds good |
||||||||||
|
||||||||||
const isShallowImport = (node, context) => { | ||||||||||
const filenamePath = context.getFilename(); | ||||||||||
const sourcePath = node.source.value; | ||||||||||
const segments = sourcePath.split('/'); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
const lastSegment = segments[segments.length - 1]; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
||||||||||
if (sourcePath[0] !== '.') { | ||||||||||
return false; // not a relative path, ie. it does not start with '.' or '..' | ||||||||||
} | ||||||||||
|
||||||||||
// Source ends in '..' (eg. '..', '../..', etc.) | ||||||||||
if (lastSegment === '..') return true; | ||||||||||
|
||||||||||
// Source ends in 'index(\..*)?' (eg. 'index', 'index.js', 'index.etc') | ||||||||||
if (isCalledIndex(lastSegment)) return true; | ||||||||||
|
||||||||||
// Source is a directory | ||||||||||
if (isDirectory(filenamePath, sourcePath)) return true; | ||||||||||
|
||||||||||
return false; | ||||||||||
}; | ||||||||||
|
||||||||||
const isDirectory = (filename, source) => { | ||||||||||
const filenameSegments = filename.split('/'); | ||||||||||
const sourceSegments = source.split('/'); | ||||||||||
|
||||||||||
if (sourceSegments[0] === '.') { | ||||||||||
sourceSegments.shift(); | ||||||||||
} | ||||||||||
|
||||||||||
while (sourceSegments[0] === '..') { | ||||||||||
filenameSegments.pop(); | ||||||||||
sourceSegments.shift(); | ||||||||||
} | ||||||||||
|
||||||||||
// Swap out the last element in filenameSegments with the remaining sourceSegments | ||||||||||
filenameSegments.splice(filenameSegments.length - 1, 1, ...sourceSegments); | ||||||||||
|
||||||||||
const absoluteSource = filenameSegments.join('/'); | ||||||||||
|
||||||||||
try { | ||||||||||
const stat = fs.statSync(absoluteSource); | ||||||||||
return stat.isDirectory(); | ||||||||||
} catch (e) { | ||||||||||
return false; | ||||||||||
} | ||||||||||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { test, testFilePath } from '../utils'; | ||
const rule = require('../../../src/rules/no-shallow-imports'); | ||
const { RuleTester } = require('eslint'); | ||
|
||
const ruleTester = new RuleTester(); | ||
const errors = [{ message: 'Please deep import!' }]; | ||
const options = [{ 'allow': ['**/index.test.js'] }]; | ||
const filename = testFilePath('./index.test.js'); | ||
|
||
ruleTester.run('no-shallow-imports', rule, { | ||
valid: [ | ||
test({ | ||
code: 'import { module } from "package"', | ||
}), | ||
test({ | ||
code: 'import { module } from "@scope/package"', | ||
}), | ||
test({ | ||
code: 'import * as index from "../index"', | ||
options, | ||
filename, | ||
}), | ||
], | ||
|
||
invalid: [ | ||
test({ | ||
code: 'import { barrel } from ".."', | ||
errors, | ||
}), | ||
test({ | ||
code: 'import { barrel } from "../.."', | ||
errors, | ||
}), | ||
// test({ | ||
// code: `import { barrel } from '../../dir';`, | ||
// errors, | ||
// }), | ||
test({ | ||
code: 'import { barrel } from "../dir/index"', | ||
errors, | ||
}), | ||
test({ | ||
code: 'import { barrel } from "./dir/index.js"', | ||
errors, | ||
}), | ||
], | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this line needs to be moved up to the "unreleased" section
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! :)