UNPKG

6.07 kBJavaScriptView Raw
1/**
2 * @fileoverview Standardize the way function component get defined
3 * @author Stefan Wullems
4 */
5
6'use strict';
7
8const Components = require('../util/Components');
9const docsUrl = require('../util/docsUrl');
10
11// ------------------------------------------------------------------------------
12// Rule Definition
13// ------------------------------------------------------------------------------
14
15function buildFunction(template, parts) {
16 return Object.keys(parts)
17 .reduce((acc, key) => acc.replace(`{${key}}`, parts[key] || ''), template);
18}
19
20const 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
26const UNNAMED_FUNCTION_TEMPLATES = {
27 'function-expression': 'function{typeParams}({params}){returnType} {body}',
28 'arrow-function': '{typeParams}({params}){returnType} => {body}'
29};
30
31const 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
37function 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
45function hasName(node) {
46 return node.type === 'FunctionDeclaration' || node.parent.type === 'VariableDeclarator';
47}
48
49function getNodeText(prop, source) {
50 if (!prop) return null;
51 return source.slice(prop.range[0], prop.range[1]);
52}
53
54function 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
64function 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
69function 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
83function 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
91function isUnfixableBecauseOfExport(node) {
92 return node.type === 'FunctionDeclaration' && node.parent && node.parent.type === 'ExportDefaultDeclaration';
93}
94
95function isFunctionExpressionWithName(node) {
96 return node.type === 'FunctionExpression' && node.id && node.id.name;
97}
98
99module.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 // Public
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};