diff --git a/packages/plugins/eslint-plugin-react-debug/src/rules/function-component.spec.ts b/packages/plugins/eslint-plugin-react-debug/src/rules/function-component.spec.ts index 02bcb4bf7c..59134c98ed 100644 --- a/packages/plugins/eslint-plugin-react-debug/src/rules/function-component.spec.ts +++ b/packages/plugins/eslint-plugin-react-debug/src/rules/function-component.spec.ts @@ -1366,6 +1366,62 @@ ruleTester.run(RULE_NAME, rule, { }, ], }, + { + code: /* tsx */ ` + const MyComponent1 = (() => null)!; + const MyComponent2 = (() => null)!!; + const MyComponent3 = (() => null)!!! as A; + const MyComponent4 = (() => null)!!! satisfies A; + const MyComponent5 = (() => null)!!! as A satisfies B; + `, + errors: [ + { + messageId: "functionComponent", + data: { + name: "MyComponent1", + forwardRef: false, + hookCalls: 0, + memo: false, + }, + }, + { + messageId: "functionComponent", + data: { + name: "MyComponent2", + forwardRef: false, + hookCalls: 0, + memo: false, + }, + }, + { + messageId: "functionComponent", + data: { + name: "MyComponent3", + forwardRef: false, + hookCalls: 0, + memo: false, + }, + }, + { + messageId: "functionComponent", + data: { + name: "MyComponent4", + forwardRef: false, + hookCalls: 0, + memo: false, + }, + }, + { + messageId: "functionComponent", + data: { + name: "MyComponent5", + forwardRef: false, + hookCalls: 0, + memo: false, + }, + }, + ], + }, ], valid: [ ...allFunctions, @@ -1375,25 +1431,21 @@ ruleTester.run(RULE_NAME, rule, { "const results = allSettled.mapLike((x) => (x.status === 'fulfilled' ? format(x.value) : null))", /* tsx */ ` export const action = (() => { - // ^? return null; }); `, /* tsx */ ` export const action = (() => { - // ^? return null; }) as ActionFUnction; `, /* tsx */ ` export const action = (() => { - // ^? return null; }) satisfies ActionFUnction; `, /* tsx */ ` export const action = (() => { - // ^? return null; }) as ActionFUnction satisfies ActionFUnction; `, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-access-state-in-setstate.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-access-state-in-setstate.ts index 5b37715584..54a31b98b6 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-access-state-in-setstate.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-access-state-in-setstate.ts @@ -17,7 +17,7 @@ export const RULE_FEATURES = [ export type MessageID = CamelCase; function getName(node: TSESTree.Expression | TSESTree.PrivateIdentifier): O.Option { - if (node.type === AST_NODE_TYPES.TSAsExpression) { + if (AST.isTypeExpression(node)) { return getName(node.expression); } if (node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-mutation-state.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-mutation-state.ts index 4434600beb..1e94db9dd7 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-mutation-state.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-mutation-state.ts @@ -18,7 +18,7 @@ export const RULE_FEATURES = [ export type MessageID = CamelCase; function getName(node: TSESTree.Expression | TSESTree.PrivateIdentifier): O.Option { - if (node.type === AST_NODE_TYPES.TSAsExpression) { + if (AST.isTypeExpression(node)) { return getName(node.expression); } if (node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-duplicate-key.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-duplicate-key.spec.ts index dbaef793fa..9e447f1dbb 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-duplicate-key.spec.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-duplicate-key.spec.ts @@ -17,19 +17,19 @@ ruleTester.run(RULE_NAME, rule, { { messageId: "noDuplicateKey", data: { - value: '"1"', + value: 'key="1"', }, }, { messageId: "noDuplicateKey", data: { - value: '"1"', + value: 'key="1"', }, }, { messageId: "noDuplicateKey", data: { - value: '"1"', + value: 'key="1"', }, }, ], @@ -48,19 +48,19 @@ ruleTester.run(RULE_NAME, rule, { { messageId: "noDuplicateKey", data: { - value: '"1"', + value: 'key="1"', }, }, { messageId: "noDuplicateKey", data: { - value: '"1"', + value: 'key="1"', }, }, { messageId: "noDuplicateKey", data: { - value: '"1"', + value: 'key="1"', }, }, ], @@ -75,7 +75,126 @@ ruleTester.run(RULE_NAME, rule, { { messageId: "noDuplicateKey", data: { - value: '"1"', + value: 'key="1"', + }, + }, + ], + }, + { + code: /* tsx */ ` + const App = () => { + return [1, 2, 3].map((item) => { return
{item}
}) + }; + `, + errors: [ + { + messageId: "noDuplicateKey", + data: { + value: 'key="1"', + }, + }, + ], + }, + { + code: /* tsx */ ` + const App = () => { + return nested.map((item) => { + return
{item.map((i) =>
{i}
)}
+ }) + }; + `, + errors: [ + { + messageId: "noDuplicateKey", + data: { + value: 'key="1"', + }, + }, + { + messageId: "noDuplicateKey", + data: { + value: 'key="a"', + }, + }, + ], + }, + { + code: /* tsx */ ` + const App = () => { + return nested.map((foo) => { + return
{foo.map((bar) =>
{bar.map((baz) =>
{baz}
)}
)}
+ }) + }; + `, + errors: [ + { + messageId: "noDuplicateKey", + data: { + value: 'key="foo"', + }, + }, + { + messageId: "noDuplicateKey", + data: { + value: 'key="bar"', + }, + }, + { + messageId: "noDuplicateKey", + data: { + value: 'key="baz"', + }, + }, + ], + }, + { + code: /* tsx */ ` + const App = () => { + return nested?.map((foo) => { + return
{foo!.map((bar) =>
{bar!!.map(((baz) =>
{baz}
)!!! as A satisfies B)}
)}
+ }) + }; + `, + errors: [ + { + messageId: "noDuplicateKey", + data: { + value: 'key="foo"', + }, + }, + { + messageId: "noDuplicateKey", + data: { + value: 'key="bar"', + }, + }, + { + messageId: "noDuplicateKey", + data: { + value: 'key="baz"', + }, + }, + ], + }, + { + code: /* tsx */ ` + const App = () => { + return nested.map((foo) => { + return
{foo.notmap((bar) =>
{bar.map((baz) =>
{baz}
)}
)}
+ }) + }; + `, + errors: [ + { + messageId: "noDuplicateKey", + data: { + value: 'key="foo"', + }, + }, + { + messageId: "noDuplicateKey", + data: { + value: 'key="baz"', }, }, ], @@ -102,5 +221,12 @@ ruleTester.run(RULE_NAME, rule, { return [1, 2, 3].map((item) => { const key = item; return
{item}
}) }; `, + /* tsx */ ` + const App = () => { + return nested.map((item) => { + return
{item.map((i) => { return
{i}
})}
+ }) + }; + `, ], }); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-duplicate-key.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-duplicate-key.ts index 80bb0cb16e..34df3888f0 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-duplicate-key.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-duplicate-key.ts @@ -1,14 +1,10 @@ import * as AST from "@eslint-react/ast"; -import { isChildrenToArrayCall } from "@eslint-react/core"; import { F, O } from "@eslint-react/eff"; -import * as JSX from "@eslint-react/jsx"; import type { RuleFeature } from "@eslint-react/types"; import * as VAR from "@eslint-react/var"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES } from "@typescript-eslint/types"; -import type { ReportDescriptor } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; -import { isMatching, match } from "ts-pattern"; import { createRule } from "../utils"; @@ -20,7 +16,12 @@ export const RULE_FEATURES = [ export type MessageID = CamelCase; -// TODO: Re-implement this rule to improve performance +type KeyedEntry = { + hasDuplicate: boolean; + keys: TSESTree.JSXAttribute[]; + root: TSESTree.Expression; +}; + export default createRule<[], MessageID>({ meta: { type: "problem", @@ -35,135 +36,66 @@ export default createRule<[], MessageID>({ }, name: RULE_NAME, create(context) { - const state = { isWithinChildrenToArray: false }; - function isKeyEqual(a: TSESTree.Node, b: TSESTree.Node) { - return VAR.isNodeValueEqual(a, b, [context.sourceCode.getScope(a), context.sourceCode.getScope(b)]); - } - function checkIteratorElement(node: TSESTree.Node): O.Option> { - if (node.type !== AST_NODE_TYPES.JSXElement) return O.none(); - const initialScope = context.sourceCode.getScope(node); - return F.pipe( - JSX.findPropInAttributes(node.openingElement.attributes, initialScope)("key"), - O.flatMap((k) => "value" in k ? O.fromNullable(k.value) : O.none()), - O.flatMap((v) => { - return isKeyEqual(v, v) - ? O.some({ - messageId: "noDuplicateKey", - node: v, - data: { - value: context.sourceCode.getText(v), - }, - }) - : O.none(); - }), - ); - } - - function checkExpression(node: TSESTree.Expression): O.Option> { - switch (node.type) { - case AST_NODE_TYPES.ConditionalExpression: - if (!("consequent" in node)) return O.none(); - return F.pipe( - checkIteratorElement(node.consequent), - O.orElse(() => checkIteratorElement(node.alternate)), - ); - case AST_NODE_TYPES.JSXElement: - case AST_NODE_TYPES.JSXFragment: - return checkIteratorElement(node); - case AST_NODE_TYPES.LogicalExpression: - if (!("left" in node)) return O.none(); - return F.pipe( - checkIteratorElement(node.left), - O.orElse(() => checkIteratorElement(node.right)), - ); - default: - return O.none(); - } - } - - function checkBlockStatement(node: TSESTree.BlockStatement) { - return AST.getNestedReturnStatements(node) - .reduce[]>((acc, statement) => { - if (!statement.argument) return acc; - const maybeDescriptor = checkIteratorElement(statement.argument); - if (O.isNone(maybeDescriptor)) return acc; - const descriptor = maybeDescriptor.value; - - return [...acc, descriptor]; - }, []); + const keyedEntries: Map = new Map(); + function isKeyValueEqual( + a: TSESTree.JSXAttribute, + b: TSESTree.JSXAttribute, + ): boolean { + const aValue = a.value; + const bValue = b.value; + if (aValue === null || bValue === null) return false; + return VAR.isNodeValueEqual(aValue, bValue, [ + context.sourceCode.getScope(aValue), + context.sourceCode.getScope(bValue), + ]); } - - const seen = new WeakSet(); - return { - "ArrayExpression, JSXElement > JSXElement"(node: TSESTree.ArrayExpression | TSESTree.JSXElement) { - if (state.isWithinChildrenToArray) return; - const elements = match(node) - .with({ type: AST_NODE_TYPES.ArrayExpression }, ({ elements }) => elements) - .with({ type: AST_NODE_TYPES.JSXElement }, ({ parent }) => "children" in parent ? parent.children : []) - .otherwise(() => []) - .filter(AST.is(AST_NODE_TYPES.JSXElement)) - .filter((element) => !seen.has(element)); - const keys = elements.reduce<[ - TSESTree.JSXElement, - TSESTree.JSXAttribute, - TSESTree.JSXElement | TSESTree.JSXExpression | TSESTree.Literal, - ][]>( - (acc, element) => { - const attr = element.openingElement.attributes - .findLast(attr => { - if (attr.type !== AST_NODE_TYPES.JSXAttribute) return false; - return attr.name.name === "key"; + "JSXAttribute[name.name='key']"(node: TSESTree.JSXAttribute) { + const jsxElement = node.parent.parent; + switch (jsxElement.parent.type) { + case AST_NODE_TYPES.ArrayExpression: + case AST_NODE_TYPES.JSXElement: + case AST_NODE_TYPES.JSXFragment: { + const root = jsxElement.parent; + const prevKeys = keyedEntries.get(root)?.keys ?? []; + keyedEntries.set(root, { + hasDuplicate: prevKeys.some((prevKey) => isKeyValueEqual(prevKey, node)), + keys: [...prevKeys, node], + root: jsxElement.parent, + }); + break; + } + default: { + const entry = F.pipe( + O.Do, + O.bind("call", () => AST.traverseUpGuard(jsxElement, AST.isMapCallLoose)), + O.bind("ietr", ({ call }) => AST.traverseUpStop(jsxElement, call, AST.isFunction)), + O.bind("arg0", ({ call }) => O.fromNullable(call.arguments[0])), + ); + for (const { arg0, call, ietr } of O.toArray(entry)) { + if (AST.unwrapTypeExpression(arg0) !== ietr) continue; + keyedEntries.set(call, { + hasDuplicate: node.value?.type === AST_NODE_TYPES.Literal, + keys: [node], + root: call, }); - if (!attr || !("value" in attr) || attr.value === null) return acc; - const { value } = attr; - if (acc.length === 0) return [[element, attr, value]]; - if (acc.some(([_, _1, v]) => isKeyEqual(v, value))) { - return [...acc, [element, attr, value]]; } - return acc; - }, - [], - ); - if (keys.length < 2) return; - for (const [element, attr, value] of keys) { - seen.add(element); - context.report({ - messageId: "noDuplicateKey", - node: attr, - data: { - value: context.sourceCode.getText(value), - }, - }); + } } }, - CallExpression(node) { - state.isWithinChildrenToArray ||= isChildrenToArrayCall(node, context); - if (state.isWithinChildrenToArray) return; - const isMapCall = AST.isMapCallLoose(node); - const isArrayFromCall = isMatching({ - type: AST_NODE_TYPES.CallExpression, - callee: { - type: AST_NODE_TYPES.MemberExpression, - property: { - name: "from", - }, - }, - }, node); - if (!isMapCall && !isArrayFromCall) return; - const fn = node.arguments[isMapCall ? 0 : 1]; - if (!AST.isOneOf([AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionExpression])(fn)) return; - if (fn.body.type === AST_NODE_TYPES.BlockStatement) { - for (const descriptor of checkBlockStatement(fn.body)) { - context.report(descriptor); + "Program:exit"() { + for (const { hasDuplicate, keys } of keyedEntries.values()) { + if (!hasDuplicate) continue; + for (const key of keys) { + context.report({ + messageId: "noDuplicateKey", + node: key, + data: { + value: context.sourceCode.getText(key), + }, + }); } - return; } - O.map(checkExpression(fn.body), context.report); - }, - "CallExpression:exit"(node) { - if (!isChildrenToArrayCall(node, context)) return; - state.isWithinChildrenToArray = false; }, }; }, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-class-component-members.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-class-component-members.ts index 6dfc9796c1..5baadf07d6 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-class-component-members.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-class-component-members.ts @@ -42,7 +42,7 @@ const LIFECYCLE_METHODS = new Set([ // anywhere that a literal may be used as a key (e.g., member expressions, // method definitions, ObjectExpression property keys). function getName(node: TSESTree.Expression | TSESTree.PrivateIdentifier): O.Option { - if (node.type === AST_NODE_TYPES.TSAsExpression) { + if (AST.isTypeExpression(node)) { return getName(node.expression); } if (node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier) { diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-state.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-state.ts index 42ed5d71cc..6239831058 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-state.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unused-state.ts @@ -18,7 +18,7 @@ export const RULE_FEATURES = [ export type MessageID = CamelCase; function getName(node: TSESTree.Expression | TSESTree.PrivateIdentifier): O.Option { - if (node.type === AST_NODE_TYPES.TSAsExpression) { + if (AST.isTypeExpression(node)) { return getName(node.expression); } if (node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.PrivateIdentifier) { diff --git a/packages/utilities/ast/src/get-function-identifier.spec.ts b/packages/utilities/ast/src/get-function-identifier.spec.ts index 76be4794f4..ffbc2873a9 100644 --- a/packages/utilities/ast/src/get-function-identifier.spec.ts +++ b/packages/utilities/ast/src/get-function-identifier.spec.ts @@ -42,6 +42,11 @@ describe("get function identifier from function expression", () => { ["const foo = function() {};", "foo"], ["const bar = function() {};", "bar"], ["const baz = function() {};", "baz"], + ["const qux = (() => {})!;", "qux"], + ["const quux = (() => {})!!;", "quux"], + ["const quuz = (() => {}) as Function;", "quuz"], + ["const corge = (() => {}) as () => void;", "corge"], + ["const grault = (() => {}) satisfies Function;", "grault"], ["qux = function() {};", "qux"], ["const { Foo = function() {} } = {};", "Foo"], ["const { Foo = () => {} } = {};", "Foo"], diff --git a/packages/utilities/ast/src/get-function-identifier.ts b/packages/utilities/ast/src/get-function-identifier.ts index b9d1ade331..e93a7cbc3c 100644 --- a/packages/utilities/ast/src/get-function-identifier.ts +++ b/packages/utilities/ast/src/get-function-identifier.ts @@ -11,7 +11,7 @@ import { O } from "@eslint-react/eff"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES } from "@typescript-eslint/types"; -import { isOneOf } from "./is"; +import { isOneOf, isTypeExpression } from "./is"; import type { TSESTreeFunction } from "./types"; export function getFunctionIdentifier(node: TSESTree.Expression | TSESTreeFunction): O.Option { @@ -52,9 +52,10 @@ export function getFunctionIdentifier(node: TSESTree.Expression | TSESTreeFuncti && node.parent.right === node && node.parent.left.type === AST_NODE_TYPES.Identifier: return O.some(node.parent.left); + // const MaybeComponent = (() => {})!; // const MaybeComponent = (() => {}) as FunctionComponent; // const MaybeComponent = (() => {}) satisfies FunctionComponent; - case isOneOf([AST_NODE_TYPES.TSAsExpression, AST_NODE_TYPES.TSSatisfiesExpression])(node.parent): + case isTypeExpression(node.parent): return getFunctionIdentifier(node.parent); } return O.none(); diff --git a/packages/utilities/ast/src/get-nested-expressions-of-type.ts b/packages/utilities/ast/src/get-nested-expressions-of-type.ts index 7649029d79..17ad1a95b6 100644 --- a/packages/utilities/ast/src/get-nested-expressions-of-type.ts +++ b/packages/utilities/ast/src/get-nested-expressions-of-type.ts @@ -90,6 +90,14 @@ export function getNestedExpressionsOfType( const chunk = boundGetNestedExpressionsOfType(node.expression); expressions.push(...chunk); } + if (node.type === AST_NODE_TYPES.TSAsExpression) { + const chunk = boundGetNestedExpressionsOfType(node.expression); + expressions.push(...chunk); + } + if (node.type === AST_NODE_TYPES.TSSatisfiesExpression) { + const chunk = boundGetNestedExpressionsOfType(node.expression); + expressions.push(...chunk); + } return expressions; }; } diff --git a/packages/utilities/ast/src/get-nested-identifiers.ts b/packages/utilities/ast/src/get-nested-identifiers.ts index c957f9eb85..d7d632f411 100644 --- a/packages/utilities/ast/src/get-nested-identifiers.ts +++ b/packages/utilities/ast/src/get-nested-identifiers.ts @@ -59,5 +59,13 @@ export function getNestedIdentifiers(node: TSESTree.Node): readonly TSESTree.Ide const chunk = getNestedIdentifiers(node.expression); identifiers.push(...chunk); } + if (node.type === AST_NODE_TYPES.TSAsExpression) { + const chunk = getNestedIdentifiers(node.expression); + identifiers.push(...chunk); + } + if (node.type === AST_NODE_TYPES.TSSatisfiesExpression) { + const chunk = getNestedIdentifiers(node.expression); + identifiers.push(...chunk); + } return identifiers; } diff --git a/packages/utilities/ast/src/index.ts b/packages/utilities/ast/src/index.ts index 10faf8a9b8..b0541d2a14 100644 --- a/packages/utilities/ast/src/index.ts +++ b/packages/utilities/ast/src/index.ts @@ -4,6 +4,7 @@ export * from "./get-function-identifier"; export * from "./get-identifiers-from-binary-expression"; export * from "./get-literal-value-type"; export * from "./get-nested-call-expressions"; +export * from "./get-nested-expressions-of-type"; export * from "./get-nested-identifiers"; export * from "./get-nested-new-expressions"; export * from "./get-nested-return-statements"; @@ -25,4 +26,6 @@ export * from "./to-readable-node-name"; export * from "./to-readable-node-type"; export * from "./traverse-up"; export * from "./traverse-up-guard"; +export * from "./traverse-up-stop"; export type * from "./types"; +export * from "./unwrap-type-expression"; diff --git a/packages/utilities/ast/src/is-node-equal.ts b/packages/utilities/ast/src/is-node-equal.ts index 4d392356ab..1b91ce892a 100644 --- a/packages/utilities/ast/src/is-node-equal.ts +++ b/packages/utilities/ast/src/is-node-equal.ts @@ -14,24 +14,42 @@ export const isNodeEqual: { (a: TSESTree.Node): (b: TSESTree.Node) => boolean; (a: TSESTree.Node, b: TSESTree.Node): boolean; } = F.dual(2, (a: TSESTree.Node, b: TSESTree.Node): boolean => { - if (a.type !== b.type) return false; - if (a.type === AST_NODE_TYPES.ThisExpression && b.type === AST_NODE_TYPES.ThisExpression) return true; - if (a.type === AST_NODE_TYPES.Literal && b.type === AST_NODE_TYPES.Literal) return a.value === b.value; - if (a.type === AST_NODE_TYPES.TemplateElement && b.type === AST_NODE_TYPES.TemplateElement) { - return a.value.raw === b.value.raw; + switch (true) { + case a === b: + return true; + case a.type !== b.type: + return false; + case a.type === AST_NODE_TYPES.Literal + && b.type === AST_NODE_TYPES.Literal: + return a.value === b.value; + case a.type === AST_NODE_TYPES.TemplateElement + && b.type === AST_NODE_TYPES.TemplateElement: + return a.value.raw === b.value.raw; + case a.type === AST_NODE_TYPES.TemplateLiteral + && b.type === AST_NODE_TYPES.TemplateLiteral: + if (a.quasis.length !== b.quasis.length || a.expressions.length !== b.expressions.length) return false; + if (!zip(a.quasis, b.quasis).every(([a, b]) => isNodeEqual(a, b))) return false; + if (!zip(a.expressions, b.expressions).every(([a, b]) => isNodeEqual(a, b))) return false; + return true; + case a.type === AST_NODE_TYPES.Identifier + && b.type === AST_NODE_TYPES.Identifier: + return a.name === b.name; + case a.type === AST_NODE_TYPES.PrivateIdentifier + && b.type === AST_NODE_TYPES.PrivateIdentifier: + return a.name === b.name; + case a.type === AST_NODE_TYPES.MemberExpression + && b.type === AST_NODE_TYPES.MemberExpression: + return isNodeEqual(a.property, b.property) && isNodeEqual(a.object, b.object); + case a.type === AST_NODE_TYPES.JSXAttribute + && b.type === AST_NODE_TYPES.JSXAttribute: { + if (a.name.name !== b.name.name) return false; + if (a.value === null || b.value === null) return a.value === b.value; + return isNodeEqual(a.value, b.value); + } + case a.type === AST_NODE_TYPES.ThisExpression + && b.type === AST_NODE_TYPES.ThisExpression: + return true; + default: + return false; } - if (a.type === AST_NODE_TYPES.TemplateLiteral && b.type === AST_NODE_TYPES.TemplateLiteral) { - if (a.quasis.length !== b.quasis.length || a.expressions.length !== b.expressions.length) return false; - if (!zip(a.quasis, b.quasis).every(([a, b]) => isNodeEqual(a, b))) return false; - if (!zip(a.expressions, b.expressions).every(([a, b]) => isNodeEqual(a, b))) return false; - return true; - } - if (a.type === AST_NODE_TYPES.Identifier && b.type === AST_NODE_TYPES.Identifier) return a.name === b.name; - if (a.type === AST_NODE_TYPES.PrivateIdentifier && b.type === AST_NODE_TYPES.PrivateIdentifier) { - return a.name === b.name; - } - if (a.type === AST_NODE_TYPES.MemberExpression && b.type === AST_NODE_TYPES.MemberExpression) { - return isNodeEqual(a.property, b.property) && isNodeEqual(a.object, b.object); - } - return false; }); diff --git a/packages/utilities/ast/src/is-this-expression.ts b/packages/utilities/ast/src/is-this-expression.ts index 9e29b03ea4..ce94ee99c8 100644 --- a/packages/utilities/ast/src/is-this-expression.ts +++ b/packages/utilities/ast/src/is-this-expression.ts @@ -1,8 +1,10 @@ import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES } from "@typescript-eslint/types"; +import { isTypeExpression } from "./is"; + export function isThisExpression(node: TSESTree.Expression) { - if (node.type === AST_NODE_TYPES.TSAsExpression) { + if (isTypeExpression(node)) { return isThisExpression(node.expression); } diff --git a/packages/utilities/ast/src/is.ts b/packages/utilities/ast/src/is.ts index c6d645f733..589defd1d4 100644 --- a/packages/utilities/ast/src/is.ts +++ b/packages/utilities/ast/src/is.ts @@ -148,3 +148,10 @@ export const isLeftHandSideExpressionType = isOneOf([ AST_NODE_TYPES.TSNonNullExpression, AST_NODE_TYPES.TSTypeAssertion, ]); + +export const isTypeExpression = isOneOf([ + AST_NODE_TYPES.TSAsExpression, + AST_NODE_TYPES.TSTypeAssertion, + AST_NODE_TYPES.TSNonNullExpression, + AST_NODE_TYPES.TSSatisfiesExpression, +]); diff --git a/packages/utilities/ast/src/traverse-up-stop.ts b/packages/utilities/ast/src/traverse-up-stop.ts new file mode 100644 index 0000000000..b2e93eff84 --- /dev/null +++ b/packages/utilities/ast/src/traverse-up-stop.ts @@ -0,0 +1,23 @@ +import { O } from "@eslint-react/eff"; +import type { TSESTree } from "@typescript-eslint/types"; +import { AST_NODE_TYPES } from "@typescript-eslint/types"; + +/** + * Traverses up the AST tree until the predicate returns `true` or the stop node is reached + * @param node The AST node to start traversing from + * @param stopNode The AST node to stop traversing at + * @param predicate The predicate to check each node + * @returns The first node that matches the predicate or `null` if no node matches + */ +export function traverseUpStop( + node: TSESTree.Node, + stopNode: TSESTree.Node, + predicate: (node: TSESTree.Node) => boolean, +): O.Option { + const { parent } = node; + if (!parent || parent === stopNode || parent.type === AST_NODE_TYPES.Program) return O.none(); + + return predicate(parent) + ? O.some(parent) + : traverseUpStop(parent, stopNode, predicate); +} diff --git a/packages/utilities/ast/src/unwrap-type-expression.ts b/packages/utilities/ast/src/unwrap-type-expression.ts new file mode 100644 index 0000000000..9547a77991 --- /dev/null +++ b/packages/utilities/ast/src/unwrap-type-expression.ts @@ -0,0 +1,8 @@ +import type { TSESTree } from "@typescript-eslint/types"; + +import { isTypeExpression } from "./is"; + +export function unwrapTypeExpression(node: TSESTree.Node): TSESTree.Node { + if (isTypeExpression(node)) return unwrapTypeExpression(node.expression); + return node; +} diff --git a/packages/utilities/var/src/construction.ts b/packages/utilities/var/src/construction.ts index 7f9130fb2b..0c9282bcfe 100644 --- a/packages/utilities/var/src/construction.ts +++ b/packages/utilities/var/src/construction.ts @@ -176,18 +176,12 @@ export function inspectConstruction( } return Construction.None(); }) - .when( - AST.isOneOf([ - AST_NODE_TYPES.TSAsExpression, - AST_NODE_TYPES.TSTypeAssertion, - ]), - () => { - if (!("expression" in node) || !isObject(node.expression)) { - return Construction.None(); - } - return detect(node.expression); - }, - ) + .when(AST.isTypeExpression, () => { + if (!("expression" in node) || !isObject(node.expression)) { + return Construction.None(); + } + return detect(node.expression); + }) .otherwise(() => Construction.None()); }; return detect(node); diff --git a/packages/utilities/var/src/is-node-value-equal.ts b/packages/utilities/var/src/is-node-value-equal.ts index ec3f5fb916..08102c0067 100644 --- a/packages/utilities/var/src/is-node-value-equal.ts +++ b/packages/utilities/var/src/is-node-value-equal.ts @@ -32,6 +32,9 @@ export function isNodeValueEqual( ): boolean { const [aScope, bScope] = initialScopes; switch (true) { + case a === b: { + return true; + } case a.type === AST_NODE_TYPES.Literal && b.type === AST_NODE_TYPES.Literal: { return a.value === b.value;