283 lines
8.0 KiB
JavaScript
283 lines
8.0 KiB
JavaScript
|
/**
|
||
|
* @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) {
|
||
|
const nodeTypeParams = node.typeParameters;
|
||
|
|
||
|
return nodeTypeParams
|
||
|
&& nodeTypeParams.params
|
||
|
&& nodeTypeParams.params.length === 1
|
||
|
&& !nodeTypeParams.params[0].constraint;
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
},
|
||
|
};
|
||
|
}),
|
||
|
};
|