Skip to content

Commit 4b4bba9

Browse files
JohnBerdXavier Le CunffSimeonC
authored andcommitted
[New] function-component-definition: replace var by const in certain situations
Signed-off-by: Xavier Le Cunff <[email protected]> Co-authored-by: Xavier Le Cunff <[email protected]> Co-authored-by: Simeon Cheeseman <[email protected]>
1 parent 18de0a6 commit 4b4bba9

File tree

4 files changed

+251
-59
lines changed

4 files changed

+251
-59
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
99
* [`destructuring-assignment`]: add option `destructureInSignature` ([#3235][] @golopot)
1010
* [`no-unknown-property`]: Allow crossOrigin on image tag (SVG) ([#3251][] @zpao)
1111
* [`jsx-tag-spacing`]: Add `multiline-always` option ([#3260][] @Nokel81)
12+
* [`function-component-definition`]: replace `var` by `const` in certain situations ([#3248][] @JohnBerd @SimeonC)
1213

1314
### Fixed
1415
* [`hook-use-state`]: Allow UPPERCASE setState setter prefixes ([#3244][] @duncanbeevers)
@@ -38,6 +39,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
3839
[#3258]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3258
3940
[#3254]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3254
4041
[#3251]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3251
42+
[#3248]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3248
4143
[#3244]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3244
4244
[#3235]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3235
4345
[#3230]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3230

docs/rules/function-component-definition.md

+15-15
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ Examples of **incorrect** code for this rule:
1212

1313
```jsx
1414
// function expression for named component
15-
var Component = function (props) {
15+
const Component = function (props) {
1616
return <div>{props.content}</div>;
1717
};
1818

1919
// arrow function for named component
20-
var Component = (props) => {
20+
const Component = (props) => {
2121
return <div>{props.content}</div>;
2222
};
2323

@@ -49,11 +49,11 @@ Examples of **incorrect** code for this rule:
4949
```jsx
5050
// only function declarations for named components
5151
// [2, { "namedComponents": "function-declaration" }]
52-
var Component = function (props) {
52+
const Component = function (props) {
5353
return <div />;
5454
};
5555

56-
var Component = (props) => {
56+
const Component = (props) => {
5757
return <div />;
5858
};
5959

@@ -63,7 +63,7 @@ function Component (props) {
6363
return <div />;
6464
};
6565

66-
var Component = (props) => {
66+
const Component = (props) => {
6767
return <div />;
6868
};
6969

@@ -73,7 +73,7 @@ function Component (props) {
7373
return <div />;
7474
};
7575

76-
var Component = function (props) {
76+
const Component = function (props) {
7777
return <div />;
7878
};
7979

@@ -107,13 +107,13 @@ function Component (props) {
107107

108108
// only function expressions for named components
109109
// [2, { "namedComponents": "function-expression" }]
110-
var Component = function (props) {
110+
const Component = function (props) {
111111
return <div />;
112112
};
113113

114114
// only arrow functions for named components
115115
// [2, { "namedComponents": "arrow-function" }]
116-
var Component = (props) => {
116+
const Component = (props) => {
117117
return <div />;
118118
};
119119

@@ -170,11 +170,11 @@ The following patterns can **not** be autofixed in TypeScript:
170170
```tsx
171171
// function expressions and arrow functions that have type annotations cannot be autofixed to function declarations
172172
// [2, { "namedComponents": "function-declaration" }]
173-
var Component: React.FC<Props> = function (props) {
173+
const Component: React.FC<Props> = function (props) {
174174
return <div />;
175175
};
176176

177-
var Component: React.FC<Props> = (props) => {
177+
const Component: React.FC<Props> = (props) => {
178178
return <div />;
179179
};
180180

@@ -184,7 +184,7 @@ function Component<T>(props: Props<T>) {
184184
return <div />;
185185
};
186186

187-
var Component = function <T>(props: Props<T>) {
187+
const Component = function <T>(props: Props<T>) {
188188
return <div />;
189189
};
190190

@@ -203,13 +203,13 @@ The following patterns can be autofixed in TypeScript:
203203
```tsx
204204
// autofix to function expression with type annotation
205205
// [2, { "namedComponents": "function-expression" }]
206-
var Component: React.FC<Props> = (props) => {
206+
const Component: React.FC<Props> = (props) => {
207207
return <div />;
208208
};
209209

210210
// autofix to arrow function with type annotation
211211
// [2, { "namedComponents": "function-expression" }]
212-
var Component: React.FC<Props> = function (props) {
212+
const Component: React.FC<Props> = function (props) {
213213
return <div />;
214214
};
215215

@@ -219,7 +219,7 @@ function Component<T extends {}>(props: Props<T>) {
219219
return <div />;
220220
}
221221

222-
var Component = function <T extends {}>(props: Props<T>) {
222+
const Component = function <T extends {}>(props: Props<T>) {
223223
return <div />;
224224
};
225225

@@ -229,7 +229,7 @@ function Component<T1, T2>(props: Props<T1, T2>) {
229229
return <div />;
230230
}
231231

232-
var Component = function <T1, T2>(props: Props<T2>) {
232+
const Component = function <T1, T2>(props: Props<T2>) {
233233
return <div />;
234234
};
235235

lib/rules/function-component-definition.js

+101-36
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ const reportC = require('../util/report');
1515
// ------------------------------------------------------------------------------
1616

1717
function buildFunction(template, parts) {
18-
return Object.keys(parts)
19-
.reduce((acc, key) => acc.replace(`{${key}}`, () => (parts[key] || '')), template);
18+
return Object.keys(parts).reduce(
19+
(acc, key) => acc.replace(`{${key}}`, () => parts[key] || ''),
20+
template
21+
);
2022
}
2123

2224
const NAMED_FUNCTION_TEMPLATES = {
2325
'function-declaration': 'function {name}{typeParams}({params}){returnType} {body}',
24-
'arrow-function': 'var {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}',
25-
'function-expression': 'var {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}',
26+
'arrow-function': '{varType} {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}',
27+
'function-expression': '{varType} {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}',
2628
};
2729

2830
const UNNAMED_FUNCTION_TEMPLATES = {
@@ -32,14 +34,20 @@ const UNNAMED_FUNCTION_TEMPLATES = {
3234

3335
function hasOneUnconstrainedTypeParam(node) {
3436
if (node.typeParameters) {
35-
return node.typeParameters.params.length === 1 && !node.typeParameters.params[0].constraint;
37+
return (
38+
node.typeParameters.params.length === 1
39+
&& !node.typeParameters.params[0].constraint
40+
);
3641
}
3742

3843
return false;
3944
}
4045

4146
function hasName(node) {
42-
return node.type === 'FunctionDeclaration' || node.parent.type === 'VariableDeclarator';
47+
return (
48+
node.type === 'FunctionDeclaration'
49+
|| node.parent.type === 'VariableDeclarator'
50+
);
4351
}
4452

4553
function getNodeText(prop, source) {
@@ -52,25 +60,27 @@ function getName(node) {
5260
return node.id.name;
5361
}
5462

55-
if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
63+
if (
64+
node.type === 'ArrowFunctionExpression'
65+
|| node.type === 'FunctionExpression'
66+
) {
5667
return hasName(node) && node.parent.id.name;
5768
}
5869
}
5970

6071
function getParams(node, source) {
6172
if (node.params.length === 0) return null;
62-
return source.slice(node.params[0].range[0], node.params[node.params.length - 1].range[1]);
73+
return source.slice(
74+
node.params[0].range[0],
75+
node.params[node.params.length - 1].range[1]
76+
);
6377
}
6478

6579
function getBody(node, source) {
6680
const range = node.body.range;
6781

6882
if (node.body.type !== 'BlockStatement') {
69-
return [
70-
'{',
71-
` return ${source.slice(range[0], range[1])}`,
72-
'}',
73-
].join('\n');
83+
return ['{', ` return ${source.slice(range[0], range[1])}`, '}'].join('\n');
7484
}
7585

7686
return source.slice(range[0], range[1]);
@@ -79,13 +89,20 @@ function getBody(node, source) {
7989
function getTypeAnnotation(node, source) {
8090
if (!hasName(node) || node.type === 'FunctionDeclaration') return;
8191

82-
if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
92+
if (
93+
node.type === 'ArrowFunctionExpression'
94+
|| node.type === 'FunctionExpression'
95+
) {
8396
return getNodeText(node.parent.id.typeAnnotation, source);
8497
}
8598
}
8699

87100
function isUnfixableBecauseOfExport(node) {
88-
return node.type === 'FunctionDeclaration' && node.parent && node.parent.type === 'ExportDefaultDeclaration';
101+
return (
102+
node.type === 'FunctionDeclaration'
103+
&& node.parent
104+
&& node.parent.type === 'ExportDefaultDeclaration'
105+
);
89106
}
90107

91108
function isFunctionExpressionWithName(node) {
@@ -116,12 +133,22 @@ module.exports = {
116133
properties: {
117134
namedComponents: {
118135
oneOf: [
119-
{ enum: ['function-declaration', 'arrow-function', 'function-expression'] },
136+
{
137+
enum: [
138+
'function-declaration',
139+
'arrow-function',
140+
'function-expression',
141+
],
142+
},
120143
{
121144
type: 'array',
122145
items: {
123146
type: 'string',
124-
enum: ['function-declaration', 'arrow-function', 'function-expression'],
147+
enum: [
148+
'function-declaration',
149+
'arrow-function',
150+
'function-expression',
151+
],
125152
},
126153
},
127154
],
@@ -145,29 +172,49 @@ module.exports = {
145172

146173
create: Components.detect((context, components) => {
147174
const configuration = context.options[0] || {};
175+
let fileVarType = 'var';
148176

149-
const namedConfig = [].concat(configuration.namedComponents || 'function-declaration');
150-
const unnamedConfig = [].concat(configuration.unnamedComponents || 'function-expression');
177+
const namedConfig = [].concat(
178+
configuration.namedComponents || 'function-declaration'
179+
);
180+
const unnamedConfig = [].concat(
181+
configuration.unnamedComponents || 'function-expression'
182+
);
151183

152184
function getFixer(node, options) {
153185
const sourceCode = context.getSourceCode();
154186
const source = sourceCode.getText();
155187

156188
const typeAnnotation = getTypeAnnotation(node, source);
157189

158-
if (options.type === 'function-declaration' && typeAnnotation) return;
159-
if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) return;
190+
if (options.type === 'function-declaration' && typeAnnotation) {
191+
return;
192+
}
193+
if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) {
194+
return;
195+
}
160196
if (isUnfixableBecauseOfExport(node)) return;
161197
if (isFunctionExpressionWithName(node)) return;
198+
let varType = fileVarType;
199+
if (
200+
(node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression')
201+
&& node.parent.type === 'VariableDeclarator'
202+
) {
203+
varType = node.parent.parent.kind;
204+
}
162205

163-
return (fixer) => fixer.replaceTextRange(options.range, buildFunction(options.template, {
164-
typeAnnotation,
165-
typeParams: getNodeText(node.typeParameters, source),
166-
params: getParams(node, source),
167-
returnType: getNodeText(node.returnType, source),
168-
body: getBody(node, source),
169-
name: getName(node),
170-
}));
206+
return (fixer) => fixer.replaceTextRange(
207+
options.range,
208+
buildFunction(options.template, {
209+
typeAnnotation,
210+
typeParams: getNodeText(node.typeParameters, source),
211+
params: getParams(node, source),
212+
returnType: getNodeText(node.returnType, source),
213+
body: getBody(node, source),
214+
name: getName(node),
215+
varType,
216+
})
217+
);
171218
}
172219

173220
function report(node, options) {
@@ -188,9 +235,10 @@ module.exports = {
188235
fixerOptions: {
189236
type: namedConfig[0],
190237
template: NAMED_FUNCTION_TEMPLATES[namedConfig[0]],
191-
range: node.type === 'FunctionDeclaration'
192-
? node.range
193-
: node.parent.parent.range,
238+
range:
239+
node.type === 'FunctionDeclaration'
240+
? node.range
241+
: node.parent.parent.range,
194242
},
195243
});
196244
}
@@ -209,11 +257,28 @@ module.exports = {
209257
// --------------------------------------------------------------------------
210258
// Public
211259
// --------------------------------------------------------------------------
212-
260+
const validatePairs = [];
261+
let hasES6OrJsx = false;
213262
return {
214-
FunctionDeclaration(node) { validate(node, 'function-declaration'); },
215-
ArrowFunctionExpression(node) { validate(node, 'arrow-function'); },
216-
FunctionExpression(node) { validate(node, 'function-expression'); },
263+
FunctionDeclaration(node) {
264+
validatePairs.push([node, 'function-declaration']);
265+
},
266+
ArrowFunctionExpression(node) {
267+
validatePairs.push([node, 'arrow-function']);
268+
},
269+
FunctionExpression(node) {
270+
validatePairs.push([node, 'function-expression']);
271+
},
272+
VariableDeclaration(node) {
273+
hasES6OrJsx = hasES6OrJsx || node.kind === 'const' || node.kind === 'let';
274+
},
275+
'Program:exit'() {
276+
if (hasES6OrJsx) fileVarType = 'const';
277+
validatePairs.forEach((pair) => validate(pair[0], pair[1]));
278+
},
279+
'ImportDeclaration, ExportNamedDeclaration, ExportDefaultDeclaration, ExportAllDeclaration, ExportSpecifier, ExportDefaultSpecifier, JSXElement, TSExportAssignment, TSImportEqualsDeclaration'() {
280+
hasES6OrJsx = true;
281+
},
217282
};
218283
}),
219284
};

0 commit comments

Comments
 (0)