UNPKG

eslint-plugin-react-naming-convention

Version:

ESLint React's ESLint plugin for naming convention related rules.

468 lines (456 loc) • 13.3 kB
import { getConfigAdapters, getDocsUrl, getSettingsFromContext } from "@eslint-react/shared"; import * as AST from "@eslint-react/ast"; import * as ER from "@eslint-react/core"; import { RegExp } from "@eslint-react/kit"; import { ESLintUtils } from "@typescript-eslint/utils"; import { identity, isObject } from "@eslint-react/eff"; import { AST_NODE_TYPES } from "@typescript-eslint/types"; import { P, match } from "ts-pattern"; import path from "node:path"; import { camelCase, kebabCase, pascalCase, snakeCase } from "string-ts"; //#region rolldown:runtime var __defProp = Object.defineProperty; var __export = (all) => { let target = {}; for (var name$2 in all) __defProp(target, name$2, { get: all[name$2], enumerable: true }); return target; }; //#endregion //#region src/configs/recommended.ts var recommended_exports = __export({ name: () => name$1, rules: () => rules }); const name$1 = "react-naming-convention/recommended"; const rules = { "react-naming-convention/context-name": "warn" }; //#endregion //#region package.json var name = "eslint-plugin-react-naming-convention"; var version = "1.53.1"; //#endregion //#region src/utils/create-rule.ts const createRule = ESLintUtils.RuleCreator(getDocsUrl("naming-convention")); //#endregion //#region src/rules/component-name.ts const defaultOptions$2 = [{ allowAllCaps: false, excepts: [], rule: "PascalCase" }]; const schema$2 = [{ anyOf: [{ type: "string", enum: ["PascalCase", "CONSTANT_CASE"] }, { type: "object", additionalProperties: false, properties: { allowAllCaps: { type: "boolean" }, excepts: { type: "array", items: { type: "string", format: "regex" } }, rule: { type: "string", enum: ["PascalCase", "CONSTANT_CASE"] } } }] }]; const RULE_NAME$4 = "component-name"; var component_name_default = createRule({ meta: { type: "problem", defaultOptions: [...defaultOptions$2], docs: { description: "Enforces naming conventions for components." }, messages: { invalidComponentName: "A component name '{{name}}' does not match {{rule}}." }, schema: schema$2 }, name: RULE_NAME$4, create: create$4, defaultOptions: defaultOptions$2 }); function create$4(context) { const options = normalizeOptions(context.options); const { rule } = options; const collector = ER.useComponentCollector(context); const collectorLegacy = ER.useComponentCollectorLegacy(); return { ...collector.listeners, ...collectorLegacy.listeners, "Program:exit"(program) { const functionComponents = collector.ctx.getAllComponents(program); const classComponents = collectorLegacy.ctx.getAllComponents(program); for (const { node: component } of functionComponents.values()) { const id = AST.getFunctionId(component); if (id?.name == null) continue; const name$2 = id.name; if (isValidName(name$2, options)) continue; context.report({ messageId: "invalidComponentName", node: id, data: { name: name$2, rule } }); } for (const { node: component } of classComponents.values()) { const id = AST.getClassId(component); if (id?.name == null) continue; const name$2 = id.name; if (isValidName(name$2, options)) continue; context.report({ messageId: "invalidComponentName", node: id, data: { name: name$2, rule } }); } } }; } function normalizeOptions(options) { const opts = options[0]; const defaultOpts = defaultOptions$2[0]; if (opts == null) return defaultOpts; return { ...defaultOpts, ...typeof opts === "string" ? { rule: opts } : { ...opts, excepts: opts.excepts?.map((s) => RegExp.toRegExp(s)) ?? [] } }; } function isValidName(name$2, options) { if (name$2 == null) return true; if (options.excepts.some((regex) => regex.test(name$2))) return true; const normalized = name$2.split(".").at(-1) ?? name$2; switch (options.rule) { case "CONSTANT_CASE": return RegExp.CONSTANT_CASE.test(normalized); case "PascalCase": if (normalized.length > 3 && /^[A-Z]+$/u.test(normalized)) return options.allowAllCaps; return RegExp.PASCAL_CASE.test(normalized); } } //#endregion //#region src/rules/context-name.ts const RULE_NAME$3 = "context-name"; var context_name_default = createRule({ meta: { type: "problem", docs: { description: "Enforces context name to be a valid component name with the suffix `Context`." }, messages: { invalidContextName: "A context name must be a valid component name with the suffix 'Context'." }, schema: [] }, name: RULE_NAME$3, create: create$3, defaultOptions: [] }); function create$3(context) { if (!context.sourceCode.text.includes("createContext")) return {}; return { CallExpression(node) { if (!ER.isCreateContextCall(context, node)) return; const id = ER.getInstanceId(node); if (id == null) return; const name$2 = match(id).with({ type: AST_NODE_TYPES.Identifier, name: P.select() }, identity).with({ type: AST_NODE_TYPES.MemberExpression, property: { name: P.select(P.string) } }, identity).otherwise(() => null); if (name$2 != null && ER.isComponentName(name$2) && name$2.endsWith("Context")) return; context.report({ messageId: "invalidContextName", node: id }); } }; } //#endregion //#region src/rules/filename.ts const RULE_NAME$2 = "filename"; const defaultOptions$1 = [{ excepts: [ "index", String.raw`/^_/`, String.raw`/^\$/`, String.raw`/^[0-9]+$/`, String.raw`/^\[[^\]]+\]$/` ], rule: "PascalCase" }]; const schema$1 = [{ anyOf: [{ type: "string", enum: [ "PascalCase", "camelCase", "kebab-case", "snake_case" ] }, { type: "object", additionalProperties: false, properties: { excepts: { type: "array", items: { type: "string", format: "regex" } }, extensions: { type: "array", items: { type: "string" }, uniqueItems: true }, rule: { type: "string", enum: [ "PascalCase", "camelCase", "kebab-case", "snake_case" ] } } }] }]; var filename_default = createRule({ meta: { type: "problem", defaultOptions: [...defaultOptions$1], docs: { description: "Enforces consistent file naming conventions." }, messages: { empty: "A file must have non-empty name.", invalidCase: "A file with name '{{name}}' does not match {{rule}}. Rename it to '{{suggestion}}'." }, schema: schema$1 }, name: RULE_NAME$2, create: create$2, defaultOptions: defaultOptions$1 }); function create$2(context) { const options = context.options[0] ?? defaultOptions$1[0]; const rule = typeof options === "string" ? options : options.rule ?? "PascalCase"; const excepts = typeof options === "string" ? [] : (options.excepts ?? []).map((s) => RegExp.toRegExp(s)); function validate(name$2, casing = rule, ignores = excepts) { if (ignores.some((pattern) => pattern.test(name$2))) return true; const filteredName = name$2.match(/[\w.-]/gu)?.join("") ?? ""; if (filteredName.length === 0) return true; return match(casing).with("PascalCase", () => RegExp.PASCAL_CASE.test(filteredName)).with("camelCase", () => RegExp.CAMEL_CASE.test(filteredName)).with("kebab-case", () => RegExp.KEBAB_CASE.test(filteredName)).with("snake_case", () => RegExp.SNAKE_CASE.test(filteredName)).exhaustive(); } function getSuggestion(name$2, casing = rule) { return match(casing).with("PascalCase", () => pascalCase(name$2)).with("camelCase", () => camelCase(name$2)).with("kebab-case", () => kebabCase(name$2)).with("snake_case", () => snakeCase(name$2)).exhaustive(); } return { Program(node) { const [basename = "", ...rest] = path.basename(context.filename).split("."); if (basename.length === 0) { context.report({ messageId: "empty", node }); return; } if (validate(basename)) return; context.report({ messageId: "invalidCase", node, data: { name: context.filename, rule, suggestion: [getSuggestion(basename), ...rest].join(".") } }); } }; } //#endregion //#region src/rules/filename-extension.ts const RULE_NAME$1 = "filename-extension"; const defaultOptions = [{ allow: "as-needed", extensions: [".jsx", ".tsx"], ignoreFilesWithoutCode: false }]; const schema = [{ anyOf: [{ type: "string", enum: ["always", "as-needed"] }, { type: "object", additionalProperties: false, properties: { allow: { type: "string", enum: ["always", "as-needed"] }, extensions: { type: "array", items: { type: "string" }, uniqueItems: true }, ignoreFilesWithoutCode: { type: "boolean" } } }] }]; var filename_extension_default = createRule({ meta: { type: "problem", defaultOptions: [...defaultOptions], docs: { description: "Enforces consistent file naming conventions." }, messages: { missingJSXExtension: "Use {{extensions}} file extension for JSX files.", unnecessaryJSXExtension: "Do not use {{extensions}} file extension for files without JSX." }, schema }, name: RULE_NAME$1, create: create$1, defaultOptions }); function create$1(context) { const options = context.options[0] ?? defaultOptions[0]; const allow = isObject(options) ? options.allow : options; const extensions = isObject(options) && "extensions" in options ? options.extensions : defaultOptions[0].extensions; const extensionsString = extensions.map((ext) => `'${ext}'`).join(", "); const filename = context.filename; let hasJSXNode = false; return { JSXElement() { hasJSXNode = true; }, JSXFragment() { hasJSXNode = true; }, "Program:exit"(program) { const fileNameExt = filename.slice(filename.lastIndexOf(".")); const isJSXExt = extensions.includes(fileNameExt); if (hasJSXNode && !isJSXExt) { context.report({ messageId: "missingJSXExtension", node: program, data: { extensions: extensionsString } }); return; } const hasCode = program.body.length > 0; const ignoreFilesWithoutCode = isObject(options) && options.ignoreFilesWithoutCode === true; if (!hasCode && ignoreFilesWithoutCode) return; if (!hasJSXNode && isJSXExt && allow === "as-needed") context.report({ messageId: "unnecessaryJSXExtension", node: program, data: { extensions: extensionsString } }); } }; } //#endregion //#region src/rules/use-state.ts const RULE_NAME = "use-state"; const RULE_FEATURES = []; var use_state_default = createRule({ meta: { type: "problem", docs: { description: "Enforces destructuring and symmetric naming of `useState` hook value and setter.", [Symbol.for("rule_features")]: RULE_FEATURES }, messages: { invalidAssignment: "useState should be destructured into a value and setter pair, e.g., const [state, setState] = useState(...).", invalidSetterName: "The setter should be named 'set' followed by the capitalized state variable name, e.g., 'setState' for 'state'." }, schema: [] }, name: RULE_NAME, create, defaultOptions: [] }); function create(context) { const alias = getSettingsFromContext(context).additionalHooks.useState ?? []; const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", alias); return { CallExpression(node) { if (!isUseStateCall(node)) return; if (node.parent.type !== AST_NODE_TYPES.VariableDeclarator) { context.report({ messageId: "invalidAssignment", node }); return; } const id = ER.getInstanceId(node); if (id?.type !== AST_NODE_TYPES.ArrayPattern) { context.report({ messageId: "invalidAssignment", node: id ?? node }); return; } const [value, setter] = id.elements; if (value == null || setter == null) { context.report({ messageId: "invalidAssignment", node: id }); return; } const setterName = match(setter).with({ type: AST_NODE_TYPES.Identifier }, (id$1) => id$1.name).otherwise(() => null); if (setterName == null || !setterName.startsWith("set")) { context.report({ messageId: "invalidSetterName", node: setter }); return; } const valueName = match(value).with({ type: AST_NODE_TYPES.Identifier }, ({ name: name$2 }) => snakeCase(name$2)).with({ type: AST_NODE_TYPES.ObjectPattern }, ({ properties }) => { return properties.reduce((acc, prop) => { if (prop.type === AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier) return [...acc, prop.key.name]; return acc; }, []).join("_"); }).otherwise(() => null); if (valueName == null) { context.report({ messageId: "invalidSetterName", node: value }); return; } if (snakeCase(setterName) !== `set_${valueName}`) { context.report({ messageId: "invalidSetterName", node: setter }); return; } } }; } //#endregion //#region src/plugin.ts const plugin = { meta: { name, version }, rules: { ["component-name"]: component_name_default, ["context-name"]: context_name_default, ["filename"]: filename_default, ["filename-extension"]: filename_extension_default, ["use-state"]: use_state_default } }; //#endregion //#region src/index.ts const { toFlatConfig, toLegacyConfig } = getConfigAdapters("react-naming-convention", plugin); var src_default = { ...plugin, configs: { ["recommended"]: toFlatConfig(recommended_exports), ["recommended-legacy"]: toLegacyConfig(recommended_exports) } }; //#endregion export { src_default as default };