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
6 changes: 6 additions & 0 deletions packages/core/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@
| ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [~~getClassComponentCollector~~](functions/getClassComponentCollector.md) | Get an api and visitor object for the rule to collect class components. |
| [getClassId](functions/getClassId.md) | Get the class identifier of a class node. |
| [getCreateElementChildrenArguments](functions/getCreateElementChildrenArguments.md) | Get the children arguments (the arguments after the props object) of a `createElement` call. |
| [getCreateElementProp](functions/getCreateElementProp.md) | Find a statically named property in the props object of a `createElement` call. |
| [getCreateElementPropsObject](functions/getCreateElementPropsObject.md) | Get the props object (the second argument) of a `createElement` call. |
| [getCreateElementTypeArgument](functions/getCreateElementTypeArgument.md) | Get the type argument (the first argument) of a `createElement` call. |
| [getFullyQualifiedNameEx](functions/getFullyQualifiedNameEx.md) | Get the fully qualified name of a symbol, handling cases that `ts.TypeChecker.getFullyQualifiedName` does not handle (ex: `export as namespace preact`). |
| [getFunctionComponentCollector](functions/getFunctionComponentCollector.md) | Get an api and visitor object for the rule to collect function components. |
| [getFunctionDirectives](functions/getFunctionDirectives.md) | Get the directives of a function (ex: "use strict", "use client", "use server"). |
Expand All @@ -151,6 +155,7 @@
| [~~isAssignmentToThisState~~](functions/isAssignmentToThisState.md) | Check if the assignment expression assigns to `this.state`. |
| [isBooleanLiteralType](functions/isBooleanLiteralType.md) | Check if the type is a boolean literal type. |
| [isClassComponent](functions/isClassComponent.md) | Check if the node is a class component (extends `Component` or `PureComponent`). |
| [isCreateElementChildrenArgument](functions/isCreateElementChildrenArgument.md) | Check if the node is passed as a children argument (the third argument or later) of a `createElement` call. |
| [isFunctionComponentDefinition](functions/isFunctionComponentDefinition.md) | Check if the function node is a valid React component definition. |
| [isFunctionComponentName](functions/isFunctionComponentName.md) | Check if a string matches the strict component name pattern. |
| [isFunctionComponentNameLoose](functions/isFunctionComponentNameLoose.md) | Check if a string matches the loose component name pattern. |
Expand All @@ -165,6 +170,7 @@
| [isHookId](functions/isHookId.md) | Checks if the given node is a hook identifier. |
| [isHookName](functions/isHookName.md) | Check if the name is a hook name (starts with `use` followed by an uppercase letter or digit). |
| [isHookTag](functions/isHookTag.md) | Checks if the given expression is a hook tag (callee / tagged template tag). |
| [isInsideCreateElementProps](functions/isInsideCreateElementProps.md) | Check if the node is inside the props object (the second argument) of a `createElement` call. |
| [isJsxLike](functions/isJsxLike.md) | Check if the node represents JSX-like content based on heuristics. |
| [~~isPureComponent~~](functions/isPureComponent.md) | Check if the node is a pure component (extends `PureComponent`). |
| [isRenderMethodCallback](functions/isRenderMethodCallback.md) | Check if the function is a callback passed to a class component's render method. |
Expand Down
22 changes: 22 additions & 0 deletions packages/core/docs/functions/getCreateElementChildrenArguments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[@eslint-react/core](../README.md) / getCreateElementChildrenArguments

# Function: getCreateElementChildrenArguments()

```ts
function getCreateElementChildrenArguments(context: RuleContext, node: Node | null): CallExpressionArgument[];
```

Get the children arguments (the arguments after the props object) of a `createElement` call.

## Parameters

| Parameter | Type | Description |
| --------- | ---------------- | ------------------------ |
| `context` | `RuleContext` | The ESLint rule context. |
| `node` | `Node` \| `null` | The node to inspect. |

## Returns

`CallExpressionArgument`[]

The children arguments, or an empty array when the node is not a `createElement` call.
38 changes: 38 additions & 0 deletions packages/core/docs/functions/getCreateElementProp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[@eslint-react/core](../README.md) / getCreateElementProp

# Function: getCreateElementProp()

```ts
function getCreateElementProp(
context: RuleContext,
node: Node | null,
name: string,
): Property | null;
```

Find a statically named property in the props object of a `createElement` call.

Statically resolvable names include plain identifier keys as well as
string-literal and simple template-literal keys (computed or not).

## Parameters

| Parameter | Type | Description |
| --------- | ---------------- | ---------------------------------------------------------- |
| `context` | `RuleContext` | The ESLint rule context. |
| `node` | `Node` \| `null` | The node to inspect. |
| `name` | `string` | The property name to look for (ex: `"children"`, `"key"`). |

## Returns

`Property` \| `null`

The matching `Property` node, or `null` when the call has no static property with that name.

## Example

```ts
import { getCreateElementProp } from "@eslint-react/core";

const childrenProp = getCreateElementProp(context, node, "children");
```
26 changes: 26 additions & 0 deletions packages/core/docs/functions/getCreateElementPropsObject.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[@eslint-react/core](../README.md) / getCreateElementPropsObject

# Function: getCreateElementPropsObject()

```ts
function getCreateElementPropsObject(context: RuleContext, node: Node | null): ObjectExpression | null;
```

Get the props object (the second argument) of a `createElement` call.

Type expressions and chain expressions wrapping the argument are unwrapped
before the object check; `null`, spread, or otherwise non-object props
arguments yield `null`.

## Parameters

| Parameter | Type | Description |
| --------- | ---------------- | ------------------------ |
| `context` | `RuleContext` | The ESLint rule context. |
| `node` | `Node` \| `null` | The node to inspect. |

## Returns

`ObjectExpression` \| `null`

The props `ObjectExpression`, or `null` when absent or not statically an object literal.
22 changes: 22 additions & 0 deletions packages/core/docs/functions/getCreateElementTypeArgument.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[@eslint-react/core](../README.md) / getCreateElementTypeArgument

# Function: getCreateElementTypeArgument()

```ts
function getCreateElementTypeArgument(context: RuleContext, node: Node | null): CallExpressionArgument | null;
```

Get the type argument (the first argument) of a `createElement` call.

## Parameters

| Parameter | Type | Description |
| --------- | ---------------- | ------------------------ |
| `context` | `RuleContext` | The ESLint rule context. |
| `node` | `Node` \| `null` | The node to inspect. |

## Returns

`CallExpressionArgument` \| `null`

The type argument, or `null` when the node is not a `createElement` call or has no arguments.
23 changes: 23 additions & 0 deletions packages/core/docs/functions/isCreateElementChildrenArgument.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[@eslint-react/core](../README.md) / isCreateElementChildrenArgument

# Function: isCreateElementChildrenArgument()

```ts
function isCreateElementChildrenArgument(context: RuleContext, node: Node): boolean;
```

Check if the node is passed as a children argument (the third argument or
later) of a `createElement` call.

## Parameters

| Parameter | Type | Description |
| --------- | ------------- | ------------------------ |
| `context` | `RuleContext` | The ESLint rule context. |
| `node` | `Node` | The node to check. |

## Returns

`boolean`

`true` if the node is a direct children argument of a `createElement` call.
22 changes: 22 additions & 0 deletions packages/core/docs/functions/isInsideCreateElementProps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[@eslint-react/core](../README.md) / isInsideCreateElementProps

# Function: isInsideCreateElementProps()

```ts
function isInsideCreateElementProps(context: RuleContext, node: Node): boolean;
```

Check if the node is inside the props object (the second argument) of a `createElement` call.

## Parameters

| Parameter | Type | Description |
| --------- | ------------- | ------------------------ |
| `context` | `RuleContext` | The ESLint rule context. |
| `node` | `Node` | The node to check. |

## Returns

`boolean`

`true` if the node is inside `createElement`'s props object.
59 changes: 58 additions & 1 deletion packages/core/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AST_NODE_TYPES as AST, type TSESTree } from "@typescript-eslint/types";
import { simpleTraverse } from "@typescript-eslint/typescript-estree";
import { describe, expect, it } from "vitest";

import { isAPI } from "./api";
import { isAPI, isAPICall, isCreateElementCall } from "./api";

/**
* This function mirrors the core matching logic inside `isAPI` from
Expand Down Expand Up @@ -267,3 +267,60 @@ describe("isAPI (actual export)", () => {
testAPI("React.memo;", "createElement", false);
});
});

describe("dual signature: curried form (context first)", () => {
function createMockContext(code: string): RuleContext {
return {
sourceCode: {
getText: (node: TSESTree.Node) => code.slice(node.range[0], node.range[1]),
getScope: () => ({}),
},
} as unknown as RuleContext;
}

function parseLastExpression(code: string) {
const parsed = parseCode(code);
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 ?? "unknown"}`);
}
return { context: createMockContext(code), node: last.expression };
}

it("isAPI curried form agrees with the two-argument form", () => {
const { context, node } = parseLastExpression("React.createElement;");
expect(isAPI("createElement")(context, node)).toBe(true);
expect(isAPI("createElement")(context)(node)).toBe(true);
});

it("isAPI curried form rejects non-matching nodes", () => {
const { context, node } = parseLastExpression("React.memo;");
expect(isAPI("createElement")(context, node)).toBe(false);
expect(isAPI("createElement")(context)(node)).toBe(false);
});

it("isAPICall curried form agrees with the two-argument form", () => {
const { context, node } = parseLastExpression(`React.createElement("div", null);`);
expect(isAPICall("createElement")(context, node)).toBe(true);
expect(isAPICall("createElement")(context)(node)).toBe(true);
});

it("isAPICall curried form rejects non-matching calls", () => {
const { context, node } = parseLastExpression(`React.cloneElement(element);`);
expect(isAPICall("createElement")(context, node)).toBe(false);
expect(isAPICall("createElement")(context)(node)).toBe(false);
});

it("isAPICall curried form handles null and non-call nodes", () => {
const { context, node } = parseLastExpression(`React.createElement("div", null);`);
const predicate = isAPICall("createElement")(context);
expect(predicate(null)).toBe(false);
expect(predicate(node.type === AST.CallExpression ? node.arguments[0] as TSESTree.Node : node)).toBe(false);
});

it("derived predicates (ex: isCreateElementCall) work in curried form", () => {
const { context, node } = parseLastExpression(`createElement("div", null);`);
expect(isCreateElementCall(context, node)).toBe(true);
expect(isCreateElementCall(context)(node)).toBe(true);
});
});
15 changes: 12 additions & 3 deletions packages/core/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Extract } from "@eslint-react/ast";
import type { RuleContext } from "@eslint-react/eslint";
import { dual } from "@local/eff";
import { AST_NODE_TYPES as AST, type TSESTree } from "@typescript-eslint/types";

export declare namespace isAPI {
Expand Down Expand Up @@ -29,7 +28,12 @@ export function isAPI(api: string): isAPI.ReturnType {
if (name.endsWith(`.${api}`)) return true;
return false;
};
return dual(2, func);
function dual(context: RuleContext, node: null | TSESTree.Node): boolean;
function dual(context: RuleContext): (node: null | TSESTree.Node) => boolean;
function dual(context: RuleContext, ...rest: [] | [null | TSESTree.Node]) {
return rest.length === 1 ? func(context, rest[0]) : (node: null | TSESTree.Node) => func(context, node);
}
return dual;
}

export declare namespace isAPICall {
Expand All @@ -51,7 +55,12 @@ export function isAPICall(api: string): isAPICall.ReturnType {
if (node.type !== AST.CallExpression) return false;
return isAPI(api)(context, Extract.unwrap(node.callee));
};
return dual(2, func);
function dual(context: RuleContext, node: null | TSESTree.Node): node is TSESTree.CallExpression;
function dual(context: RuleContext): (node: null | TSESTree.Node) => node is TSESTree.CallExpression;
function dual(context: RuleContext, ...rest: [] | [null | TSESTree.Node]) {
return rest.length === 1 ? func(context, rest[0]) : (node: null | TSESTree.Node): node is TSESTree.CallExpression => func(context, node);
}
return dual;
}

// React API checks
Expand Down
Loading
Loading