eslint-plugin-code-style 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +26 -26
- package/CHANGELOG.md +80 -0
- package/README.md +198 -40
- package/index.d.ts +8 -4
- package/index.js +709 -87
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -6494,6 +6494,10 @@ const enumTypeEnforcement = {
|
|
|
6494
6494
|
* Local paths (starting with @/) should only import from
|
|
6495
6495
|
* folder-level index files.
|
|
6496
6496
|
*
|
|
6497
|
+
* Exception: Files within the same module folder should use
|
|
6498
|
+
* relative imports (./sibling) to avoid circular dependencies
|
|
6499
|
+
* through the index file.
|
|
6500
|
+
*
|
|
6497
6501
|
* Options:
|
|
6498
6502
|
* - aliasPrefix: string (default: "@/") - Change path alias prefix if your project uses something other than @/ (e.g., ~/, src/)
|
|
6499
6503
|
* - extraAllowedFolders: string[] - Add custom folders that can be imported with @/folder. Extends defaults without replacing them
|
|
@@ -6506,10 +6510,14 @@ const enumTypeEnforcement = {
|
|
|
6506
6510
|
* ✓ Good:
|
|
6507
6511
|
* import { Button } from "@/components";
|
|
6508
6512
|
* import { useAuth } from "@/hooks";
|
|
6513
|
+
* // Sibling import within same folder (avoids circular deps):
|
|
6514
|
+
* import { helpers } from "./helpers"; (when in data/app.js importing data/helpers.js)
|
|
6509
6515
|
*
|
|
6510
6516
|
* ✗ Bad:
|
|
6511
6517
|
* import { Button } from "@/components/buttons/primary-button";
|
|
6512
6518
|
* import { useAuth } from "@/hooks/auth/useAuth";
|
|
6519
|
+
* // Same-folder absolute import (circular dependency risk):
|
|
6520
|
+
* import { helpers } from "@/data"; (when file is inside data/ folder)
|
|
6513
6521
|
*
|
|
6514
6522
|
* Configuration Example:
|
|
6515
6523
|
* "code-style/absolute-imports-only": ["error", {
|
|
@@ -6606,15 +6614,34 @@ const absoluteImportsOnly = {
|
|
|
6606
6614
|
// to use relative imports for app root and styles (e.g., ./index.css, ./app)
|
|
6607
6615
|
const isEntryFile = /\/main\.(js|jsx|ts|tsx)$/.test(normalizedFilename);
|
|
6608
6616
|
|
|
6617
|
+
// Detect if the file is inside a module folder at any depth
|
|
6618
|
+
// e.g., data/app.js, data/auth/login/guest.tsx are both inside "data"
|
|
6619
|
+
const getParentModuleFolderHandler = () => {
|
|
6620
|
+
for (const folder of allowedFolders) {
|
|
6621
|
+
const pattern = new RegExp(`/(${folder})/`);
|
|
6622
|
+
|
|
6623
|
+
if (pattern.test(normalizedFilename)) return folder;
|
|
6624
|
+
}
|
|
6625
|
+
|
|
6626
|
+
return null;
|
|
6627
|
+
};
|
|
6628
|
+
|
|
6629
|
+
const parentModuleFolder = getParentModuleFolderHandler();
|
|
6630
|
+
|
|
6609
6631
|
// 1. Disallow relative imports (starting with ./ or ../)
|
|
6610
|
-
// EXCEPT in index files
|
|
6632
|
+
// EXCEPT in index files, entry files, and sibling files within the same module folder
|
|
6611
6633
|
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6634
|
+
// Always allow in index files and entry files
|
|
6635
|
+
if (isIndexFile || isEntryFile) return;
|
|
6636
|
+
|
|
6637
|
+
// Allow relative imports within the same module folder (any depth)
|
|
6638
|
+
// e.g., data/app.js → "./helpers", data/auth/forget-password/index.ts → "../../login/guest"
|
|
6639
|
+
if (parentModuleFolder) return;
|
|
6640
|
+
|
|
6641
|
+
context.report({
|
|
6642
|
+
message: `Relative imports are not allowed. Use absolute imports with "${aliasPrefix}" prefix instead.`,
|
|
6643
|
+
node: node.source,
|
|
6644
|
+
});
|
|
6618
6645
|
|
|
6619
6646
|
return;
|
|
6620
6647
|
}
|
|
@@ -6662,6 +6689,39 @@ const absoluteImportsOnly = {
|
|
|
6662
6689
|
return;
|
|
6663
6690
|
}
|
|
6664
6691
|
|
|
6692
|
+
// Check if importing from own parent module folder (circular dependency risk)
|
|
6693
|
+
// e.g., data/app.js importing @/data → should use relative import instead
|
|
6694
|
+
if (parentModuleFolder && folderName === parentModuleFolder) {
|
|
6695
|
+
const targetRelativeToModule = segments.slice(1).join("/");
|
|
6696
|
+
|
|
6697
|
+
// Deep path within own module (e.g., @/data/auth/login/guest) — auto-fixable
|
|
6698
|
+
if (targetRelativeToModule) {
|
|
6699
|
+
const moduleIndex = normalizedFilename.lastIndexOf(`/${parentModuleFolder}/`);
|
|
6700
|
+
const fileRelativeToModule = normalizedFilename.slice(moduleIndex + parentModuleFolder.length + 2);
|
|
6701
|
+
const fileDir = fileRelativeToModule.substring(0, fileRelativeToModule.lastIndexOf("/"));
|
|
6702
|
+
|
|
6703
|
+
let relativePath = nodePath.posix.relative(fileDir, targetRelativeToModule);
|
|
6704
|
+
|
|
6705
|
+
if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
|
|
6706
|
+
|
|
6707
|
+
context.report({
|
|
6708
|
+
message: `Files within "${parentModuleFolder}/" should use relative imports (e.g., "${relativePath}") instead of "${importPath}" to avoid circular dependencies.`,
|
|
6709
|
+
node: node.source,
|
|
6710
|
+
fix: (fixer) => fixer.replaceText(node.source, `"${relativePath}"`),
|
|
6711
|
+
});
|
|
6712
|
+
|
|
6713
|
+
return;
|
|
6714
|
+
}
|
|
6715
|
+
|
|
6716
|
+
// Barrel import to own module (e.g., @/data from inside data/) — report only
|
|
6717
|
+
context.report({
|
|
6718
|
+
message: `Files within "${parentModuleFolder}/" should use relative imports instead of "${importPath}" to avoid circular dependencies through the index file.`,
|
|
6719
|
+
node: node.source,
|
|
6720
|
+
});
|
|
6721
|
+
|
|
6722
|
+
return;
|
|
6723
|
+
}
|
|
6724
|
+
|
|
6665
6725
|
// Only one segment is allowed (import from index file)
|
|
6666
6726
|
// e.g., @/atoms is OK, @/atoms/menus is NOT OK
|
|
6667
6727
|
// EXCEPTION: Redux ecosystem - @/redux/actions, @/redux/types, etc. are allowed
|
|
@@ -6707,7 +6767,8 @@ const absoluteImportsOnly = {
|
|
|
6707
6767
|
};
|
|
6708
6768
|
},
|
|
6709
6769
|
meta: {
|
|
6710
|
-
docs: { description: "Enforce absolute imports from index files only for local paths" },
|
|
6770
|
+
docs: { description: "Enforce absolute imports from index files only for local paths, with relative imports required for files within the same module folder" },
|
|
6771
|
+
fixable: "code",
|
|
6711
6772
|
schema: [
|
|
6712
6773
|
{
|
|
6713
6774
|
additionalProperties: false,
|
|
@@ -8036,6 +8097,149 @@ const indexExportsOnly = {
|
|
|
8036
8097
|
},
|
|
8037
8098
|
};
|
|
8038
8099
|
|
|
8100
|
+
/**
|
|
8101
|
+
* ───────────────────────────────────────────────────────────────
|
|
8102
|
+
* Rule: Inline Export Declaration
|
|
8103
|
+
* ───────────────────────────────────────────────────────────────
|
|
8104
|
+
*
|
|
8105
|
+
* Description:
|
|
8106
|
+
* In non-index files, enforce inline export declarations instead
|
|
8107
|
+
* of declaring variables/functions separately and then exporting
|
|
8108
|
+
* them with a grouped export statement at the end.
|
|
8109
|
+
*
|
|
8110
|
+
* This does NOT apply to index files (which use barrel re-exports).
|
|
8111
|
+
*
|
|
8112
|
+
* ✓ Good:
|
|
8113
|
+
* export const strings = { ... };
|
|
8114
|
+
*
|
|
8115
|
+
* export const buttonTypeData = { button: "button" };
|
|
8116
|
+
*
|
|
8117
|
+
* export const submitHandler = () => { ... };
|
|
8118
|
+
*
|
|
8119
|
+
* ✗ Bad:
|
|
8120
|
+
* const strings = { ... };
|
|
8121
|
+
* export { strings };
|
|
8122
|
+
*
|
|
8123
|
+
* ✗ Bad:
|
|
8124
|
+
* const foo = 1;
|
|
8125
|
+
* const bar = 2;
|
|
8126
|
+
* export { foo, bar };
|
|
8127
|
+
*
|
|
8128
|
+
* Auto-fixable: Yes — adds "export" to each declaration and removes
|
|
8129
|
+
* the grouped export statement.
|
|
8130
|
+
*/
|
|
8131
|
+
const inlineExportDeclaration = {
|
|
8132
|
+
create(context) {
|
|
8133
|
+
const filename = context.filename || context.getFilename();
|
|
8134
|
+
const normalizedFilename = filename.replace(/\\/g, "/");
|
|
8135
|
+
const isIndexFile = /\/index\.(js|jsx|ts|tsx)$/.test(normalizedFilename)
|
|
8136
|
+
|| /^index\.(js|jsx|ts|tsx)$/.test(normalizedFilename);
|
|
8137
|
+
|
|
8138
|
+
// Only apply to non-index files
|
|
8139
|
+
if (isIndexFile) return {};
|
|
8140
|
+
|
|
8141
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
8142
|
+
|
|
8143
|
+
return {
|
|
8144
|
+
Program(programNode) {
|
|
8145
|
+
// Find all grouped export statements: export { a, b, c };
|
|
8146
|
+
// These have specifiers but no source (not re-exports) and no declaration
|
|
8147
|
+
const groupedExports = programNode.body.filter(
|
|
8148
|
+
(node) => node.type === "ExportNamedDeclaration"
|
|
8149
|
+
&& !node.source
|
|
8150
|
+
&& !node.declaration
|
|
8151
|
+
&& node.specifiers.length > 0,
|
|
8152
|
+
);
|
|
8153
|
+
|
|
8154
|
+
if (groupedExports.length === 0) return;
|
|
8155
|
+
|
|
8156
|
+
// Build a map of all top-level declarations: name → node
|
|
8157
|
+
const declarationMap = new Map();
|
|
8158
|
+
|
|
8159
|
+
programNode.body.forEach((node) => {
|
|
8160
|
+
if (node.type === "VariableDeclaration") {
|
|
8161
|
+
node.declarations.forEach((decl) => {
|
|
8162
|
+
if (decl.id && decl.id.type === "Identifier") {
|
|
8163
|
+
declarationMap.set(decl.id.name, { declarationNode: node, kind: node.kind });
|
|
8164
|
+
}
|
|
8165
|
+
});
|
|
8166
|
+
} else if (node.type === "FunctionDeclaration" && node.id) {
|
|
8167
|
+
declarationMap.set(node.id.name, { declarationNode: node, kind: "function" });
|
|
8168
|
+
} else if (node.type === "ClassDeclaration" && node.id) {
|
|
8169
|
+
declarationMap.set(node.id.name, { declarationNode: node, kind: "class" });
|
|
8170
|
+
}
|
|
8171
|
+
});
|
|
8172
|
+
|
|
8173
|
+
groupedExports.forEach((exportNode) => {
|
|
8174
|
+
// Skip if any specifier has an alias (export { a as b })
|
|
8175
|
+
const hasAlias = exportNode.specifiers.some(
|
|
8176
|
+
(spec) => spec.local.name !== spec.exported.name,
|
|
8177
|
+
);
|
|
8178
|
+
|
|
8179
|
+
if (hasAlias) return;
|
|
8180
|
+
|
|
8181
|
+
// Check all specifiers have matching declarations
|
|
8182
|
+
const allFound = exportNode.specifiers.every(
|
|
8183
|
+
(spec) => declarationMap.has(spec.local.name),
|
|
8184
|
+
);
|
|
8185
|
+
|
|
8186
|
+
if (!allFound) return;
|
|
8187
|
+
|
|
8188
|
+
// Check if any declaration is already exported (would cause duplicate)
|
|
8189
|
+
const anyAlreadyExported = exportNode.specifiers.some((spec) => {
|
|
8190
|
+
const info = declarationMap.get(spec.local.name);
|
|
8191
|
+
const declNode = info.declarationNode;
|
|
8192
|
+
const parent = declNode.parent;
|
|
8193
|
+
|
|
8194
|
+
// Check if declaration is inside an ExportNamedDeclaration
|
|
8195
|
+
if (parent && parent.type === "ExportNamedDeclaration") return true;
|
|
8196
|
+
|
|
8197
|
+
// Check source text starts with "export"
|
|
8198
|
+
const declText = sourceCode.getText(declNode);
|
|
8199
|
+
|
|
8200
|
+
return declText.startsWith("export ");
|
|
8201
|
+
});
|
|
8202
|
+
|
|
8203
|
+
if (anyAlreadyExported) return;
|
|
8204
|
+
|
|
8205
|
+
context.report({
|
|
8206
|
+
fix(fixer) {
|
|
8207
|
+
const fixes = [];
|
|
8208
|
+
|
|
8209
|
+
// Add "export " before each declaration
|
|
8210
|
+
exportNode.specifiers.forEach((spec) => {
|
|
8211
|
+
const info = declarationMap.get(spec.local.name);
|
|
8212
|
+
const declNode = info.declarationNode;
|
|
8213
|
+
|
|
8214
|
+
fixes.push(fixer.insertTextBefore(declNode, "export "));
|
|
8215
|
+
});
|
|
8216
|
+
|
|
8217
|
+
// Remove the grouped export statement and any preceding blank line
|
|
8218
|
+
const tokenBefore = sourceCode.getTokenBefore(exportNode, { includeComments: true });
|
|
8219
|
+
|
|
8220
|
+
if (tokenBefore) {
|
|
8221
|
+
fixes.push(fixer.removeRange([tokenBefore.range[1], exportNode.range[1]]));
|
|
8222
|
+
} else {
|
|
8223
|
+
fixes.push(fixer.remove(exportNode));
|
|
8224
|
+
}
|
|
8225
|
+
|
|
8226
|
+
return fixes;
|
|
8227
|
+
},
|
|
8228
|
+
message: "Use inline export declarations (export const x = ...) instead of grouped export statements (export { x }).",
|
|
8229
|
+
node: exportNode,
|
|
8230
|
+
});
|
|
8231
|
+
});
|
|
8232
|
+
},
|
|
8233
|
+
};
|
|
8234
|
+
},
|
|
8235
|
+
meta: {
|
|
8236
|
+
docs: { description: "Enforce inline export declarations instead of grouped export statements in non-index files" },
|
|
8237
|
+
fixable: "code",
|
|
8238
|
+
schema: [],
|
|
8239
|
+
type: "layout",
|
|
8240
|
+
},
|
|
8241
|
+
};
|
|
8242
|
+
|
|
8039
8243
|
/**
|
|
8040
8244
|
* ───────────────────────────────────────────────────────────────
|
|
8041
8245
|
* Rule: JSX Children On New Line
|
|
@@ -18750,7 +18954,7 @@ const componentPropsInlineType = {
|
|
|
18750
18954
|
* <button>{children}</button>
|
|
18751
18955
|
* );
|
|
18752
18956
|
*/
|
|
18753
|
-
const
|
|
18957
|
+
const svgIconNamingConvention = {
|
|
18754
18958
|
create(context) {
|
|
18755
18959
|
// Get the component name from node
|
|
18756
18960
|
const getComponentNameHandler = (node) => {
|
|
@@ -18885,32 +19089,90 @@ const svgComponentIconNaming = {
|
|
|
18885
19089
|
* // In layouts/main.tsx:
|
|
18886
19090
|
* export const Main = () => <div>Main</div>; // Should be "MainLayout"
|
|
18887
19091
|
*/
|
|
18888
|
-
const
|
|
19092
|
+
const folderBasedNamingConvention = {
|
|
18889
19093
|
create(context) {
|
|
18890
19094
|
const filename = context.filename || context.getFilename();
|
|
18891
19095
|
const normalizedFilename = filename.replace(/\\/g, "/");
|
|
18892
19096
|
|
|
18893
19097
|
// Folder-to-suffix mapping
|
|
19098
|
+
// - JSX component folders: require function returning JSX (PascalCase)
|
|
19099
|
+
// - camelCase folders: check camelCase exported identifiers
|
|
19100
|
+
// - Other non-JSX folders: check PascalCase exported identifiers
|
|
18894
19101
|
const folderSuffixMap = {
|
|
19102
|
+
atoms: "",
|
|
19103
|
+
components: "",
|
|
19104
|
+
constants: "Constants",
|
|
19105
|
+
contexts: "Context",
|
|
19106
|
+
data: "Data",
|
|
18895
19107
|
layouts: "Layout",
|
|
18896
19108
|
pages: "Page",
|
|
19109
|
+
providers: "Provider",
|
|
19110
|
+
reducers: "Reducer",
|
|
19111
|
+
services: "Service",
|
|
19112
|
+
strings: "Strings",
|
|
19113
|
+
theme: "Theme",
|
|
19114
|
+
themes: "Theme",
|
|
18897
19115
|
views: "View",
|
|
18898
19116
|
};
|
|
18899
19117
|
|
|
18900
|
-
//
|
|
18901
|
-
const
|
|
18902
|
-
for (const [folder, suffix] of Object.entries(folderSuffixMap)) {
|
|
18903
|
-
const pattern = new RegExp(`/${folder}/[^/]+\\.(jsx?|tsx?)$`);
|
|
19118
|
+
// Folders where JSX return is required (component folders)
|
|
19119
|
+
const jsxRequiredFolders = new Set(["atoms", "components", "layouts", "pages", "providers", "views"]);
|
|
18904
19120
|
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
19121
|
+
// Folders where exports use camelCase naming (non-component, non-context)
|
|
19122
|
+
const camelCaseFolders = new Set(["constants", "data", "reducers", "services", "strings"]);
|
|
19123
|
+
|
|
19124
|
+
// Convert kebab-case to PascalCase
|
|
19125
|
+
const toPascalCaseHandler = (str) => str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
19126
|
+
|
|
19127
|
+
// Convert PascalCase to camelCase (lowercase first letter)
|
|
19128
|
+
const toCamelCaseHandler = (str) => str.charAt(0).toLowerCase() + str.slice(1);
|
|
19129
|
+
|
|
19130
|
+
// Build regex dynamically from folder names
|
|
19131
|
+
const folderNames = Object.keys(folderSuffixMap).join("|");
|
|
19132
|
+
|
|
19133
|
+
// Parse the file path to extract module folder info at any depth
|
|
19134
|
+
const getModuleInfoHandler = () => {
|
|
19135
|
+
const pattern = new RegExp(`\\/(${folderNames})\\/(.+)\\.(jsx?|tsx?)$`);
|
|
19136
|
+
const match = normalizedFilename.match(pattern);
|
|
19137
|
+
|
|
19138
|
+
if (!match) return null;
|
|
19139
|
+
|
|
19140
|
+
const moduleFolderName = match[1];
|
|
19141
|
+
const suffix = folderSuffixMap[moduleFolderName];
|
|
19142
|
+
const relativePath = match[2];
|
|
19143
|
+
const segments = relativePath.split("/");
|
|
19144
|
+
const fileName = segments[segments.length - 1];
|
|
19145
|
+
const intermediateFolders = segments.slice(0, -1);
|
|
19146
|
+
|
|
19147
|
+
return { fileName, folder: moduleFolderName, intermediateFolders, suffix };
|
|
19148
|
+
};
|
|
19149
|
+
|
|
19150
|
+
// Build the expected name based on file position
|
|
19151
|
+
const buildExpectedNameHandler = (moduleInfo) => {
|
|
19152
|
+
const { fileName, folder, intermediateFolders, suffix } = moduleInfo;
|
|
19153
|
+
|
|
19154
|
+
// Module barrel file (e.g., views/index.ts) — skip
|
|
19155
|
+
if (fileName === "index" && intermediateFolders.length === 0) return null;
|
|
19156
|
+
|
|
19157
|
+
let nameParts;
|
|
19158
|
+
|
|
19159
|
+
if (fileName === "index") {
|
|
19160
|
+
// Index in subfolder (e.g., layouts/auth/index.tsx) → parts from folders only
|
|
19161
|
+
nameParts = [...intermediateFolders].reverse();
|
|
19162
|
+
} else {
|
|
19163
|
+
// Regular file (e.g., layouts/auth/login.tsx) → file name + folders deepest-to-shallowest
|
|
19164
|
+
nameParts = [fileName, ...[...intermediateFolders].reverse()];
|
|
18908
19165
|
}
|
|
18909
19166
|
|
|
18910
|
-
|
|
19167
|
+
const pascalName = nameParts.map(toPascalCaseHandler).join("") + suffix;
|
|
19168
|
+
|
|
19169
|
+
// camelCase folders produce camelCase names
|
|
19170
|
+
if (camelCaseFolders.has(folder)) return toCamelCaseHandler(pascalName);
|
|
19171
|
+
|
|
19172
|
+
return pascalName;
|
|
18911
19173
|
};
|
|
18912
19174
|
|
|
18913
|
-
// Get the component name from node
|
|
19175
|
+
// Get the component name from function node
|
|
18914
19176
|
const getComponentNameHandler = (node) => {
|
|
18915
19177
|
// Arrow function: const Name = () => ...
|
|
18916
19178
|
if (node.parent && node.parent.type === "VariableDeclarator" && node.parent.id && node.parent.id.type === "Identifier") {
|
|
@@ -18925,8 +19187,8 @@ const folderComponentSuffix = {
|
|
|
18925
19187
|
return null;
|
|
18926
19188
|
};
|
|
18927
19189
|
|
|
18928
|
-
// Check if
|
|
18929
|
-
const
|
|
19190
|
+
// Check if name starts with uppercase (PascalCase)
|
|
19191
|
+
const isPascalCaseHandler = (name) => name && /^[A-Z]/.test(name);
|
|
18930
19192
|
|
|
18931
19193
|
// Check if the function returns JSX
|
|
18932
19194
|
const returnsJsxHandler = (node) => {
|
|
@@ -18966,11 +19228,80 @@ const folderComponentSuffix = {
|
|
|
18966
19228
|
return false;
|
|
18967
19229
|
};
|
|
18968
19230
|
|
|
19231
|
+
// Shared fix logic for renaming identifier and all references
|
|
19232
|
+
const createRenameFixer = (node, name, expectedName, identifierNode) => (fixer) => {
|
|
19233
|
+
const scope = context.sourceCode
|
|
19234
|
+
? context.sourceCode.getScope(node)
|
|
19235
|
+
: context.getScope();
|
|
19236
|
+
|
|
19237
|
+
const findVariableHandler = (s, varName) => {
|
|
19238
|
+
const v = s.variables.find((variable) => variable.name === varName);
|
|
19239
|
+
|
|
19240
|
+
if (v) return v;
|
|
19241
|
+
if (s.upper) return findVariableHandler(s.upper, varName);
|
|
19242
|
+
|
|
19243
|
+
return null;
|
|
19244
|
+
};
|
|
19245
|
+
|
|
19246
|
+
const variable = findVariableHandler(scope, name);
|
|
19247
|
+
|
|
19248
|
+
if (!variable) return fixer.replaceText(identifierNode, expectedName);
|
|
19249
|
+
|
|
19250
|
+
const fixes = [];
|
|
19251
|
+
const fixedRanges = new Set();
|
|
19252
|
+
|
|
19253
|
+
variable.defs.forEach((def) => {
|
|
19254
|
+
const rangeKey = `${def.name.range[0]}-${def.name.range[1]}`;
|
|
19255
|
+
|
|
19256
|
+
if (!fixedRanges.has(rangeKey)) {
|
|
19257
|
+
fixedRanges.add(rangeKey);
|
|
19258
|
+
fixes.push(fixer.replaceText(def.name, expectedName));
|
|
19259
|
+
}
|
|
19260
|
+
});
|
|
19261
|
+
|
|
19262
|
+
variable.references.forEach((ref) => {
|
|
19263
|
+
const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
|
|
19264
|
+
|
|
19265
|
+
if (!fixedRanges.has(rangeKey)) {
|
|
19266
|
+
fixedRanges.add(rangeKey);
|
|
19267
|
+
fixes.push(fixer.replaceText(ref.identifier, expectedName));
|
|
19268
|
+
}
|
|
19269
|
+
});
|
|
19270
|
+
|
|
19271
|
+
return fixes;
|
|
19272
|
+
};
|
|
19273
|
+
|
|
19274
|
+
// Build the error message based on folder type
|
|
19275
|
+
const buildMessageHandler = (name, folder, suffix, expectedName) => {
|
|
19276
|
+
if (suffix) return `"${name}" in "${folder}" folder must be named "${expectedName}" (expected "${suffix}" suffix with chained folder names)`;
|
|
19277
|
+
|
|
19278
|
+
return `"${name}" in "${folder}" folder must be named "${expectedName}" (expected chained folder names)`;
|
|
19279
|
+
};
|
|
19280
|
+
|
|
19281
|
+
// Check if name starts with lowercase (camelCase)
|
|
19282
|
+
const isCamelCaseHandler = (name) => name && /^[a-z]/.test(name);
|
|
19283
|
+
|
|
19284
|
+
// Build suffix message for camelCase folders
|
|
19285
|
+
const buildSuffixMessageHandler = (name, folder, suffix) => `"${name}" in "${folder}" folder must end with "${suffix}" suffix (e.g., "myItem${suffix}")`;
|
|
19286
|
+
|
|
19287
|
+
// Check camelCase naming (suffix-only enforcement for camelCase folders)
|
|
19288
|
+
const checkCamelCaseHandler = (name, folder, suffix, identifierNode) => {
|
|
19289
|
+
// For camelCase folders, only enforce the suffix (not full chained name)
|
|
19290
|
+
// because these folders often have multiple exports per file
|
|
19291
|
+
// Suffix stays PascalCase even in camelCase names (e.g., buttonTypeData)
|
|
19292
|
+
if (!name.endsWith(suffix)) {
|
|
19293
|
+
context.report({
|
|
19294
|
+
message: buildSuffixMessageHandler(name, folder, suffix),
|
|
19295
|
+
node: identifierNode,
|
|
19296
|
+
});
|
|
19297
|
+
}
|
|
19298
|
+
};
|
|
19299
|
+
|
|
19300
|
+
// Check function declarations (JSX components + non-JSX functions like reducers)
|
|
18969
19301
|
const checkFunctionHandler = (node) => {
|
|
18970
|
-
const
|
|
19302
|
+
const moduleInfo = getModuleInfoHandler();
|
|
18971
19303
|
|
|
18972
|
-
|
|
18973
|
-
if (!folderInfo) return;
|
|
19304
|
+
if (!moduleInfo) return;
|
|
18974
19305
|
|
|
18975
19306
|
const componentInfo = getComponentNameHandler(node);
|
|
18976
19307
|
|
|
@@ -18978,65 +19309,74 @@ const folderComponentSuffix = {
|
|
|
18978
19309
|
|
|
18979
19310
|
const { name, identifierNode } = componentInfo;
|
|
18980
19311
|
|
|
18981
|
-
|
|
18982
|
-
if (!isReactComponentNameHandler(name)) return;
|
|
19312
|
+
const { folder, suffix } = moduleInfo;
|
|
18983
19313
|
|
|
18984
|
-
//
|
|
18985
|
-
if (
|
|
19314
|
+
// For camelCase folders, only enforce suffix
|
|
19315
|
+
if (camelCaseFolders.has(folder)) {
|
|
19316
|
+
if (!isCamelCaseHandler(name) || !suffix) return;
|
|
18986
19317
|
|
|
18987
|
-
|
|
19318
|
+
checkCamelCaseHandler(name, folder, suffix, identifierNode);
|
|
18988
19319
|
|
|
18989
|
-
|
|
18990
|
-
|
|
18991
|
-
const newName = `${name}${suffix}`;
|
|
19320
|
+
return;
|
|
19321
|
+
}
|
|
18992
19322
|
|
|
19323
|
+
if (!isPascalCaseHandler(name)) return;
|
|
19324
|
+
|
|
19325
|
+
// For JSX-required folders, only check functions that return JSX
|
|
19326
|
+
if (jsxRequiredFolders.has(folder) && !returnsJsxHandler(node)) return;
|
|
19327
|
+
|
|
19328
|
+
const expectedName = buildExpectedNameHandler(moduleInfo);
|
|
19329
|
+
|
|
19330
|
+
if (!expectedName) return;
|
|
19331
|
+
|
|
19332
|
+
if (name !== expectedName) {
|
|
18993
19333
|
context.report({
|
|
18994
|
-
fix(
|
|
18995
|
-
|
|
18996
|
-
|
|
18997
|
-
|
|
19334
|
+
fix: createRenameFixer(node, name, expectedName, identifierNode),
|
|
19335
|
+
message: buildMessageHandler(name, folder, suffix, expectedName),
|
|
19336
|
+
node: identifierNode,
|
|
19337
|
+
});
|
|
19338
|
+
}
|
|
19339
|
+
};
|
|
18998
19340
|
|
|
18999
|
-
|
|
19000
|
-
|
|
19001
|
-
|
|
19341
|
+
// Check variable declarations for non-JSX folders (contexts, themes, data, etc.)
|
|
19342
|
+
const checkVariableHandler = (node) => {
|
|
19343
|
+
// Only check VariableDeclarators with an identifier name
|
|
19344
|
+
if (!node.id || node.id.type !== "Identifier") return;
|
|
19002
19345
|
|
|
19003
|
-
|
|
19004
|
-
|
|
19346
|
+
// Skip if init is a function (handled by checkFunctionHandler)
|
|
19347
|
+
if (node.init && (node.init.type === "ArrowFunctionExpression" || node.init.type === "FunctionExpression")) return;
|
|
19005
19348
|
|
|
19006
|
-
|
|
19007
|
-
};
|
|
19349
|
+
const moduleInfo = getModuleInfoHandler();
|
|
19008
19350
|
|
|
19009
|
-
|
|
19351
|
+
if (!moduleInfo) return;
|
|
19010
19352
|
|
|
19011
|
-
|
|
19353
|
+
const { folder, suffix } = moduleInfo;
|
|
19012
19354
|
|
|
19013
|
-
|
|
19014
|
-
|
|
19355
|
+
// Only check non-JSX folders (contexts, themes, data, constants, strings, etc.)
|
|
19356
|
+
if (jsxRequiredFolders.has(folder)) return;
|
|
19015
19357
|
|
|
19016
|
-
|
|
19017
|
-
variable.defs.forEach((def) => {
|
|
19018
|
-
const rangeKey = `${def.name.range[0]}-${def.name.range[1]}`;
|
|
19358
|
+
const name = node.id.name;
|
|
19019
19359
|
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
}
|
|
19024
|
-
});
|
|
19360
|
+
// For camelCase folders, only enforce suffix
|
|
19361
|
+
if (camelCaseFolders.has(folder)) {
|
|
19362
|
+
if (!isCamelCaseHandler(name) || !suffix) return;
|
|
19025
19363
|
|
|
19026
|
-
|
|
19027
|
-
variable.references.forEach((ref) => {
|
|
19028
|
-
const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
|
|
19364
|
+
checkCamelCaseHandler(name, folder, suffix, node.id);
|
|
19029
19365
|
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
fixes.push(fixer.replaceText(ref.identifier, newName));
|
|
19033
|
-
}
|
|
19034
|
-
});
|
|
19366
|
+
return;
|
|
19367
|
+
}
|
|
19035
19368
|
|
|
19036
|
-
|
|
19037
|
-
|
|
19038
|
-
|
|
19039
|
-
|
|
19369
|
+
if (!isPascalCaseHandler(name)) return;
|
|
19370
|
+
|
|
19371
|
+
const expectedName = buildExpectedNameHandler(moduleInfo);
|
|
19372
|
+
|
|
19373
|
+
if (!expectedName) return;
|
|
19374
|
+
|
|
19375
|
+
if (name !== expectedName) {
|
|
19376
|
+
context.report({
|
|
19377
|
+
fix: createRenameFixer(node, name, expectedName, node.id),
|
|
19378
|
+
message: buildMessageHandler(name, folder, suffix, expectedName),
|
|
19379
|
+
node: node.id,
|
|
19040
19380
|
});
|
|
19041
19381
|
}
|
|
19042
19382
|
};
|
|
@@ -19045,16 +19385,250 @@ const folderComponentSuffix = {
|
|
|
19045
19385
|
ArrowFunctionExpression: checkFunctionHandler,
|
|
19046
19386
|
FunctionDeclaration: checkFunctionHandler,
|
|
19047
19387
|
FunctionExpression: checkFunctionHandler,
|
|
19388
|
+
VariableDeclarator: checkVariableHandler,
|
|
19048
19389
|
};
|
|
19049
19390
|
},
|
|
19050
19391
|
meta: {
|
|
19051
|
-
docs: { description: "Enforce
|
|
19392
|
+
docs: { description: "Enforce naming conventions based on folder location — suffix for views/layouts/pages/providers/reducers/contexts/themes, chained folder names for nested files" },
|
|
19052
19393
|
fixable: "code",
|
|
19053
19394
|
schema: [],
|
|
19054
19395
|
type: "suggestion",
|
|
19055
19396
|
},
|
|
19056
19397
|
};
|
|
19057
19398
|
|
|
19399
|
+
/**
|
|
19400
|
+
* ───────────────────────────────────────────────────────────────
|
|
19401
|
+
* Rule: Folder Structure Consistency
|
|
19402
|
+
* ───────────────────────────────────────────────────────────────
|
|
19403
|
+
*
|
|
19404
|
+
* Description:
|
|
19405
|
+
* Enforces that module folders have a consistent internal
|
|
19406
|
+
* structure — either all flat files or all wrapped in folders.
|
|
19407
|
+
*
|
|
19408
|
+
* Applies to the same folders as module-index-exports: atoms,
|
|
19409
|
+
* components, hooks, utils, enums, types, reducers, etc.
|
|
19410
|
+
*
|
|
19411
|
+
* - Flat mode: All items are direct files (e.g., atoms/input.tsx)
|
|
19412
|
+
* - Wrapped mode: All items are in subfolders (e.g., atoms/input/index.tsx)
|
|
19413
|
+
* - Wrapped mode is only justified when at least one subfolder has 2+ files
|
|
19414
|
+
*
|
|
19415
|
+
* ✓ Good (flat — all direct files):
|
|
19416
|
+
* atoms/input.tsx
|
|
19417
|
+
* atoms/calendar.tsx
|
|
19418
|
+
*
|
|
19419
|
+
* ✓ Good (wrapped — justified, input has multiple files):
|
|
19420
|
+
* atoms/input/input.tsx
|
|
19421
|
+
* atoms/input/helpers.ts
|
|
19422
|
+
* atoms/calendar/index.tsx
|
|
19423
|
+
*
|
|
19424
|
+
* ✗ Bad (mixed — some flat, some wrapped):
|
|
19425
|
+
* atoms/input.tsx
|
|
19426
|
+
* atoms/calendar/index.tsx
|
|
19427
|
+
*
|
|
19428
|
+
* ✗ Bad (wrapped but unnecessary — each folder has only 1 file):
|
|
19429
|
+
* atoms/input/index.tsx
|
|
19430
|
+
* atoms/calendar/index.tsx
|
|
19431
|
+
*/
|
|
19432
|
+
const folderStructureConsistency = {
|
|
19433
|
+
create(context) {
|
|
19434
|
+
const filename = context.filename || context.getFilename();
|
|
19435
|
+
const normalizedFilename = filename.replace(/\\/g, "/");
|
|
19436
|
+
|
|
19437
|
+
const options = context.options[0] || {};
|
|
19438
|
+
const defaultModuleFolders = [
|
|
19439
|
+
"actions",
|
|
19440
|
+
"apis",
|
|
19441
|
+
"assets",
|
|
19442
|
+
"atoms",
|
|
19443
|
+
"components",
|
|
19444
|
+
"config",
|
|
19445
|
+
"configs",
|
|
19446
|
+
"constants",
|
|
19447
|
+
"contexts",
|
|
19448
|
+
"data",
|
|
19449
|
+
"enums",
|
|
19450
|
+
"helpers",
|
|
19451
|
+
"hooks",
|
|
19452
|
+
"interfaces",
|
|
19453
|
+
"layouts",
|
|
19454
|
+
"lib",
|
|
19455
|
+
"middlewares",
|
|
19456
|
+
"molecules",
|
|
19457
|
+
"organisms",
|
|
19458
|
+
"pages",
|
|
19459
|
+
"providers",
|
|
19460
|
+
"reducers",
|
|
19461
|
+
"redux",
|
|
19462
|
+
"requests",
|
|
19463
|
+
"routes",
|
|
19464
|
+
"schemas",
|
|
19465
|
+
"sections",
|
|
19466
|
+
"services",
|
|
19467
|
+
"store",
|
|
19468
|
+
"strings",
|
|
19469
|
+
"styles",
|
|
19470
|
+
"theme",
|
|
19471
|
+
"thunks",
|
|
19472
|
+
"types",
|
|
19473
|
+
"ui",
|
|
19474
|
+
"utils",
|
|
19475
|
+
"utilities",
|
|
19476
|
+
"views",
|
|
19477
|
+
"widgets",
|
|
19478
|
+
];
|
|
19479
|
+
|
|
19480
|
+
const moduleFolders = options.moduleFolders
|
|
19481
|
+
|| [...defaultModuleFolders, ...(options.extraModuleFolders || [])];
|
|
19482
|
+
|
|
19483
|
+
// Find the module folder in the file path
|
|
19484
|
+
const getModuleFolderInfoHandler = () => {
|
|
19485
|
+
for (const folder of moduleFolders) {
|
|
19486
|
+
const pattern = new RegExp(`(.*/${folder})/`);
|
|
19487
|
+
const match = normalizedFilename.match(pattern);
|
|
19488
|
+
|
|
19489
|
+
if (match) return { folder, fullPath: match[1] };
|
|
19490
|
+
}
|
|
19491
|
+
|
|
19492
|
+
return null;
|
|
19493
|
+
};
|
|
19494
|
+
|
|
19495
|
+
const moduleFolderInfo = getModuleFolderInfoHandler();
|
|
19496
|
+
|
|
19497
|
+
// Check for loose module files (e.g., src/data.js instead of src/data/)
|
|
19498
|
+
if (!moduleFolderInfo) {
|
|
19499
|
+
const fileBaseName = normalizedFilename.replace(/.*\//, "").replace(/\.(jsx?|tsx?)$/, "");
|
|
19500
|
+
|
|
19501
|
+
if (moduleFolders.includes(fileBaseName)) {
|
|
19502
|
+
return {
|
|
19503
|
+
Program(node) {
|
|
19504
|
+
context.report({
|
|
19505
|
+
message: `"${fileBaseName}" should be a folder, not a standalone file. Use "${fileBaseName}/" folder with an index file instead.`,
|
|
19506
|
+
node,
|
|
19507
|
+
});
|
|
19508
|
+
},
|
|
19509
|
+
};
|
|
19510
|
+
}
|
|
19511
|
+
|
|
19512
|
+
return {};
|
|
19513
|
+
}
|
|
19514
|
+
|
|
19515
|
+
const { folder, fullPath } = moduleFolderInfo;
|
|
19516
|
+
|
|
19517
|
+
// Read module folder children
|
|
19518
|
+
let children;
|
|
19519
|
+
|
|
19520
|
+
try {
|
|
19521
|
+
children = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
19522
|
+
} catch {
|
|
19523
|
+
return {};
|
|
19524
|
+
}
|
|
19525
|
+
|
|
19526
|
+
const codeFilePattern = /\.(tsx?|jsx?)$/;
|
|
19527
|
+
|
|
19528
|
+
// Categorize children
|
|
19529
|
+
const directFiles = children.filter(
|
|
19530
|
+
(child) => child.isFile() && codeFilePattern.test(child.name) && !child.name.startsWith("index."),
|
|
19531
|
+
);
|
|
19532
|
+
|
|
19533
|
+
const subdirectories = children.filter((child) => child.isDirectory());
|
|
19534
|
+
|
|
19535
|
+
// If only index files or empty, no issue
|
|
19536
|
+
if (directFiles.length === 0 && subdirectories.length === 0) return {};
|
|
19537
|
+
|
|
19538
|
+
// If only one non-index file and no subdirectories, no issue
|
|
19539
|
+
if (directFiles.length <= 1 && subdirectories.length === 0) return {};
|
|
19540
|
+
|
|
19541
|
+
// Check if wrapped mode is justified (any subfolder has 2+ code files)
|
|
19542
|
+
const isWrappedJustifiedHandler = () => {
|
|
19543
|
+
for (const dir of subdirectories) {
|
|
19544
|
+
try {
|
|
19545
|
+
const dirPath = `${fullPath}/${dir.name}`;
|
|
19546
|
+
const dirChildren = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
19547
|
+
const codeFiles = dirChildren.filter(
|
|
19548
|
+
(child) => child.isFile() && codeFilePattern.test(child.name),
|
|
19549
|
+
);
|
|
19550
|
+
|
|
19551
|
+
if (codeFiles.length >= 2) return true;
|
|
19552
|
+
} catch {
|
|
19553
|
+
// Skip unreadable directories
|
|
19554
|
+
}
|
|
19555
|
+
}
|
|
19556
|
+
|
|
19557
|
+
return false;
|
|
19558
|
+
};
|
|
19559
|
+
|
|
19560
|
+
const hasDirectFiles = directFiles.length > 0;
|
|
19561
|
+
const hasSubdirectories = subdirectories.length > 0;
|
|
19562
|
+
const isMixed = hasDirectFiles && hasSubdirectories;
|
|
19563
|
+
const wrappedJustified = hasSubdirectories ? isWrappedJustifiedHandler() : false;
|
|
19564
|
+
|
|
19565
|
+
return {
|
|
19566
|
+
Program(node) {
|
|
19567
|
+
// Case: All folders, NOT justified → unnecessary wrapping
|
|
19568
|
+
if (!hasDirectFiles && hasSubdirectories && !wrappedJustified) {
|
|
19569
|
+
context.report({
|
|
19570
|
+
message: `Unnecessary wrapper folders in "${folder}/". Each item has only one file, use direct files instead (e.g., ${folder}/component.tsx).`,
|
|
19571
|
+
node,
|
|
19572
|
+
});
|
|
19573
|
+
|
|
19574
|
+
return;
|
|
19575
|
+
}
|
|
19576
|
+
|
|
19577
|
+
// Case: Mixed + wrapped justified → error on direct files
|
|
19578
|
+
if (isMixed && wrappedJustified) {
|
|
19579
|
+
// Only report if this file IS a direct file in the module folder
|
|
19580
|
+
const relativePart = normalizedFilename.slice(fullPath.length + 1);
|
|
19581
|
+
const isDirectFile = !relativePart.includes("/");
|
|
19582
|
+
|
|
19583
|
+
if (isDirectFile) {
|
|
19584
|
+
context.report({
|
|
19585
|
+
message: `Since some items in "${folder}/" contain multiple files, all items should be wrapped in folders.`,
|
|
19586
|
+
node,
|
|
19587
|
+
});
|
|
19588
|
+
}
|
|
19589
|
+
|
|
19590
|
+
return;
|
|
19591
|
+
}
|
|
19592
|
+
|
|
19593
|
+
// Case: Mixed + NOT justified → error on subfolder files
|
|
19594
|
+
if (isMixed && !wrappedJustified) {
|
|
19595
|
+
// Only report if this file IS inside a subfolder
|
|
19596
|
+
const relativePart = normalizedFilename.slice(fullPath.length + 1);
|
|
19597
|
+
const isInSubfolder = relativePart.includes("/");
|
|
19598
|
+
|
|
19599
|
+
if (isInSubfolder) {
|
|
19600
|
+
context.report({
|
|
19601
|
+
message: `Unnecessary wrapper folder. Each item in "${folder}/" has only one file, use direct files instead.`,
|
|
19602
|
+
node,
|
|
19603
|
+
});
|
|
19604
|
+
}
|
|
19605
|
+
}
|
|
19606
|
+
},
|
|
19607
|
+
};
|
|
19608
|
+
},
|
|
19609
|
+
meta: {
|
|
19610
|
+
docs: { description: "Enforce consistent folder structure (flat vs wrapped) in module folders like atoms, components, hooks, enums, views, layouts, and pages" },
|
|
19611
|
+
fixable: null,
|
|
19612
|
+
schema: [
|
|
19613
|
+
{
|
|
19614
|
+
additionalProperties: false,
|
|
19615
|
+
properties: {
|
|
19616
|
+
extraModuleFolders: {
|
|
19617
|
+
items: { type: "string" },
|
|
19618
|
+
type: "array",
|
|
19619
|
+
},
|
|
19620
|
+
moduleFolders: {
|
|
19621
|
+
items: { type: "string" },
|
|
19622
|
+
type: "array",
|
|
19623
|
+
},
|
|
19624
|
+
},
|
|
19625
|
+
type: "object",
|
|
19626
|
+
},
|
|
19627
|
+
],
|
|
19628
|
+
type: "suggestion",
|
|
19629
|
+
},
|
|
19630
|
+
};
|
|
19631
|
+
|
|
19058
19632
|
/**
|
|
19059
19633
|
* ───────────────────────────────────────────────────────────────
|
|
19060
19634
|
* Rule: No Redundant Folder Suffix
|
|
@@ -19091,9 +19665,6 @@ const noRedundantFolderSuffix = {
|
|
|
19091
19665
|
const fileWithExt = parts[parts.length - 1];
|
|
19092
19666
|
const baseName = fileWithExt.replace(/\.(jsx?|tsx?)$/, "");
|
|
19093
19667
|
|
|
19094
|
-
// Skip index files
|
|
19095
|
-
if (baseName === "index") return {};
|
|
19096
|
-
|
|
19097
19668
|
// Singularize: convert folder name to singular form
|
|
19098
19669
|
const singularizeHandler = (word) => {
|
|
19099
19670
|
if (word.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
@@ -19113,35 +19684,84 @@ const noRedundantFolderSuffix = {
|
|
|
19113
19684
|
|
|
19114
19685
|
if (ancestorFolders.length === 0) return {};
|
|
19115
19686
|
|
|
19116
|
-
// Check
|
|
19117
|
-
const
|
|
19687
|
+
// Check intermediate folder names for redundant suffixes
|
|
19688
|
+
const folderErrors = [];
|
|
19689
|
+
|
|
19690
|
+
for (let i = 1; i < ancestorFolders.length; i++) {
|
|
19691
|
+
const folderName = ancestorFolders[i];
|
|
19692
|
+
|
|
19693
|
+
for (let j = 0; j < i; j++) {
|
|
19694
|
+
const ancestorFolder = ancestorFolders[j];
|
|
19695
|
+
const singular = singularizeHandler(ancestorFolder);
|
|
19696
|
+
const suffix = `-${singular}`;
|
|
19697
|
+
|
|
19698
|
+
if (folderName.endsWith(suffix)) {
|
|
19699
|
+
folderErrors.push({
|
|
19700
|
+
ancestorFolder,
|
|
19701
|
+
folderName,
|
|
19702
|
+
singular,
|
|
19703
|
+
suffix,
|
|
19704
|
+
suggestedName: folderName.slice(0, -suffix.length),
|
|
19705
|
+
});
|
|
19706
|
+
}
|
|
19707
|
+
}
|
|
19708
|
+
}
|
|
19709
|
+
|
|
19710
|
+
// Check if the file name matches the immediate parent folder name (e.g., input/input.tsx → should be input/index.tsx)
|
|
19711
|
+
let fileMatchesFolder = null;
|
|
19712
|
+
|
|
19713
|
+
if (baseName !== "index" && ancestorFolders.length >= 2) {
|
|
19714
|
+
const immediateFolder = ancestorFolders[ancestorFolders.length - 1];
|
|
19715
|
+
|
|
19716
|
+
if (baseName === immediateFolder) {
|
|
19717
|
+
fileMatchesFolder = immediateFolder;
|
|
19718
|
+
}
|
|
19719
|
+
}
|
|
19720
|
+
|
|
19721
|
+
// Check if the file name ends with any ancestor folder name (singularized) — skip index files
|
|
19722
|
+
let fileRedundancy = null;
|
|
19723
|
+
|
|
19724
|
+
if (baseName !== "index" && !fileMatchesFolder) {
|
|
19118
19725
|
for (const folder of ancestorFolders) {
|
|
19119
19726
|
const singular = singularizeHandler(folder);
|
|
19120
19727
|
const suffix = `-${singular}`;
|
|
19121
19728
|
|
|
19122
19729
|
if (baseName.endsWith(suffix)) {
|
|
19123
|
-
|
|
19730
|
+
fileRedundancy = { folder, singular, suffix };
|
|
19731
|
+
break;
|
|
19124
19732
|
}
|
|
19125
19733
|
}
|
|
19734
|
+
}
|
|
19126
19735
|
|
|
19127
|
-
|
|
19128
|
-
};
|
|
19129
|
-
|
|
19130
|
-
const redundancy = checkRedundantSuffixHandler();
|
|
19131
|
-
|
|
19132
|
-
if (!redundancy) return {};
|
|
19736
|
+
if (folderErrors.length === 0 && !fileRedundancy && !fileMatchesFolder) return {};
|
|
19133
19737
|
|
|
19134
19738
|
return {
|
|
19135
19739
|
Program(node) {
|
|
19136
|
-
|
|
19137
|
-
|
|
19138
|
-
|
|
19139
|
-
|
|
19740
|
+
for (const error of folderErrors) {
|
|
19741
|
+
context.report({
|
|
19742
|
+
message: `Folder name "${error.folderName}" has redundant suffix "${error.suffix}" — the "${error.ancestorFolder}/" ancestor folder already provides this context. Rename to "${error.suggestedName}".`,
|
|
19743
|
+
node,
|
|
19744
|
+
});
|
|
19745
|
+
}
|
|
19746
|
+
|
|
19747
|
+
if (fileMatchesFolder) {
|
|
19748
|
+
context.report({
|
|
19749
|
+
message: `File name "${baseName}" is the same as its parent folder "${fileMatchesFolder}/". Use "index" instead (e.g., "${fileMatchesFolder}/index${fileWithExt.match(/\.\w+$/)[0]}").`,
|
|
19750
|
+
node,
|
|
19751
|
+
});
|
|
19752
|
+
}
|
|
19753
|
+
|
|
19754
|
+
if (fileRedundancy) {
|
|
19755
|
+
context.report({
|
|
19756
|
+
message: `File name "${baseName}" has redundant suffix "${fileRedundancy.suffix}" — the "${fileRedundancy.folder}/" folder already provides this context. Rename to "${baseName.slice(0, -fileRedundancy.suffix.length)}".`,
|
|
19757
|
+
node,
|
|
19758
|
+
});
|
|
19759
|
+
}
|
|
19140
19760
|
},
|
|
19141
19761
|
};
|
|
19142
19762
|
},
|
|
19143
19763
|
meta: {
|
|
19144
|
-
docs: { description: "Disallow file names that redundantly include the parent or ancestor folder name as a suffix" },
|
|
19764
|
+
docs: { description: "Disallow file and folder names that redundantly include the parent or ancestor folder name as a suffix" },
|
|
19145
19765
|
fixable: null,
|
|
19146
19766
|
schema: [],
|
|
19147
19767
|
type: "suggestion",
|
|
@@ -22436,9 +23056,10 @@ export default {
|
|
|
22436
23056
|
// Component rules
|
|
22437
23057
|
"component-props-destructure": componentPropsDestructure,
|
|
22438
23058
|
"component-props-inline-type": componentPropsInlineType,
|
|
22439
|
-
"folder-
|
|
23059
|
+
"folder-based-naming-convention": folderBasedNamingConvention,
|
|
23060
|
+
"folder-structure-consistency": folderStructureConsistency,
|
|
22440
23061
|
"no-redundant-folder-suffix": noRedundantFolderSuffix,
|
|
22441
|
-
"svg-
|
|
23062
|
+
"svg-icon-naming-convention": svgIconNamingConvention,
|
|
22442
23063
|
|
|
22443
23064
|
// React rules
|
|
22444
23065
|
"react-code-order": reactCodeOrder,
|
|
@@ -22477,6 +23098,7 @@ export default {
|
|
|
22477
23098
|
"import-source-spacing": importSourceSpacing,
|
|
22478
23099
|
"index-export-style": indexExportStyle,
|
|
22479
23100
|
"index-exports-only": indexExportsOnly,
|
|
23101
|
+
"inline-export-declaration": inlineExportDeclaration,
|
|
22480
23102
|
"module-index-exports": moduleIndexExports,
|
|
22481
23103
|
|
|
22482
23104
|
// JSX rules
|