eslint-plugin-react-naming-convention
Version:
ESLint React's ESLint plugin for naming convention related rules.
524 lines (512 loc) • 15.6 kB
JavaScript
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;
;