blob: e582dc48f64cf67de8295efa7e034274e0117a28 [file] [log] [blame]
/**
* @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'); }
};
})
};