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/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 which need relative imports for re-exports
6632
+ // EXCEPT in index files, entry files, and sibling files within the same module folder
6611
6633
  if (importPath.startsWith("./") || importPath.startsWith("../")) {
6612
- if (!isIndexFile && !isEntryFile) {
6613
- context.report({
6614
- message: `Relative imports are not allowed. Use absolute imports with "${aliasPrefix}" prefix instead.`,
6615
- node: node.source,
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 svgComponentIconNaming = {
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 folderComponentSuffix = {
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
- // Check which folder the file is in
18901
- const getFolderSuffixHandler = () => {
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
- if (pattern.test(normalizedFilename)) {
18906
- return { folder, suffix };
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
- return null;
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 component name starts with uppercase (React component convention)
18929
- const isReactComponentNameHandler = (name) => name && /^[A-Z]/.test(name);
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 folderInfo = getFolderSuffixHandler();
19302
+ const moduleInfo = getModuleInfoHandler();
18971
19303
 
18972
- // Not in a folder that requires specific suffix
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
- // Only check React components (PascalCase)
18982
- if (!isReactComponentNameHandler(name)) return;
19312
+ const { folder, suffix } = moduleInfo;
18983
19313
 
18984
- // Only check functions that return JSX
18985
- if (!returnsJsxHandler(node)) return;
19314
+ // For camelCase folders, only enforce suffix
19315
+ if (camelCaseFolders.has(folder)) {
19316
+ if (!isCamelCaseHandler(name) || !suffix) return;
18986
19317
 
18987
- const { folder, suffix } = folderInfo;
19318
+ checkCamelCaseHandler(name, folder, suffix, identifierNode);
18988
19319
 
18989
- // Check if component name ends with the required suffix
18990
- if (!name.endsWith(suffix)) {
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(fixer) {
18995
- const scope = context.sourceCode
18996
- ? context.sourceCode.getScope(node)
18997
- : context.getScope();
19334
+ fix: createRenameFixer(node, name, expectedName, identifierNode),
19335
+ message: buildMessageHandler(name, folder, suffix, expectedName),
19336
+ node: identifierNode,
19337
+ });
19338
+ }
19339
+ };
18998
19340
 
18999
- // Find the variable in scope
19000
- const findVariableHandler = (s, varName) => {
19001
- const v = s.variables.find((variable) => variable.name === varName);
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
- if (v) return v;
19004
- if (s.upper) return findVariableHandler(s.upper, varName);
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
- return null;
19007
- };
19349
+ const moduleInfo = getModuleInfoHandler();
19008
19350
 
19009
- const variable = findVariableHandler(scope, name);
19351
+ if (!moduleInfo) return;
19010
19352
 
19011
- if (!variable) return fixer.replaceText(identifierNode, newName);
19353
+ const { folder, suffix } = moduleInfo;
19012
19354
 
19013
- const fixes = [];
19014
- const fixedRanges = new Set();
19355
+ // Only check non-JSX folders (contexts, themes, data, constants, strings, etc.)
19356
+ if (jsxRequiredFolders.has(folder)) return;
19015
19357
 
19016
- // Fix definition
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
- if (!fixedRanges.has(rangeKey)) {
19021
- fixedRanges.add(rangeKey);
19022
- fixes.push(fixer.replaceText(def.name, newName));
19023
- }
19024
- });
19360
+ // For camelCase folders, only enforce suffix
19361
+ if (camelCaseFolders.has(folder)) {
19362
+ if (!isCamelCaseHandler(name) || !suffix) return;
19025
19363
 
19026
- // Fix all references
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
- if (!fixedRanges.has(rangeKey)) {
19031
- fixedRanges.add(rangeKey);
19032
- fixes.push(fixer.replaceText(ref.identifier, newName));
19033
- }
19034
- });
19366
+ return;
19367
+ }
19035
19368
 
19036
- return fixes;
19037
- },
19038
- message: `Component "${name}" in "${folder}" folder must end with "${suffix}" suffix (e.g., "${newName}")`,
19039
- node: identifierNode,
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 components in 'views' folder end with 'View', components in 'pages' folder end with 'Page', and components in 'layouts' folder end with 'Layout'" },
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 if the file name ends with any ancestor folder name (singularized)
19117
- const checkRedundantSuffixHandler = () => {
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
- return { folder, singular, suffix };
19730
+ fileRedundancy = { folder, singular, suffix };
19731
+ break;
19124
19732
  }
19125
19733
  }
19734
+ }
19126
19735
 
19127
- return null;
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
- context.report({
19137
- message: `File name "${baseName}" has redundant suffix "${redundancy.suffix}" — the "${redundancy.folder}/" folder already provides this context. Rename to "${baseName.slice(0, -redundancy.suffix.length)}".`,
19138
- node,
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-component-suffix": folderComponentSuffix,
23059
+ "folder-based-naming-convention": folderBasedNamingConvention,
23060
+ "folder-structure-consistency": folderStructureConsistency,
22440
23061
  "no-redundant-folder-suffix": noRedundantFolderSuffix,
22441
- "svg-component-icon-naming": svgComponentIconNaming,
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