eslint-plugin-vtex
Version:
VTEX's ESLint plugin
276 lines (265 loc) • 9.38 kB
JavaScript
"use strict";
// src/configs/recommended.ts
var recommended = {
rules: {
// Prefer an early return to prevent nesting and improve code readability.
"vtex/prefer-early-return": [
"warn",
{
maxStatements: 2
}
],
"vtex/consistent-props-type": "off"
}
};
// src/utils/react/isComponentName.ts
var isComponentName = (name) => name.length > 0 && name[0] === name[0].toUpperCase();
// src/createRule.ts
var import_utils = require("@typescript-eslint/utils");
var createRule = import_utils.ESLintUtils.RuleCreator(
(name) => `https://github.com/vtex/typescript/tree/main/packages/eslint-plugin-vtex/docs/rules/${name}.md`
);
// src/utils/estree/isFunctionNode.ts
var isFunctionNode = (node) => {
return node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression" || node.type === "FunctionDeclaration";
};
// src/utils/estree/isInsideAnotherFunction.ts
var isInsideAnotherFunction = (context) => {
const functions = context.getAncestors().filter(isFunctionNode);
return functions.length > 1;
};
// src/utils/estree/getFunctionNodeName.ts
var getFunctionNodeName = (node) => {
var _a, _b;
if (node.type === "FunctionDeclaration") {
return ((_a = node.id) == null ? void 0 : _a.name) ?? "";
}
if (((_b = node.parent) == null ? void 0 : _b.type) === "VariableDeclarator" && node.parent.id.type === "Identifier") {
return node.parent.id.name;
}
return "";
};
// src/rules/consistent-props-type.ts
var consistentPropsType = createRule({
name: "consistent-props-type",
meta: {
type: "suggestion",
docs: {
recommended: false,
description: "Consistent way to define props type"
},
messages: {
invalidPropsTypeName: "Invalid props type name. Must be: {{expectedPropsTypeName}}",
propsTypeIncomplete: "Invalid inline props type. {{expectedPropsTypeName}} must define all the props.",
invalidInlinePropsType: "Invalid inline props type. You must to create an type {{expectedPropsTypeName}}",
genericsPropsTypeName: "Invalid generics props type name. Must be: Props"
},
schema: []
},
defaultOptions: [],
create(context) {
return {
":function > :matches(Identifier, ObjectPattern) > TSTypeAnnotation": function(node) {
var _a, _b, _c;
if (isInsideAnotherFunction(context))
return;
const functionNode = context.getAncestors().reverse().find(isFunctionNode);
const functionName = getFunctionNodeName(functionNode);
const [typeParameter] = ((_a = functionNode.typeParameters) == null ? void 0 : _a.params) ?? [];
if (!isComponentName(functionName))
return;
const expectedPropsTypeName = `${functionName}Props`;
if (typeParameter) {
if (typeParameter.name.name !== "Props") {
context.report({
node: typeParameter.name,
messageId: "genericsPropsTypeName"
});
}
if (typeParameter.constraint && ((_b = typeParameter.constraint) == null ? void 0 : _b.type) !== "TSTypeReference") {
context.report({
node: typeParameter.constraint,
messageId: "propsTypeIncomplete",
data: { expectedPropsTypeName }
});
}
if (((_c = typeParameter.constraint) == null ? void 0 : _c.type) === "TSTypeReference" && typeParameter.constraint.typeName.type === "Identifier" && typeParameter.constraint.typeName.name !== expectedPropsTypeName) {
context.report({
node: typeParameter.constraint,
messageId: "invalidPropsTypeName",
data: { expectedPropsTypeName }
});
}
return;
}
if (node.typeAnnotation.type !== "TSTypeReference") {
context.report({
node: node.typeAnnotation,
messageId: "invalidInlinePropsType",
data: { expectedPropsTypeName }
});
return;
}
if (node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name !== expectedPropsTypeName) {
context.report({
node: node.typeAnnotation,
messageId: "invalidPropsTypeName",
data: { expectedPropsTypeName }
});
}
},
"VariableDeclarator > Identifier > TSTypeAnnotation": function(node) {
if (isInsideAnotherFunction(context))
return;
const expectedPropsTypeName = `${node.parent.name}Props`;
if (node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "FC" && node.typeAnnotation.typeParameters) {
const [typeParameter] = node.typeAnnotation.typeParameters.params;
if (typeParameter.type === "TSTypeReference" && typeParameter.typeName.type === "Identifier" && typeParameter.typeName.name !== expectedPropsTypeName) {
context.report({
node: typeParameter,
messageId: "invalidPropsTypeName",
data: { expectedPropsTypeName }
});
}
if (typeParameter.type !== "TSTypeReference") {
context.report({
node: typeParameter,
messageId: "propsTypeIncomplete",
data: { expectedPropsTypeName }
});
}
}
}
};
}
});
// src/rules/prefer-early-return.ts
function isLonelyIfStatement(node) {
return node.type === "IfStatement" && node.alternate == null;
}
var preferEarlyReturn = createRule({
name: "prefer-early-return",
meta: {
type: "layout",
docs: {
recommended: "error",
description: "Prefer early returns over full-body conditional wrapping in function declarations."
},
schema: [
{
type: "object",
properties: {
maxStatements: {
type: "integer"
}
},
additionalProperties: false
}
],
messages: {
default: "Prefer an early return to prevent nesting and improve code readability"
}
},
defaultOptions: [{ maxStatements: 1 }],
create(context) {
const options = {
...context.options[0]
};
const { maxStatements } = options;
function isOffendingConsequent(consequentNode) {
return consequentNode.type === "ExpressionStatement" && maxStatements === 0 || consequentNode.type === "BlockStatement" && consequentNode.body.length > maxStatements;
}
function isOffendingIfStatement(ifNode) {
return isLonelyIfStatement(ifNode) && isOffendingConsequent(ifNode.consequent);
}
function checkFunctionBody(fnNode) {
const bodyNode = fnNode.body;
if (bodyNode.type !== "BlockStatement" || bodyNode.body.length === 0) {
return;
}
const lastNode = bodyNode.body[bodyNode.body.length - 1];
if (!isOffendingIfStatement(lastNode)) {
return;
}
context.report({
node: lastNode,
messageId: "default"
});
}
return {
FunctionDeclaration: checkFunctionBody,
FunctionExpression: checkFunctionBody,
ArrowFunctionExpression: checkFunctionBody
};
}
});
// src/rules/prefer-use-effect-named-callback.ts
var USE_EFFECT_NAME = "useEffect";
function checkCallExpressionIsUseEffect(node) {
if (node.type !== "CallExpression") {
return false;
}
if (!node.callee) {
return false;
}
if (node.callee.type === "Identifier" && node.callee.name === USE_EFFECT_NAME) {
return true;
}
if (node.callee.type === "MemberExpression" && node.callee.property && node.callee.property.type === "Identifier" && node.callee.property.name === USE_EFFECT_NAME) {
return true;
}
return false;
}
var preferUseEffectNamedCallback = createRule({
name: "prefer-use-effect-named-callback",
meta: {
type: "layout",
docs: {
recommended: "error",
description: "Prefer useEffect with named function or constant callbacks."
},
messages: {
default: "Prefer useEffect with named function or constant callbacks."
},
schema: []
},
defaultOptions: [],
create(context) {
function checkUseEffectCallExpression(callExpressionNode) {
if (!checkCallExpressionIsUseEffect(callExpressionNode)) {
return;
}
const [useEffectFirstArgument] = callExpressionNode.arguments;
let hasError = false;
if (useEffectFirstArgument.type === "ArrowFunctionExpression" && useEffectFirstArgument.id === null) {
hasError = true;
}
if (useEffectFirstArgument.type === "FunctionExpression" && useEffectFirstArgument.id === null) {
hasError = true;
}
if (hasError) {
context.report({
node: useEffectFirstArgument,
messageId: "default"
});
}
}
return {
CallExpression: checkUseEffectCallExpression
};
}
});
// src/index.ts
module.exports = {
configs: {
recommended
},
// TODO: these rules could be auto-generated using fs+path,
// but tsdx doesn't works well with dynamic imports,
// so we need to change our build system first
rules: {
"consistent-props-type": consistentPropsType,
"prefer-early-return": preferEarlyReturn,
"prefer-use-effect-named-callback": preferUseEffectNamedCallback
}
};