From 5789bd65e57cd9103c6633ba4d8739bafda50592 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Tue, 24 Oct 2023 19:21:19 +0800 Subject: [PATCH] feat: add no-namespace --- .../src/rules/no-namespace.md | 19 +++ .../src/rules/no-namespace.spec.ts | 151 ++++++++++++++++++ .../src/rules/no-namespace.ts | 58 +++++++ packages/eslint-react-jsx/src/element-type.ts | 37 +++++ packages/eslint-react-jsx/src/index.ts | 1 + 5 files changed, 266 insertions(+) create mode 100644 packages/eslint-plugin-react/src/rules/no-namespace.md create mode 100644 packages/eslint-plugin-react/src/rules/no-namespace.spec.ts create mode 100644 packages/eslint-plugin-react/src/rules/no-namespace.ts create mode 100644 packages/eslint-react-jsx/src/element-type.ts diff --git a/packages/eslint-plugin-react/src/rules/no-namespace.md b/packages/eslint-plugin-react/src/rules/no-namespace.md new file mode 100644 index 000000000..b0912fa39 --- /dev/null +++ b/packages/eslint-plugin-react/src/rules/no-namespace.md @@ -0,0 +1,19 @@ +# @eslint-react/no-namespace + +Enforces the absence of a namespace in React elements, such as with `svg:circle`, as they are not supported in React. + +## Rule Details + +### ❌ Incorrect + +```jsx +; +; +``` + +### ✅ Correct + +```jsx +; +; +``` diff --git a/packages/eslint-plugin-react/src/rules/no-namespace.spec.ts b/packages/eslint-plugin-react/src/rules/no-namespace.spec.ts new file mode 100644 index 000000000..1d45db20c --- /dev/null +++ b/packages/eslint-plugin-react/src/rules/no-namespace.spec.ts @@ -0,0 +1,151 @@ +import { allValid } from "@eslint-react/shared"; + +import RuleTester, { getFixturesRootDir } from "../../../../test/rule-tester"; +import rule, { RULE_NAME } from "./no-namespace"; + +const rootDir = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2021, + sourceType: "module", + project: "./tsconfig.json", + tsconfigRootDir: rootDir, + }, +}); + +ruleTester.run(RULE_NAME, rule, { + valid: [ + ...allValid, + "", + 'React.createElement("testcomponent")', + "", + 'React.createElement("testComponent")', + "", + 'React.createElement("test_component")', + "", + 'React.createElement("TestComponent")', + "", + 'React.createElement("object.testcomponent")', + "", + 'React.createElement("object.testComponent")', + "", + 'React.createElement("object.test_component")', + "", + 'React.createElement("object.TestComponent")', + "", + 'React.createElement("Object.testcomponent")', + "", + 'React.createElement("Object.testComponent")', + "", + 'React.createElement("Object.test_component")', + "", + 'React.createElement("Object.TestComponent")', + "React.createElement(null)", + "React.createElement(true)", + "React.createElement({})", + ], + + invalid: [ + { + code: "", + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: 'React.createElement("ns:testcomponent")', + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: "", + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: 'React.createElement("ns:testComponent")', + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: "", + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: 'React.createElement("ns:test_component")', + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: "", + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: 'React.createElement("ns:TestComponent")', + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: "", + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: 'React.createElement("Ns:testcomponent")', + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: "", + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: 'React.createElement("Ns:testComponent")', + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: "", + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: 'React.createElement("Ns:test_component")', + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: "", + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + { + code: 'React.createElement("Ns:TestComponent")', + errors: [{ + messageId: "NO_NAMESPACE", + }], + }, + ], +}); diff --git a/packages/eslint-plugin-react/src/rules/no-namespace.ts b/packages/eslint-plugin-react/src/rules/no-namespace.ts new file mode 100644 index 000000000..630e3a5b7 --- /dev/null +++ b/packages/eslint-plugin-react/src/rules/no-namespace.ts @@ -0,0 +1,58 @@ +import { isCreateElement } from "@eslint-react/create-element"; +import { elementType } from "@eslint-react/jsx"; +import { createRule } from "@eslint-react/shared"; +import { AST_NODE_TYPES } from "@typescript-eslint/types"; + +export const RULE_NAME = "no-namespace"; + +type MessageID = "NO_NAMESPACE"; + +export default createRule<[], MessageID>({ + name: RULE_NAME, + meta: { + type: "problem", + docs: { + description: "enforce that namespaces are not used in React elements", + }, + schema: [], + messages: { + NO_NAMESPACE: "React component {{name}} must not be in a namespace, as React does not support them", + }, + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if ( + isCreateElement(node, context) && node.arguments.length > 0 + && node.arguments[0]?.type === AST_NODE_TYPES.Literal + ) { + const name = node.arguments[0].value; + if (typeof name !== "string" || !name.includes(":")) { + return; + } + context.report({ + data: { + name, + }, + messageId: "NO_NAMESPACE", + node, + }); + } + }, + JSXOpeningElement(node) { + const name = elementType(node); + if (typeof name !== "string" || !name.includes(":")) { + return; + } + context.report({ + data: { + name, + }, + messageId: "NO_NAMESPACE", + node, + }); + }, + }; + }, +}); diff --git a/packages/eslint-react-jsx/src/element-type.ts b/packages/eslint-react-jsx/src/element-type.ts new file mode 100644 index 000000000..44be33432 --- /dev/null +++ b/packages/eslint-react-jsx/src/element-type.ts @@ -0,0 +1,37 @@ +import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/types"; + +function resolveMemberExpressions(object: TSESTree.JSXTagNameExpression, property: TSESTree.JSXIdentifier): string { + if (object.type === AST_NODE_TYPES.JSXMemberExpression) { + return `${resolveMemberExpressions(object.object, object.property)}.${property.name}`; + } + if (object.type === AST_NODE_TYPES.JSXNamespacedName) { + return `${object.namespace.name}:${object.name.name}.${property.name}`; + } + + return `${object.name}.${property.name}`; +} + +/** + * Returns the tag name associated with a JSXOpeningElement. + * @param node The visited JSXOpeningElement node object. + * @returns The element's tag name. + */ +export function elementType(node: TSESTree.JSXOpeningElement | TSESTree.JSXOpeningFragment): string { + if (node.type === AST_NODE_TYPES.JSXOpeningFragment) { + return "<>"; + } + + const { name } = node; + + if (name.type === AST_NODE_TYPES.JSXMemberExpression) { + const { object, property } = name; + + return resolveMemberExpressions(object, property); + } + + if (name.type === AST_NODE_TYPES.JSXNamespacedName) { + return `${name.namespace.name}:${name.name.name}`; + } + + return name.name; +} diff --git a/packages/eslint-react-jsx/src/index.ts b/packages/eslint-react-jsx/src/index.ts index 10ded7dfe..4e4296423 100644 --- a/packages/eslint-react-jsx/src/index.ts +++ b/packages/eslint-react-jsx/src/index.ts @@ -1,4 +1,5 @@ export * from "./children"; +export * from "./element-type"; export * from "./event-handler"; export * from "./misc"; export * from "./prop";