You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
284 lines
8.0 KiB
284 lines
8.0 KiB
/** |
|
* @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: 'Standardize the way function component get defined', |
|
category: 'Stylistic Issues', |
|
recommended: false, |
|
url: docsUrl('function-component-definition'), |
|
}, |
|
fixable: 'code', |
|
|
|
messages, |
|
|
|
schema: [ |
|
{ |
|
type: 'object', |
|
properties: { |
|
namedComponents: { |
|
oneOf: [ |
|
{ |
|
enum: [ |
|
'function-declaration', |
|
'arrow-function', |
|
'function-expression', |
|
], |
|
}, |
|
{ |
|
type: 'array', |
|
items: { |
|
type: 'string', |
|
enum: [ |
|
'function-declaration', |
|
'arrow-function', |
|
'function-expression', |
|
], |
|
}, |
|
}, |
|
], |
|
}, |
|
unnamedComponents: { |
|
oneOf: [ |
|
{ 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; |
|
}, |
|
}; |
|
}), |
|
};
|
|
|