| /** |
| * @fileoverview Standardize the way function component get defined |
| * @author Stefan Wullems |
| */ |
| |
| 'use strict'; |
| |
| const Components = require('../util/Components'); |
| const docsUrl = require('../util/docsUrl'); |
| |
| // ------------------------------------------------------------------------------ |
| // Rule Definition |
| // ------------------------------------------------------------------------------ |
| |
| function buildFunction(template, parts) { |
| return Object.keys(parts) |
| .reduce((acc, key) => acc.replace(`{${key}}`, parts[key] || ''), template); |
| } |
| |
| const NAMED_FUNCTION_TEMPLATES = { |
| 'function-declaration': 'function {name}{typeParams}({params}){returnType} {body}', |
| 'arrow-function': 'var {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}', |
| 'function-expression': 'var {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}' |
| }; |
| |
| const UNNAMED_FUNCTION_TEMPLATES = { |
| 'function-expression': 'function{typeParams}({params}){returnType} {body}', |
| 'arrow-function': '{typeParams}({params}){returnType} => {body}' |
| }; |
| |
| const ERROR_MESSAGES = { |
| 'function-declaration': 'Function component is not a function declaration', |
| 'function-expression': 'Function component is not a function expression', |
| 'arrow-function': 'Function component is not an arrow function' |
| }; |
| |
| function hasOneUnconstrainedTypeParam(node) { |
| if (node.typeParameters) { |
| return node.typeParameters.params.length === 1 && !node.typeParameters.params[0].constraint; |
| } |
| |
| return false; |
| } |
| |
| function hasName(node) { |
| return node.type === 'FunctionDeclaration' || node.parent.type === 'VariableDeclarator'; |
| } |
| |
| function getNodeText(prop, source) { |
| if (!prop) return null; |
| return source.slice(prop.range[0], prop.range[1]); |
| } |
| |
| function getName(node) { |
| if (node.type === 'FunctionDeclaration') { |
| return node.id.name; |
| } |
| |
| if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') { |
| return hasName(node) && node.parent.id.name; |
| } |
| } |
| |
| function getParams(node, source) { |
| if (node.params.length === 0) return null; |
| return source.slice(node.params[0].range[0], node.params[node.params.length - 1].range[1]); |
| } |
| |
| function getBody(node, source) { |
| const range = node.body.range; |
| |
| if (node.body.type !== 'BlockStatement') { |
| return [ |
| '{', |
| ` return ${source.slice(range[0], range[1])}`, |
| '}' |
| ].join('\n'); |
| } |
| |
| return source.slice(range[0], range[1]); |
| } |
| |
| function getTypeAnnotation(node, source) { |
| if (!hasName(node) || node.type === 'FunctionDeclaration') return; |
| |
| if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') { |
| return getNodeText(node.parent.id.typeAnnotation, source); |
| } |
| } |
| |
| function isUnfixableBecauseOfExport(node) { |
| return node.type === 'FunctionDeclaration' && node.parent && node.parent.type === 'ExportDefaultDeclaration'; |
| } |
| |
| function isFunctionExpressionWithName(node) { |
| return node.type === 'FunctionExpression' && node.id && node.id.name; |
| } |
| |
| module.exports = { |
| meta: { |
| docs: { |
| description: 'Standardize the way function component get defined', |
| category: 'Stylistic issues', |
| recommended: false, |
| url: docsUrl('function-component-definition') |
| }, |
| fixable: 'code', |
| |
| schema: [{ |
| type: 'object', |
| properties: { |
| namedComponents: { |
| enum: ['function-declaration', 'arrow-function', 'function-expression'] |
| }, |
| unnamedComponents: { |
| enum: ['arrow-function', 'function-expression'] |
| } |
| } |
| }] |
| }, |
| |
| create: Components.detect((context, components) => { |
| const configuration = context.options[0] || {}; |
| |
| const namedConfig = configuration.namedComponents || 'function-declaration'; |
| const unnamedConfig = configuration.unnamedComponents || 'function-expression'; |
| |
| function getFixer(node, options) { |
| const sourceCode = context.getSourceCode(); |
| const source = sourceCode.getText(); |
| |
| const typeAnnotation = getTypeAnnotation(node, source); |
| |
| if (options.type === 'function-declaration' && typeAnnotation) return; |
| if (options.type === 'arrow-function' && hasOneUnconstrainedTypeParam(node)) return; |
| if (isUnfixableBecauseOfExport(node)) return; |
| if (isFunctionExpressionWithName(node)) return; |
| |
| return fixer => fixer.replaceTextRange(options.range, buildFunction(options.template, { |
| typeAnnotation, |
| typeParams: getNodeText(node.typeParameters, source), |
| params: getParams(node, source), |
| returnType: getNodeText(node.returnType, source), |
| body: getBody(node, source), |
| name: getName(node) |
| })); |
| } |
| |
| function report(node, options) { |
| context.report({ |
| node, |
| message: options.message, |
| fix: getFixer(node, options.fixerOptions) |
| }); |
| } |
| |
| function validate(node, functionType) { |
| if (!components.get(node)) return; |
| if (hasName(node) && namedConfig !== functionType) { |
| report(node, { |
| message: ERROR_MESSAGES[namedConfig], |
| fixerOptions: { |
| type: namedConfig, |
| template: NAMED_FUNCTION_TEMPLATES[namedConfig], |
| range: node.type === 'FunctionDeclaration' ? |
| node.range : |
| node.parent.parent.range |
| } |
| }); |
| } |
| if (!hasName(node) && unnamedConfig !== functionType) { |
| report(node, { |
| message: ERROR_MESSAGES[unnamedConfig], |
| fixerOptions: { |
| type: unnamedConfig, |
| template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig], |
| range: node.range |
| } |
| }); |
| } |
| } |
| |
| // -------------------------------------------------------------------------- |
| // Public |
| // -------------------------------------------------------------------------- |
| |
| return { |
| FunctionDeclaration(node) { validate(node, 'function-declaration'); }, |
| ArrowFunctionExpression(node) { validate(node, 'arrow-function'); }, |
| FunctionExpression(node) { validate(node, 'function-expression'); } |
| }; |
| }) |
| }; |