function-component-definition.js 8.0 KB

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