1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const Components = require('../util/Components');
|
9 | const docsUrl = require('../util/docsUrl');
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | function buildFunction(template, parts) {
|
16 | return Object.keys(parts)
|
17 | .reduce((acc, key) => acc.replace(`{${key}}`, parts[key] || ''), template);
|
18 | }
|
19 |
|
20 | const NAMED_FUNCTION_TEMPLATES = {
|
21 | 'function-declaration': 'function {name}{typeParams}({params}){returnType} {body}',
|
22 | 'arrow-function': 'var {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}',
|
23 | 'function-expression': 'var {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}'
|
24 | };
|
25 |
|
26 | const UNNAMED_FUNCTION_TEMPLATES = {
|
27 | 'function-expression': 'function{typeParams}({params}){returnType} {body}',
|
28 | 'arrow-function': '{typeParams}({params}){returnType} => {body}'
|
29 | };
|
30 |
|
31 | const ERROR_MESSAGES = {
|
32 | 'function-declaration': 'Function component is not a function declaration',
|
33 | 'function-expression': 'Function component is not a function expression',
|
34 | 'arrow-function': 'Function component is not an arrow function'
|
35 | };
|
36 |
|
37 | function hasOneUnconstrainedTypeParam(node) {
|
38 | if (node.typeParameters) {
|
39 | return node.typeParameters.params.length === 1 && !node.typeParameters.params[0].constraint;
|
40 | }
|
41 |
|
42 | return false;
|
43 | }
|
44 |
|
45 | function hasName(node) {
|
46 | return node.type === 'FunctionDeclaration' || node.parent.type === 'VariableDeclarator';
|
47 | }
|
48 |
|
49 | function getNodeText(prop, source) {
|
50 | if (!prop) return null;
|
51 | return source.slice(prop.range[0], prop.range[1]);
|
52 | }
|
53 |
|
54 | function getName(node) {
|
55 | if (node.type === 'FunctionDeclaration') {
|
56 | return node.id.name;
|
57 | }
|
58 |
|
59 | if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
|
60 | return hasName(node) && node.parent.id.name;
|
61 | }
|
62 | }
|
63 |
|
64 | function getParams(node, source) {
|
65 | if (node.params.length === 0) return null;
|
66 | return source.slice(node.params[0].range[0], node.params[node.params.length - 1].range[1]);
|
67 | }
|
68 |
|
69 | function getBody(node, source) {
|
70 | const range = node.body.range;
|
71 |
|
72 | if (node.body.type !== 'BlockStatement') {
|
73 | return [
|
74 | '{',
|
75 | ` return ${source.slice(range[0], range[1])}`,
|
76 | '}'
|
77 | ].join('\n');
|
78 | }
|
79 |
|
80 | return source.slice(range[0], range[1]);
|
81 | }
|
82 |
|
83 | function getTypeAnnotation(node, source) {
|
84 | if (!hasName(node) || node.type === 'FunctionDeclaration') return;
|
85 |
|
86 | if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') {
|
87 | return getNodeText(node.parent.id.typeAnnotation, source);
|
88 | }
|
89 | }
|
90 |
|
91 | function isUnfixableBecauseOfExport(node) {
|
92 | return node.type === 'FunctionDeclaration' && node.parent && node.parent.type === 'ExportDefaultDeclaration';
|
93 | }
|
94 |
|
95 | function isFunctionExpressionWithName(node) {
|
96 | return node.type === 'FunctionExpression' && node.id && node.id.name;
|
97 | }
|
98 |
|
99 | module.exports = {
|
100 | meta: {
|
101 | docs: {
|
102 | description: 'Standardize the way function component get defined',
|
103 | category: 'Stylistic Issues',
|
104 | recommended: false,
|
105 | url: docsUrl('function-component-definition')
|
106 | },
|
107 | fixable: 'code',
|
108 |
|
109 | schema: [{
|
110 | type: 'object',
|
111 | properties: {
|
112 | namedComponents: {
|
113 | enum: ['function-declaration', 'arrow-function', 'function-expression']
|
114 | },
|
115 | unnamedComponents: {
|
116 | enum: ['arrow-function', 'function-expression']
|
117 | }
|
118 | }
|
119 | }]
|
120 | },
|
121 |
|
122 | create: Components.detect((context, components) => {
|
123 | const configuration = context.options[0] || {};
|
124 |
|
125 | const namedConfig = configuration.namedComponents || 'function-declaration';
|
126 | const unnamedConfig = configuration.unnamedComponents || 'function-expression';
|
127 |
|
128 | function getFixer(node, options) {
|
129 | const sourceCode = context.getSourceCode();
|
130 | const source = sourceCode.getText();
|
131 |
|
132 | const typeAnnotation = getTypeAnnotation(node, source);
|
133 |
|
134 | if (options.type === 'function-declaration' && typeAnnotation) return;
|
135 | if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) return;
|
136 | if (isUnfixableBecauseOfExport(node)) return;
|
137 | if (isFunctionExpressionWithName(node)) return;
|
138 |
|
139 | return (fixer) => fixer.replaceTextRange(options.range, buildFunction(options.template, {
|
140 | typeAnnotation,
|
141 | typeParams: getNodeText(node.typeParameters, source),
|
142 | params: getParams(node, source),
|
143 | returnType: getNodeText(node.returnType, source),
|
144 | body: getBody(node, source),
|
145 | name: getName(node)
|
146 | }));
|
147 | }
|
148 |
|
149 | function report(node, options) {
|
150 | context.report({
|
151 | node,
|
152 | message: options.message,
|
153 | fix: getFixer(node, options.fixerOptions)
|
154 | });
|
155 | }
|
156 |
|
157 | function validate(node, functionType) {
|
158 | if (!components.get(node)) return;
|
159 | if (hasName(node) && namedConfig !== functionType) {
|
160 | report(node, {
|
161 | message: ERROR_MESSAGES[namedConfig],
|
162 | fixerOptions: {
|
163 | type: namedConfig,
|
164 | template: NAMED_FUNCTION_TEMPLATES[namedConfig],
|
165 | range: node.type === 'FunctionDeclaration'
|
166 | ? node.range
|
167 | : node.parent.parent.range
|
168 | }
|
169 | });
|
170 | }
|
171 | if (!hasName(node) && unnamedConfig !== functionType) {
|
172 | report(node, {
|
173 | message: ERROR_MESSAGES[unnamedConfig],
|
174 | fixerOptions: {
|
175 | type: unnamedConfig,
|
176 | template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig],
|
177 | range: node.range
|
178 | }
|
179 | });
|
180 | }
|
181 | }
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 | return {
|
188 | FunctionDeclaration(node) { validate(node, 'function-declaration'); },
|
189 | ArrowFunctionExpression(node) { validate(node, 'arrow-function'); },
|
190 | FunctionExpression(node) { validate(node, 'function-expression'); }
|
191 | };
|
192 | })
|
193 | };
|