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