eslint-plugin-react-naming-convention
Version:
ESLint React's ESLint plugin for naming convention related rules.
472 lines (463 loc) • 14.3 kB
JavaScript
import * as AST from '@eslint-react/ast';
import { useComponentCollector, useComponentCollectorLegacy, isCreateContextCall, getInstanceId } from '@eslint-react/core';
import { createRuleForPlugin, RE_PASCAL_CASE, RE_CONSTANT_CASE, RE_CAMEL_CASE, RE_KEBAB_CASE, RE_SNAKE_CASE } from '@eslint-react/shared';
import { identity, _, isObject } from '@eslint-react/eff';
import { AST_NODE_TYPES } from '@typescript-eslint/types';
import { match, P } from 'ts-pattern';
import path from 'node:path';
import { snakeCase, pascalCase, camelCase, kebabCase } from 'string-ts';
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.31.0";
var createRule = createRuleForPlugin("naming-convention");
// src/utils/regexp.ts
var RE_REGEXP_STR = /^\/(.+)\/([A-Za-z]*)$/u;
function toRegExp(string) {
const [, pattern, flags = "u"] = RE_REGEXP_STR.exec(string) ?? [];
if (pattern) return new RegExp(pattern, flags);
return { test: (s) => s === string };
}
// 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: "enforce naming convention for components"
},
messages: {
invalid: "A component name '{{name}}' does not match {{rule}}."
},
schema
},
name: RULE_NAME,
create(context) {
const options = normalizeOptions(context.options);
const { rule } = options;
const collector = useComponentCollector(context);
const collectorLegacy = useComponentCollectorLegacy();
return {
...collector.listeners,
...collectorLegacy.listeners,
"Program:exit"(node) {
const functionComponents = collector.ctx.getAllComponents(node);
const classComponents = collectorLegacy.ctx.getAllComponents(node);
for (const { node: component } of functionComponents.values()) {
const id = AST.getFunctionIdentifier(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.getClassIdentifier(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 }
});
}
}
};
},
defaultOptions
});
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(toRegExp) ?? []
}
};
}
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 RE_CONSTANT_CASE.test(normalized);
case "PascalCase":
if (normalized.length > 3 && /^[A-Z]+$/u.test(normalized)) {
return options.allowAllCaps;
}
return RE_PASCAL_CASE.test(normalized);
}
}
var RULE_NAME2 = "context-name";
var context_name_default = createRule({
meta: {
type: "problem",
docs: {
description: "enforce 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(context) {
if (!context.sourceCode.text.includes("createContext")) return {};
return {
CallExpression(node) {
if (!isCreateContextCall(context, node)) return;
const id = getInstanceId(node);
if (id == null) return;
const name3 = 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(() => _);
if (name3 != null && /^[A-Z]/u.test(name3) && name3.endsWith("Context")) return;
context.report({
messageId: "invalid",
node: id
});
}
};
},
defaultOptions: []
});
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: "enforce naming convention for JSX filenames"
},
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(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(toRegExp).some((pattern) => pattern.test(name3));
if (shouldIgnore) return true;
return match(casing).with("PascalCase", () => RE_PASCAL_CASE.test(name3)).with("camelCase", () => RE_CAMEL_CASE.test(name3)).with("kebab-case", () => RE_KEBAB_CASE.test(name3)).with("snake_case", () => RE_SNAKE_CASE.test(name3)).exhaustive();
}
function getSuggestion(name3, casing = rule) {
return match(casing).with("PascalCase", () => pascalCase(name3)).with("camelCase", () => camelCase(name3)).with("kebab-case", () => kebabCase(name3)).with("snake_case", () => snakeCase(name3)).exhaustive();
}
return {
Program(node) {
const [basename = "", ...rest] = path.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(".")
}
});
}
};
},
defaultOptions: defaultOptions2
});
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: "enforce naming convention for JSX file extensions"
},
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(context) {
const options = context.options[0] ?? defaultOptions3[0];
const allow = isObject(options) ? options.allow : options;
const extensions = 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"(node) {
const fileNameExt = filename.slice(filename.lastIndexOf("."));
const isJSXExt = extensions.includes(fileNameExt);
if (hasJSXNode && !isJSXExt) {
context.report({
messageId: "useJsxFileExtension",
node,
data: {
extensions: extensionsString
}
});
return;
}
const hasCode = node.body.length > 0;
const ignoreFilesWithoutCode = isObject(options) && options.ignoreFilesWithoutCode === true;
if (!hasCode && ignoreFilesWithoutCode) {
return;
}
if (!hasJSXNode && isJSXExt && allow === "as-needed") {
context.report({
messageId: "useNonJsxFileExtension",
node,
data: {
extensions: extensionsString
}
});
}
}
};
},
defaultOptions: defaultOptions3
});
var RULE_NAME5 = "use-state";
var RULE_FEATURES = [
"CHK"
];
var use_state_default = createRule({
meta: {
type: "problem",
docs: {
description: "enforce destructuring and symmetric naming of 'useState' hook value and setter",
[Symbol.for("rule_features")]: RULE_FEATURES
},
messages: {
invalid: "An useState call is not destructured into value + setter pair."
},
schema: []
},
name: RULE_NAME5,
create(context) {
return {
"CallExpression[callee.name='useState']"(node) {
if (node.parent.type !== AST_NODE_TYPES.VariableDeclarator) {
context.report({ messageId: "invalid", node });
}
const id = getInstanceId(node);
if (id?.type !== AST_NODE_TYPES.ArrayPattern) {
context.report({ messageId: "invalid", node });
return;
}
const [value, setter] = id.elements;
if (value == null || setter == null) {
context.report({ messageId: "invalid", node });
return;
}
const setterName = match(setter).with({ type: AST_NODE_TYPES.Identifier }, (id2) => id2.name).otherwise(() => _);
if (setterName == null || !setterName.startsWith("set")) {
context.report({ messageId: "invalid", node });
return;
}
const valueName = match(value).with({ type: AST_NODE_TYPES.Identifier }, ({ name: name3 }) => snakeCase(name3)).with({ type: AST_NODE_TYPES.ObjectPattern }, ({ properties }) => {
const values = 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;
}, []);
return values.join("_");
}).otherwise(() => _);
if (valueName == null || `set_${valueName}` !== snakeCase(setterName)) {
context.report({ messageId: "invalid", node });
return;
}
}
};
},
defaultOptions: []
});
// 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)
}
};
export { index_default as default };