Skip to content

fix: rework 'naming-convention/component-name' rule #959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { RuleFeature } from "@eslint-react/shared";
import { getSettingsFromContext } from "@eslint-react/shared";
import type { CamelCase } from "string-ts";

import { createRule, findCustomComponent, findCustomComponentProp, getElementNameOnJsxAndDom } from "../utils";
import { createRule, findCustomComponent, findCustomComponentProp, getElementTypeOnJsxAndDom } from "../utils";

export const RULE_NAME = "no-missing-button-type";

Expand Down Expand Up @@ -33,7 +33,7 @@ export default createRule<[], MessageID>({

return {
JSXElement(node) {
const [elementNameOnJsx, elementNameOnDom] = getElementNameOnJsxAndDom(
const [elementNameOnJsx, elementNameOnDom] = getElementTypeOnJsxAndDom(
context,
node,
polymorphicPropName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { RuleFeature } from "@eslint-react/shared";
import { getSettingsFromContext } from "@eslint-react/shared";
import type { CamelCase } from "string-ts";

import { createRule, findCustomComponent, findCustomComponentProp, getElementNameOnJsxAndDom } from "../utils";
import { createRule, findCustomComponent, findCustomComponentProp, getElementTypeOnJsxAndDom } from "../utils";

export const RULE_NAME = "no-missing-iframe-sandbox";

Expand Down Expand Up @@ -58,7 +58,7 @@ export default createRule<[], MessageID>({
const additionalComponents = settings.additionalComponents.filter((c) => c.as === "iframe");
return {
JSXElement(node) {
const [elementNameOnJsx, elementNameOnDom] = getElementNameOnJsxAndDom(
const [elementNameOnJsx, elementNameOnDom] = getElementTypeOnJsxAndDom(
context,
node,
polymorphicPropName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default createRule<[], MessageID>({
create(context) {
return {
JSXElement(node) {
const name = JSX.getElementName(node);
const name = JSX.getElementType(node);
if (typeof name !== "string" || !name.includes(":")) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { RuleFeature } from "@eslint-react/shared";
import { getSettingsFromContext } from "@eslint-react/shared";
import type { CamelCase } from "string-ts";

import { createRule, findCustomComponent, findCustomComponentProp, getElementNameOnJsxAndDom } from "../utils";
import { createRule, findCustomComponent, findCustomComponentProp, getElementTypeOnJsxAndDom } from "../utils";

export const RULE_NAME = "no-unsafe-iframe-sandbox";

Expand Down Expand Up @@ -43,7 +43,7 @@ export default createRule<[], MessageID>({
const additionalComponents = settings.additionalComponents.filter((c) => c.as === "iframe");
return {
JSXElement(node) {
const [elementNameOnJsx, elementNameOnDom] = getElementNameOnJsxAndDom(
const [elementNameOnJsx, elementNameOnDom] = getElementTypeOnJsxAndDom(
context,
node,
polymorphicPropName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getSettingsFromContext } from "@eslint-react/shared";
import type { TSESTree } from "@typescript-eslint/types";
import type { CamelCase } from "string-ts";

import { createRule, findCustomComponent, findCustomComponentProp, getElementNameOnJsxAndDom } from "../utils";
import { createRule, findCustomComponent, findCustomComponentProp, getElementTypeOnJsxAndDom } from "../utils";

export const RULE_NAME = "no-unsafe-target-blank";

Expand Down Expand Up @@ -48,7 +48,7 @@ export default createRule<[], MessageID>({

return {
JSXElement(node: TSESTree.JSXElement) {
const [elementNameOnJsx, elementNameOnDom] = getElementNameOnJsxAndDom(
const [elementNameOnJsx, elementNameOnDom] = getElementTypeOnJsxAndDom(
context,
node,
polymorphicPropName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default createRule<[], MessageID>({
create(context) {
return {
JSXElement(node) {
const elementName = JSX.getElementName(node);
const elementName = JSX.getElementType(node);
if (elementName.length === 0 || !voidElements.has(elementName)) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import * as JSX from "@eslint-react/jsx";
import type { CustomComponentNormalized, RuleContext } from "@eslint-react/shared";
import type { TSESTree } from "@typescript-eslint/types";

export function getElementNameOnJsxAndDom(
export function getElementTypeOnJsxAndDom(
context: RuleContext,
node: TSESTree.JSXElement,
polymorphicPropName?: string,
additionalComponents: CustomComponentNormalized[] = [],
): [string, string] {
const name = JSX.getElementName(node);
const name = JSX.getElementType(node);
// Skip JsxIntrinsicElements
if (name === name.toLowerCase()) return [name, name];
// Get the component name using the `settings["react-x"].additionalComponents` setting
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "./create-rule";
export * from "./find-custom-component";
export * from "./get-element-name-on-jsx-and-dom";
export * from "./get-element-type-on-jsx-and-dom";
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export const plugin = {
version,
},
rules: {
"component-name": componentName,
"context-name": contextName,
filename,
"filename-extension": filenameExtension,
"use-state": useState,
["component-name"]: componentName,
["context-name"]: contextName,
["filename"]: filename,
["filename-extension"]: filenameExtension,
["use-state"]: useState,
},
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Enforces naming conventions for components.

## Examples

This rule enforces naming conventions for components. Can be used to enforce PascalCase and CONSTANT_CASE. By default, it enforces PascalCase.
This rule enforces naming conventions for components. Can be used to enforce `PascalCase` and `CONSTANT_CASE`. By default, it enforces `PascalCase`.

### Failing

Expand All @@ -50,13 +50,13 @@ function MyComponent() {

## Rule Options

- `rule`: The rule to apply to the file name. Default is `"PascalCase"`. Possible values:
1. `PascalCase`: PascalCase
2. `CONSTANT_CASE`: CONSTANT_CASE
- `rule`: The rule to apply to the component name. Possible values:
- `PascalCase` (default)
- `CONSTANT_CASE`
- `excepts`: (optional) An array of component names that are allowed to not follow the rule.
- `allowAllCaps`: (optional) If `true`, allows all caps file names. Default is `false`.
- `allowNamespace`: (optional) If `true`, allows namespace in JSX elements. Default is `false`.
- `allowLeadingUnderscore`: (optional) If `true`, allows leading underscore in file names. Default is `false`.
- `allowAllCaps`: (optional) If `true`, allows all caps component names. Default is `false`.

## Rule Options Examples

```json
{
Expand All @@ -66,10 +66,7 @@ function MyComponent() {

```json
{
"@eslint-react/naming-convention/component-name": [
"warn",
{ "rule": "PascalCase", "excepts": ["MyComponent"] }
]
"@eslint-react/naming-convention/component-name": ["warn", { "rule": "PascalCase", "allowAllCaps": true }]
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,40 @@ import rule, { RULE_NAME } from "./component-name";
ruleTester.run(RULE_NAME, rule, {
invalid: [
{
code: /* tsx */ `<Test_component />`,
code: /* tsx */ `
function Test_component() {
return <div>foo</div>
}
`,
errors: [{ messageId: "invalid", data: { name: "Test_component", rule: "PascalCase" } }],
},
{
code: /* tsx */ `<TestComponent />`,
code: /* tsx */ `
function TestComponent() {
return <div>foo</div>
}
`,
errors: [{ messageId: "invalid", data: { name: "TestComponent", rule: "CONSTANT_CASE" } }],
options: [{ rule: "CONSTANT_CASE" }],
},
{
code: /* tsx */ `<TestComponent />`,
errors: [{ messageId: "invalid", data: { name: "TestComponent", rule: "CONSTANT_CASE" } }],
code: /* tsx */ `
function TestComponent() {
return <div>foo</div>
}
`,
errors: [{
messageId: "invalid",
data: { name: "TestComponent", rule: "CONSTANT_CASE" },
}],
options: ["CONSTANT_CASE"],
},
{
code: /* tsx */ `<FULLUPPERCASE />`,
code: /* tsx */ `
function FULLUPPERCASE() {
return <div>foo</div>
}
`,
errors: [{ messageId: "invalid", data: { name: "FULLUPPERCASE", rule: "PascalCase" } }],
options: [{ allowAllCaps: false, rule: "PascalCase" }],
},
Expand All @@ -39,50 +58,51 @@ ruleTester.run(RULE_NAME, rule, {
)
}
`,
errors: [{ messageId: "invalid", data: { name: "_Test", rule: "CONSTANT_CASE" } }],
options: [{ allowLeadingUnderscore: false, rule: "CONSTANT_CASE" }],
},
],
valid: [
...allFunctions,
"<testcomponent />",
"<testComponent />",
"<test_component />",
"<TestComponent />",
"<CSSTransitionGroup />",
"<BetterThanCSS />",
"<TestComponent><div /></TestComponent>",
"<Test1Component />",
"<TestComponent1 />",
"<T3StComp0Nent />",
"<Modal.Header />",
"<qualification.T3StComp0Nent />",
"<H1>Hello!</H1>",
"<motion.div />",
{
code: "<T />",
options: [{ rule: "PascalCase" }],
},
{
code: "<UI />",
options: [{ rule: "PascalCase" }],
},
{
code: "<CSS />",
options: [{ rule: "PascalCase" }],
errors: [{
messageId: "invalid",
data: {
name: "_Test",
rule: "PascalCase",
},
}],
},
{
code: "<Typography.P />",
code: /* tsx */ `
export function _Test() {
return (
<div />
)
}
`,
errors: [{
messageId: "invalid",
data: {
name: "_Test",
rule: "PascalCase",
},
}],
options: [{ rule: "PascalCase" }],
},
{
code: "<FULLUPPERCASE />",
options: [{ allowAllCaps: true, rule: "PascalCase" }],
},
{
code: "<Modal:Header />",
options: [{ allowNamespace: true, rule: "PascalCase" }],
code: /* tsx */ `
export function _TEST() {
return (
<div />
)
}
`,
errors: [{
messageId: "invalid",
data: {
name: "_TEST",
rule: "CONSTANT_CASE",
},
}],
options: [{ rule: "CONSTANT_CASE" }],
},
],
valid: [
...allFunctions,
/* tsx */ `
function AppHome() {
return <div>foo</div>
Expand All @@ -94,7 +114,7 @@ ruleTester.run(RULE_NAME, rule, {
return <div>foo</div>
}
`,
options: [{ allowLeadingUnderscore: true, rule: "CONSTANT_CASE" }],
options: [{ rule: "CONSTANT_CASE" }],
},
/* tsx */ `
const AppHome = () => {
Expand All @@ -107,7 +127,7 @@ ruleTester.run(RULE_NAME, rule, {
return <div>foo</div>
}
`,
options: [{ allowAllCaps: true, allowLeadingUnderscore: true, rule: "CONSTANT_CASE" }],
options: [{ allowAllCaps: true, rule: "CONSTANT_CASE" }],
},
{
code: /* tsx */ `
Expand Down
Loading
Loading