Skip to content

Commit

Permalink
feat(plugins/x): add 'no-use-context', closes #930 (#931)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rel1cx authored Jan 28, 2025
1 parent d42bb89 commit 0d60aac
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/plugins/eslint-plugin-react-x/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default [
"react-x/no-unstable-default-props": "warn",
"react-x/no-unused-class-component-members": "warn",
"react-x/no-unused-state": "warn",
"react-x/no-use-context": "warn",
"react-x/use-jsx-vars": "warn",
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/eslint-plugin-react-x/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import noUnstableContextValue from "./rules/no-unstable-context-value";
import noUnstableDefaultProps from "./rules/no-unstable-default-props";
import noUnusedClassComponentMembers from "./rules/no-unused-class-component-members";
import noUnusedState from "./rules/no-unused-state";
import noUseContext from "./rules/no-use-context";
import noUselessFragment from "./rules/no-useless-fragment";
import preferDestructuringAssignment from "./rules/prefer-destructuring-assignment";
import preferReactNamespaceImport from "./rules/prefer-react-namespace-import";
Expand Down Expand Up @@ -99,6 +100,7 @@ export default {
"no-unstable-default-props": noUnstableDefaultProps,
"no-unused-class-component-members": noUnusedClassComponentMembers,
"no-unused-state": noUnusedState,
"no-use-context": noUseContext,
"no-useless-fragment": noUselessFragment,
"prefer-destructuring-assignment": preferDestructuringAssignment,
"prefer-react-namespace-import": preferReactNamespaceImport,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { ruleTester } from "../../../../../test";
import rule, { RULE_NAME } from "./no-use-context";

ruleTester.run(RULE_NAME, rule, {
invalid: [
{
code: /* tsx */ `
import { useContext } from 'react'
export const Component = () => {
const value = useContext(MyContext)
return <div>{value}</div>
}
`,
errors: [
{ messageId: "noUseContext" },
{ messageId: "noUseContext" },
],
output: /* tsx */ `
import { use } from 'react'
export const Component = () => {
const value = use(MyContext)
return <div>{value}</div>
}
`,
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import { useContext } from 'react'
export const Component = () => {
const value = useContext<MyContext>(MyContext)
return <div>{value}</div>
}
`,
errors: [
{ messageId: "noUseContext" },
{ messageId: "noUseContext" },
],
output: /* tsx */ `
import { use } from 'react'
export const Component = () => {
const value = use<MyContext>(MyContext)
return <div>{value}</div>
}
`,
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import { use, useContext } from 'react'
export const Component = () => {
const value = useContext(MyContext)
return <div>{value}</div>
}
`,
errors: [
{ messageId: "noUseContext" },
{ messageId: "noUseContext" },
],
output: /* tsx */ `
import { use, } from 'react'
export const Component = () => {
const value = use(MyContext)
return <div>{value}</div>
}
`,
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import React from 'react'
export const Component = () => {
const value = React.useContext(MyContext)
return <div>{value}</div>
}
`,
errors: [
{ messageId: "noUseContext" },
],
output: /* tsx */ `
import React from 'react'
export const Component = () => {
const value = React.use(MyContext)
return <div>{value}</div>
}
`,
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import { use, useContext as useCtx } from 'react'
export const Component = () => {
const value = useCtx(MyContext)
return <div>{value}</div>
}
`,
errors: [
{ messageId: "noUseContext" },
{ messageId: "noUseContext" },
],
output: /* tsx */ `
import { use, useContext as useCtx } from 'react'
export const Component = () => {
const value = use(MyContext)
return <div>{value}</div>
}
`,
settings: {
"react-x": {
version: "19.0.0",
},
},
},
],
valid: [
{
code: /* tsx */ `
import { useContext } from 'react'
export const Component = () => {
const value = useContext(MyContext)
return <div>{value}</div>
}
`,
settings: {
"react-x": {
version: "18.3.1",
},
},
},
{
code: /* tsx */ `
import { use } from 'react'
export const Component = () => {
const value = use(MyContext)
return <div>{value}</div>
}
`,
settings: {
"react-x": {
version: "19.0.0",
},
},
},
{
code: /* tsx */ `
import React from 'react'
export const Component = () => {
const value = React.use(MyContext)
return <div>{value}</div>
}
`,
settings: {
"react-x": {
version: "19.0.0",
},
},
},
],
});
100 changes: 100 additions & 0 deletions packages/plugins/eslint-plugin-react-x/src/rules/no-use-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { isReactHookCall, isReactHookCallWithNameAlias } from "@eslint-react/core";
import type { RuleFeature } from "@eslint-react/shared";
import { getSettingsFromContext } from "@eslint-react/shared";
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
import { compare } from "compare-versions";
import type { CamelCase } from "string-ts";
import { isMatching } from "ts-pattern";

import { createRule } from "../utils";

export const RULE_NAME = "no-use-context";

export const RULE_FEATURES = [
"CHK",
"MOD",
] as const satisfies RuleFeature[];

export type MessageID = CamelCase<typeof RULE_NAME>;

export default createRule<[], MessageID>({
meta: {
type: "problem",
docs: {
description: "disallow the use of 'useContext'",
[Symbol.for("rule_features")]: RULE_FEATURES,
},
fixable: "code",
messages: {
noUseContext: "In React 19, 'use' is preferred over 'useContext' because it is more flexible.",
},
schema: [],
},
name: RULE_NAME,
create(context) {
const settings = getSettingsFromContext(context);
const useContextAlias = new Set<string>();

if (!context.sourceCode.text.includes("useContext")) {
return {};
}
if (compare(settings.version, "19.0.0", "<")) {
return {};
}
return {
CallExpression(node) {
if (!isReactHookCall(node)) {
return;
}
if (!isReactHookCallWithNameAlias("useContext", context, [...useContextAlias])(node)) {
return;
}
context.report({
messageId: "noUseContext",
node,
fix(fixer) {
switch (node.callee.type) {
case T.Identifier:
return fixer.replaceText(node.callee, "use");
case T.MemberExpression:
return fixer.replaceText(node.callee.property, "use");
}
return null;
},
});
},
ImportDeclaration(node) {
if (node.source.value !== settings.importSource) {
return;
}
const isUseImported = node.specifiers
.some(isMatching({ local: { type: T.Identifier, name: "use" } }));
for (const specifier of node.specifiers) {
if (specifier.type !== T.ImportSpecifier) continue;
if (specifier.imported.type !== T.Identifier) continue;
if (specifier.imported.name === "useContext") {
if (specifier.local.name !== "useContext") {
useContextAlias.add(specifier.local.name);
context.report({
messageId: "noUseContext",
node: specifier,
});
return;
}
context.report({
messageId: "noUseContext",
node: specifier,
fix(fixer) {
if (isUseImported) {
return fixer.replaceText(specifier, " ".repeat(specifier.range[1] - specifier.range[0]));
}
return fixer.replaceText(specifier.imported, "use");
},
});
}
}
},
};
},
defaultOptions: [],
});
1 change: 1 addition & 0 deletions packages/plugins/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const rules = {
"@eslint-react/no-unstable-default-props": "warn",
"@eslint-react/no-unused-class-component-members": "warn",
"@eslint-react/no-unused-state": "warn",
"@eslint-react/no-use-context": "warn",
"@eslint-react/no-useless-fragment": "warn",
"@eslint-react/prefer-destructuring-assignment": "warn",
"@eslint-react/prefer-shorthand-boolean": "warn",
Expand Down
1 change: 1 addition & 0 deletions packages/plugins/eslint-plugin/src/configs/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const rules = {
"@eslint-react/no-unstable-default-props": "warn",
"@eslint-react/no-unused-class-component-members": "warn",
"@eslint-react/no-unused-state": "warn",
"@eslint-react/no-use-context": "warn",
"@eslint-react/use-jsx-vars": "warn",
} as const satisfies RulePreset;

Expand Down
1 change: 1 addition & 0 deletions website/content/docs/rules/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"no-unstable-default-props",
"no-unused-class-component-members",
"no-unused-state",
"no-use-context",
"no-useless-fragment",
"prefer-destructuring-assignment",
"prefer-react-namespace-import",
Expand Down
Loading

0 comments on commit 0d60aac

Please sign in to comment.