diff --git a/packages/core/docs/README.md b/packages/core/docs/README.md index e50b525e04..5bbcb9e718 100644 --- a/packages/core/docs/README.md +++ b/packages/core/docs/README.md @@ -17,6 +17,7 @@ - [ClassComponent](interfaces/ClassComponent.md) - [FunctionComponent](interfaces/FunctionComponent.md) - [Hook](interfaces/Hook.md) +- [JsxConfig](interfaces/JsxConfig.md) - [SemanticEntry](interfaces/SemanticEntry.md) - [SemanticNode](interfaces/SemanticNode.md) @@ -107,6 +108,7 @@ - [isUseSyncExternalStoreCall](variables/isUseSyncExternalStoreCall.md) - [isUseTransitionCall](variables/isUseTransitionCall.md) - [JSXDetectionHint](variables/JSXDetectionHint.md) +- [JsxEmit](variables/JsxEmit.md) - [REACT\_BUILTIN\_HOOK\_NAMES](variables/REACT_BUILTIN_HOOK_NAMES.md) ## Functions @@ -118,6 +120,8 @@ - [getComponentNameFromId](functions/getComponentNameFromId.md) - [getElementType](functions/getElementType.md) - [getFunctionComponentId](functions/getFunctionComponentId.md) +- [getJsxConfigFromAnnotation](functions/getJsxConfigFromAnnotation.md) +- [getJsxConfigFromContext](functions/getJsxConfigFromContext.md) - [hasAnyAttribute](functions/hasAnyAttribute.md) - [hasAttribute](functions/hasAttribute.md) - [hasEveryAttribute](functions/hasEveryAttribute.md) diff --git a/packages/core/docs/functions/getJsxConfigFromAnnotation.md b/packages/core/docs/functions/getJsxConfigFromAnnotation.md new file mode 100644 index 0000000000..aeb7967fe4 --- /dev/null +++ b/packages/core/docs/functions/getJsxConfigFromAnnotation.md @@ -0,0 +1,25 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / getJsxConfigFromAnnotation + +# Function: getJsxConfigFromAnnotation() + +> **getJsxConfigFromAnnotation**(`context`): [`JsxConfig`](../interfaces/JsxConfig.md) + +Get JsxConfig from pragma comments (annotations) in the source code. + +## Parameters + +### context + +`RuleContext` + +The RuleContext. + +## Returns + +[`JsxConfig`](../interfaces/JsxConfig.md) + +JsxConfig derived from pragma comments. diff --git a/packages/core/docs/functions/getJsxConfigFromContext.md b/packages/core/docs/functions/getJsxConfigFromContext.md new file mode 100644 index 0000000000..5168fad6d9 --- /dev/null +++ b/packages/core/docs/functions/getJsxConfigFromContext.md @@ -0,0 +1,41 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / getJsxConfigFromContext + +# Function: getJsxConfigFromContext() + +> **getJsxConfigFromContext**(`context`): `object` + +Get JsxConfig from the rule context by reading compiler options. + +## Parameters + +### context + +`RuleContext` + +The RuleContext. + +## Returns + +`object` + +JsxConfig derived from compiler options. + +### jsx + +> **jsx**: `4` \| `JsxEmit` + +### jsxFactory + +> **jsxFactory**: `string` + +### jsxFragmentFactory + +> **jsxFragmentFactory**: `string` + +### jsxImportSource + +> **jsxImportSource**: `string` diff --git a/packages/core/docs/functions/hasNoneOrLooseComponentName.md b/packages/core/docs/functions/hasNoneOrLooseComponentName.md index 5f01daca6a..f9981f99e5 100644 --- a/packages/core/docs/functions/hasNoneOrLooseComponentName.md +++ b/packages/core/docs/functions/hasNoneOrLooseComponentName.md @@ -6,7 +6,7 @@ # Function: hasNoneOrLooseComponentName() -> **hasNoneOrLooseComponentName**(`context`, `fn`): `any` +> **hasNoneOrLooseComponentName**(`context`, `fn`): `boolean` ## Parameters @@ -20,4 +20,4 @@ ## Returns -`any` +`boolean` diff --git a/packages/core/docs/functions/isComponentName.md b/packages/core/docs/functions/isComponentName.md index 84bc496dbb..48494ddc69 100644 --- a/packages/core/docs/functions/isComponentName.md +++ b/packages/core/docs/functions/isComponentName.md @@ -6,7 +6,7 @@ # Function: isComponentName() -> **isComponentName**(`name`): `any` +> **isComponentName**(`name`): `boolean` ## Parameters @@ -16,4 +16,4 @@ ## Returns -`any` +`boolean` diff --git a/packages/core/docs/functions/isComponentNameLoose.md b/packages/core/docs/functions/isComponentNameLoose.md index 817a026481..240d17f939 100644 --- a/packages/core/docs/functions/isComponentNameLoose.md +++ b/packages/core/docs/functions/isComponentNameLoose.md @@ -6,7 +6,7 @@ # Function: isComponentNameLoose() -> **isComponentNameLoose**(`name`): `any` +> **isComponentNameLoose**(`name`): `boolean` ## Parameters @@ -16,4 +16,4 @@ ## Returns -`any` +`boolean` diff --git a/packages/core/docs/interfaces/JsxConfig.md b/packages/core/docs/interfaces/JsxConfig.md new file mode 100644 index 0000000000..143ae09875 --- /dev/null +++ b/packages/core/docs/interfaces/JsxConfig.md @@ -0,0 +1,31 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / JsxConfig + +# Interface: JsxConfig + +## Properties + +### jsx? + +> `optional` **jsx**: `number` + +*** + +### jsxFactory? + +> `optional` **jsxFactory**: `string` + +*** + +### jsxFragmentFactory? + +> `optional` **jsxFragmentFactory**: `string` + +*** + +### jsxImportSource? + +> `optional` **jsxImportSource**: `string` diff --git a/packages/core/docs/variables/JsxEmit.md b/packages/core/docs/variables/JsxEmit.md new file mode 100644 index 0000000000..5a8257eab4 --- /dev/null +++ b/packages/core/docs/variables/JsxEmit.md @@ -0,0 +1,35 @@ +[**@eslint-react/core**](../README.md) + +*** + +[@eslint-react/core](../README.md) / JsxEmit + +# Variable: JsxEmit + +> `const` **JsxEmit**: `object` + +## Type Declaration + +### None + +> `readonly` **None**: `0` = `0` + +### Preserve + +> `readonly` **Preserve**: `1` = `1` + +### React + +> `readonly` **React**: `2` = `2` + +### ReactJSX + +> `readonly` **ReactJSX**: `4` = `4` + +### ReactJSXDev + +> `readonly` **ReactJSXDev**: `5` = `5` + +### ReactNative + +> `readonly` **ReactNative**: `3` = `3` diff --git a/packages/core/src/component/component-collector.ts b/packages/core/src/component/component-collector.ts index 199aa042d6..dc02d1446e 100644 --- a/packages/core/src/component/component-collector.ts +++ b/packages/core/src/component/component-collector.ts @@ -1,6 +1,6 @@ import * as AST from "@eslint-react/ast"; import { unit } from "@eslint-react/eff"; -import { type RuleContext, Selector as SEL } from "@eslint-react/kit"; +import { type RuleContext, SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION } from "@eslint-react/kit"; import { getId } from "@eslint-react/shared"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; @@ -123,7 +123,7 @@ export function useComponentCollector( }, ...collectDisplayName ? { - [SEL.DISPLAY_NAME_ASSIGNMENT_EXPRESSION](node: TSESTree.AssignmentExpression) { + [SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION](node: TSESTree.AssignmentExpression) { const { left, right } = node; if (left.type !== T.MemberExpression) return; const componentName = left.object.type === T.Identifier diff --git a/packages/core/src/component/component-name.ts b/packages/core/src/component/component-name.ts index 7af94a3be5..e23caa5577 100644 --- a/packages/core/src/component/component-name.ts +++ b/packages/core/src/component/component-name.ts @@ -1,17 +1,16 @@ import type * as AST from "@eslint-react/ast"; import { unit } from "@eslint-react/eff"; -import type { RuleContext } from "@eslint-react/kit"; -import { RegExp as RE } from "@eslint-react/kit"; +import { RE_COMPONENT_NAME, RE_COMPONENT_NAME_LOOSE, type RuleContext } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import { getFunctionComponentId } from "./component-id"; export function isComponentName(name: string) { - return RE.COMPONENT_NAME.test(name); + return RE_COMPONENT_NAME.test(name); } export function isComponentNameLoose(name: string) { - return RE.COMPONENT_NAME_LOOSE.test(name); + return RE_COMPONENT_NAME_LOOSE.test(name); } export function getComponentNameFromId(id: TSESTree.Identifier | TSESTree.Identifier[] | unit) { diff --git a/packages/core/src/jsx/index.ts b/packages/core/src/jsx/index.ts index c88fd56967..423a7b65c7 100644 --- a/packages/core/src/jsx/index.ts +++ b/packages/core/src/jsx/index.ts @@ -1,6 +1,7 @@ export * from "./jsx-attribute"; export * from "./jsx-attribute-name"; export * from "./jsx-attribute-value"; +export * from "./jsx-config"; export * from "./jsx-detection"; export * from "./jsx-element-is"; export * from "./jsx-element-type"; diff --git a/packages/utilities/kit/src/JsxConfig/JsxConfig.ts b/packages/core/src/jsx/jsx-config.ts similarity index 53% rename from packages/utilities/kit/src/JsxConfig/JsxConfig.ts rename to packages/core/src/jsx/jsx-config.ts index 278e79b132..57590c1b49 100644 --- a/packages/utilities/kit/src/JsxConfig/JsxConfig.ts +++ b/packages/core/src/jsx/jsx-config.ts @@ -1,17 +1,24 @@ /* eslint-disable perfectionist/sort-objects */ import { getOrElseUpdate } from "@eslint-react/eff"; -import * as RE from "../RegExp"; -import type { RuleContext } from "../types"; +import { + RE_ANNOTATION_JSX, + RE_ANNOTATION_JSX_FRAG, + RE_ANNOTATION_JSX_IMPORT_SOURCE, + RE_ANNOTATION_JSX_RUNTIME, + type RuleContext, +} from "@eslint-react/kit"; +// Constants for JSX emit settings. export const JsxEmit = { - None: 0, - Preserve: 1, - React: 2, - ReactNative: 3, - ReactJSX: 4, - ReactJSXDev: 5, + None: 0, // Do not emit JSX code. + Preserve: 1, // Emit .jsx files with JSX preserved. + React: 2, // Emit .js files with React.createElement calls. + ReactNative: 3, // Emit .js files with React Native specific output. + ReactJSX: 4, // Emit .js files with the new JSX transform. + ReactJSXDev: 5, // Emit .js files with the new JSX transform for development. } as const; +// Interface for JSX configuration. export interface JsxConfig { // Specifies what JSX code is generated. jsx?: number; @@ -24,52 +31,48 @@ export interface JsxConfig { } /** - * Create a JsxConfig object - * @returns JsxConfig + * Get JsxConfig from the rule context by reading compiler options. + * @param context The RuleContext. + * @returns JsxConfig derived from compiler options. */ -export function make(): JsxConfig { - return {}; -} - -/** - * Get JsxConfig from RuleContext - * @param context The RuleContext - * @returns JsxConfig - */ -export function getFromContext(context: RuleContext) { +export function getJsxConfigFromContext(context: RuleContext) { const options = context.sourceCode.parserServices?.program?.getCompilerOptions() ?? {}; return { jsx: options.jsx ?? JsxEmit.ReactJSX, jsxFactory: options.jsxFactory ?? "React.createElement", jsxFragmentFactory: options.jsxFragmentFactory ?? "React.Fragment", jsxImportSource: options.jsxImportSource ?? "react", - reactNamespace: options.reactNamespace ?? "React", }; } +// A weak map to cache JsxConfig for each source code. const cache = new WeakMap(); /** - * Get JsxConfig from annotation - * @param context The RuleContext - * @returns JsxConfig + * Get JsxConfig from pragma comments (annotations) in the source code. + * @param context The RuleContext. + * @returns JsxConfig derived from pragma comments. */ -export function getFromAnnotation(context: RuleContext) { +export function getJsxConfigFromAnnotation(context: RuleContext) { return getOrElseUpdate( cache, context.sourceCode, () => { - const options = make(); + const options: JsxConfig = {}; + // Early return if no @jsx pragma is present. if (!context.sourceCode.text.includes("@jsx")) return options; // eslint-disable-next-line perfectionist/sort-variable-declarations let jsx, jsxFrag, jsxRuntime, jsxImportSource; + // Iterate over comments in reverse to find the last pragma. for (const comment of context.sourceCode.getAllComments().reverse()) { const value = comment.value; - jsx ??= value.match(RE.ANNOTATION_JSX)?.[1]; - jsxFrag ??= value.match(RE.ANNOTATION_JSX_FRAG)?.[1]; - jsxRuntime ??= value.match(RE.ANNOTATION_JSX_RUNTIME)?.[1]; - jsxImportSource ??= value.match(RE.ANNOTATION_JSX_IMPORT_SOURCE)?.[1]; + // Match pragma comments and extract their values. + jsx ??= value.match(RE_ANNOTATION_JSX)?.[1]; + jsxFrag ??= value.match(RE_ANNOTATION_JSX_FRAG)?.[1]; + jsxRuntime ??= value.match(RE_ANNOTATION_JSX_RUNTIME)?.[1]; + jsxImportSource ??= value.match(RE_ANNOTATION_JSX_IMPORT_SOURCE)?.[1]; } + // Update options with the extracted values. if (jsx != null) options.jsxFactory = jsx; if (jsxFrag != null) options.jsxFragmentFactory = jsxFrag; if (jsxRuntime != null) options.jsx = jsxRuntime === "classic" ? JsxEmit.React : JsxEmit.ReactJSX; diff --git a/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.spec.ts b/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.spec.ts index 6caf332e70..1789edfcad 100644 --- a/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.spec.ts +++ b/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.spec.ts @@ -1,8 +1,8 @@ -import { JsxConfig } from "@eslint-react/kit"; import { RuleTester } from "@typescript-eslint/rule-tester"; import tsx from "dedent"; +import { JsxEmit } from "@eslint-react/core"; import { defaultLanguageOptionsWithTypes, getProjectForJsxEmit } from "../../../../../test"; import { stringify } from "../utils"; import rule, { RULE_NAME } from "./jsx"; @@ -12,7 +12,7 @@ const ruleTester = new RuleTester({ ...defaultLanguageOptionsWithTypes, parserOptions: { ...defaultLanguageOptionsWithTypes.parserOptions, - project: getProjectForJsxEmit(JsxConfig.JsxEmit.ReactJSX), + project: getProjectForJsxEmit(JsxEmit.ReactJSX), projectService: false, }, }, diff --git a/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.ts b/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.ts index 8deed11fb5..5d55757363 100644 --- a/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.ts +++ b/packages/plugins/eslint-plugin-react-debug/src/rules/jsx.ts @@ -1,14 +1,18 @@ -import * as ER from "@eslint-react/core"; +import { + getElementType, + getJsxConfigFromAnnotation, + getJsxConfigFromContext, + isFragmentElement, + JsxEmit, +} from "@eslint-react/core"; import { flow } from "@eslint-react/eff"; -import { JsxConfig, Reporter as RPT, type RuleContext, type RuleFeature } from "@eslint-react/kit"; +import { report, type RuleContext, type RuleFeature } from "@eslint-react/kit"; import { AST_NODE_TYPES as T, type TSESTree } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; import { match, P } from "ts-pattern"; import { createRule, stringify } from "../utils"; -const { JsxEmit } = JsxConfig; - export const RULE_NAME = "jsx"; export const RULE_FEATURES = [ @@ -35,8 +39,8 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { - const jsxConfigFromContext = JsxConfig.getFromContext(context); - const jsxConfigFromAnnotation = JsxConfig.getFromAnnotation(context); + const jsxConfigFromContext = getJsxConfigFromContext(context); + const jsxConfigFromAnnotation = getJsxConfigFromAnnotation(context); const jsxConfig = { ...jsxConfigFromContext, ...jsxConfigFromAnnotation, @@ -49,10 +53,10 @@ export function create(context: RuleContext): RuleListener { data: { json: stringify({ kind: match(node) - .with({ type: T.JSXElement }, (n) => ER.isFragmentElement(context, n) ? "fragment" : "element") + .with({ type: T.JSXElement }, (n) => isFragmentElement(context, n) ? "fragment" : "element") .with({ type: T.JSXFragment }, () => "fragment") .exhaustive(), - type: ER.getElementType(context, node), + type: getElementType(context, node), jsx: match(jsxConfig.jsx) .with(JsxEmit.None, () => "none") .with(JsxEmit.ReactJSX, () => "react-jsx") @@ -72,6 +76,6 @@ export function create(context: RuleContext): RuleListener { } as const); } return { - "JSXElement, JSXFragment": flow(getReportDescriptor(context), RPT.make(context).send), + "JSXElement, JSXFragment": flow(getReportDescriptor(context), report(context)), }; } diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts index ce918515d6..13be56efd8 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-script-url.ts @@ -1,6 +1,5 @@ import * as ER from "@eslint-react/core"; -import type { RuleContext, RuleFeature } from "@eslint-react/kit"; -import { RegExp as RE } from "@eslint-react/kit"; +import { RE_JAVASCRIPT_PROTOCOL, type RuleContext, type RuleFeature } from "@eslint-react/kit"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; @@ -35,7 +34,7 @@ export function create(context: RuleContext): RuleListener { JSXAttribute(node) { if (node.name.type !== T.JSXIdentifier || node.value == null) return; const value = ER.resolveAttributeValue(context, node).toStatic(); - if (typeof value === "string" && RE.JAVASCRIPT_PROTOCOL.test(value)) { + if (typeof value === "string" && RE_JAVASCRIPT_PROTOCOL.test(value)) { context.report({ messageId: "noScriptUrl", node: node.value, diff --git a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unknown-property.ts b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unknown-property.ts index 901e09a39a..ae3194ad2d 100644 --- a/packages/plugins/eslint-plugin-react-dom/src/rules/no-unknown-property.ts +++ b/packages/plugins/eslint-plugin-react-dom/src/rules/no-unknown-property.ts @@ -2,7 +2,7 @@ // Ported from https://github.com/jsx-eslint/eslint-plugin-react/blob/master/lib/rules/no-unknown-property.js // TODO: Port to TypeScript // @ts-nocheck -import { Reporter as RPT } from "@eslint-react/kit"; +import { report } from "@eslint-react/kit"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import { getSettingsFromContext } from "@eslint-react/shared"; import type { JSXAttribute } from "@typescript-eslint/types/dist/ast-spec"; @@ -1171,8 +1171,6 @@ export default createRule({ * @returns Rule listener */ export function create(context: RuleContext): RuleListener { - const report = RPT.make(context); - /** * Gets the ignore configuration from rule options * @returns Array of attribute names to ignore @@ -1211,7 +1209,7 @@ export function create(context: RuleContext): RuleListener // Handle data-* attributes if (isValidDataAttribute(name)) { if (getRequireDataLowercase() && hasUpperCaseCharacter(name)) { - report.send({ + context.report({ node, messageId: "dataLowercaseRequired", data: { @@ -1242,7 +1240,7 @@ export function create(context: RuleContext): RuleListener if (tagName && allowedTags) { // Report if attribute is used on a tag where it's not allowed if (allowedTags.indexOf(tagName) === -1) { - report.send({ + context.report({ node, messageId: "invalidPropOnTag", data: { @@ -1268,7 +1266,7 @@ export function create(context: RuleContext): RuleListener if (hasStandardNameButIsNotUsed) { // Suggest the correct standard name - report.send({ + context.report({ node, messageId: "unknownPropWithStandardName", data: { @@ -1283,7 +1281,7 @@ export function create(context: RuleContext): RuleListener } // Report unknown attribute - report.send({ + context.report({ node, messageId: "unknownProp", data: { diff --git a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/component-name.ts b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/component-name.ts index c0f0870e70..8a81095629 100644 --- a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/component-name.ts +++ b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/component-name.ts @@ -2,7 +2,7 @@ import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; import type { unit } from "@eslint-react/eff"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; -import { RegExp as RE } from "@eslint-react/kit"; +import { RE_CONSTANT_CASE, RE_PASCAL_CASE, toRegExp } from "@eslint-react/kit"; import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; @@ -127,7 +127,7 @@ function normalizeOptions(options: Options) { ? { rule: opts } : { ...opts, - excepts: opts.excepts?.map((s) => RE.toRegExp(s)) ?? [], + excepts: opts.excepts?.map((s) => toRegExp(s)) ?? [], }, } as const; } @@ -138,11 +138,11 @@ function isValidName(name: string | unit, options: ReturnType 3 && /^[A-Z]+$/u.test(normalized)) { return options.allowAllCaps; } - return RE.PASCAL_CASE.test(normalized); + return RE_PASCAL_CASE.test(normalized); } } diff --git a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/filename.ts b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/filename.ts index ee30a2328c..7f882c5013 100644 --- a/packages/plugins/eslint-plugin-react-naming-convention/src/rules/filename.ts +++ b/packages/plugins/eslint-plugin-react-naming-convention/src/rules/filename.ts @@ -1,6 +1,6 @@ import type { unit } from "@eslint-react/eff"; import type { RuleContext, RuleFeature } from "@eslint-react/kit"; -import { RegExp as RE } from "@eslint-react/kit"; +import { RE_CAMEL_CASE, RE_KEBAB_CASE, RE_PASCAL_CASE, RE_SNAKE_CASE, toRegExp } from "@eslint-react/kit"; import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import path from "node:path"; @@ -98,17 +98,17 @@ export function create(context: RuleContext): RuleListener { : options.rule ?? "PascalCase"; const excepts = typeof options === "string" ? [] - : (options.excepts ?? []).map((s) => RE.toRegExp(s)); + : (options.excepts ?? []).map((s) => toRegExp(s)); function validate(name: string, casing: Case = rule, ignores = excepts) { if (ignores.some((pattern) => pattern.test(name))) return true; const filteredName = name.match(/[\w.-]/gu)?.join("") ?? ""; if (filteredName.length === 0) return true; return match(casing) - .with("PascalCase", () => RE.PASCAL_CASE.test(filteredName)) - .with("camelCase", () => RE.CAMEL_CASE.test(filteredName)) - .with("kebab-case", () => RE.KEBAB_CASE.test(filteredName)) - .with("snake_case", () => RE.SNAKE_CASE.test(filteredName)) + .with("PascalCase", () => RE_PASCAL_CASE.test(filteredName)) + .with("camelCase", () => RE_CAMEL_CASE.test(filteredName)) + .with("kebab-case", () => RE_KEBAB_CASE.test(filteredName)) + .with("snake_case", () => RE_SNAKE_CASE.test(filteredName)) .exhaustive(); } diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.ts index 734d11c3ea..56f9d4cfcb 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-shorthand-fragment.ts @@ -5,7 +5,7 @@ import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; import * as ER from "@eslint-react/core"; -import { JsxConfig, type RuleContext, type RuleFeature, type RulePolicy } from "@eslint-react/kit"; +import { type RuleContext, type RuleFeature, type RulePolicy } from "@eslint-react/kit"; import { match } from "ts-pattern"; import { createRule } from "../utils"; @@ -53,8 +53,8 @@ export default createRule({ export function create(context: RuleContext): RuleListener { const policy = context.options[0] ?? defaultOptions[0]; const jsxConfig = { - ...JsxConfig.getFromContext(context), - ...JsxConfig.getFromAnnotation(context), + ...ER.getJsxConfigFromContext(context), + ...ER.getJsxConfigFromAnnotation(context), }; const { jsxFragmentFactory } = jsxConfig; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts index bd78642942..2b21448ee7 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.spec.ts @@ -1,7 +1,7 @@ -import { JsxConfig } from "@eslint-react/kit"; +import { JsxEmit } from "@eslint-react/core"; import { RuleTester } from "@typescript-eslint/rule-tester"; - import tsx from "dedent"; + import { defaultLanguageOptionsWithTypes, getProjectForJsxEmit } from "../../../../../test"; import rule, { RULE_NAME } from "./jsx-uses-react"; @@ -10,7 +10,7 @@ const ruleTester = new RuleTester({ ...defaultLanguageOptionsWithTypes, parserOptions: { ...defaultLanguageOptionsWithTypes.parserOptions, - project: getProjectForJsxEmit(JsxConfig.JsxEmit.React), + project: getProjectForJsxEmit(JsxEmit.React), projectService: false, }, }, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.ts b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.ts index ced6d67ca2..6ddc01e830 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/jsx-uses-react.ts @@ -1,11 +1,11 @@ +import * as ER from "@eslint-react/core"; +import { type RuleContext, type RuleFeature } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; - -import { JsxConfig, type RuleContext, type RuleFeature } from "@eslint-react/kit"; import type { CamelCase } from "string-ts"; import { createRule } from "../utils"; -const { JsxEmit } = JsxConfig; +const JsxEmit = ER.JsxEmit; export const RULE_NAME = "jsx-uses-react"; @@ -32,8 +32,8 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { const jsxConfig = { - ...JsxConfig.getFromContext(context), - ...JsxConfig.getFromAnnotation(context), + ...ER.getJsxConfigFromContext(context), + ...ER.getJsxConfigFromAnnotation(context), }; const { jsx, jsxFactory, jsxFragmentFactory } = jsxConfig; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.ts index dddbf0b682..c04d727979 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-array-index-key.ts @@ -1,7 +1,7 @@ import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; import { unit } from "@eslint-react/eff"; -import { Reporter as RPT, type RuleContext, type RuleFeature } from "@eslint-react/kit"; +import { report, type RuleContext, type RuleFeature } from "@eslint-react/kit"; import { coerceSettings } from "@eslint-react/shared"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { TSESTree } from "@typescript-eslint/utils"; @@ -109,7 +109,6 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { - const report = RPT.make(context); const indexParamNames: Array = []; function isArrayIndex(node: TSESTree.Node): node is TSESTree.Identifier { @@ -200,7 +199,7 @@ export function create(context: RuleContext): RuleListener { continue; } for (const descriptor of getReportDescriptors(prop.value)) { - report.send(descriptor); + report(context)(descriptor); } } }, @@ -218,7 +217,7 @@ export function create(context: RuleContext): RuleListener { return; } for (const descriptor of getReportDescriptors(node.value.expression)) { - report.send(descriptor); + report(context)(descriptor); } }, }; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-forbidden-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-forbidden-props.ts index 2d08390411..38cfd674da 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-forbidden-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-forbidden-props.ts @@ -1,4 +1,4 @@ -import { RegExp as RE, type RuleContext, type RuleFeature } from "@eslint-react/kit"; +import { type RuleContext, type RuleFeature, toRegExp } from "@eslint-react/kit"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; @@ -115,7 +115,7 @@ export function create(context: RuleContext, [option]: Optio } const forbiddenProp = typeof forbiddenPropItem === "string" ? forbiddenPropItem : forbiddenPropItem.prop; - const forbiddenPropRegExp = RE.toRegExp(forbiddenProp); + const forbiddenPropRegExp = toRegExp(forbiddenProp); if (forbiddenPropRegExp.test(name)) { context.report({ messageId, diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts index 6ca4a0e24f..0bf429bd24 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-leaked-conditional-rendering.ts @@ -1,20 +1,20 @@ import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; import { flow, unit } from "@eslint-react/eff"; -import { Reporter as RPT, type RuleContext, type RuleFeature } from "@eslint-react/kit"; +import { report, type RuleContext, type RuleFeature } from "@eslint-react/kit"; import { getSettingsFromContext } from "@eslint-react/shared"; import * as VAR from "@eslint-react/var"; import { getConstrainedTypeAtLocation } from "@typescript-eslint/type-utils"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import { ESLintUtils } from "@typescript-eslint/utils"; +import { getStaticValue } from "@typescript-eslint/utils/ast-utils"; import type { ReportDescriptor, RuleListener } from "@typescript-eslint/utils/ts-eslint"; import { compare } from "compare-versions"; import type { CamelCase } from "string-ts"; import { unionConstituents } from "ts-api-utils"; import { match, P } from "ts-pattern"; -import { getStaticValue } from "@typescript-eslint/utils/ast-utils"; import { createRule } from "../utils"; export const RULE_NAME = "no-leaked-conditional-rendering"; @@ -118,6 +118,6 @@ export function create(context: RuleContext): RuleListener { .otherwise(() => unit); } return { - JSXExpressionContainer: flow(getReportDescriptor, RPT.make(context).send), + JSXExpressionContainer: flow(getReportDescriptor, report(context)), }; } diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts index 70b2c0eff3..19f56c27eb 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-context-display-name.ts @@ -1,5 +1,10 @@ import * as ER from "@eslint-react/core"; -import { type RuleContext, type RuleFeature, Selector as SEL } from "@eslint-react/kit"; +import { + type DisplayNameAssignmentExpression, + type RuleContext, + type RuleFeature, + SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION, +} from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; @@ -86,7 +91,7 @@ export function create(context: RuleContext): RuleListener { } } }, - [SEL.DISPLAY_NAME_ASSIGNMENT_EXPRESSION](node: SEL.DisplayNameAssignmentExpression) { + [SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION](node: DisplayNameAssignmentExpression) { displayNameAssignments.push(node); }, }; diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-key.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-key.ts index c812d10fe2..60b1620d9d 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-key.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-missing-key.ts @@ -3,7 +3,7 @@ import * as ER from "@eslint-react/core"; import type { TSESTree } from "@typescript-eslint/types"; import type { ReportDescriptor, RuleListener } from "@typescript-eslint/utils/ts-eslint"; -import { Reporter as RPT, type RuleContext, type RuleFeature } from "@eslint-react/kit"; +import { report, type RuleContext, type RuleFeature } from "@eslint-react/kit"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import { match } from "ts-pattern"; @@ -36,7 +36,6 @@ export default createRule<[], MessageID>({ }); export function create(context: RuleContext): RuleListener { - const report = RPT.make(context); const state = { isWithinChildrenToArray: false }; function checkIteratorElement(node: TSESTree.Node): null | ReportDescriptor { @@ -104,7 +103,7 @@ export function create(context: RuleContext): RuleListener { const initialScope = context.sourceCode.getScope(node); for (const element of elements) { if (!ER.hasAttribute(context, "key", element.openingElement.attributes, initialScope)) { - report.send({ + context.report({ messageId: "missingKey", node: element, }); @@ -121,10 +120,11 @@ export function create(context: RuleContext): RuleListener { if (!AST.isFunction(callback)) return; const body = callback.body; if (body.type === T.BlockStatement) { - checkBlockStatement(body).forEach(report.send); + checkBlockStatement(body) + .forEach(report(context)); return; } - report.send(checkExpression(body)); + report(context)(checkExpression(body)); }, "CallExpression:exit"(node) { if (!ER.isChildrenToArrayCall(context, node)) { @@ -137,7 +137,7 @@ export function create(context: RuleContext): RuleListener { return; } if (node.parent.type === T.ArrayExpression) { - report.send({ + context.report({ messageId: "unexpectedFragmentSyntax", node, }); diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.ts index 6234186345..f1db9ac9cb 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-misused-capture-owner-stack.ts @@ -1,10 +1,11 @@ import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; -import { ContextDetection, type RuleContext, type RuleFeature } from "@eslint-react/kit"; +import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import { getSettingsFromContext } from "@eslint-react/shared"; import { AST_NODE_TYPES as T, type TSESTree } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import { isProcessEnvNodeEnvCompare } from "@eslint-react/kit"; import { createRule } from "../utils"; export const RULE_NAME = "no-misused-capture-owner-stack"; @@ -71,6 +72,6 @@ export function create(context: RuleContext): RuleListener { function isDevelopmentOnlyCheck(node: TSESTree.Node) { if (node.type !== T.IfStatement) return false; - if (ContextDetection.isProcessEnvNodeEnvCompare(node.test, "!==", "production")) return true; + if (isProcessEnvNodeEnvCompare(node.test, "!==", "production")) return true; return false; } diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-prefix.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-prefix.ts index ef961305f9..4058ff19b8 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-prefix.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unnecessary-use-prefix.ts @@ -1,7 +1,6 @@ import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; -import type { RuleContext, RuleFeature } from "@eslint-react/kit"; -import { ContextDetection } from "@eslint-react/kit"; +import { isViMockCallback, type RuleContext, type RuleFeature } from "@eslint-react/kit"; import type { TSESTree } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; @@ -65,7 +64,7 @@ export function create(context: RuleContext): RuleListener { continue; } // Skip hooks that are in a vi mock callback - if (AST.findParentNode(node, ContextDetection.isViMockCallback) != null) { + if (AST.findParentNode(node, isViMockCallback) != null) { continue; } context.report({ diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-default-props.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-default-props.ts index 16b11a92cd..c8a8aa436b 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-default-props.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-unstable-default-props.ts @@ -1,8 +1,12 @@ import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; import { getOrElseUpdate } from "@eslint-react/eff"; -import type { RuleContext, RuleFeature } from "@eslint-react/kit"; -import { Selector as SEL } from "@eslint-react/kit"; +import { + type ObjectDestructuringVariableDeclarator, + type RuleContext, + type RuleFeature, + SEL_OBJECT_DESTRUCTURING_VARIABLE_DECLARATOR, +} from "@eslint-react/kit"; import * as VAR from "@eslint-react/var"; import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; @@ -37,10 +41,7 @@ export default createRule<[], MessageID>({ export function create(context: RuleContext): RuleListener { const { ctx, listeners } = ER.useComponentCollector(context); - const declarators = new WeakMap< - AST.TSESTreeFunction, - SEL.ObjectDestructuringVariableDeclarator[] - >(); + const declarators = new WeakMap(); return { ...listeners, @@ -89,7 +90,7 @@ export function create(context: RuleContext): RuleListener { } } }, - [SEL.OBJECT_DESTRUCTURING_VARIABLE_DECLARATOR](node: SEL.ObjectDestructuringVariableDeclarator) { + [SEL_OBJECT_DESTRUCTURING_VARIABLE_DECLARATOR](node: ObjectDestructuringVariableDeclarator) { const functionEntry = ctx.getCurrentEntry(); if (functionEntry == null) return; getOrElseUpdate( diff --git a/packages/plugins/eslint-plugin/README.md b/packages/plugins/eslint-plugin/README.md index e17fab26a1..b6a0d3adab 100644 --- a/packages/plugins/eslint-plugin/README.md +++ b/packages/plugins/eslint-plugin/README.md @@ -167,8 +167,8 @@ export default defineConfig([ Contributions are welcome! -Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/feat/remove-language-preference/.github/CONTRIBUTING.md). +Please follow our [contributing guidelines](https://github.com/Rel1cx/eslint-react/tree/2.0.0/.github/CONTRIBUTING.md). ## License -This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/feat/remove-language-preference/LICENSE) file for details. +This project is licensed under the MIT License - see the [LICENSE](https://github.com/Rel1cx/eslint-react/tree/2.0.0/LICENSE) file for details. diff --git a/packages/utilities/kit/src/JsxConfig/index.ts b/packages/utilities/kit/src/JsxConfig/index.ts deleted file mode 100644 index 039fc31338..0000000000 --- a/packages/utilities/kit/src/JsxConfig/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./JsxConfig"; diff --git a/packages/utilities/kit/src/RegExp.ts b/packages/utilities/kit/src/RegExp.ts deleted file mode 100644 index 827af921b1..0000000000 --- a/packages/utilities/kit/src/RegExp.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Regular expressions for matching a HTML tag name - */ -export const HTML_TAG = /^[a-z][^-]*$/u; - -/** - * Regular expression for matching a TypeScript file extension. - */ -export const TS_EXT = /^[cm]?tsx?$/u; - -/** - * Regular expression for matching a JavaScript file extension. - */ -export const JS_EXT = /^[cm]?jsx?$/u; - -/** - * Regular expression for matching a PascalCase string. - */ -export const PASCAL_CASE = /^[A-Z][\dA-Za-z]*$/u; - -/** - * Regular expression for matching a camelCase string. - */ -export const CAMEL_CASE = /^[a-z][\dA-Za-z]*$/u; - -/** - * Regular expression for matching a kebab-case string. - */ -export const KEBAB_CASE = /^[a-z][\d\-a-z]*$/u; - -/** - * Regular expression for matching a snake_case string. - */ -export const SNAKE_CASE = /^[a-z][\d_a-z]*$/u; - -/** - * Regular expression for matching a CONSTANT_CASE string. - */ -export const CONSTANT_CASE = /^[A-Z][\d_A-Z]*$/u; - -// @see https://github.com/facebook/react/blob/6db7f4209e6f32ebde298a0b7451710dd6aa3e19/packages/react-dom-bindings/src/shared/sanitizeURL.js#L22 -// dprint-ignore -// eslint-disable-next-line no-control-regex -export const JAVASCRIPT_PROTOCOL = /^[\u0000-\u001F ]*j[\t\n\r]*a[\t\n\r]*v[\t\n\r]*a[\t\n\r]*s[\t\n\r]*c[\t\n\r]*r[\t\n\r]*i[\t\n\r]*p[\t\n\r]*t[\t\n\r]*:/iu; - -/** - * Regular expression for matching a valid JavaScript identifier. - */ -export const JS_IDENTIFIER = /^[_$a-z][\w$]*$/i; - -/** - * Regular expression for matching a RegExp string. - */ -export const REGEXP_STR = /^\/(.+)\/([A-Za-z]*)$/u; - -/** - * Regular expression for matching a `@jsx` annotation comment. - */ -export const ANNOTATION_JSX = /@jsx\s+(\S+)/u; - -/** - * Regular expression for matching a `@jsxFrag` annotation comment. - */ -export const ANNOTATION_JSX_FRAG = /@jsxFrag\s+(\S+)/u; - -/** - * Regular expression for matching a `@jsxRuntime` annotation comment. - */ -export const ANNOTATION_JSX_RUNTIME = /@jsxRuntime\s+(\S+)/u; - -/** - * Regular expression for matching a `@jsxImportSource` annotation comment. - */ -export const ANNOTATION_JSX_IMPORT_SOURCE = /@jsxImportSource\s+(\S+)/u; - -/** - * Regular expression for matching a React component name. - */ -export const COMPONENT_NAME = /^[A-Z]/u; - -/** - * Regular expression for matching a React component name (loose). - */ -export const COMPONENT_NAME_LOOSE = /^_?[A-Z]/u; - -/** - * Regular expression for matching a React Hook name. - */ -export const HOOK_NAME = /^use/u; - -/** - * Convert a string to the `RegExp`. - * Normal strings (e.g. `"foo"`) is converted to `/^foo$/` of `RegExp`. - * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`. - * @see https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts - * @param string The string to convert. - * @returns Returns the `RegExp`. - */ -export function toRegExp(string: string): { test(s: string): boolean } { - const [, pattern, flags = "u"] = REGEXP_STR.exec(string) ?? []; - if (pattern != null) return new RegExp(pattern, flags); - return { test: (s) => s === string }; -} - -/** - * Checks whether given string is regexp string - * @param string The string to check - * @returns boolean - */ -export function isRegExp(string: string): boolean { - return REGEXP_STR.test(string); -} diff --git a/packages/utilities/kit/src/Reporter.ts b/packages/utilities/kit/src/Reporter.ts deleted file mode 100644 index 4926349b12..0000000000 --- a/packages/utilities/kit/src/Reporter.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { dual, type unit } from "@eslint-react/eff"; -import type { ReportDescriptor } from "@typescript-eslint/utils/ts-eslint"; -import type { RuleContext } from "./types"; - -export interface Reporter { - send: (descriptor: unit | null | ReportDescriptor) => void; - // dprint-ignore - sendOrElse: (descriptor: unit | null | ReportDescriptor, cb: () => TElse) => unit | TElse; -} - -export const send: { - (context: RuleContext, descriptor: unit | null | ReportDescriptor): void; - (context: RuleContext): (descriptor: unit | null | ReportDescriptor) => void; -} = dual(2, (context: RuleContext, descriptor: unit | null | ReportDescriptor) => { - if (descriptor == null) return; - return context.report(descriptor); -}); - -export const sendOrElse: { - // dprint-ignore - (context: RuleContext, descriptor: unit | null | ReportDescriptor, cb: () => TElse): unit| TElse; - // dprint-ignore - (context: RuleContext): (descriptor: unit | null | ReportDescriptor) => (cb: () => TElse) => unit | TElse; -} = dual(3, (context: RuleContext, descriptor: unit | null | ReportDescriptor, cb: () => unknown) => { - if (descriptor == null) return cb(); - return context.report(descriptor); -}); - -export function make(context: RuleContext): Reporter { - return { - send: (...args) => send(context, ...args), - sendOrElse: (...args) => sendOrElse(context, ...args), - }; -} diff --git a/packages/utilities/kit/src/ContextDetection.ts b/packages/utilities/kit/src/ast/env-checks.ts similarity index 98% rename from packages/utilities/kit/src/ContextDetection.ts rename to packages/utilities/kit/src/ast/env-checks.ts index 582260f56a..463afffb56 100644 --- a/packages/utilities/kit/src/ContextDetection.ts +++ b/packages/utilities/kit/src/ast/env-checks.ts @@ -20,7 +20,7 @@ export function isProcessEnvNodeEnv(node: TSESTree.Node | null | unit): node is } /** - * Check if the given node is a binary expression that compares `process.env.NODE_ENV` with a string literal + * Check if the given node is a binary expression that compares `process.env.NODE_ENV` with a string literal. * @param node The AST node * @param operator The operator used in the comparison * @param value The string literal value to compare against diff --git a/packages/utilities/kit/src/ast/index.ts b/packages/utilities/kit/src/ast/index.ts new file mode 100644 index 0000000000..43b97bd428 --- /dev/null +++ b/packages/utilities/kit/src/ast/index.ts @@ -0,0 +1,2 @@ +export * from "./env-checks"; +export * from "./selectors"; diff --git a/packages/utilities/kit/src/Selector.ts b/packages/utilities/kit/src/ast/selectors.ts similarity index 78% rename from packages/utilities/kit/src/Selector.ts rename to packages/utilities/kit/src/ast/selectors.ts index 68e35ba1c7..9670bd2a94 100644 --- a/packages/utilities/kit/src/Selector.ts +++ b/packages/utilities/kit/src/ast/selectors.ts @@ -20,15 +20,15 @@ export type DisplayNameAssignmentExpression = TSESTree.AssignmentExpression & { right: TSESTree.Literal; }; -export const IMPLICIT_RETURN_ARROW_FUNCTION_EXPRESSION = "ArrowFunctionExpression[body.type!='BlockStatement']"; +export const SEL_IMPLICIT_RETURN_ARROW_FUNCTION_EXPRESSION = "ArrowFunctionExpression[body.type!='BlockStatement']"; -export const OBJECT_DESTRUCTURING_VARIABLE_DECLARATOR = [ +export const SEL_OBJECT_DESTRUCTURING_VARIABLE_DECLARATOR = [ "VariableDeclarator", "[id.type='ObjectPattern']", "[init.type='Identifier']", ].join(""); -export const DISPLAY_NAME_ASSIGNMENT_EXPRESSION = [ +export const SEL_DISPLAY_NAME_ASSIGNMENT_EXPRESSION = [ "AssignmentExpression", "[operator='=']", "[left.type='MemberExpression']", diff --git a/packages/utilities/kit/src/constants.ts b/packages/utilities/kit/src/constants.ts new file mode 100644 index 0000000000..7dfc3e9754 --- /dev/null +++ b/packages/utilities/kit/src/constants.ts @@ -0,0 +1,89 @@ +/** + * Regular expressions for matching a HTML tag name + */ +export const RE_HTML_TAG = /^[a-z][^-]*$/u; + +/** + * Regular expression for matching a TypeScript file extension. + */ +export const RE_TS_EXT = /^[cm]?tsx?$/u; + +/** + * Regular expression for matching a JavaScript file extension. + */ +export const RE_JS_EXT = /^[cm]?jsx?$/u; + +/** + * Regular expression for matching a PascalCase string. + */ +export const RE_PASCAL_CASE = /^[A-Z][\dA-Za-z]*$/u; + +/** + * Regular expression for matching a camelCase string. + */ +export const RE_CAMEL_CASE = /^[a-z][\dA-Za-z]*$/u; + +/** + * Regular expression for matching a kebab-case string. + */ +export const RE_KEBAB_CASE = /^[a-z][\d\-a-z]*$/u; + +/** + * Regular expression for matching a snake_case string. + */ +export const RE_SNAKE_CASE = /^[a-z][\d_a-z]*$/u; + +/** + * Regular expression for matching a CONSTANT_CASE string. + */ +export const RE_CONSTANT_CASE = /^[A-Z][\d_A-Z]*$/u; + +// @see https://github.com/facebook/react/blob/6db7f4209e6f32ebde298a0b7451710dd6aa3e19/packages/react-dom-bindings/src/shared/sanitizeURL.js#L22 +// dprint-ignore +// eslint-disable-next-line no-control-regex +export const RE_JAVASCRIPT_PROTOCOL = /^[\u0000-\u001F ]*j[\t\n\r]*a[\t\n\r]*v[\t\n\r]*a[\t\n\r]*s[\t\n\r]*c[\t\n\r]*r[\t\n\r]*i[\t\n\r]*p[\t\n\r]*t[\t\n\r]*:/iu; + +/** + * Regular expression for matching a valid JavaScript identifier. + */ +export const RE_JS_IDENTIFIER = /^[_$a-z][\w$]*$/i; + +/** + * Regular expression for matching a RegExp string. + */ +export const RE_REGEXP_STR = /^\/(.+)\/([A-Za-z]*)$/u; + +/** + * Regular expression for matching a `@jsx` annotation comment. + */ +export const RE_ANNOTATION_JSX = /@jsx\s+(\S+)/u; + +/** + * Regular expression for matching a `@jsxFrag` annotation comment. + */ +export const RE_ANNOTATION_JSX_FRAG = /@jsxFrag\s+(\S+)/u; + +/** + * Regular expression for matching a `@jsxRuntime` annotation comment. + */ +export const RE_ANNOTATION_JSX_RUNTIME = /@jsxRuntime\s+(\S+)/u; + +/** + * Regular expression for matching a `@jsxImportSource` annotation comment. + */ +export const RE_ANNOTATION_JSX_IMPORT_SOURCE = /@jsxImportSource\s+(\S+)/u; + +/** + * Regular expression for matching a React component name. + */ +export const RE_COMPONENT_NAME = /^[A-Z]/u; + +/** + * Regular expression for matching a React component name (loose). + */ +export const RE_COMPONENT_NAME_LOOSE = /^_?[A-Z]/u; + +/** + * Regular expression for matching a React Hook name. + */ +export const RE_HOOK_NAME = /^use/u; diff --git a/packages/utilities/kit/src/index.ts b/packages/utilities/kit/src/index.ts index e93d8fbfe6..c111280861 100644 --- a/packages/utilities/kit/src/index.ts +++ b/packages/utilities/kit/src/index.ts @@ -1,6 +1,4 @@ -export * as ContextDetection from "./ContextDetection"; -export * as JsxConfig from "./JsxConfig"; -export * as RegExp from "./RegExp"; -export * as Reporter from "./Reporter"; -export * as Selector from "./Selector"; +export * from "./ast"; +export * from "./constants"; export type * from "./types"; +export * from "./utils"; diff --git a/packages/utilities/kit/src/types.ts b/packages/utilities/kit/src/types/index.ts similarity index 100% rename from packages/utilities/kit/src/types.ts rename to packages/utilities/kit/src/types/index.ts diff --git a/packages/utilities/kit/src/utils/index.ts b/packages/utilities/kit/src/utils/index.ts new file mode 100644 index 0000000000..a918fcd891 --- /dev/null +++ b/packages/utilities/kit/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./regexp"; +export * from "./reporting"; diff --git a/packages/utilities/kit/src/utils/regexp.ts b/packages/utilities/kit/src/utils/regexp.ts new file mode 100644 index 0000000000..5351970cd2 --- /dev/null +++ b/packages/utilities/kit/src/utils/regexp.ts @@ -0,0 +1,24 @@ +import { RE_REGEXP_STR } from "../constants"; + +/** + * Convert a string to the `RegExp`. + * Normal strings (e.g. `"foo"`) is converted to `/^foo$/` of `RegExp`. + * Strings like `"/^foo/i"` are converted to `/^foo/i` of `RegExp`. + * @see https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/utils/regexp.ts + * @param string The string to convert. + * @returns Returns the `RegExp`. + */ +export function toRegExp(string: string): { test(s: string): boolean } { + const [, pattern, flags = "u"] = RE_REGEXP_STR.exec(string) ?? []; + if (pattern != null) return new RegExp(pattern, flags); + return { test: (s) => s === string }; +} + +/** + * Checks whether given string is regexp string + * @param string The string to check + * @returns boolean + */ +export function isRegExp(string: string): boolean { + return RE_REGEXP_STR.test(string); +} diff --git a/packages/utilities/kit/src/utils/reporting.ts b/packages/utilities/kit/src/utils/reporting.ts new file mode 100644 index 0000000000..8ad602fa4d --- /dev/null +++ b/packages/utilities/kit/src/utils/reporting.ts @@ -0,0 +1,10 @@ +import { type unit } from "@eslint-react/eff"; +import type { ReportDescriptor } from "@typescript-eslint/utils/ts-eslint"; +import type { RuleContext } from "../types"; + +export function report(context: RuleContext) { + return (descriptor: unit | null | ReportDescriptor) => { + if (descriptor == null) return; + return context.report(descriptor); + }; +}