function-component-definition.js 6.0 KB

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