diff --git a/.pkgs/configs/tsl.config.base.js b/.pkgs/configs/tsl.config.base.js index 7eb2b1a83e..c1eddad932 100644 --- a/.pkgs/configs/tsl.config.base.js +++ b/.pkgs/configs/tsl.config.base.js @@ -1,5 +1,5 @@ import { globSync } from "tinyglobby"; -import { defineConfig } from "tsl"; +import { core, defineConfig } from "tsl"; import { noDuplicateExports, noDuplicateImports, noMultilineTemplateExpressionWithoutAutoDedent, noUnsafeAs, nullish } from "tsl-dx"; export function buildConfig(cwd) { return defineConfig({ @@ -10,23 +10,20 @@ export function buildConfig(cwd) { "/build/", ], rules: [ - // ...core.all(), - // core.strictBooleanExpressions({ - // allowAny: false, - // allowNullableBoolean: false, - // allowNullableEnum: false, - // allowNullableNumber: false, - // allowNullableObject: false, - // allowNullableString: false, - // allowNumber: true, - // allowString: false, - // }), - // core.noConfusingVoidExpression("off"), - // core.preferOptionalChain("off"), - // core.switchExhaustivenessCheck("off"), // This rule has a issue with `switch (true)` statements - // core.switchExhaustivenessCheck({ - // considerDefaultExhaustiveForUnions: true, - // }), + ...core.all(), + core.strictBooleanExpressions({ + allowAny: false, + allowNullableBoolean: false, + allowNullableEnum: false, + allowNullableNumber: false, + allowNullableObject: false, + allowNullableString: false, + allowNumber: true, + allowString: false, + }), + core.noConfusingVoidExpression("off"), + core.preferOptionalChain("off"), + core.switchExhaustivenessCheck("off"), // This rule has a issue with `switch (true)` statements nullish({ runtimeLibrary: "@eslint-react/eff", }), diff --git a/.pkgs/configs/tsl.config.base.ts b/.pkgs/configs/tsl.config.base.ts index 2290ab926e..c593159c97 100644 --- a/.pkgs/configs/tsl.config.base.ts +++ b/.pkgs/configs/tsl.config.base.ts @@ -1,5 +1,5 @@ import { globSync } from "tinyglobby"; -import { defineConfig } from "tsl"; +import { core, defineConfig } from "tsl"; import { noDuplicateExports, noDuplicateImports, noMultilineTemplateExpressionWithoutAutoDedent, noUnsafeAs, nullish } from "tsl-dx"; export function buildConfig(cwd: string) { @@ -11,23 +11,20 @@ export function buildConfig(cwd: string) { "/build/", ], rules: [ - // ...core.all(), - // core.strictBooleanExpressions({ - // allowAny: false, - // allowNullableBoolean: false, - // allowNullableEnum: false, - // allowNullableNumber: false, - // allowNullableObject: false, - // allowNullableString: false, - // allowNumber: true, - // allowString: false, - // }), - // core.noConfusingVoidExpression("off"), - // core.preferOptionalChain("off"), - // core.switchExhaustivenessCheck("off"), // This rule has a issue with `switch (true)` statements - // core.switchExhaustivenessCheck({ - // considerDefaultExhaustiveForUnions: true, - // }), + ...core.all(), + core.strictBooleanExpressions({ + allowAny: false, + allowNullableBoolean: false, + allowNullableEnum: false, + allowNullableNumber: false, + allowNullableObject: false, + allowNullableString: false, + allowNumber: true, + allowString: false, + }), + core.noConfusingVoidExpression("off"), + core.preferOptionalChain("off"), + core.switchExhaustivenessCheck("off"), // This rule has a issue with `switch (true)` statements nullish({ runtimeLibrary: "@eslint-react/eff", }), diff --git a/.pkgs/samples/src/booleanPropNaming.ts b/.pkgs/samples/src/booleanPropNaming.ts index b5110f37d8..10fd4fe217 100644 --- a/.pkgs/samples/src/booleanPropNaming.ts +++ b/.pkgs/samples/src/booleanPropNaming.ts @@ -31,7 +31,6 @@ export function booleanPropNaming(options?: BooleanPropNamingOptions): RuleFunct "Program:exit"(prog) { const comps = query.all(prog); - // ─── Iterate Components ──────────────────────── for (const comp of comps) { const [propsParam] = comp.node.params; if (propsParam == null) continue; @@ -40,7 +39,6 @@ export function booleanPropNaming(options?: BooleanPropNamingOptions): RuleFunct const propsType = chk.getTypeAtLocation(tsNode); const declaredProps = propsType.getProperties(); - // ─── Iterate Props ───────────────────────────── for (const prop of declaredProps) { const propType = chk.getTypeOfSymbolAtLocation(prop, tsNode); @@ -57,7 +55,6 @@ export function booleanPropNaming(options?: BooleanPropNamingOptions): RuleFunct if (decl == null) continue; const declNode = srv.tsNodeToESTreeNodeMap.get(decl); - if (declNode == null) continue; const node = "key" in declNode ? declNode.key : declNode; diff --git a/.pkgs/samples/src/forbidComponentProps.ts b/.pkgs/samples/src/forbidComponentProps.ts index cb8e6d2792..7bd577a2b9 100644 --- a/.pkgs/samples/src/forbidComponentProps.ts +++ b/.pkgs/samples/src/forbidComponentProps.ts @@ -16,8 +16,7 @@ export function forbidComponentProps(options: ForbidComponentPropsOptions): Rule if (propName == null || !forbidden.includes(propName)) return; // Verify context is JSX opening element - const parent = node.parent; - if (parent?.type !== "JSXOpeningElement") return; + const { parent } = node; // Extract element name const elemName = parent.name.type === "JSXIdentifier" ? parent.name.name : null; diff --git a/.pkgs/samples/src/forbidDomProps.ts b/.pkgs/samples/src/forbidDomProps.ts index 741576362b..5892842e0a 100644 --- a/.pkgs/samples/src/forbidDomProps.ts +++ b/.pkgs/samples/src/forbidDomProps.ts @@ -16,8 +16,7 @@ export function forbidDomProps(options: ForbidDomPropsOptions): RuleFunction { if (propName == null || !forbidden.includes(propName)) return; // Verify context is JSX opening element - const parent = node.parent; - if (parent?.type !== "JSXOpeningElement") return; + const { parent } = node; // Extract element name const elemName = parent.name.type === "JSXIdentifier" ? parent.name.name : null; diff --git a/.pkgs/samples/src/functionComponentDefinition.ts b/.pkgs/samples/src/functionComponentDefinition.ts index 8242776f95..01aefebc15 100644 --- a/.pkgs/samples/src/functionComponentDefinition.ts +++ b/.pkgs/samples/src/functionComponentDefinition.ts @@ -11,7 +11,6 @@ export function functionComponentDefinition(): RuleFunction { visitor, { "Program:exit"(program) { - // ─── Iterate all components ──────────────────── for (const { node } of query.all(program)) { // Guard: must not already be arrow function if (node.type === "ArrowFunctionExpression") continue; @@ -27,24 +26,21 @@ export function functionComponentDefinition(): RuleFunction { if (node.generator) return null; const prefix = node.async ? "async " : ""; - const typeParams = node.typeParameters ? src.getText(node.typeParameters) : ""; + const typeParams = node.typeParameters != null ? src.getText(node.typeParameters) : ""; const params = `(${node.params.map((p) => src.getText(p)).join(", ")})`; - const returnType = node.returnType ? src.getText(node.returnType) : ""; + const returnType = node.returnType != null ? src.getText(node.returnType) : ""; const body = src.getText(node.body); - // ─── Case: function declaration ────────────── - if (node.type === "FunctionDeclaration" && node.id) { + if (node.type === "FunctionDeclaration" && node.id != null) { // dprint-ignore return fixer.replaceText(node, `const ${node.id.name} = ${prefix}${typeParams}${params}${returnType} => ${body};`); } - // ─── Case: function expression in variable ─── if (node.type === "FunctionExpression" && node.parent.type === "VariableDeclarator") { // dprint-ignore return fixer.replaceText(node, `${prefix}${typeParams}${params}${returnType} => ${body}`); } - // ─── Case: object method shorthand ─────────── if (node.type === "FunctionExpression" && node.parent.type === "Property") { // dprint-ignore return fixer.replaceText(node.parent, `${src.getText(node.parent.key)}: ${prefix}${typeParams}${params}${returnType} => ${body}`); diff --git a/.pkgs/samples/src/jsxFragments.ts b/.pkgs/samples/src/jsxFragments.ts index d65a6db160..a03b7e8f9f 100644 --- a/.pkgs/samples/src/jsxFragments.ts +++ b/.pkgs/samples/src/jsxFragments.ts @@ -11,8 +11,6 @@ export type JsxsFragmentsOptions = { export function jsxFragments(options: JsxsFragmentsOptions = {}): RuleFunction { const { mode = "syntax" } = options; return (context) => { - // ── Helpers ───────────────────────────────────── - function reportSyntaxPreferred(node: TSESTree.JSXOpeningElement, pattern: "React.Fragment" | "Fragment") { // Guard: has key prop (legitimate use of standard form) const hasAttributes = node.attributes.length > 0; @@ -22,20 +20,17 @@ export function jsxFragments(options: JsxsFragmentsOptions = {}): RuleFunction { node, message: `Use shorthand fragment syntax '<>...' instead of '<${pattern}>..."), fixer.replaceText(closing, "")]; }, }); } - // ── Listeners ──────────────────────────────────── - return { JSXOpeningElement(node) { const name = node.name; - // ─── Handle (JSXIdentifier) ─────── if (name.type === "JSXIdentifier" && name.name === "Fragment") { if (mode === "syntax") { reportSyntaxPreferred(node, "Fragment"); @@ -43,10 +38,9 @@ export function jsxFragments(options: JsxsFragmentsOptions = {}): RuleFunction { return; } - // ─── Handle (JSXMemberExpression) ─ if (name.type !== "JSXMemberExpression") return; if (name.object.type !== "JSXIdentifier" || name.object.name !== "React") return; - if (name.property.type !== "JSXIdentifier" || name.property.name !== "Fragment") return; + if (name.property.name !== "Fragment") return; if (mode === "syntax") { reportSyntaxPreferred(node, "React.Fragment"); diff --git a/.pkgs/samples/src/jsxHandlerNames.ts b/.pkgs/samples/src/jsxHandlerNames.ts index 2359440772..9f888e61b6 100644 --- a/.pkgs/samples/src/jsxHandlerNames.ts +++ b/.pkgs/samples/src/jsxHandlerNames.ts @@ -28,9 +28,8 @@ export function jsxHandlerNames(options: JsxHandlerNamesOptions = {}): RuleFunct if (!EVENT_HANDLER_REGEX.test(propName)) return; const value = node.value; - if (!value) return; + if (value == null) return; - // ─── Check expression value ──────────────────── if (value.type === "JSXExpressionContainer") { const expression = ast.unwrap(value.expression); diff --git a/.pkgs/samples/src/jsxMaxDepth.ts b/.pkgs/samples/src/jsxMaxDepth.ts index 619963dc79..7b4859ad37 100644 --- a/.pkgs/samples/src/jsxMaxDepth.ts +++ b/.pkgs/samples/src/jsxMaxDepth.ts @@ -12,11 +12,9 @@ export function jsxMaxDepth(options: JsxMaxDepthOptions): RuleFunction { return (context) => ({ JSXElement(node) { let depth = 0; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let parent: any = node.parent; + let parent: typeof node.parent | null | undefined = node.parent; - // ─── Walk up the tree ────────────────────────── - while (parent) { + while (parent != null) { if (parent.type === "JSXElement") { depth++; } diff --git a/.pkgs/samples/src/jsxNoBind.ts b/.pkgs/samples/src/jsxNoBind.ts index db8cedc989..b58402d739 100644 --- a/.pkgs/samples/src/jsxNoBind.ts +++ b/.pkgs/samples/src/jsxNoBind.ts @@ -11,7 +11,6 @@ export function jsxNoBind(): RuleFunction { const expr = ast.unwrap(value.expression); - // ─── Detect forbidden patterns ───────────────── switch (true) { case expr.type === "ArrowFunctionExpression": case expr.type === "FunctionExpression": diff --git a/.pkgs/samples/src/jsxNoDuplicateProps.ts b/.pkgs/samples/src/jsxNoDuplicateProps.ts index 87c7fabd4d..8039fc214e 100644 --- a/.pkgs/samples/src/jsxNoDuplicateProps.ts +++ b/.pkgs/samples/src/jsxNoDuplicateProps.ts @@ -13,7 +13,6 @@ export function jsxNoDuplicateProps(options: JsxNoDuplicatePropsOptions = {}): R JSXOpeningElement(node) { const seen = new Map(); - // ─── Check each attribute ────────────────────── for (const attr of node.attributes) { if (attr.type !== "JSXAttribute") continue; if (attr.name.type !== "JSXIdentifier") continue; diff --git a/.pkgs/samples/src/jsxNoLiterals.ts b/.pkgs/samples/src/jsxNoLiterals.ts index 9059e5b33d..a0384355f9 100644 --- a/.pkgs/samples/src/jsxNoLiterals.ts +++ b/.pkgs/samples/src/jsxNoLiterals.ts @@ -15,16 +15,13 @@ export function jsxNoLiterals(options: JsxNoLiteralsOptions = {}): RuleFunction const { noStrings = false, allowedStrings = [], ignoreProps = true } = options; const allowedSet = new Set(allowedStrings); return (context) => ({ - // ─── Check literal text children ─────────────── Literal(node) { if (typeof node.value !== "string") return; const text = node.value.trim(); if (text === "" || allowedSet.has(text)) return; const parent = node.parent; - if (!parent) return; - // ─── Case: prop value ──────────────────────── if (parent.type === "JSXAttribute") { if (!ignoreProps) { context.report({ @@ -35,10 +32,8 @@ export function jsxNoLiterals(options: JsxNoLiteralsOptions = {}): RuleFunction return; } - // ─── Case: already wrapped ─────────────────── if (parent.type === "JSXExpressionContainer") return; - // ─── Case: child of element/fragment ───────── if (parent.type === "JSXElement" || parent.type === "JSXFragment") { if (noStrings) { context.report({ @@ -54,7 +49,6 @@ export function jsxNoLiterals(options: JsxNoLiteralsOptions = {}): RuleFunction } }, - // ─── Check JSX text nodes ────────────────────── JSXText(node) { const text = node.value.trim(); if (text === "" || allowedSet.has(text)) return; diff --git a/.pkgs/samples/src/jsxPascalCase.ts b/.pkgs/samples/src/jsxPascalCase.ts index 05f5991ad0..5d62950257 100644 --- a/.pkgs/samples/src/jsxPascalCase.ts +++ b/.pkgs/samples/src/jsxPascalCase.ts @@ -21,7 +21,6 @@ export function jsxPascalCase(options: JsxPascalCaseOptions = {}): RuleFunction const componentName = name.name; - // ─── Handle leading underscore ─────────────── if (componentName.startsWith("_")) { if (!allowLeadingUnderscore) { context.report({ @@ -37,7 +36,6 @@ export function jsxPascalCase(options: JsxPascalCaseOptions = {}): RuleFunction if (firstChar == null) return; if (firstChar === firstChar.toLowerCase()) return; - // ─── Handle all-caps ───────────────────────── if (componentName === componentName.toUpperCase()) { if (!allowAllCaps) { context.report({ @@ -48,7 +46,6 @@ export function jsxPascalCase(options: JsxPascalCaseOptions = {}): RuleFunction return; } - // ─── Validate PascalCase ───────────────────── if (!pascalCaseRegex.test(componentName)) { context.report({ node: name, diff --git a/.pkgs/samples/src/jsxPropsNoSpreadMulti.ts b/.pkgs/samples/src/jsxPropsNoSpreadMulti.ts index 954b4049d8..a23d196518 100644 --- a/.pkgs/samples/src/jsxPropsNoSpreadMulti.ts +++ b/.pkgs/samples/src/jsxPropsNoSpreadMulti.ts @@ -6,7 +6,6 @@ export function jsxPropsNoSpreadMulti(): RuleFunction { JSXOpeningElement(node) { const seen = new Set(); - // ─── Check each spread attribute ─────────────── for (const attr of node.attributes) { if (attr.type !== "JSXSpreadAttribute") continue; diff --git a/.pkgs/samples/src/noAdjacentInlineElements.ts b/.pkgs/samples/src/noAdjacentInlineElements.ts index e68b5bf4e7..ca91374230 100644 --- a/.pkgs/samples/src/noAdjacentInlineElements.ts +++ b/.pkgs/samples/src/noAdjacentInlineElements.ts @@ -41,7 +41,6 @@ export function noAdjacentInlineElements(): RuleFunction { JSXElement(node) { const children = node.children; - // ─── Check adjacent pairs ────────────────────── for (let i = 0; i < children.length - 1; i++) { const current = children[i]; const next = children[i + 1]; diff --git a/.pkgs/samples/src/noMultiComp.ts b/.pkgs/samples/src/noMultiComp.ts index 1e45b09530..65e40e3ed2 100644 --- a/.pkgs/samples/src/noMultiComp.ts +++ b/.pkgs/samples/src/noMultiComp.ts @@ -9,7 +9,6 @@ export function noMultiComp(): RuleFunction { "Program:exit"(program) { const components = query.all(program); - // ─── Report excess components ────────────────── for (const { node, name } of components.slice(1)) { context.report({ node, diff --git a/.pkgs/samples/src/noUnnecessaryUsePrefix.ts b/.pkgs/samples/src/noUnnecessaryUsePrefix.ts index ef9b82359e..d7cb49ba3b 100644 --- a/.pkgs/samples/src/noUnnecessaryUsePrefix.ts +++ b/.pkgs/samples/src/noUnnecessaryUsePrefix.ts @@ -11,7 +11,9 @@ export function noUnnecessaryUsePrefix(): RuleFunction { if (hook.hookCalls.length === 0) { context.report({ node: hook.node, - message: `Custom hook "${hook.name}" doesn't call any hooks. A custom hook should use at least one hook, otherwise it's just a regular function.`, + message: `Custom hook "${ + hook.name ?? "unknown" + }" doesn't call any hooks. A custom hook should use at least one hook, otherwise it's just a regular function.`, }); } } diff --git a/apps/website/content/docs/migrating-from-eslint-plugin-react.mdx b/apps/website/content/docs/migrating-from-eslint-plugin-react.mdx index 1d507ab508..ab6e31a359 100644 --- a/apps/website/content/docs/migrating-from-eslint-plugin-react.mdx +++ b/apps/website/content/docs/migrating-from-eslint-plugin-react.mdx @@ -307,7 +307,6 @@ export function booleanPropNaming(options?: BooleanPropNamingOptions): RuleFunct "Program:exit"(prog) { const comps = query.all(prog); - // ─── Iterate Components ──────────────────────── for (const comp of comps) { const [propsParam] = comp.node.params; if (propsParam == null) continue; @@ -316,7 +315,6 @@ export function booleanPropNaming(options?: BooleanPropNamingOptions): RuleFunct const propsType = chk.getTypeAtLocation(tsNode); const declaredProps = propsType.getProperties(); - // ─── Iterate Props ───────────────────────────── for (const prop of declaredProps) { const propType = chk.getTypeOfSymbolAtLocation(prop, tsNode); @@ -333,7 +331,6 @@ export function booleanPropNaming(options?: BooleanPropNamingOptions): RuleFunct if (decl == null) continue; const declNode = srv.tsNodeToESTreeNodeMap.get(decl); - if (declNode == null) continue; const node = "key" in declNode ? declNode.key : declNode; @@ -412,8 +409,7 @@ export function forbidComponentProps(options: ForbidComponentPropsOptions): Rule if (propName == null || !forbidden.includes(propName)) return; // Verify context is JSX opening element - const parent = node.parent; - if (parent?.type !== "JSXOpeningElement") return; + const { parent } = node; // Extract element name const elemName = parent.name.type === "JSXIdentifier" ? parent.name.name : null; @@ -453,8 +449,7 @@ export function forbidDomProps(options: ForbidDomPropsOptions): RuleFunction { if (propName == null || !forbidden.includes(propName)) return; // Verify context is JSX opening element - const parent = node.parent; - if (parent?.type !== "JSXOpeningElement") return; + const { parent } = node; // Extract element name const elemName = parent.name.type === "JSXIdentifier" ? parent.name.name : null; @@ -520,7 +515,6 @@ export function functionComponentDefinition(): RuleFunction { visitor, { "Program:exit"(program) { - // ─── Iterate all components ──────────────────── for (const { node } of query.all(program)) { // Guard: must not already be arrow function if (node.type === "ArrowFunctionExpression") continue; @@ -536,24 +530,21 @@ export function functionComponentDefinition(): RuleFunction { if (node.generator) return null; const prefix = node.async ? "async " : ""; - const typeParams = node.typeParameters ? src.getText(node.typeParameters) : ""; + const typeParams = node.typeParameters != null ? src.getText(node.typeParameters) : ""; const params = `(${node.params.map((p) => src.getText(p)).join(", ")})`; - const returnType = node.returnType ? src.getText(node.returnType) : ""; + const returnType = node.returnType != null ? src.getText(node.returnType) : ""; const body = src.getText(node.body); - // ─── Case: function declaration ────────────── - if (node.type === "FunctionDeclaration" && node.id) { + if (node.type === "FunctionDeclaration" && node.id != null) { // dprint-ignore return fixer.replaceText(node, `const ${node.id.name} = ${prefix}${typeParams}${params}${returnType} => ${body};`); } - // ─── Case: function expression in variable ─── if (node.type === "FunctionExpression" && node.parent.type === "VariableDeclarator") { // dprint-ignore return fixer.replaceText(node, `${prefix}${typeParams}${params}${returnType} => ${body}`); } - // ─── Case: object method shorthand ─────────── if (node.type === "FunctionExpression" && node.parent.type === "Property") { // dprint-ignore return fixer.replaceText(node.parent, `${src.getText(node.parent.key)}: ${prefix}${typeParams}${params}${returnType} => ${body}`); @@ -621,8 +612,6 @@ export type JsxsFragmentsOptions = { export function jsxFragments(options: JsxsFragmentsOptions = {}): RuleFunction { const { mode = "syntax" } = options; return (context) => { - // ── Helpers ───────────────────────────────────── - function reportSyntaxPreferred(node: TSESTree.JSXOpeningElement, pattern: "React.Fragment" | "Fragment") { // Guard: has key prop (legitimate use of standard form) const hasAttributes = node.attributes.length > 0; @@ -632,20 +621,17 @@ export function jsxFragments(options: JsxsFragmentsOptions = {}): RuleFunction { node, message: `Use shorthand fragment syntax '<>...' instead of '<${pattern}>..."), fixer.replaceText(closing, "")]; }, }); } - // ── Listeners ──────────────────────────────────── - return { JSXOpeningElement(node) { const name = node.name; - // ─── Handle (JSXIdentifier) ─────── if (name.type === "JSXIdentifier" && name.name === "Fragment") { if (mode === "syntax") { reportSyntaxPreferred(node, "Fragment"); @@ -653,10 +639,9 @@ export function jsxFragments(options: JsxsFragmentsOptions = {}): RuleFunction { return; } - // ─── Handle (JSXMemberExpression) ─ if (name.type !== "JSXMemberExpression") return; if (name.object.type !== "JSXIdentifier" || name.object.name !== "React") return; - if (name.property.type !== "JSXIdentifier" || name.property.name !== "Fragment") return; + if (name.property.name !== "Fragment") return; if (mode === "syntax") { reportSyntaxPreferred(node, "React.Fragment"); @@ -716,9 +701,8 @@ export function jsxHandlerNames(options: JsxHandlerNamesOptions = {}): RuleFunct if (!EVENT_HANDLER_REGEX.test(propName)) return; const value = node.value; - if (!value) return; + if (value == null) return; - // ─── Check expression value ──────────────────── if (value.type === "JSXExpressionContainer") { const expression = ast.unwrap(value.expression); @@ -728,9 +712,7 @@ export function jsxHandlerNames(options: JsxHandlerNamesOptions = {}): RuleFunct if (!HANDLER_FUNC_REGEX.test(handlerName)) { context.report({ node: expression, - message: `Handler function "${handlerName}" should be named "${eventHandlerPrefix}${ - propName.slice(eventHandlerPropPrefix.length) - }..."`, + message: `Handler function "${handlerName}" should be named "${eventHandlerPrefix}${propName.slice(eventHandlerPropPrefix.length)}..."`, }); } return; @@ -741,10 +723,9 @@ export function jsxHandlerNames(options: JsxHandlerNamesOptions = {}): RuleFunct if (checkInlineFunction) { context.report({ node: expression, - message: - `Inline function handlers are not allowed for "${propName}". Extract it to a named "${eventHandlerPrefix}${ - propName.slice(eventHandlerPropPrefix.length) - }" function.`, + message: `Inline function handlers are not allowed for "${propName}". Extract it to a named "${eventHandlerPrefix}${ + propName.slice(eventHandlerPropPrefix.length) + }" function.`, }); } return; @@ -774,11 +755,9 @@ export function jsxMaxDepth(options: JsxMaxDepthOptions): RuleFunction { return (context) => ({ JSXElement(node) { let depth = 0; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let parent: any = node.parent; + let parent: typeof node.parent | null | undefined = node.parent; - // ─── Walk up the tree ────────────────────────── - while (parent) { + while (parent != null) { if (parent.type === "JSXElement") { depth++; } @@ -815,7 +794,6 @@ export function jsxNoBind(): RuleFunction { const expr = ast.unwrap(value.expression); - // ─── Detect forbidden patterns ───────────────── switch (true) { case expr.type === "ArrowFunctionExpression": case expr.type === "FunctionExpression": @@ -823,11 +801,7 @@ export function jsxNoBind(): RuleFunction { break; case expr.type === "CallExpression": { const callee = ast.unwrap(expr.callee); - if ( - callee.type === "MemberExpression" - && callee.property.type === "Identifier" - && callee.property.name === "bind" - ) { + if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.name === "bind") { context.report({ node, message: "JSX props should not use .bind()." }); } break; @@ -858,7 +832,6 @@ export function jsxNoDuplicateProps(options: JsxNoDuplicatePropsOptions = {}): R JSXOpeningElement(node) { const seen = new Map(); - // ─── Check each attribute ────────────────────── for (const attr of node.attributes) { if (attr.type !== "JSXAttribute") continue; if (attr.name.type !== "JSXIdentifier") continue; @@ -902,16 +875,13 @@ export function jsxNoLiterals(options: JsxNoLiteralsOptions = {}): RuleFunction const { noStrings = false, allowedStrings = [], ignoreProps = true } = options; const allowedSet = new Set(allowedStrings); return (context) => ({ - // ─── Check literal text children ─────────────── Literal(node) { if (typeof node.value !== "string") return; const text = node.value.trim(); if (text === "" || allowedSet.has(text)) return; const parent = node.parent; - if (!parent) return; - // ─── Case: prop value ──────────────────────── if (parent.type === "JSXAttribute") { if (!ignoreProps) { context.report({ @@ -922,10 +892,8 @@ export function jsxNoLiterals(options: JsxNoLiteralsOptions = {}): RuleFunction return; } - // ─── Case: already wrapped ─────────────────── if (parent.type === "JSXExpressionContainer") return; - // ─── Case: child of element/fragment ───────── if (parent.type === "JSXElement" || parent.type === "JSXFragment") { if (noStrings) { context.report({ @@ -941,7 +909,6 @@ export function jsxNoLiterals(options: JsxNoLiteralsOptions = {}): RuleFunction } }, - // ─── Check JSX text nodes ────────────────────── JSXText(node) { const text = node.value.trim(); if (text === "" || allowedSet.has(text)) return; @@ -990,7 +957,6 @@ export function jsxPascalCase(options: JsxPascalCaseOptions = {}): RuleFunction const componentName = name.name; - // ─── Handle leading underscore ─────────────── if (componentName.startsWith("_")) { if (!allowLeadingUnderscore) { context.report({ @@ -1003,10 +969,9 @@ export function jsxPascalCase(options: JsxPascalCaseOptions = {}): RuleFunction // Guard: ignore DOM elements (lowercase) const firstChar = componentName[0]; - if (firstChar === undefined) return; + if (firstChar == null) return; if (firstChar === firstChar.toLowerCase()) return; - // ─── Handle all-caps ───────────────────────── if (componentName === componentName.toUpperCase()) { if (!allowAllCaps) { context.report({ @@ -1017,7 +982,6 @@ export function jsxPascalCase(options: JsxPascalCaseOptions = {}): RuleFunction return; } - // ─── Validate PascalCase ───────────────────── if (!pascalCaseRegex.test(componentName)) { context.report({ node: name, @@ -1042,7 +1006,6 @@ export function jsxPropsNoSpreadMulti(): RuleFunction { JSXOpeningElement(node) { const seen = new Set(); - // ─── Check each spread attribute ─────────────── for (const attr of node.attributes) { if (attr.type !== "JSXSpreadAttribute") continue; @@ -1096,22 +1059,48 @@ Disallow adjacent inline elements not separated by whitespace. ```ts twoslash import type { RuleFunction } from "@eslint-react/kit"; -import type { TSESTree } from "@typescript-eslint/types"; /** Disallow adjacent inline elements not separated by whitespace. */ export function noAdjacentInlineElements(): RuleFunction { /** Set of inline HTML elements. */ const INLINE_ELEMENTS = new Set([ - "a", "abbr", "acronym", "b", "bdi", "bdo", "big", "br", "cite", "code", - "dfn", "em", "i", "img", "input", "kbd", "label", "map", "object", "q", - "samp", "script", "select", "small", "span", "strong", "sub", "sup", - "textarea", "time", "tt", "var", + "a", + "abbr", + "acronym", + "b", + "bdi", + "bdo", + "big", + "br", + "cite", + "code", + "dfn", + "em", + "i", + "img", + "input", + "kbd", + "label", + "map", + "object", + "q", + "samp", + "script", + "select", + "small", + "span", + "strong", + "sub", + "sup", + "textarea", + "time", + "tt", + "var", ]); return (context) => ({ JSXElement(node) { const children = node.children; - // ─── Check adjacent pairs ────────────────────── for (let i = 0; i < children.length - 1; i++) { const current = children[i]; const next = children[i + 1]; @@ -1225,7 +1214,7 @@ export function noStringRefs(): RuleFunction { if (value?.type === "Literal" && typeof value.value === "string") { context.report({ node, - message: `String refs are deprecated and should not be used. Use callback refs or React.createRef() instead.`, + message: `String refs are deprecated and should not be used. Use React.useRef() instead.`, }); } }, @@ -1249,7 +1238,6 @@ export function noMultiComp(): RuleFunction { "Program:exit"(program) { const components = query.all(program); - // ─── Report excess components ────────────────── for (const { node, name } of components.slice(1)) { context.report({ node, diff --git a/packages/ast/docs/@eslint-react/namespaces/Extract/README.md b/packages/ast/docs/@eslint-react/namespaces/Extract/README.md index 5eb2485e05..be9e2b89e6 100644 --- a/packages/ast/docs/@eslint-react/namespaces/Extract/README.md +++ b/packages/ast/docs/@eslint-react/namespaces/Extract/README.md @@ -6,9 +6,9 @@ Helpers for extracting information from `TSESTree` nodes. ## Functions -| Function | Description | -| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| [getCalleeName](functions/getCalleeName.md) | Get the name of a call expression's callee when it is an identifier or a member expression whose property is an identifier | -| [getFullyQualifiedName](functions/getFullyQualifiedName.md) | - | -| [getRootIdentifier](functions/getRootIdentifier.md) | - | -| [unwrap](functions/unwrap.md) | - | +| Function | Description | +| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| [getCalleeName](functions/getCalleeName.md) | Get the name of a call expression's callee when it is an identifier or a non-computed member expression whose property is an identifier | +| [getFullyQualifiedName](functions/getFullyQualifiedName.md) | - | +| [getRootIdentifier](functions/getRootIdentifier.md) | - | +| [unwrap](functions/unwrap.md) | - | diff --git a/packages/ast/docs/@eslint-react/namespaces/Extract/functions/getCalleeName.md b/packages/ast/docs/@eslint-react/namespaces/Extract/functions/getCalleeName.md index 61619b0dbf..65ed5e3b4f 100644 --- a/packages/ast/docs/@eslint-react/namespaces/Extract/functions/getCalleeName.md +++ b/packages/ast/docs/@eslint-react/namespaces/Extract/functions/getCalleeName.md @@ -7,7 +7,7 @@ function getCalleeName(node: CallExpression): string | null; ``` Get the name of a call expression's callee when it is an identifier -or a member expression whose property is an identifier +or a non-computed member expression whose property is an identifier ## Parameters diff --git a/packages/core/src/jsx.test.ts b/packages/core/src/jsx.test.ts index 1dd47ac30e..f30fdcc180 100644 --- a/packages/core/src/jsx.test.ts +++ b/packages/core/src/jsx.test.ts @@ -24,14 +24,14 @@ function createContext(parsed: ReturnType): RuleContext { getScope(node: TSESTree.Node) { const inner = node.type !== AST.Program; for (let current: TSESTree.Node | undefined = node; current != null; current = current.parent) { - const scope = scopeManager?.acquire(current, inner); + const scope = scopeManager.acquire(current, inner); if (scope != null) { return scope.type === "function-expression-name" ? scope.childScopes[0] : scope; } } - return scopeManager?.scopes[0]; + return scopeManager.scopes[0]; }, }, } as unknown as RuleContext; @@ -46,7 +46,7 @@ function parseLastExpression(code: string) { const context = createContext(parsed); const last = parsed.ast.body.at(-1); if (last?.type !== AST.ExpressionStatement) { - throw new Error(`expected last statement to be an ExpressionStatement, got ${last?.type}`); + throw new Error(`expected last statement to be an ExpressionStatement, got ${last?.type ?? "unknown"}`); } return { context, node: last.expression }; } diff --git a/packages/eslint/src/utils.ts b/packages/eslint/src/utils.ts index bfa16025d6..9439d9ff37 100644 --- a/packages/eslint/src/utils.ts +++ b/packages/eslint/src/utils.ts @@ -26,8 +26,7 @@ export function merge(base: RuleListener, ...rest: RuleListener[]): RuleListener for (const r of rest) { for (const key in r) { const existing = base[key]; - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - base[key] = existing + base[key] = existing != null ? (...args) => { existing(...args); r[key]?.(...args);