Skip to content

Commit c77759a

Browse files
committed
Implemented fixer for no-named-import rule.
1 parent 2aa44a3 commit c77759a

File tree

4 files changed

+206
-66
lines changed

4 files changed

+206
-66
lines changed

lib/rules/no-named-import.js

Lines changed: 116 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@
55

66
'use strict';
77

8-
const arrayIncludes = require('array-includes');
9-
108
const docsUrl = require('../util/docsUrl');
119

1210
const DEFAULT_TYPE = 'import';
1311

1412
module.exports = {
1513
meta: {
14+
fixable: 'code',
1615
docs: {
1716
description: 'No named imports on React',
1817
category: 'Best Practices',
@@ -37,11 +36,16 @@ module.exports = {
3736

3837
],
3938
messages: {
40-
useProperty:
41-
'Don\'t import {{name}} from React. Use React.{{name}} to be consistent.',
42-
useImport: 'Import {{name}} from React'
39+
usePropertyAccessor: 'use React.{{name}}',
40+
useNamedImport: 'Import {{name}} from React',
41+
fixImportStatement: 'Fix import statement'
4342
}
4443
},
44+
/**
45+
*
46+
* @param {RuleContext} context
47+
* @returns {any}
48+
*/
4549
create(context) {
4650
// ignore non-modules
4751
if (context.parserOptions.sourceType !== 'module') {
@@ -51,43 +55,118 @@ module.exports = {
5155
const mode = options[0] || DEFAULT_TYPE;
5256
const overrides = options[1] || {};
5357

54-
const shouldReport = (name, type) => (overrides[name] || mode) !== type;
58+
const specifiers = [];
59+
60+
/** @type {Context} */
61+
const sourceCode = context.getSourceCode();
62+
63+
function isNameOfType(name, type) {
64+
return (overrides[name] || mode) === type;
65+
}
66+
67+
function getImportDeclarationString() {
68+
let str = 'React';
69+
70+
if (specifiers.length) {
71+
str += ', { ';
72+
str += specifiers.join(', ');
73+
str += ' }';
74+
}
75+
76+
return str;
77+
}
78+
79+
function fixImportDeclaration(node) {
80+
const tokens = sourceCode.getTokens(node);
81+
82+
const importToken = tokens[0];
83+
const fromToken = tokens.filter((token) => token.value === 'from')[0];
84+
85+
const range = [
86+
importToken.range[1] + 1,
87+
fromToken.range[0] - 1
88+
];
89+
90+
return (fixer) => fixer.replaceTextRange(range, getImportDeclarationString(specifiers));
91+
}
92+
93+
function shouldUpdateImportStatement(node) {
94+
let shouldUpdate = false;
95+
const currentSpecifiers = node.specifiers
96+
.filter((specifier) => specifier.type !== 'ImportDefaultSpecifier')
97+
.map((specifier) => specifier.imported.name);
98+
99+
specifiers.forEach((spec) => {
100+
shouldUpdate = currentSpecifiers.indexOf(spec) === -1;
101+
});
102+
103+
currentSpecifiers.forEach((spec) => {
104+
shouldUpdate = isNameOfType(spec, 'property');
105+
});
106+
107+
return shouldUpdate;
108+
}
109+
110+
function getFixer(node, type) {
111+
if (type === 'import') {
112+
return (fixer) => fixer.replaceTextRange(node.parent.range, node.name);
113+
}
114+
115+
return (fixer) => fixer.insertTextBefore(node, 'React.');
116+
}
117+
118+
function getDeclaredNodes(node, type) {
119+
if (type === 'import') {
120+
return [node];
121+
}
122+
123+
const variables = context.getDeclaredVariables(node);
124+
125+
return variables[0].references.map((reference) => reference.identifier);
126+
}
127+
128+
function reporter(node, type) {
129+
if (isNameOfType(node.name, type)) {
130+
if (type === 'import') { specifiers.push(node.name); }
131+
132+
const nodes = getDeclaredNodes(node, type);
133+
134+
nodes.forEach((reportNode) => context.report({
135+
node: reportNode,
136+
messageId: type === 'import' ? 'useNamedImport' : 'usePropertyAccessor',
137+
data: {name: reportNode.name},
138+
fix: getFixer(reportNode, type)
139+
}));
140+
}
141+
}
55142

56143
return {
57-
ImportDeclaration(node) {
58-
if (node.source.value !== 'react') {
59-
return;
60-
}
61-
node.specifiers.forEach((importNode) => {
62-
if (importNode.type === 'ImportSpecifier') {
63-
if (shouldReport(importNode.imported.name, 'import')) {
64-
context.report({
65-
node: importNode,
66-
messageId: 'useProperty',
67-
data: {
68-
name: importNode.imported.name
69-
}
70-
});
71-
}
72-
}
73-
});
74-
},
75-
MemberExpression(node) {
76-
if (node.object.type === 'Identifier') {
77-
const identifier = node.object;
78-
if (identifier.name !== 'React' || node.property.type !== 'Identifier') {
144+
'ImportDeclaration[source.value=\'react\']'(node) {
145+
node.specifiers.forEach((specifier) => {
146+
if (specifier.type === 'ImportDefaultSpecifier') {
147+
const variables = context.getDeclaredVariables(specifier);
148+
variables[0].references.forEach((reference) => {
149+
const memberExpression = reference.identifier.parent;
150+
151+
reporter(memberExpression.property, 'import');
152+
});
153+
79154
return;
80155
}
81-
const property = node.property;
82-
if (shouldReport(property.name, 'property')) {
83-
context.report({
84-
node,
85-
messageId: 'useImport',
86-
data: {
87-
name: property.name
88-
}
89-
});
156+
157+
if (isNameOfType(specifier.imported.name, 'property')) {
158+
reporter(specifier, 'property');
159+
} else {
160+
specifiers.push(specifier.imported.name);
90161
}
162+
});
163+
164+
if (shouldUpdateImportStatement(node)) {
165+
context.report({
166+
node,
167+
messageId: 'fixImportStatement',
168+
fix: fixImportDeclaration(node, specifiers)
169+
});
91170
}
92171
}
93172
};

lib/types.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ declare global {
88
type Scope = eslint.Scope.Scope;
99
type Token = eslint.AST.Token;
1010
type Fixer = eslint.Rule.RuleFixer;
11+
type RuleContext = eslint.Rule.RuleContext;
1112
type JSXAttribute = ASTNode;
1213
type JSXElement = ASTNode;
1314
type JSXFragment = ASTNode;
@@ -17,6 +18,8 @@ declare global {
1718
getFirstTokens(node: estree.Node | ASTNode, options?: eslint.SourceCode.CursorWithCountOptions): eslint.AST.Token[];
1819
}
1920

21+
interface Listener extends eslint.Rule.RuleListener { }
22+
2023
type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set<typeof annotation>) => object;
2124

2225
type TypeDeclarationBuilders = {

tests/lib/rules/function-component-definition.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,21 @@ ruleTester.run('function-component-definition', rule, {
146146
code: [
147147
'function Hello(props) {',
148148
' return <div/>',
149+
'}',
150+
'function Hello2(props) {',
151+
' return <div/>',
149152
'}'
150153
].join('\n'),
151154
output: [
152155
'var Hello = (props) => {',
153156
' return <div/>',
157+
'}',
158+
'var Hello2 = (props) => {',
159+
' return <div/>',
154160
'}'
155161
].join('\n'),
156162
options: [{namedComponents: 'arrow-function'}],
157-
errors: [{message: 'Function component is not an arrow function'}],
163+
errors: [{message: 'Function component is not an arrow function'}, {message: 'Function component is not an arrow function'}],
158164
parser: parsers.BABEL_ESLINT
159165
}, {
160166
code: [

tests/lib/rules/no-named-import.js

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,32 @@ ruleTester.run('no-named-import', rule, {
3333
code: "import React, { useState } from 'react';"
3434
},
3535
{
36-
code: "import React from 'react'; const [loading,setLoading] = React.useState(false);",
36+
code: [
37+
'import React from \'react\';',
38+
'const [loading,setLoading] = React.useState(false);'
39+
].join('\n'),
3740
options: ['property']
3841
},
3942
{
4043
code: "import React, { useState } from 'react'; const [loading,setLoading] = useState(false);",
4144
options: ['import']
4245
},
4346
{
44-
code: "import { useEffect, Component } from 'react'; const [loading,setLoading] = React.useState(false);",
47+
code: `
48+
import React, { useEffect, Component } from 'react';
49+
const [loading,setLoading] = React.useState(false);
50+
`,
4551
options: ['import', {
4652
useEffect: 'import',
4753
useState: 'property'
4854
}]
4955
},
5056
{
51-
code: "import { useEffect } from 'react'; const [loading,setLoading] = React.useState(false); const a = React.Component;",
57+
code: `
58+
import { useEffect } from 'react';
59+
const [loading,setLoading] = React.useState(false);
60+
const a = React.Component;
61+
`,
5262
options: ['property', {
5363
useEffect: 'import',
5464
useState: 'property'
@@ -57,63 +67,105 @@ ruleTester.run('no-named-import', rule, {
5767
],
5868
invalid: [
5969
{
60-
code: 'const [loading, setLoading] = React.useState(false);',
70+
code: `
71+
import React from 'react';
72+
const [value, setValue] = React.useState('');
73+
`,
74+
output: `
75+
import React, { useState } from 'react';
76+
const [value, setValue] = useState('');
77+
`,
6178
errors: [
6279
{
63-
messageId: 'useImport',
64-
data: {name: 'useState'}
65-
}
66-
]
67-
},
68-
{
69-
code: "import React, { useState } from 'react'",
70-
options: ['property'],
71-
errors: [
80+
messageId: 'fixImportStatement'
81+
},
7282
{
73-
messageId: 'useProperty',
83+
messageId: 'useNamedImport',
7484
data: {name: 'useState'}
7585
}
7686
]
7787
},
7888
{
79-
code: 'const [loading, setLoading] = React.useState(false);',
89+
code: `
90+
import React from 'react';
91+
const [value, setValue] = React.useState('');
92+
`,
93+
output: `
94+
import React, { useState } from 'react';
95+
const [value, setValue] = useState('');
96+
`,
8097
options: ['import'],
8198
errors: [
8299
{
83-
messageId: 'useImport',
100+
messageId: 'fixImportStatement'
101+
},
102+
{
103+
messageId: 'useNamedImport',
84104
data: {name: 'useState'}
85105
}
86106
]
87107
},
88108
{
89-
code: 'const [loading, setLoading] = React.useState(false);',
90-
options: ['property', {
91-
useState: 'import'
92-
}],
109+
code: `
110+
import React, { useState } from 'react';
111+
const [value, setValue] = useState('');
112+
`,
113+
output: `
114+
import React from 'react';
115+
const [value, setValue] = React.useState('');
116+
`,
117+
options: ['property'],
93118
errors: [
94119
{
95-
messageId: 'useImport',
120+
messageId: 'fixImportStatement'
121+
},
122+
{
123+
messageId: 'usePropertyAccessor',
96124
data: {name: 'useState'}
97125
}
98126
]
99127
},
100128
{
101-
code: "import React, { useState } from 'react'",
102-
options: ['import', {useState: 'property'}],
129+
code: `
130+
import React from 'react';
131+
const [value, setValue] = React.useState('');
132+
React.useEffect(() => {},[]);
133+
`,
134+
output: `
135+
import React, { useEffect } from 'react';
136+
const [value, setValue] = React.useState('');
137+
useEffect(() => {},[]);
138+
`,
139+
options: ['property', {useEffect: 'import'}],
103140
errors: [
104141
{
105-
messageId: 'useProperty',
106-
data: {name: 'useState'}
142+
messageId: 'fixImportStatement'
143+
},
144+
{
145+
messageId: 'useNamedImport',
146+
data: {name: 'useEffect'}
107147
}
108148
]
109149
},
110150
{
111-
code: 'const comp = React.Component;',
151+
code: `
152+
import React from 'react';
153+
const [value, setValue] = React.useState('');
154+
React.useEffect(() => {},[]);
155+
`,
156+
output: `
157+
import React, { useEffect } from 'react';
158+
const [value, setValue] = React.useState('');
159+
useEffect(() => {},[]);
160+
`,
112161
options: ['import', {useState: 'property'}],
113162
errors: [
114163
{
115-
messageId: 'useImport',
116-
data: {name: 'Component'}
164+
messageId: 'fixImportStatement'
165+
},
166+
{
167+
messageId: 'useNamedImport',
168+
data: {name: 'useEffect'}
117169
}
118170
]
119171
}

0 commit comments

Comments
 (0)