eslint-plugin-code-style 1.6.1 → 1.6.6

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/index.js +293 -27
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.6.6] - 2026-02-01
11
+
12
+ ### Fixed
13
+
14
+ - **`component-props-destructure`** - Detect body destructuring patterns (e.g., `const { name } = data;`) even without type annotations
15
+ - **`component-props-destructure`** - Add auto-fix for body destructuring: moves props to parameter and removes body declaration
16
+
17
+ ---
18
+
19
+ ## [1.6.5] - 2026-02-01
20
+
21
+ ### Added
22
+
23
+ - **`function-object-destructure`** - Add auto-fix: replaces destructured usages with dot notation and removes declaration
24
+
25
+ ---
26
+
27
+ ## [1.6.4] - 2026-02-01
28
+
29
+ ### Enhanced
30
+
31
+ - **`function-object-destructure`** - Add more module paths: apis, configs, utilities, routes
32
+
33
+ ---
34
+
35
+ ## [1.6.3] - 2026-02-01
36
+
37
+ ### Fixed
38
+
39
+ - **`component-props-destructure`** - Preserve TypeScript type annotation when auto-fixing
40
+
41
+ ---
42
+
43
+ ## [1.6.2] - 2026-02-01
44
+
45
+ ### Enhanced
46
+
47
+ - **`function-object-destructure`** - Expand to check more module paths (services, constants, config, api, utils, helpers, lib) for dot notation enforcement
48
+
49
+ ---
50
+
10
51
  ## [1.6.1] - 2026-02-01
11
52
 
12
53
  ### Enhanced
@@ -987,6 +1028,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
987
1028
 
988
1029
  ---
989
1030
 
1031
+ [1.6.5]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.4...v1.6.5
1032
+ [1.6.4]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.3...v1.6.4
1033
+ [1.6.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.2...v1.6.3
1034
+ [1.6.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.1...v1.6.2
990
1035
  [1.6.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.0...v1.6.1
991
1036
  [1.6.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.5.2...v1.6.0
992
1037
  [1.5.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.5.1...v1.5.2
package/index.js CHANGED
@@ -12414,15 +12414,32 @@ const functionObjectDestructure = {
12414
12414
  create(context) {
12415
12415
  const sourceCode = context.sourceCode || context.getSourceCode();
12416
12416
 
12417
- // Track imports from data-related paths (should use dot notation, not destructure)
12418
- const dataImports = new Set();
12419
-
12420
- const isDataImportPath = (importPath) => {
12421
- // Match paths like @/data, ./data, ../data, or any path containing /data/
12422
- return importPath === "@/data"
12423
- || importPath.endsWith("/data")
12424
- || importPath.includes("/data/")
12425
- || /^\.\.?\/data$/.test(importPath);
12417
+ // Track imports from module paths that should use dot notation, not destructure
12418
+ // This improves searchability: api.loginHandler is easier to find than loginHandler
12419
+ const moduleImports = new Set();
12420
+
12421
+ // Folders that contain modules which should be accessed via dot notation
12422
+ const modulePathPatterns = [
12423
+ "api",
12424
+ "apis",
12425
+ "config",
12426
+ "configs",
12427
+ "constants",
12428
+ "data",
12429
+ "helpers",
12430
+ "lib",
12431
+ "routes",
12432
+ "services",
12433
+ "utils",
12434
+ "utilities",
12435
+ ];
12436
+
12437
+ const isModuleImportPath = (importPath) => {
12438
+ // Match paths like @/services, @/constants, ./data, ../config, etc.
12439
+ return modulePathPatterns.some((pattern) => importPath === `@/${pattern}`
12440
+ || importPath.endsWith(`/${pattern}`)
12441
+ || importPath.includes(`/${pattern}/`)
12442
+ || new RegExp(`^\\.?\\.?/${pattern}$`).test(importPath));
12426
12443
  };
12427
12444
 
12428
12445
  const checkImportHandler = (node) => {
@@ -12430,31 +12447,72 @@ const functionObjectDestructure = {
12430
12447
 
12431
12448
  const importPath = node.source.value;
12432
12449
 
12433
- if (isDataImportPath(importPath)) {
12434
- // Track all imported specifiers from data paths
12450
+ if (isModuleImportPath(importPath)) {
12451
+ // Track all imported specifiers from module paths
12435
12452
  node.specifiers.forEach((spec) => {
12436
12453
  if (spec.type === "ImportSpecifier" && spec.local && spec.local.name) {
12437
- dataImports.add(spec.local.name);
12454
+ moduleImports.add(spec.local.name);
12438
12455
  } else if (spec.type === "ImportDefaultSpecifier" && spec.local && spec.local.name) {
12439
- dataImports.add(spec.local.name);
12456
+ moduleImports.add(spec.local.name);
12440
12457
  }
12441
12458
  });
12442
12459
  }
12443
12460
  };
12444
12461
 
12445
- // Check for destructuring of data imports (not allowed)
12462
+ // Find all references to a variable name in a scope (with parent tracking)
12463
+ const findAllReferencesHandler = (scope, varName, declNode) => {
12464
+ const references = [];
12465
+
12466
+ const visitNode = (n, parent) => {
12467
+ if (!n || typeof n !== "object") return;
12468
+
12469
+ // Skip the declaration itself
12470
+ if (n === declNode) return;
12471
+
12472
+ // Found a reference
12473
+ if (n.type === "Identifier" && n.name === varName) {
12474
+ // Make sure it's not a property key or part of a member expression property
12475
+ const isMemberProp = parent && parent.type === "MemberExpression" && parent.property === n && !parent.computed;
12476
+ const isObjectKey = parent && parent.type === "Property" && parent.key === n && !parent.computed;
12477
+ const isShorthandValue = parent && parent.type === "Property" && parent.shorthand && parent.value === n;
12478
+
12479
+ // Include shorthand properties as references (they use the variable)
12480
+ if (!isMemberProp && !isObjectKey) {
12481
+ references.push(n);
12482
+ }
12483
+ }
12484
+
12485
+ for (const key of Object.keys(n)) {
12486
+ if (key === "parent" || key === "range" || key === "loc") continue;
12487
+
12488
+ const child = n[key];
12489
+
12490
+ if (Array.isArray(child)) {
12491
+ child.forEach((c) => visitNode(c, n));
12492
+ } else if (child && typeof child === "object" && child.type) {
12493
+ visitNode(child, n);
12494
+ }
12495
+ }
12496
+ };
12497
+
12498
+ visitNode(scope, null);
12499
+
12500
+ return references;
12501
+ };
12502
+
12503
+ // Check for destructuring of module imports (not allowed)
12446
12504
  const checkVariableDeclarationHandler = (node) => {
12447
12505
  for (const decl of node.declarations) {
12448
12506
  // Check for ObjectPattern destructuring
12449
12507
  if (decl.id.type === "ObjectPattern" && decl.init) {
12450
12508
  let sourceVarName = null;
12451
12509
 
12452
- // Direct destructuring: const { x } = dataImport
12510
+ // Direct destructuring: const { x } = moduleImport
12453
12511
  if (decl.init.type === "Identifier") {
12454
12512
  sourceVarName = decl.init.name;
12455
12513
  }
12456
12514
 
12457
- // Nested destructuring: const { x } = dataImport.nested
12515
+ // Nested destructuring: const { x } = moduleImport.nested
12458
12516
  if (decl.init.type === "MemberExpression") {
12459
12517
  let obj = decl.init;
12460
12518
 
@@ -12467,15 +12525,77 @@ const functionObjectDestructure = {
12467
12525
  }
12468
12526
  }
12469
12527
 
12470
- if (sourceVarName && dataImports.has(sourceVarName)) {
12528
+ if (sourceVarName && moduleImports.has(sourceVarName)) {
12529
+ // Get destructured properties with their local names
12471
12530
  const destructuredProps = decl.id.properties
12472
12531
  .filter((p) => p.type === "Property" && p.key && p.key.name)
12473
- .map((p) => p.key.name);
12532
+ .map((p) => ({
12533
+ key: p.key.name,
12534
+ local: p.value && p.value.type === "Identifier" ? p.value.name : p.key.name,
12535
+ }));
12474
12536
 
12475
12537
  const sourceText = sourceCode.getText(decl.init);
12476
12538
 
12539
+ // Find the containing function/program to search for references
12540
+ let scope = node.parent;
12541
+
12542
+ while (scope && scope.type !== "BlockStatement" && scope.type !== "Program") {
12543
+ scope = scope.parent;
12544
+ }
12545
+
12477
12546
  context.report({
12478
- message: `Do not destructure data imports. Use dot notation for searchability: "${sourceText}.${destructuredProps[0]}" instead of destructuring`,
12547
+ fix: scope
12548
+ ? (fixer) => {
12549
+ const fixes = [];
12550
+
12551
+ // Replace all references with dot notation
12552
+ destructuredProps.forEach(({ key, local }) => {
12553
+ const refs = findAllReferencesHandler(scope, local, decl);
12554
+
12555
+ refs.forEach((ref) => {
12556
+ fixes.push(fixer.replaceText(ref, `${sourceText}.${key}`));
12557
+ });
12558
+ });
12559
+
12560
+ // Remove the entire declaration statement
12561
+ // If it's the only declaration in the statement, remove the whole statement
12562
+ if (node.declarations.length === 1) {
12563
+ // Find the full statement including newline
12564
+ const tokenBefore = sourceCode.getTokenBefore(node);
12565
+ const tokenAfter = sourceCode.getTokenAfter(node);
12566
+ let start = node.range[0];
12567
+ let end = node.range[1];
12568
+
12569
+ // Include leading whitespace/newline
12570
+ if (tokenBefore) {
12571
+ const textBetween = sourceCode.text.slice(tokenBefore.range[1], node.range[0]);
12572
+ const newlineIndex = textBetween.lastIndexOf("\n");
12573
+
12574
+ if (newlineIndex !== -1) {
12575
+ start = tokenBefore.range[1] + newlineIndex;
12576
+ }
12577
+ }
12578
+
12579
+ // Include trailing newline
12580
+ if (tokenAfter) {
12581
+ const textBetween = sourceCode.text.slice(node.range[1], tokenAfter.range[0]);
12582
+ const newlineIndex = textBetween.indexOf("\n");
12583
+
12584
+ if (newlineIndex !== -1) {
12585
+ end = node.range[1] + newlineIndex + 1;
12586
+ }
12587
+ }
12588
+
12589
+ fixes.push(fixer.removeRange([start, end]));
12590
+ } else {
12591
+ // Remove just this declarator
12592
+ fixes.push(fixer.remove(decl));
12593
+ }
12594
+
12595
+ return fixes;
12596
+ }
12597
+ : undefined,
12598
+ message: `Do not destructure module imports. Use dot notation for searchability: "${sourceText}.${destructuredProps[0].key}" instead of destructuring`,
12479
12599
  node: decl.id,
12480
12600
  });
12481
12601
  }
@@ -13258,6 +13378,66 @@ const componentPropsDestructure = {
13258
13378
  return accesses;
13259
13379
  };
13260
13380
 
13381
+ // Find body destructuring like: const { name } = data; or const { name, age } = data;
13382
+ const findBodyDestructuringHandler = (body, paramName) => {
13383
+ const results = [];
13384
+
13385
+ if (body.type !== "BlockStatement") return results;
13386
+
13387
+ for (const statement of body.body) {
13388
+ if (statement.type === "VariableDeclaration") {
13389
+ for (const declarator of statement.declarations) {
13390
+ // Check if it's destructuring from the param: const { x } = paramName
13391
+ if (
13392
+ declarator.id.type === "ObjectPattern" &&
13393
+ declarator.init &&
13394
+ declarator.init.type === "Identifier" &&
13395
+ declarator.init.name === paramName
13396
+ ) {
13397
+ const props = [];
13398
+
13399
+ for (const prop of declarator.id.properties) {
13400
+ if (prop.type === "Property" && prop.key.type === "Identifier") {
13401
+ // Handle both { name } and { name: alias }
13402
+ const keyName = prop.key.name;
13403
+ const valueName = prop.value.type === "Identifier" ? prop.value.name : null;
13404
+ const hasDefault = prop.value.type === "AssignmentPattern";
13405
+ let defaultValue = null;
13406
+
13407
+ if (hasDefault && prop.value.right) {
13408
+ defaultValue = sourceCode.getText(prop.value.right);
13409
+ }
13410
+
13411
+ props.push({
13412
+ default: defaultValue,
13413
+ hasAlias: keyName !== valueName && !hasDefault,
13414
+ key: keyName,
13415
+ value: hasDefault ? prop.value.left.name : valueName,
13416
+ });
13417
+ } else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
13418
+ props.push({
13419
+ isRest: true,
13420
+ key: prop.argument.name,
13421
+ value: prop.argument.name,
13422
+ });
13423
+ }
13424
+ }
13425
+
13426
+ results.push({
13427
+ declarator,
13428
+ props,
13429
+ statement,
13430
+ // Track if this is the only declarator in the statement
13431
+ statementHasOnlyThisDeclarator: statement.declarations.length === 1,
13432
+ });
13433
+ }
13434
+ }
13435
+ }
13436
+ }
13437
+
13438
+ return results;
13439
+ };
13440
+
13261
13441
  const checkComponentPropsHandler = (node) => {
13262
13442
  if (!isReactComponentHandler(node)) return;
13263
13443
 
@@ -13271,14 +13451,33 @@ const componentPropsDestructure = {
13271
13451
 
13272
13452
  if (firstParam.type === "Identifier") {
13273
13453
  const paramName = firstParam.name;
13454
+
13455
+ // Find dot notation accesses: props.name
13274
13456
  const accesses = findPropAccessesHandler(node.body, paramName);
13275
- const accessedProps = [...new Set(accesses.map((a) => a.property))];
13276
13457
 
13277
- // Check if param is used directly (not just via dot notation)
13458
+ // Find body destructuring: const { name } = props
13459
+ const bodyDestructures = findBodyDestructuringHandler(node.body, paramName);
13460
+
13461
+ // Collect all accessed props from dot notation
13462
+ const dotNotationProps = [...new Set(accesses.map((a) => a.property))];
13463
+
13464
+ // Collect all props from body destructuring
13465
+ const bodyDestructuredProps = [];
13466
+
13467
+ bodyDestructures.forEach((bd) => {
13468
+ bd.props.forEach((p) => {
13469
+ bodyDestructuredProps.push(p);
13470
+ });
13471
+ });
13472
+
13473
+ // Check if param is used anywhere that we can't handle
13278
13474
  const allRefs = [];
13279
- const countRefs = (n) => {
13475
+ const countRefs = (n, skipNodes = []) => {
13280
13476
  if (!n || typeof n !== "object") return;
13281
13477
 
13478
+ // Skip nodes we're already accounting for
13479
+ if (skipNodes.includes(n)) return;
13480
+
13282
13481
  if (n.type === "Identifier" && n.name === paramName) allRefs.push(n);
13283
13482
 
13284
13483
  for (const key of Object.keys(n)) {
@@ -13286,29 +13485,96 @@ const componentPropsDestructure = {
13286
13485
 
13287
13486
  const child = n[key];
13288
13487
 
13289
- if (Array.isArray(child)) child.forEach(countRefs);
13290
- else if (child && typeof child === "object" && child.type) countRefs(child);
13488
+ if (Array.isArray(child)) child.forEach((c) => countRefs(c, skipNodes));
13489
+ else if (child && typeof child === "object" && child.type) countRefs(child, skipNodes);
13291
13490
  }
13292
13491
  };
13293
13492
 
13294
13493
  countRefs(node.body);
13295
13494
 
13296
- // Can only auto-fix if all references are covered by dot notation accesses
13297
- const canAutoFix = accessedProps.length > 0 && allRefs.length === accesses.length;
13495
+ // Count expected refs: dot notation accesses + body destructuring init nodes
13496
+ const expectedRefCount = accesses.length + bodyDestructures.length;
13497
+
13498
+ // Can auto-fix if:
13499
+ // 1. We have either dot notation props OR body destructured props
13500
+ // 2. All references to the param are accounted for
13501
+ const hasSomeProps = dotNotationProps.length > 0 || bodyDestructuredProps.length > 0;
13502
+ const canAutoFix = hasSomeProps && allRefs.length === expectedRefCount;
13298
13503
 
13299
13504
  context.report({
13300
13505
  fix: canAutoFix
13301
13506
  ? (fixer) => {
13302
13507
  const fixes = [];
13303
13508
 
13509
+ // Build the destructured props list for the parameter
13510
+ const allProps = [];
13511
+
13512
+ // Add dot notation props (simple names)
13513
+ dotNotationProps.forEach((p) => {
13514
+ if (!allProps.some((ap) => ap.key === p)) {
13515
+ allProps.push({ key: p, simple: true });
13516
+ }
13517
+ });
13518
+
13519
+ // Add body destructured props (may have aliases, defaults, rest)
13520
+ bodyDestructuredProps.forEach((p) => {
13521
+ // Don't duplicate if already in dot notation
13522
+ if (!allProps.some((ap) => ap.key === p.key)) {
13523
+ allProps.push(p);
13524
+ }
13525
+ });
13526
+
13527
+ // Build the destructured pattern string
13528
+ const propStrings = allProps.map((p) => {
13529
+ if (p.isRest) return `...${p.key}`;
13530
+
13531
+ if (p.simple) return p.key;
13532
+
13533
+ if (p.default) return `${p.key} = ${p.default}`;
13534
+
13535
+ if (p.hasAlias) return `${p.key}: ${p.value}`;
13536
+
13537
+ return p.key;
13538
+ });
13539
+ const destructuredPattern = `{ ${propStrings.join(", ")} }`;
13540
+ let replacement = destructuredPattern;
13541
+
13542
+ // Preserve TypeScript type annotation if present
13543
+ if (firstParam.typeAnnotation) {
13544
+ const typeText = sourceCode.getText(firstParam.typeAnnotation);
13545
+
13546
+ replacement = `${destructuredPattern}${typeText}`;
13547
+ }
13548
+
13304
13549
  // Replace param with destructured pattern
13305
- fixes.push(fixer.replaceText(firstParam, `{ ${accessedProps.join(", ")} }`));
13550
+ fixes.push(fixer.replaceText(firstParam, replacement));
13306
13551
 
13307
13552
  // Replace all props.x with just x
13308
13553
  accesses.forEach((access) => {
13309
13554
  fixes.push(fixer.replaceText(access.node, access.property));
13310
13555
  });
13311
13556
 
13557
+ // Remove body destructuring statements
13558
+ bodyDestructures.forEach((bd) => {
13559
+ if (bd.statementHasOnlyThisDeclarator) {
13560
+ // Remove the entire statement including newline
13561
+ const statementStart = bd.statement.range[0];
13562
+ let statementEnd = bd.statement.range[1];
13563
+
13564
+ // Try to also remove trailing newline/whitespace
13565
+ const textAfter = sourceCode.getText().slice(statementEnd, statementEnd + 2);
13566
+
13567
+ if (textAfter.startsWith("\n")) statementEnd += 1;
13568
+ else if (textAfter.startsWith("\r\n")) statementEnd += 2;
13569
+
13570
+ fixes.push(fixer.removeRange([statementStart, statementEnd]));
13571
+ } else {
13572
+ // Only remove this declarator from a multi-declarator statement
13573
+ // This is more complex - for now just remove the declarator text
13574
+ fixes.push(fixer.remove(bd.declarator));
13575
+ }
13576
+ });
13577
+
13312
13578
  return fixes;
13313
13579
  }
13314
13580
  : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.6.1",
3
+ "version": "1.6.6",
4
4
  "description": "A custom ESLint plugin for enforcing consistent code formatting and style rules in React/JSX projects",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",