123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- /**
- * @fileoverview Standardize the way function component get defined
- * @author Stefan Wullems
- */
- 'use strict';
- const arrayIncludes = require('array-includes');
- const Components = require('../util/Components');
- const docsUrl = require('../util/docsUrl');
- const reportC = require('../util/report');
- // ------------------------------------------------------------------------------
- // 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': '{varType} {name}{typeAnnotation} = {typeParams}({params}){returnType} => {body}',
- 'function-expression': '{varType} {name}{typeAnnotation} = function{typeParams}({params}){returnType} {body}',
- };
- const UNNAMED_FUNCTION_TEMPLATES = {
- 'function-expression': 'function{typeParams}({params}){returnType} {body}',
- 'arrow-function': '{typeParams}({params}){returnType} => {body}',
- };
- 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;
- }
- const 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',
- };
- module.exports = {
- meta: {
- docs: {
- description: 'Enforce a specific function type for function components',
- category: 'Stylistic Issues',
- recommended: false,
- url: docsUrl('function-component-definition'),
- },
- fixable: 'code',
- messages,
- schema: [
- {
- type: 'object',
- properties: {
- namedComponents: {
- anyOf: [
- {
- enum: [
- 'function-declaration',
- 'arrow-function',
- 'function-expression',
- ],
- },
- {
- type: 'array',
- items: {
- type: 'string',
- enum: [
- 'function-declaration',
- 'arrow-function',
- 'function-expression',
- ],
- },
- },
- ],
- },
- unnamedComponents: {
- anyOf: [
- { enum: ['arrow-function', 'function-expression'] },
- {
- type: 'array',
- items: {
- type: 'string',
- enum: ['arrow-function', 'function-expression'],
- },
- },
- ],
- },
- },
- },
- ],
- },
- create: Components.detect((context, components) => {
- const configuration = context.options[0] || {};
- let fileVarType = 'var';
- const namedConfig = [].concat(
- configuration.namedComponents || 'function-declaration'
- );
- const unnamedConfig = [].concat(
- 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;
- let varType = fileVarType;
- if (
- (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression')
- && node.parent.type === 'VariableDeclarator'
- ) {
- varType = node.parent.parent.kind;
- }
- 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),
- varType,
- })
- );
- }
- function report(node, options) {
- reportC(context, messages[options.messageId], options.messageId, {
- node,
- fix: getFixer(node, options.fixerOptions),
- });
- }
- function validate(node, functionType) {
- if (!components.get(node)) return;
- if (node.parent && node.parent.type === 'Property') return;
- if (hasName(node) && !arrayIncludes(namedConfig, functionType)) {
- report(node, {
- messageId: namedConfig[0],
- fixerOptions: {
- type: namedConfig[0],
- template: NAMED_FUNCTION_TEMPLATES[namedConfig[0]],
- range:
- node.type === 'FunctionDeclaration'
- ? node.range
- : node.parent.parent.range,
- },
- });
- }
- if (!hasName(node) && !arrayIncludes(unnamedConfig, functionType)) {
- report(node, {
- messageId: unnamedConfig[0],
- fixerOptions: {
- type: unnamedConfig[0],
- template: UNNAMED_FUNCTION_TEMPLATES[unnamedConfig[0]],
- range: node.range,
- },
- });
- }
- }
- // --------------------------------------------------------------------------
- // Public
- // --------------------------------------------------------------------------
- const validatePairs = [];
- let hasES6OrJsx = false;
- return {
- FunctionDeclaration(node) {
- validatePairs.push([node, 'function-declaration']);
- },
- ArrowFunctionExpression(node) {
- validatePairs.push([node, 'arrow-function']);
- },
- FunctionExpression(node) {
- validatePairs.push([node, 'function-expression']);
- },
- VariableDeclaration(node) {
- hasES6OrJsx = hasES6OrJsx || node.kind === 'const' || node.kind === 'let';
- },
- 'Program:exit'() {
- if (hasES6OrJsx) fileVarType = 'const';
- validatePairs.forEach((pair) => validate(pair[0], pair[1]));
- },
- 'ImportDeclaration, ExportNamedDeclaration, ExportDefaultDeclaration, ExportAllDeclaration, ExportSpecifier, ExportDefaultSpecifier, JSXElement, TSExportAssignment, TSImportEqualsDeclaration'() {
- hasES6OrJsx = true;
- },
- };
- }),
- };
|