UNPKG

eslint-plugin-react-naming-convention

Version:

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

524 lines (512 loc) • 15.6 kB
'use strict'; var AST = require('@eslint-react/ast'); var ER2 = require('@eslint-react/core'); var kit = require('@eslint-react/kit'); var shared = require('@eslint-react/shared'); var utils = require('@typescript-eslint/utils'); var eff = require('@eslint-react/eff'); var types = require('@typescript-eslint/types'); var tsPattern = require('ts-pattern'); var path = require('path'); var stringTs = require('string-ts'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var AST__namespace = /*#__PURE__*/_interopNamespace(AST); var ER2__namespace = /*#__PURE__*/_interopNamespace(ER2); var path__default = /*#__PURE__*/_interopDefault(path); var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name3 in all) __defProp(target, name3, { get: all[name3], enumerable: true }); }; // src/configs/recommended.ts var recommended_exports = {}; __export(recommended_exports, { name: () => name, rules: () => rules }); var name = "react-naming-convention/recommended"; var rules = { "react-naming-convention/context-name": "warn" // "react-naming-convention/use-state": "warn", }; // package.json var name2 = "eslint-plugin-react-naming-convention"; var version = "1.52.2"; var createRule = utils.ESLintUtils.RuleCreator(shared.getDocsUrl("naming-convention")); // src/rules/component-name.ts var defaultOptions = [ { allowAllCaps: false, excepts: [], rule: "PascalCase" } ]; var schema = [ { anyOf: [ { type: "string", enum: ["PascalCase", "CONSTANT_CASE"] }, { type: "object", additionalProperties: false, properties: { allowAllCaps: { type: "boolean" }, /** * @todo Remove in the next major version * @deprecated */ allowLeadingUnderscore: { type: "boolean" }, /** * @todo Remove in the next major version * @deprecated */ allowNamespace: { type: "boolean" }, excepts: { type: "array", items: { type: "string", format: "regex" } }, rule: { type: "string", enum: ["PascalCase", "CONSTANT_CASE"] } } } ] } ]; var RULE_NAME = "component-name"; var component_name_default = createRule({ meta: { type: "problem", defaultOptions: [...defaultOptions], docs: { description: "Enforces naming conventions for components." }, messages: { invalid: "A component name '{{name}}' does not match {{rule}}." }, schema }, name: RULE_NAME, create, defaultOptions }); function create(context) { const options = normalizeOptions(context.options); const { rule } = options; const collector = ER2__namespace.useComponentCollector(context); const collectorLegacy = ER2__namespace.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__namespace.getFunctionId(component); if (id?.name == null) continue; const name3 = id.name; if (isValidName(name3, options)) return; context.report({ messageId: "invalid", node: id, data: { name: name3, rule } }); } for (const { node: component } of classComponents.values()) { const id = AST__namespace.getClassId(component); if (id?.name == null) continue; const name3 = id.name; if (isValidName(name3, options)) continue; context.report({ messageId: "invalid", node: id, data: { name: name3, rule } }); } } }; } function normalizeOptions(options) { const opts = options[0]; const defaultOpts = defaultOptions[0]; if (opts == null) return defaultOpts; return { ...defaultOpts, ...typeof opts === "string" ? { rule: opts } : { ...opts, excepts: opts.excepts?.map((s) => kit.RegExp.toRegExp(s)) ?? [] } }; } function isValidName(name3, options) { if (name3 == null) return true; if (options.excepts.some((regex) => regex.test(name3))) return true; const normalized = name3.split(".").at(-1) ?? name3; switch (options.rule) { case "CONSTANT_CASE": return kit.RegExp.CONSTANT_CASE.test(normalized); case "PascalCase": if (normalized.length > 3 && /^[A-Z]+$/u.test(normalized)) { return options.allowAllCaps; } return kit.RegExp.PASCAL_CASE.test(normalized); } } var RULE_NAME2 = "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: { invalid: "A context name must be a valid component name with the suffix 'Context'." }, schema: [] }, name: RULE_NAME2, create: create2, defaultOptions: [] }); function create2(context) { if (!context.sourceCode.text.includes("createContext")) return {}; return { CallExpression(node) { if (!ER2__namespace.isCreateContextCall(context, node)) return; const id = ER2__namespace.getInstanceId(node); if (id == null) return; const name3 = tsPattern.match(id).with({ type: types.AST_NODE_TYPES.Identifier, name: tsPattern.P.select() }, eff.identity).with({ type: types.AST_NODE_TYPES.MemberExpression, property: { name: tsPattern.P.select(tsPattern.P.string) } }, eff.identity).otherwise(() => null); if (name3 != null && ER2__namespace.isComponentName(name3) && name3.endsWith("Context")) return; context.report({ messageId: "invalid", node: id }); } }; } var RULE_NAME3 = "filename"; var defaultOptions2 = [ { excepts: ["^index$"], extensions: [".js", ".jsx", ".ts", ".tsx"], rule: "PascalCase" } ]; var schema2 = [ { 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: [...defaultOptions2], docs: { description: "Enforces consistent file naming conventions." }, messages: { filenameEmpty: "A file must have non-empty name.", filenameInvalid: "A file with name '{{name}}' does not match {{rule}}. Rename it to '{{suggestion}}'." }, schema: schema2 }, name: RULE_NAME3, create: create3, defaultOptions: defaultOptions2 }); function create3(context) { const options = context.options[0] ?? defaultOptions2[0]; const rule = typeof options === "string" ? options : options.rule ?? "PascalCase"; const excepts = typeof options === "string" ? [] : options.excepts ?? []; function validate(name3, casing = rule, ignores = excepts) { const shouldIgnore = ignores.map((s) => kit.RegExp.toRegExp(s)).some((pattern) => pattern.test(name3)); if (shouldIgnore) return true; return tsPattern.match(casing).with("PascalCase", () => kit.RegExp.PASCAL_CASE.test(name3)).with("camelCase", () => kit.RegExp.CAMEL_CASE.test(name3)).with("kebab-case", () => kit.RegExp.KEBAB_CASE.test(name3)).with("snake_case", () => kit.RegExp.SNAKE_CASE.test(name3)).exhaustive(); } function getSuggestion(name3, casing = rule) { return tsPattern.match(casing).with("PascalCase", () => stringTs.pascalCase(name3)).with("camelCase", () => stringTs.camelCase(name3)).with("kebab-case", () => stringTs.kebabCase(name3)).with("snake_case", () => stringTs.snakeCase(name3)).exhaustive(); } return { Program(node) { const [basename = "", ...rest] = path__default.default.basename(context.filename).split("."); if (basename.length === 0) { context.report({ messageId: "filenameEmpty", node }); return; } if (validate(basename)) { return; } context.report({ messageId: "filenameInvalid", node, data: { name: context.filename, rule, suggestion: [getSuggestion(basename), ...rest].join(".") } }); } }; } var RULE_NAME4 = "filename-extension"; var defaultOptions3 = [{ allow: "as-needed", extensions: [".jsx", ".tsx"], ignoreFilesWithoutCode: false }]; var schema3 = [ { 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: [...defaultOptions3], docs: { description: "Enforces consistent file naming conventions." }, messages: { useJsxFileExtension: "Use {{extensions}} file extension for JSX files.", useNonJsxFileExtension: "Do not use {{extensions}} file extension for files without JSX." }, schema: schema3 }, name: RULE_NAME4, create: create4, defaultOptions: defaultOptions3 }); function create4(context) { const options = context.options[0] ?? defaultOptions3[0]; const allow = eff.isObject(options) ? options.allow : options; const extensions = eff.isObject(options) && "extensions" in options ? options.extensions : defaultOptions3[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: "useJsxFileExtension", node: program, data: { extensions: extensionsString } }); return; } const hasCode = program.body.length > 0; const ignoreFilesWithoutCode = eff.isObject(options) && options.ignoreFilesWithoutCode === true; if (!hasCode && ignoreFilesWithoutCode) { return; } if (!hasJSXNode && isJSXExt && allow === "as-needed") { context.report({ messageId: "useNonJsxFileExtension", node: program, data: { extensions: extensionsString } }); } } }; } var RULE_NAME5 = "use-state"; var 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: { invalidSetterNaming: "The setter should be named 'set' followed by the capitalized state variable name, e.g., 'setState' for 'state'.", missingDestructuring: "useState should be destructured into a value and setter pair, e.g., const [state, setState] = useState(...)." }, schema: [] }, name: RULE_NAME5, create: create5, defaultOptions: [] }); function create5(context) { const alias = shared.getSettingsFromContext(context).additionalHooks.useState ?? []; const isUseStateCall = ER2__namespace.isReactHookCallWithNameAlias(context, "useState", alias); return { CallExpression(node) { if (!isUseStateCall(node)) { return; } if (node.parent.type !== types.AST_NODE_TYPES.VariableDeclarator) { context.report({ messageId: "missingDestructuring", node }); return; } const id = ER2__namespace.getInstanceId(node); if (id?.type !== types.AST_NODE_TYPES.ArrayPattern) { context.report({ messageId: "missingDestructuring", node: id ?? node }); return; } const [value, setter] = id.elements; if (value == null || setter == null) { context.report({ messageId: "missingDestructuring", node: id }); return; } const setterName = tsPattern.match(setter).with({ type: types.AST_NODE_TYPES.Identifier }, (id2) => id2.name).otherwise(() => null); if (setterName == null || !setterName.startsWith("set")) { context.report({ messageId: "invalidSetterNaming", node: setter }); return; } const valueName = tsPattern.match(value).with({ type: types.AST_NODE_TYPES.Identifier }, ({ name: name3 }) => stringTs.snakeCase(name3)).with({ type: types.AST_NODE_TYPES.ObjectPattern }, ({ properties }) => { const values = properties.reduce((acc, prop) => { if (prop.type === types.AST_NODE_TYPES.Property && prop.key.type === types.AST_NODE_TYPES.Identifier) { return [...acc, prop.key.name]; } return acc; }, []); return values.join("_"); }).otherwise(() => null); if (valueName == null) { context.report({ messageId: "invalidSetterNaming", node: value }); return; } if (stringTs.snakeCase(setterName) !== `set_${valueName}`) { context.report({ messageId: "invalidSetterNaming", node: setter }); return; } } }; } // src/plugin.ts var plugin = { meta: { name: name2, 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 } }; // src/index.ts function makeConfig(config) { return { ...config, plugins: { "react-naming-convention": plugin } }; } function makeLegacyConfig({ rules: rules2 }) { return { plugins: ["react-naming-convention"], rules: rules2 }; } var index_default = { ...plugin, configs: { ["recommended"]: makeConfig(recommended_exports), ["recommended-legacy"]: makeLegacyConfig(recommended_exports) } }; module.exports = index_default;