source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/function-component-definition.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 8.1 KB
Line 
1/**
2 * @fileoverview Standardize the way function component get defined
3 * @author Stefan Wullems
4 */
5
6'use strict';
7
8const arrayIncludes = require('array-includes');
9const Components = require('../util/Components');
10const docsUrl = require('../util/docsUrl');
11const reportC = require('../util/report');
12const getText = require('../util/eslint').getText;
13
14// ------------------------------------------------------------------------------
15// Rule Definition
16// ------------------------------------------------------------------------------
17
18function buildFunction(template, parts) {
19 return Object.keys(parts).reduce(
20 (acc, key) => acc.replace(`{${key}}`, () => parts[key] || ''),
21 template
22 );
23}
24
25const NAMED_FUNCTION_TEMPLATES = {
26 'function-declaration': 'function {name}{typeParams}({params}){returnType} {body}',
27 'arrow-function': '{varType} {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}',
28 'function-expression': '{varType} {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}',
29};
30
31const UNNAMED_FUNCTION_TEMPLATES = {
32 'function-expression': 'function{typeParams}({params}){returnType} {body}',
33 'arrow-function': '{typeParams}({params}){returnType} => {body}',
34};
35
36function hasOneUnconstrainedTypeParam(node) {
37 const nodeTypeParams = node.typeParameters;
38
39 return nodeTypeParams
40 && nodeTypeParams.params
41 && nodeTypeParams.params.length === 1
42 && !nodeTypeParams.params[0].constraint;
43}
44
45function hasName(node) {
46 return (
47 node.type === 'FunctionDeclaration'
48 || node.parent.type === 'VariableDeclarator'
49 );
50}
51
52function getNodeText(prop, source) {
53 if (!prop) return null;
54 return source.slice(prop.range[0], prop.range[1]);
55}
56
57function getName(node) {
58 if (node.type === 'FunctionDeclaration') {
59 return node.id.name;
60 }
61
62 if (
63 node.type === 'ArrowFunctionExpression'
64 || node.type === 'FunctionExpression'
65 ) {
66 return hasName(node) && node.parent.id.name;
67 }
68}
69
70function getParams(node, source) {
71 if (node.params.length === 0) return null;
72 return source.slice(
73 node.params[0].range[0],
74 node.params[node.params.length - 1].range[1]
75 );
76}
77
78function getBody(node, source) {
79 const range = node.body.range;
80
81 if (node.body.type !== 'BlockStatement') {
82 return ['{', ` return ${source.slice(range[0], range[1])}`, '}'].join('\n');
83 }
84
85 return source.slice(range[0], range[1]);
86}
87
88function getTypeAnnotation(node, source) {
89 if (!hasName(node) || node.type === 'FunctionDeclaration') return;
90
91 if (
92 node.type === 'ArrowFunctionExpression'
93 || node.type === 'FunctionExpression'
94 ) {
95 return getNodeText(node.parent.id.typeAnnotation, source);
96 }
97}
98
99function isUnfixableBecauseOfExport(node) {
100 return (
101 node.type === 'FunctionDeclaration'
102 && node.parent
103 && node.parent.type === 'ExportDefaultDeclaration'
104 );
105}
106
107function isFunctionExpressionWithName(node) {
108 return node.type === 'FunctionExpression' && node.id && node.id.name;
109}
110
111const messages = {
112 'function-declaration': 'Function component is not a function declaration',
113 'function-expression': 'Function component is not a function expression',
114 'arrow-function': 'Function component is not an arrow function',
115};
116
117/** @type {import('eslint').Rule.RuleModule} */
118module.exports = {
119 meta: {
120 docs: {
121 description: 'Enforce a specific function type for function components',
122 category: 'Stylistic Issues',
123 recommended: false,
124 url: docsUrl('function-component-definition'),
125 },
126 fixable: 'code',
127
128 messages,
129
130 schema: [
131 {
132 type: 'object',
133 properties: {
134 namedComponents: {
135 anyOf: [
136 {
137 enum: [
138 'function-declaration',
139 'arrow-function',
140 'function-expression',
141 ],
142 },
143 {
144 type: 'array',
145 items: {
146 type: 'string',
147 enum: [
148 'function-declaration',
149 'arrow-function',
150 'function-expression',
151 ],
152 },
153 },
154 ],
155 },
156 unnamedComponents: {
157 anyOf: [
158 { enum: ['arrow-function', 'function-expression'] },
159 {
160 type: 'array',
161 items: {
162 type: 'string',
163 enum: ['arrow-function', 'function-expression'],
164 },
165 },
166 ],
167 },
168 },
169 },
170 ],
171 },
172
173 create: Components.detect((context, components) => {
174 const configuration = context.options[0] || {};
175 let fileVarType = 'var';
176
177 const namedConfig = [].concat(
178 configuration.namedComponents || 'function-declaration'
179 );
180 const unnamedConfig = [].concat(
181 configuration.unnamedComponents || 'function-expression'
182 );
183
184 function getFixer(node, options) {
185 const source = getText(context);
186
187 const typeAnnotation = getTypeAnnotation(node, source);
188
189 if (options.type === 'function-declaration' && typeAnnotation) {
190 return;
191 }
192 if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) {
193 return;
194 }
195 if (isUnfixableBecauseOfExport(node)) return;
196 if (isFunctionExpressionWithName(node)) return;
197 let varType = fileVarType;
198 if (
199 (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression')
200 && node.parent.type === 'VariableDeclarator'
201 ) {
202 varType = node.parent.parent.kind;
203 }
204
205 return (fixer) => fixer.replaceTextRange(
206 options.range,
207 buildFunction(options.template, {
208 typeAnnotation,
209 typeParams: getNodeText(node.typeParameters, source),
210 params: getParams(node, source),
211 returnType: getNodeText(node.returnType, source),
212 body: getBody(node, source),
213 name: getName(node),
214 varType,
215 })
216 );
217 }
218
219 function report(node, options) {
220 reportC(context, messages[options.messageId], options.messageId, {
221 node,
222 fix: getFixer(node, options.fixerOptions),
223 });
224 }
225
226 function validate(node, functionType) {
227 if (!components.get(node)) return;
228
229 if (node.parent && node.parent.type === 'Property') return;
230
231 if (hasName(node) && !arrayIncludes(namedConfig, functionType)) {
232 report(node, {
233 messageId: namedConfig[0],
234 fixerOptions: {
235 type: namedConfig[0],
236 template: NAMED_FUNCTION_TEMPLATES[namedConfig[0]],
237 range:
238 node.type === 'FunctionDeclaration'
239 ? node.range
240 : node.parent.parent.range,
241 },
242 });
243 }
244 if (!hasName(node) && !arrayIncludes(unnamedConfig, functionType)) {
245 report(node, {
246 messageId: unnamedConfig[0],
247 fixerOptions: {
248 type: unnamedConfig[0],
249 template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig[0]],
250 range: node.range,
251 },
252 });
253 }
254 }
255
256 // --------------------------------------------------------------------------
257 // Public
258 // --------------------------------------------------------------------------
259 const validatePairs = [];
260 let hasES6OrJsx = false;
261 return {
262 FunctionDeclaration(node) {
263 validatePairs.push([node, 'function-declaration']);
264 },
265 ArrowFunctionExpression(node) {
266 validatePairs.push([node, 'arrow-function']);
267 },
268 FunctionExpression(node) {
269 validatePairs.push([node, 'function-expression']);
270 },
271 VariableDeclaration(node) {
272 hasES6OrJsx = hasES6OrJsx || node.kind === 'const' || node.kind === 'let';
273 },
274 'Program:exit'() {
275 if (hasES6OrJsx) fileVarType = 'const';
276 validatePairs.forEach((pair) => validate(pair[0], pair[1]));
277 },
278 'ImportDeclaration, ExportNamedDeclaration, ExportDefaultDeclaration, ExportAllDeclaration, ExportSpecifier, ExportDefaultSpecifier, JSXElement, TSExportAssignment, TSImportEqualsDeclaration'() {
279 hasES6OrJsx = true;
280 },
281 };
282 }),
283};
Note: See TracBrowser for help on using the repository browser.