Skip to content
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
5 changes: 5 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import url from "node:url";

import eslintJs from "@eslint/js";
import eslintMarkdown from "@eslint/markdown";
import eslintStylistic from "@stylistic/eslint-plugin";
import eslintPluginImport from "eslint-plugin-import-x";
import eslintPluginJsdoc from "eslint-plugin-jsdoc";
import eslintPluginLocal from "@workspace/eslint-plugin-local";
Expand Down Expand Up @@ -128,6 +129,7 @@ export default tseslint.config(
},
},
plugins: {
["@stylistic"]: eslintStylistic,
["@susisu/safe-typescript"]: eslintPluginSafeTypeScript,
["local"]: eslintPluginLocal,
["simple-import-sort"]: eslintPluginSimpleImportSort,
Expand All @@ -137,6 +139,7 @@ export default tseslint.config(
{
files: [...GLOB_JS, ...GLOB_TS],
rules: {
curly: "warn",
eqeqeq: ["error", "always"],
"no-console": "error",
"no-else-return": "error",
Expand Down Expand Up @@ -195,6 +198,8 @@ export default tseslint.config(
// Part: simple-import-sort rules
"simple-import-sort/exports": "warn",
"simple-import-sort/imports": "warn",
// Part: stylistic rules
"@stylistic/curly-newline": ["warn", "always"],
// Part: perfectionist rules
"perfectionist/sort-exports": "off",
"perfectionist/sort-imports": "off",
Expand Down
10 changes: 5 additions & 5 deletions examples/dual-react-dom-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@examples/dual-react-dom-lib",
"version": "0.0.0",
"license": "MIT",
"sideEffects": false,
"exports": {
".": {
"import": {
Expand All @@ -16,6 +17,7 @@
"./package.json": "./package.json"
},
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist",
Expand All @@ -29,7 +31,7 @@
"prepare": "pnpm run build"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "^1.23.1",
"@eslint-react/eslint-plugin": "^1.23.2",
"@eslint/js": "^9.17.0",
"@tsconfig/node22": "^22.0.0",
"@tsconfig/strictest": "^2.0.5",
Expand All @@ -46,10 +48,8 @@
"peerDependencies": {
"react": "^19.0.0"
},
"packageManager": "[email protected]",
"engines": {
"node": ">=18.18.0"
},
"sideEffects": false,
"module": "dist/index.mjs",
"packageManager": "[email protected]"
}
}
2 changes: 1 addition & 1 deletion examples/next-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"react-dom": "latest"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "^1.23.1",
"@eslint-react/eslint-plugin": "^1.23.2",
"@eslint/config-inspector": "^0.7.0",
"@eslint/js": "^9.17.0",
"@next/eslint-plugin-next": "^15.1.3",
Expand Down
2 changes: 1 addition & 1 deletion examples/vite-react-dom-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "^1.23.1",
"@eslint-react/eslint-plugin": "^1.23.2",
"@eslint/config-inspector": "^0.7.0",
"@eslint/js": "^9.17.0",
"@tsconfig/node22": "^22.0.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/vite-react-dom-js-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "^1.23.1",
"@eslint-react/eslint-plugin": "^1.23.2",
"@eslint/config-inspector": "^0.7.0",
"@eslint/js": "^9.17.0",
"@types/react": "^19.0.3",
Expand Down
2 changes: 1 addition & 1 deletion examples/vite-react-dom-js-with-babel-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@babel/eslint-parser": "^7.25.9",
"@babel/preset-env": "^7.26.0",
"@babel/preset-react": "^7.26.3",
"@eslint-react/eslint-plugin": "^1.23.1",
"@eslint-react/eslint-plugin": "^1.23.2",
"@eslint/config-inspector": "^0.7.0",
"@eslint/js": "^9.17.0",
"@types/babel__core": "~7.20.5",
Expand Down
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"license": "MIT",
"author": "Eva1ent<[email protected]>",
"sideEffects": false,
"exports": {
".": {
"import": {
Expand All @@ -27,6 +28,7 @@
"./package.json": "./package.json"
},
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist",
Expand Down Expand Up @@ -59,7 +61,5 @@
"engines": {
"bun": ">=1.0.15",
"node": ">=18.18.0"
},
"sideEffects": false,
"module": "dist/index.mjs"
}
}
8 changes: 6 additions & 2 deletions packages/core/src/component/component-collector-legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import { ERClassComponentFlag } from "./component-flag";
* @returns `true` if the node is a class component, `false` otherwise
*/
export function isClassComponent(node: TSESTree.Node): node is AST.TSESTreeClass {
if (!("superClass" in node && node.superClass)) return false;
if (!("superClass" in node && node.superClass)) {
return false;
}
const { superClass } = node;
return match(superClass)
.with({
Expand Down Expand Up @@ -87,7 +89,9 @@ export function useComponentCollectorLegacy() {
} as const;

const collect = (node: AST.TSESTreeClass) => {
if (!isClassComponent(node)) return;
if (!isClassComponent(node)) {
return;
}
const id = AST.getClassIdentifier(node);
const key = getId();
const flag = isPureComponent(node)
Expand Down
40 changes: 30 additions & 10 deletions packages/core/src/component/component-collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ export function useComponentCollector(
};
const onFunctionExit = () => {
const { key, node, isComponent } = functionEntries.at(-1) ?? {};
if (!key || !node || !isComponent) return functionEntries.pop();
if (!key || !node || !isComponent) {
return functionEntries.pop();
}
const shouldDrop = AST.getNestedReturnStatements(node.body)
.slice()
.reverse()
Expand All @@ -88,7 +90,9 @@ export function useComponentCollector(
&& r.argument !== null
&& !JSX.isJSXValue(r.argument, jsxCtx, hint);
});
if (shouldDrop) components.delete(key);
if (shouldDrop) {
components.delete(key);
}
return functionEntries.pop();
};

Expand All @@ -107,13 +111,17 @@ export function useComponentCollector(
":function[type]:exit": onFunctionExit,
"ArrowFunctionExpression[type][body.type!='BlockStatement']"() {
const mbEntry = getCurrentFunction();
if (O.isNone(mbEntry)) return;
if (O.isNone(mbEntry)) {
return;
}
const entry = mbEntry.value;
const { body } = entry.node;
const isComponent = hasNoneOrValidComponentName(entry.node, context)
&& JSX.isJSXValue(body, jsxCtx, hint)
&& hasValidHierarchy(entry.node, context, hint);
if (!isComponent) return;
if (!isComponent) {
return;
}
const initPath = AST.getFunctionInitPath(entry.node);
const id = getFunctionComponentIdentifier(entry.node, context);
const name = O.flatMapNullable(id, getComponentNameFromIdentifier);
Expand All @@ -138,33 +146,45 @@ export function useComponentCollector(
const mbComponentName = match(left.object)
.with({ type: T.Identifier }, n => O.some(n.name))
.otherwise(O.none);
if (O.isNone(mbComponentName)) return;
if (O.isNone(mbComponentName)) {
return;
}
const componentName = mbComponentName.value;
const component = Array
.from(components.values())
.findLast(({ name }) => O.exists(name, n => n === componentName));
if (!component) return;
if (!component) {
return;
}
components.set(component._, {
...component,
displayName: O.some(right),
});
},
"CallExpression[type]:exit"(node: TSESTree.CallExpression) {
if (!isReactHookCall(node)) return;
if (!isReactHookCall(node)) {
return;
}
const mbEntry = getCurrentFunction();
if (O.isNone(mbEntry)) return;
if (O.isNone(mbEntry)) {
return;
}
const entry = mbEntry.value;
functionEntries.pop();
functionEntries.push({ ...entry, hookCalls: [...entry.hookCalls, node] });
},
"ReturnStatement[type]"(node: TSESTree.ReturnStatement) {
const mbEntry = getCurrentFunction();
if (O.isNone(mbEntry)) return;
if (O.isNone(mbEntry)) {
return;
}
const entry = mbEntry.value;
const isComponent = hasNoneOrValidComponentName(entry.node, context)
&& JSX.isJSXValue(node.argument, jsxCtx, hint)
&& hasValidHierarchy(entry.node, context, hint);
if (!isComponent) return;
if (!isComponent) {
return;
}
functionEntries.pop();
functionEntries.push({ ...entry, isComponent });
const initPath = AST.getFunctionInitPath(entry.node);
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/component/component-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import { isReactHookCallWithNameLoose } from "../hook";
import { isForwardRefCall, isMemoCall } from "../utils";

function isComponentWrapperCall(node: TSESTree.Node, context: RuleContext) {
if (node.type !== T.CallExpression) return false;
if (node.type !== T.CallExpression) {
return false;
}
return isMemoCall(node, context)
|| isForwardRefCall(node, context)
|| isReactHookCallWithNameLoose(node)("useCallback");
Expand All @@ -19,7 +21,9 @@ export function getFunctionComponentIdentifier(
context: RuleContext,
): O.Option<TSESTree.Identifier | TSESTree.Identifier[]> {
const functionId = AST.getFunctionIdentifier(node);
if (O.isSome(functionId)) return functionId;
if (O.isSome(functionId)) {
return functionId;
}
const { parent } = node;
// Get function component identifier from `const Component = memo(() => {});`
if (
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/component/component-render-method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const isRenderMethodLike = isMatching({
});

export function isFunctionOfRenderMethod(node: AST.TSESTreeFunction) {
if (!isRenderMethodLike(node.parent)) return false;
if (!isRenderMethodLike(node.parent)) {
return false;
}

return isClassComponent(node.parent.parent.parent);
}
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/effect/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ export function isSetupFunction(node: TSESTree.Node) {

export function isCleanupFunction(node: TSESTree.Node) {
const nearestRet = O.getOrNull(AST.findParentNodeGuard(node, AST.is(T.ReturnStatement)));
if (!nearestRet) return false;
if (!nearestRet) {
return false;
}
const nearestFunction = O.getOrNull(AST.findParentNodeGuard(node, AST.isFunction));
const nearestFunctionOfRet = O.getOrNull(AST.findParentNodeGuard(nearestRet, AST.isFunction));
if (!nearestFunction || !nearestFunctionOfRet) return false;
if (!nearestFunction || !nearestFunctionOfRet) {
return false;
}
return nearestFunction === nearestFunctionOfRet && isSetupFunction(nearestFunction);
}
8 changes: 6 additions & 2 deletions packages/core/src/element/get-element-represent-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import type { TSESTree } from "@typescript-eslint/types";

export function getElementRepresentName(node: TSESTree.JSXOpeningElement, context: RuleContext) {
const rawElementName = JSX.getElementName(node);
if (rawElementName === rawElementName.toLowerCase()) return rawElementName;
if (rawElementName === rawElementName.toLowerCase()) {
return rawElementName;
}
const { components, polymorphicPropName } = getSettingsFromContext(context);
const asElementName = components.get(rawElementName);
if (isString(asElementName)) return asElementName;
if (isString(asElementName)) {
return asElementName;
}
return F.pipe(
O.fromNullable(polymorphicPropName),
O.flatMap(JSX.findPropInAttributes(node.attributes, context.sourceCode.getScope(node))),
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/hook/hook-collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@ export function useHookCollector() {
":function[type]": onFunctionEnter,
":function[type]:exit": onFunctionExit,
"CallExpression[type]"(node) {
if (!isReactHookCall(node)) return;
if (!isReactHookCall(node)) {
return;
}
const [fNode, hookId] = fStack.at(-1) ?? [];
if (!fNode || !hookId) return;
if (!fNode || !hookId) {
return;
}
F.pipe(
O.Do,
O.bind("id", () => hookId),
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/hook/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ export function isReactHook(node: AST.TSESTreeFunction) {
* @returns `true` if the node is a React Hook call, `false` otherwise.
*/
export function isReactHookCall(node: TSESTree.Node) {
if (node.type !== T.CallExpression) return false;
if (node.callee.type === T.Identifier) return isReactHookName(node.callee.name);
if (node.type !== T.CallExpression) {
return false;
}
if (node.callee.type === T.Identifier) {
return isReactHookName(node.callee.name);
}
if (node.callee.type === T.MemberExpression) {
return node.callee.property.type === T.Identifier && isReactHookName(node.callee.property.name);
}
Expand Down Expand Up @@ -86,7 +90,9 @@ export function isReactHookCallWithNameAlias(name: string, context: RuleContext,
}

export function isUseEffectCallLoose(node: TSESTree.Node) {
if (node.type !== T.CallExpression) return false;
if (node.type !== T.CallExpression) {
return false;
}
switch (node.callee.type) {
case T.Identifier:
return /^use\w*Effect$/u.test(node.callee.name);
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/render-prop/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export function isRenderFunctionLoose(node: AST.TSESTreeFunction, context: RuleC
* @returns `true` if node is a render prop, `false` if not
*/
export function isRenderPropLoose(node: TSESTree.JSXAttribute, context: RuleContext) {
if (node.name.type !== T.JSXIdentifier) return false;
if (node.name.type !== T.JSXIdentifier) {
return false;
}
return node.name.name.startsWith("render")
&& node.value?.type === T.JSXExpressionContainer
&& AST.isFunction(node.value.expression)
Expand Down
Loading
Loading