Skip to content

Commit

Permalink
Merge pull request #4 from meitner-se/feature/no-use-prefix-for-non-hook
Browse files Browse the repository at this point in the history
Feature/no use prefix for non hook
  • Loading branch information
MattisAbrahamsson authored May 6, 2024
2 parents aabacd5 + be15a89 commit 795c370
Show file tree
Hide file tree
Showing 5 changed files with 338 additions and 1 deletion.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,35 @@ export type Options = {

export default myFunction(parameters: Options) {...}
```

### no-use-prefix-for-non-hook

Custom hooks are identified using a `use` prefix, naming normal functions, variables or others with a `use` prefix can cause confusion.

This rule forbids functions and variables being prefixed with `use` if they do not contain other hooks.

Examples of valid code

```js
const useCustom = () => {
const [state, setState] = useState("");

return { state, setState };
};

const useCustom = () => useState("");

const useCustom = useState;
```

Examples of invalid code

```js
const useCustom = () => {
return "Hello world";
};

const useCustom = () => new Date();

const useCustom = new Date();
```
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"name": "@meitner/eslint-plugin",
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 2 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { noInlineFunctionParameterTypeAnnotation } from "./noInlineFunctionParameterTypeAnnotation";
import { noMixedExports } from "./noMixedExports";
import { noUsePrefixForNonHook } from "./noUsePrefixForNonHook";

const rules = {
"no-inline-function-parameter-type-annotation":
noInlineFunctionParameterTypeAnnotation,
"no-mixed-exports": noMixedExports,
"no-use-prefix-for-non-hook": noUsePrefixForNonHook,
};

export { rules };
195 changes: 195 additions & 0 deletions src/rules/noUsePrefixForNonHook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import {
ArrowFunctionExpression,
FunctionDeclaration,
LeftHandSideExpression,
} from "@typescript-eslint/types/dist/generated/ast-spec";
import { ESLintUtils } from "@typescript-eslint/utils";

const PREFIX_REGEX = /^use[A-Z]/;

function hasUsePrefix(name: string) {
return name.match(PREFIX_REGEX) !== null;
}

function calleeHasUsePrefix(callee: LeftHandSideExpression) {
if (callee.type === "Identifier") {
return hasUsePrefix(callee.name);
}

if (callee.type === "MemberExpression") {
return callee.property.type === "Identifier"
? hasUsePrefix(callee.property.name)
: false;
}

return false;
}

function hasVariableDeclarationWithUsePrefix(
node: ArrowFunctionExpression | FunctionDeclaration
) {
return (
node.body.type === "BlockStatement" &&
node.body.body.some(
(statement) =>
statement.type === "VariableDeclaration" &&
statement.declarations.some(
(declaration) =>
declaration.init &&
declaration.init.type === "CallExpression" &&
calleeHasUsePrefix(declaration.init.callee)
)
)
);
}

function hasReturnStatementWithUsePrefix(
node: ArrowFunctionExpression | FunctionDeclaration
) {
return (
node.body.type === "BlockStatement" &&
node.body.body.some(
(statement) =>
statement.type === "ReturnStatement" &&
statement.argument &&
statement.argument.type === "CallExpression" &&
calleeHasUsePrefix(statement.argument.callee)
)
);
}

function hasFunctionInvokationWithUsePrefix(
node: ArrowFunctionExpression | FunctionDeclaration
) {
return (
node.body.type === "BlockStatement" &&
node.body.body.some(
(statement) =>
statement.type === "ExpressionStatement" &&
statement.expression.type === "CallExpression" &&
calleeHasUsePrefix(statement.expression.callee)
)
);
}

function arrowFunctionHasImplicitReturnWithUsePrefix(
node: ArrowFunctionExpression
) {
return (
node.body.type !== "BlockStatement" &&
node.body.type === "CallExpression" &&
node.body.callee.type === "Identifier" &&
hasUsePrefix(node.body.callee.name)
);
}

function getArrowFunctionName(node: ArrowFunctionExpression) {
if (!("id" in node.parent)) {
return;
}

if (!node.parent.id) {
return;
}

if (!("name" in node.parent.id)) {
return;
}

return node.parent.id.name;
}

export const noUsePrefixForNonHook = ESLintUtils.RuleCreator.withoutDocs({
create(context) {
return {
ArrowFunctionExpression(node) {
const name = getArrowFunctionName(node);

if (!name || !hasUsePrefix(name)) {
return;
}

// Check if the function uses hooks
if (
hasVariableDeclarationWithUsePrefix(node) ||
hasReturnStatementWithUsePrefix(node) ||
hasFunctionInvokationWithUsePrefix(node) ||
arrowFunctionHasImplicitReturnWithUsePrefix(node)
) {
return;
}

context.report({
node,
messageId: "noUsePrefixForNonHook",
});
},
FunctionDeclaration(node) {
const name = node.id?.name;

if (!name || !hasUsePrefix(name)) {
return;
}

// Check if the function uses hooks
if (
hasVariableDeclarationWithUsePrefix(node) ||
hasReturnStatementWithUsePrefix(node) ||
hasFunctionInvokationWithUsePrefix(node)
) {
return;
}

context.report({
node,
messageId: "noUsePrefixForNonHook",
});
},
VariableDeclaration(node) {
const declaration = node.declarations[0];

if (!declaration) {
return;
}

const name =
"name" in declaration.id ? declaration?.id.name : "";

if (!name || !hasUsePrefix(name)) {
return;
}

// Check if the variable is assigned an arrow function
if (
declaration.init &&
declaration.init.type === "ArrowFunctionExpression"
) {
// We check arrow functions in the ArrowFunctionExpression visitor
return;
}

if (
declaration.init &&
declaration.init.type === "Identifier" &&
hasUsePrefix(declaration.init.name)
) {
return;
}

context.report({
node,
messageId: "noUsePrefixForNonHook",
});
},
};
},
meta: {
messages: {
noUsePrefixForNonHook:
"Do not use 'use' prefix for non-hook functions.",
},
type: "problem",
schema: [],
},
defaultOptions: [],
});
109 changes: 109 additions & 0 deletions src/tests/noUsePrefixForNonHook.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import * as vitest from "vitest";
import { noUsePrefixForNonHook } from "../rules/noUsePrefixForNonHook";

RuleTester.afterAll = vitest.afterAll;
RuleTester.it = vitest.it;
RuleTester.itOnly = vitest.it.only;
RuleTester.describe = vitest.describe;

const ruleTester = new RuleTester({
parser: "@typescript-eslint/parser",
});

ruleTester.run("noUsePrefixForNonHook", noUsePrefixForNonHook, {
valid: [
'const useCustom = () => {const [state, setState] = useState(""); return {state, setState};};',
'const useCustom = () => {const [state, setState] = useState(""); return {state, setState};};',
"const useCustom = () => {const query = Queries.useUserQuery(); return query};",
'const useCustom = () => {return useState("");};',
"const useCustom = () => {useEffect(() => {});};",
'const useCustom = () => useState("");',
"const useCustom = useState;",
"const myFunction = () => {};",
"const userFunction = () => {};",
'function useCustom() {const [state, setState] = useState(""); return {state, setState};}',
"function useCustom() {const query = Queries.useUserQuery(); return query};",
'function useCustom() {return useState("");}',
"function useCustom() {useEffect(() => {});}",
"function myFunction() {}",
"function userFunction() {}",
"const user = null;",
"const myVariable = null;",
"const data = useUserData();",
],
invalid: [
{
code: "const useCustom = () => {};",
errors: [
{
messageId: "noUsePrefixForNonHook",
},
],
},
{
code: "const useCustom = () => {return userFunction();};",
errors: [
{
messageId: "noUsePrefixForNonHook",
},
],
},
{
code: "const useCustom = () => {const data = userFunction(); return data;};",
errors: [
{
messageId: "noUsePrefixForNonHook",
},
],
},
{
code: "function useCustom() {}",
errors: [
{
messageId: "noUsePrefixForNonHook",
},
],
},
{
code: "function useCustom() {return userFunction();}",
errors: [
{
messageId: "noUsePrefixForNonHook",
},
],
},
{
code: "function useCustom() {const data = userFunction(); return data;}",
errors: [
{
messageId: "noUsePrefixForNonHook",
},
],
},
{
code: "const useCustom = null;",
errors: [
{
messageId: "noUsePrefixForNonHook",
},
],
},
{
code: "function myFunction() {}; const useCustom = myFunction;",
errors: [
{
messageId: "noUsePrefixForNonHook",
},
],
},
{
code: "const myVariable = null; const useCustom = myVariable;",
errors: [
{
messageId: "noUsePrefixForNonHook",
},
],
},
],
});

0 comments on commit 795c370

Please sign in to comment.