UNPKG

eslint-plugin-react-edge

Version:
362 lines (349 loc) 11.7 kB
import { AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils"; //#region package.json var name = "eslint-plugin-react-edge"; var version = "0.1.2"; //#endregion //#region src/utils/index.ts const docBaseUrl = "https://github.com/kokororin/eslint-plugin-react-edge/blob/master/src/rules/"; const createRule = ESLintUtils.RuleCreator((name$1) => `${docBaseUrl}${name$1}.md`); /** * check if a value is a valid camel case string * @param value - value to check */ function isCamelCase(value) { return /^[a-z][a-zA-Z0-9]*$/.test(value); } /** * check if a value is a valid pascal case string * @param value - value to check */ function isPascalCase(value) { return /^[A-Z][A-Za-z0-9]*$/.test(value); } /** * check if a value is a valid upper case string * @param value - value to check */ function isUpperCase(value) { return /^[A-Z0-9_]+$/.test(value); } //#endregion //#region src/rules/prefer-named-property-access.ts const RULE_NAME$1 = "prefer-named-property-access"; /** * Ensures that passed key is imported from 'react' package. * @param context - The rule context * @param fixer - The rule fixer * @param key - The key to import * @yields The fix to apply */ function* updateImportStatement(context, fixer, key) { const sourceCode = context.sourceCode; const importNode = sourceCode.ast.body.find((node) => node.type === AST_NODE_TYPES.ImportDeclaration && node.source.value === "react"); if (!importNode) { yield fixer.insertTextBefore(sourceCode.ast.body[0], `import { ${key} } from 'react';\n`); return; } if (importNode.specifiers.length === 1 && importNode.specifiers[0].type === AST_NODE_TYPES.ImportDefaultSpecifier) { yield fixer.insertTextAfter(importNode.specifiers[0], `, { ${key} }`); return; } const alreadyImportedKeys = importNode.specifiers.filter((specifier) => specifier.type === AST_NODE_TYPES.ImportSpecifier).map((specifier) => { if (specifier.imported.type === AST_NODE_TYPES.Identifier) return specifier.imported.name; return void 0; }).filter((name$1) => name$1 !== void 0); if (alreadyImportedKeys.includes(key)) return; const lastSpecifier = importNode.specifiers[importNode.specifiers.length - 1]; if (lastSpecifier == null) { yield fixer.insertTextBefore(sourceCode.ast.body[0], `import { ${key} } from 'react';\n`); return; } yield fixer.insertTextAfter(lastSpecifier, `, ${key}`); } var prefer_named_property_access_default = createRule({ name: RULE_NAME$1, meta: { type: "problem", fixable: "code", docs: { description: "Enforce importing each member of React namespace separately instead of accessing them through React namespace" }, messages: { illegalReactPropertyAccess: "Illegal React property access: {{name}}. Use named import instead.", disallowImportReactEvent: "Disallow importing React event types to avoid conflicts with global event types." }, schema: [] }, defaultOptions: [], create(context) { return { TSQualifiedName(node) { if (!("name" in node.left) || node.left.name !== "React" || !("name" in node.right) || node.right.name.endsWith("Event")) return; context.report({ node, messageId: "illegalReactPropertyAccess", data: { name: node.right.name }, *fix(fixer) { yield fixer.replaceText(node, node.right.name); yield* updateImportStatement(context, fixer, node.right.name); } }); }, MemberExpression(node) { if (node.object.type !== AST_NODE_TYPES.Identifier || node.object.name !== "React" || node.property.type !== AST_NODE_TYPES.Identifier) return; const propertyName = node.property.name; context.report({ node, messageId: "illegalReactPropertyAccess", data: { name: propertyName }, *fix(fixer) { yield fixer.replaceText(node, propertyName); yield* updateImportStatement(context, fixer, propertyName); } }); }, ImportDeclaration(node) { if (node.source.value !== "react" && node.source.value !== "preact") return; node.specifiers.forEach((specifier) => { if (specifier.type === AST_NODE_TYPES.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES.Identifier && specifier.imported.name.endsWith("Event")) context.report({ node: specifier, messageId: "disallowImportReactEvent" }); }); } }; } }); //#endregion //#region src/rules/var-naming.ts const RULE_NAME = "var-naming"; const reactGlobalFuncs = new Set([ "createContext", "forwardRef", "lazy", "memo" ]); const reactFCTypes = new Set([ "FC", "FunctionComponent", "VFC", "VoidFunctionComponent" ]); const defaultExcludeTypes = ["StoryObj", "StoryFn"]; const defaultExcludeNames = ["^(__dirname|__filename)$", "(.*)Event$"]; const defaultOptions = [{ funcFormat: ["camelCase"], varFormat: ["camelCase", "UPPER_CASE"], excludeNames: [], excludeFuncs: [], excludeTypes: [] }]; var var_naming_default = createRule({ name: RULE_NAME, meta: { type: "problem", docs: { description: "Enforce variable and function naming convention" }, schema: [{ type: "object", properties: { funcFormat: { type: "array", items: { type: "string", enum: [ "camelCase", "PascalCase", "UPPER_CASE" ] } }, varFormat: { type: "array", items: { type: "string", enum: [ "camelCase", "PascalCase", "UPPER_CASE" ] } }, excludeNames: { type: "array", items: { type: "string" } }, excludeFuncs: { type: "array", items: { type: "string" } }, excludeTypes: { type: "array", items: { type: "string" } } } }], messages: { invalidFuncNaming: "Invalid function naming for non-React component, expected {{formats}}", invalidReactFCNaming: "Invalid naming convention for React functional component, expected PascalCase", invalidVarNaming: "Invalid variable naming, expected {{formats}}" } }, defaultOptions, create(context) { const options = { ...defaultOptions[0], ...context.options[0] }; const funcFormat = options.funcFormat; const varFormat = options.varFormat; const excludeNames = [...defaultExcludeNames, ...options.excludeNames ?? []]; const excludeFuncs = options.excludeFuncs; const excludeTypes = [...defaultExcludeTypes, ...options.excludeTypes ?? []]; function validate(type, { node, name: name$1 }) { let isPass = false; let formats; let messageId; switch (type) { case "func": formats = funcFormat; messageId = "invalidFuncNaming"; break; case "var": formats = varFormat; messageId = "invalidVarNaming"; break; } for (const format of formats) switch (format) { case "camelCase": if (isCamelCase(name$1)) isPass = true; break; case "PascalCase": if (isPascalCase(name$1)) isPass = true; break; case "UPPER_CASE": if (isUpperCase(name$1)) isPass = true; break; } if (!isPass) context.report({ node, messageId, data: { formats: formats.join(", ") } }); } function checkJSXElement(node) { if (!node) return false; if (node.type === AST_NODE_TYPES.JSXElement || node.type === AST_NODE_TYPES.JSXFragment) return true; if (node.type === AST_NODE_TYPES.BlockStatement) { for (const statement of node.body) if (statement.type === AST_NODE_TYPES.ReturnStatement) { if (checkJSXElement(statement.argument)) return true; } else if (checkJSXElement(statement)) return true; } if (node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.FunctionExpression) return checkJSXElement(node.body); return false; } function getTypeReference(node) { if (node.id.typeAnnotation?.typeAnnotation && node.id.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTypeReference && node.id.typeAnnotation.typeAnnotation.typeName.type === AST_NODE_TYPES.Identifier) { const typeName = node.id.typeAnnotation.typeAnnotation.typeName.name; return typeName.split(".").pop(); } return void 0; } return { FunctionDeclaration(node) { if (node.id) { const fnName = node.id.name; const isReactComponent = checkJSXElement(node.body); if (!isReactComponent) validate("func", { node, name: fnName }); } }, VariableDeclarator(node) { if (node.id != null && node.init && (node.init.type === AST_NODE_TYPES.FunctionExpression || node.init.type === AST_NODE_TYPES.ArrowFunctionExpression)) { const fnName = "name" in node.id ? node.id.name : ""; if (!fnName) return; let isReactComponent = checkJSXElement(node.init.body); const typeName = getTypeReference(node); if (typeName != null && reactFCTypes.has(typeName)) isReactComponent = true; if (!isReactComponent) validate("func", { node, name: fnName }); } else if (node.id != null && node.init && node.init.type === AST_NODE_TYPES.LogicalExpression) { const varName = "name" in node.id ? node.id.name : ""; if (!varName) return; const parts = [node.init.left, node.init.right]; let partIsReactComponent = false; for (const part of parts) if (part.type === AST_NODE_TYPES.FunctionExpression || part.type === AST_NODE_TYPES.ArrowFunctionExpression) { const isReactComponent = checkJSXElement(part.body); if (isReactComponent) partIsReactComponent = true; } if (!partIsReactComponent) validate("var", { node, name: varName }); } else if (node.id != null && "name" in node.id) { const varName = node.id.name; for (const excludeRegex of excludeNames) if (new RegExp(excludeRegex).test(varName)) return; const typeName = getTypeReference(node); if (typeName != null) { for (const excludeRegex of excludeTypes) if (new RegExp(excludeRegex).test(typeName)) return; } if (node.init) { let calleeName; let shouldCheckReact = false; let initNode; if (node.init.type === AST_NODE_TYPES.CallExpression) initNode = node.init; else if (node.init.type === AST_NODE_TYPES.TSAsExpression && node.init.expression != null && node.init.expression.type === AST_NODE_TYPES.CallExpression) initNode = node.init.expression; if (initNode) { shouldCheckReact = true; if (initNode.callee.type === AST_NODE_TYPES.Identifier) calleeName = initNode.callee.name; else if (initNode.callee.type === AST_NODE_TYPES.MemberExpression && initNode.callee.property.type === AST_NODE_TYPES.Identifier) calleeName = initNode.callee.property.name; } if (calleeName != null) { for (const excludeRegex of excludeFuncs) if (new RegExp(excludeRegex).test(calleeName)) return; } if (shouldCheckReact) { if (calleeName == null) return; if (reactGlobalFuncs.has(calleeName) || reactGlobalFuncs.has(calleeName.split(".").pop() ?? "")) return; } } validate("var", { node, name: varName }); } } }; } }); //#endregion //#region src/rules/index.ts const rules = { [RULE_NAME$1]: prefer_named_property_access_default, [RULE_NAME]: var_naming_default }; //#endregion //#region src/index.ts const reactEdge = { meta: { name, version }, rules }; const allRules = Object.fromEntries(Object.keys(rules).map((name$1) => [`react-edge/${name$1}`, "error"])); const configs = { recommended: createConfig(allRules, "react-edge/recommended") }; function createConfig(rules$1, configName) { return { name: configName, plugins: { "react-edge": reactEdge }, rules: rules$1 }; } const allConfigs = { ...reactEdge, configs }; var src_default = allConfigs; //#endregion export { src_default as default };