Skip to content

Commit dcb5956

Browse files
committed
[New]: add rule default-import-match-filename
1 parent 2e047e6 commit dcb5956

File tree

6 files changed

+230
-1
lines changed

6 files changed

+230
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
77

88
### Added
99
- [`group-exports`]: make aggregate module exports valid ([#1472], thanks [@atikenny])
10-
10+
- `default`: make error message less confusing ([#1470], thanks [@golopot])
11+
- Add [`default-import-match-filename`] rule, ([#1476], thanks [@golopot])
1112
### Added
1213
- support `parseForESLint` from custom parser ([#1435], thanks [@JounQin])
1314

@@ -986,3 +987,4 @@ for info on changes for earlier releases.
986987
[@lencioni]: https://github.com/lencioni
987988
[@JounQin]: https://github.com/JounQin
988989
[@atikenny]: https://github.com/atikenny
990+
[@golopot]: https://github.com/golopot

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
9494
* Forbid anonymous values as default exports ([`no-anonymous-default-export`])
9595
* Prefer named exports to be grouped together in a single export declaration ([`group-exports`])
9696
* Enforce a leading comment with the webpackChunkName for dynamic imports ([`dynamic-import-chunkname`])
97+
* Enforce default import name to match filename ([`default-import-match-filename`])
9798

9899
[`first`]: ./docs/rules/first.md
99100
[`exports-last`]: ./docs/rules/exports-last.md
@@ -111,6 +112,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
111112
[`no-default-export`]: ./docs/rules/no-default-export.md
112113
[`no-named-export`]: ./docs/rules/no-named-export.md
113114
[`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md
115+
[`default-import-match-filename`]: ./docs/rules/default-import-match-filename.md
114116

115117
## Support
116118

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# import/default-import-match-filename
2+
3+
Enforces default import name to match filename. Name matching is case-insensitive, and characters `._-` are stripped.
4+
5+
## Rule Details
6+
7+
### Fail
8+
9+
```js
10+
import bar from './foo';
11+
import utilsFoo from '../utils/foo';
12+
import foo from '../foo/index.js';
13+
const bar = require('./foo');
14+
const bar = require('../foo');
15+
```
16+
17+
### Pass
18+
19+
```js
20+
import foo from './foo';
21+
import foo_ from './foo';
22+
import foo from './foo.js';
23+
import fooBar from './foo-bar';
24+
import FoObAr from './foo-bar';
25+
import catModel from './cat.model.js';
26+
const foo = require('./foo');
27+
```

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const rules = {
3838
'unambiguous': require('./rules/unambiguous'),
3939
'no-unassigned-import': require('./rules/no-unassigned-import'),
4040
'no-useless-path-segments': require('./rules/no-useless-path-segments'),
41+
'default-import-match-filename': require('./rules/default-import-match-filename'),
4142
'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'),
4243

4344
// export
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import docsUrl from '../docsUrl'
2+
import isStaticRequire from '../core/staticRequire'
3+
4+
/**
5+
* @param {string} filename
6+
* @returns {string}
7+
*/
8+
function removeExtension(filename) {
9+
return filename.replace(/\.[^.]+$/, '')
10+
}
11+
12+
/**
13+
* @param {string} filename
14+
* @returns {string}
15+
*/
16+
function normalizeFilename(filename) {
17+
return filename.replace(/[-_.]/g, '').toLowerCase()
18+
}
19+
20+
/**
21+
* Test if local name matches filename.
22+
* @param {string} localName
23+
* @param {string} filename
24+
* @returns {boolean}
25+
*/
26+
function isCompatible(localName, filename) {
27+
const normalizedLocalName = localName.replace(/_/g, '').toLowerCase()
28+
29+
return (
30+
normalizedLocalName === normalizeFilename(filename) ||
31+
normalizedLocalName === normalizeFilename(removeExtension(filename))
32+
)
33+
}
34+
35+
/**
36+
* Test if path starts with "./" or "../".
37+
* @param {string} path
38+
* @returns {boolean}
39+
*/
40+
function isLocalModule(path) {
41+
return /^(\.\/|\.\.\/)/.test(path)
42+
}
43+
44+
/**
45+
* Get filename from a path.
46+
* @param {string} path
47+
* @returns {string | undefined}
48+
*/
49+
function getFilename(path) {
50+
if (!isLocalModule(path)) return undefined
51+
const [, filename] = /\/([^/]*)$/.exec(path)
52+
if (filename === '' || filename === '.' || filename === '..') {
53+
return undefined
54+
}
55+
return filename
56+
}
57+
58+
module.exports = {
59+
meta: {
60+
type: 'suggestion',
61+
docs: {
62+
url: docsUrl('default-import-match-filename'),
63+
},
64+
},
65+
66+
create(context) {
67+
return {
68+
ImportDeclaration(node) {
69+
const defaultImportSpecifier = node.specifiers.find(
70+
({type}) => type === 'ImportDefaultSpecifier'
71+
)
72+
73+
const defaultImportName =
74+
defaultImportSpecifier && defaultImportSpecifier.local.name
75+
76+
if (!defaultImportName) {
77+
return
78+
}
79+
80+
const filename = getFilename(node.source.value)
81+
82+
if (!filename) {
83+
return
84+
}
85+
86+
if (!isCompatible(defaultImportName, filename)) {
87+
context.report({
88+
node: defaultImportSpecifier,
89+
message: `Default import name does not match filename "${filename}".`,
90+
})
91+
}
92+
},
93+
94+
CallExpression(node) {
95+
if (
96+
!isStaticRequire(node) ||
97+
node.parent.type !== 'VariableDeclarator' ||
98+
node.parent.id.type !== 'Identifier'
99+
) {
100+
return
101+
}
102+
103+
const localName = node.parent.id.name
104+
105+
const filename = getFilename(node.arguments[0].value)
106+
107+
if (!filename) {
108+
return
109+
}
110+
111+
if (!isCompatible(localName, filename)) {
112+
context.report({
113+
node: node.parent.id,
114+
message: `Default import name does not match filename "${filename}".`,
115+
})
116+
}
117+
},
118+
}
119+
},
120+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {RuleTester} from 'eslint'
2+
import rule from '../../../src/rules/default-import-match-filename' // eslint-disable-line import/default
3+
4+
const ruleTester = new RuleTester({
5+
parserOptions: {
6+
sourceType: 'module',
7+
ecmaVersion: 6,
8+
},
9+
})
10+
11+
function getMessage(filename) {
12+
return `Default import name does not match filename "${filename}".`
13+
}
14+
15+
function fail(code, filename) {
16+
return {
17+
code,
18+
errors: [
19+
{
20+
message: getMessage(filename),
21+
},
22+
],
23+
}
24+
}
25+
26+
ruleTester.run('default-import-match-filename', rule, {
27+
valid: [
28+
'import Cat from "./cat"',
29+
'import cat from "./cat"',
30+
'import cat from "./Cat"',
31+
'import Cat from "./Cat"',
32+
'import cat from "./cat.js"',
33+
'import cat from "./cat.ts"',
34+
'import cat from "./cat.jpeg"',
35+
'import cat_ from "./cat"',
36+
'import loudCat from "./loud-cat"',
37+
'import LOUDCAT from "./loud-cat"',
38+
'import loud_cat from "./loud-cat"',
39+
'import loudcat from "./loud_cat"',
40+
'import loud_cat from "./loud_cat"',
41+
'import catModel from "./cat.model"',
42+
'import catModel from "./cat.model.js"',
43+
'import doge from "."',
44+
'import doge from "./"',
45+
'import doge from "./.."',
46+
'import doge from "./../"',
47+
'import doge from ".."',
48+
'import doge from "../"',
49+
'import doge from "../.."',
50+
'import doge from "cat"',
51+
'import doge from "loud-cat"',
52+
'import doge from ".cat"',
53+
'import doge from "/cat"',
54+
'import doge from ""',
55+
'import {doge} from "./cat"',
56+
'import cat, {doge} from "./cat"',
57+
'const cat = require("..")',
58+
'const cat = require("./cat")',
59+
'const cat = require("../cat")',
60+
'const {f, g} = require("./cat")',
61+
],
62+
invalid: [
63+
fail('import cat0 from "./cat"', 'cat'),
64+
fail('import catfish from "./cat"', 'cat'),
65+
fail('import catfish, {cat} from "./cat"', 'cat'),
66+
fail('import catModel from "./models/cat"', 'cat'),
67+
fail('import cat from "./cat.model.js"', 'cat.model.js'),
68+
fail('import cat from "./cat/index"', 'index'),
69+
fail('import cat from "./cat/index.js"', 'index.js'),
70+
fail('import cat from "../cat/index.js"', 'index.js'),
71+
fail('import cat7 from "./cat8"', 'cat8'),
72+
fail('const catfish = require("./cat")', 'cat'),
73+
fail('const cat = require("./cat/index")', 'index'),
74+
fail('const cat = require("./cat/index.js")', 'index.js'),
75+
fail('const doge = require("../models/cat")', 'cat'),
76+
],
77+
})

0 commit comments

Comments
 (0)