eslint-plugin-code-style 1.7.0 → 1.7.2

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 +22 -0
  2. package/index.js +442 -38
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.7.2] - 2026-02-02
11
+
12
+ ### Fixed
13
+
14
+ - **`enum-format`** - Fix double comma bug when auto-fixing trailing comma and closing brace position; check for comma token after member, not just member text
15
+ - **`interface-format`** - Same fix as enum-format for trailing comma detection
16
+
17
+ ---
18
+
19
+ ## [1.7.1] - 2026-02-02
20
+
21
+ ### Fixed
22
+
23
+ - **`no-empty-lines-in-function-params`** - Detect empty lines between destructured properties inside ObjectPattern params
24
+ - **`component-props-inline-type`** - Handle TSIntersectionType (e.g., `ButtonHTMLAttributes & { prop: Type }`): check `&` position, opening brace position, and apply formatting rules to type literals within intersections
25
+ - **`enum-type-enforcement`** - Handle TSIntersectionType to track typed props; fix extractTypeInfoHandler argument for TSPropertySignature members
26
+ - **`ternary-condition-multiline`** - Improve simple ternary prefix calculation for object properties; add checks for `?` on same line as condition end and empty lines before `?` or `:`; fix multiline formatting to not add leading newline
27
+
28
+ ---
29
+
10
30
  ## [1.7.0] - 2026-02-02
11
31
 
12
32
  **New Rules for Blocks, Classes & Enum Enforcement + Multiple Enhancements**
@@ -1064,6 +1084,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1064
1084
 
1065
1085
  ---
1066
1086
 
1087
+ [1.7.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.1...v1.7.2
1088
+ [1.7.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.0...v1.7.1
1067
1089
  [1.7.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.6...v1.7.0
1068
1090
  [1.6.6]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.5...v1.6.6
1069
1091
  [1.6.5]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.4...v1.6.5
package/index.js CHANGED
@@ -4049,20 +4049,28 @@ const ternaryConditionMultiline = {
4049
4049
  const singleLineText = getTernarySingleLineHandler(node);
4050
4050
  const indent = getLineIndentHandler(node);
4051
4051
 
4052
- // Check if the parent needs prefix text (like "const x = ")
4052
+ // Check if the parent needs prefix text (like "const x = " or "key: ")
4053
4053
  let prefixLength = 0;
4054
4054
  const parent = node.parent;
4055
4055
 
4056
4056
  if (parent && parent.type === "VariableDeclarator" && parent.init === node) {
4057
- const declarationLine = sourceCode.lines[parent.loc.start.line - 1];
4058
- const beforeTernary = declarationLine.slice(0, node.loc.start.column);
4057
+ // Calculate prefix based on parent's start line (where "const x = " is)
4058
+ const varDecl = parent.parent;
4059
+ const declKeyword = varDecl ? sourceCode.getFirstToken(varDecl).value : "const";
4060
+ const varName = parent.id.name || sourceCode.getText(parent.id);
4059
4061
 
4060
- prefixLength = beforeTernary.length - indent;
4062
+ // Prefix is "const varName = " or similar
4063
+ prefixLength = declKeyword.length + 1 + varName.length + 3; // keyword + space + name + " = "
4061
4064
  } else if (parent && parent.type === "AssignmentExpression" && parent.right === node) {
4062
- const assignmentLine = sourceCode.lines[parent.loc.start.line - 1];
4063
- const beforeTernary = assignmentLine.slice(0, node.loc.start.column);
4065
+ // Calculate prefix based on left side of assignment
4066
+ const leftText = sourceCode.getText(parent.left);
4064
4067
 
4065
- prefixLength = beforeTernary.length - indent;
4068
+ prefixLength = leftText.length + 3; // left + " = "
4069
+ } else if (parent && parent.type === "Property" && parent.value === node) {
4070
+ // Object property: key: ternary
4071
+ const keyText = sourceCode.getText(parent.key);
4072
+
4073
+ prefixLength = keyText.length + 2; // key + ": "
4066
4074
  }
4067
4075
 
4068
4076
  // Check if single line would fit
@@ -4143,6 +4151,33 @@ const ternaryConditionMultiline = {
4143
4151
  if (!isCorrectionNeeded && hasOperatorAtEndOfLineHandler(test)) {
4144
4152
  isCorrectionNeeded = true;
4145
4153
  }
4154
+
4155
+ // Check if ? is on same line as end of multiline condition (BAD - should be on its own line)
4156
+ if (!isCorrectionNeeded) {
4157
+ const questionToken = sourceCode.getTokenAfter(test, (t) => t.value === "?");
4158
+
4159
+ if (questionToken && questionToken.loc.start.line === test.loc.end.line) {
4160
+ isCorrectionNeeded = true;
4161
+ }
4162
+ }
4163
+
4164
+ // Check for empty lines before ? (between condition and ?)
4165
+ if (!isCorrectionNeeded) {
4166
+ const questionToken = sourceCode.getTokenAfter(test, (t) => t.value === "?");
4167
+
4168
+ if (questionToken && questionToken.loc.start.line > test.loc.end.line + 1) {
4169
+ isCorrectionNeeded = true;
4170
+ }
4171
+ }
4172
+
4173
+ // Check for empty lines before : (between consequent and :)
4174
+ if (!isCorrectionNeeded) {
4175
+ const colonToken = sourceCode.getTokenAfter(node.consequent, (t) => t.value === ":");
4176
+
4177
+ if (colonToken && colonToken.loc.start.line > node.consequent.loc.end.line + 1) {
4178
+ isCorrectionNeeded = true;
4179
+ }
4180
+ }
4146
4181
  }
4147
4182
 
4148
4183
  if (isCorrectionNeeded) {
@@ -4152,7 +4187,6 @@ const ternaryConditionMultiline = {
4152
4187
  const lineText = sourceCode.lines[node.loc.start.line - 1];
4153
4188
  const baseIndent = lineText.match(/^\s*/)[0];
4154
4189
  const conditionIndent = baseIndent + " ";
4155
- const branchIndent = baseIndent + " ";
4156
4190
 
4157
4191
  const buildMultilineHandler = (n) => {
4158
4192
  if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
@@ -4168,7 +4202,9 @@ const ternaryConditionMultiline = {
4168
4202
  const consequentText = sourceCode.getText(node.consequent);
4169
4203
  const alternateText = sourceCode.getText(node.alternate);
4170
4204
 
4171
- const newText = `\n${conditionIndent}${buildMultilineHandler(test)}\n${branchIndent}? ${consequentText}\n${branchIndent}: ${alternateText}`;
4205
+ // No leading newline - keep first operand on same line as whatever precedes it
4206
+ // Use conditionIndent for ? and : to align with || operators
4207
+ const newText = `${buildMultilineHandler(test)}\n${conditionIndent}? ${consequentText}\n${conditionIndent}: ${alternateText}`;
4172
4208
 
4173
4209
  return fixer.replaceText(node, newText);
4174
4210
  },
@@ -4469,6 +4505,20 @@ const enumTypeEnforcement = {
4469
4505
  return null;
4470
4506
  };
4471
4507
 
4508
+ // Helper to process TSTypeLiteral members
4509
+ const processTypeLiteralMembersHandler = (members) => {
4510
+ members.forEach((member) => {
4511
+ if (member.type === "TSPropertySignature" && member.key?.type === "Identifier") {
4512
+ const propName = member.key.name;
4513
+ const typeInfo = extractTypeInfoHandler(member.typeAnnotation);
4514
+
4515
+ if (typeInfo) {
4516
+ typeAnnotatedVars.set(propName, typeInfo);
4517
+ }
4518
+ }
4519
+ });
4520
+ };
4521
+
4472
4522
  // Track type-annotated parameters in function/component definitions
4473
4523
  const trackTypedParamsHandler = (params) => {
4474
4524
  params.forEach((param) => {
@@ -4477,14 +4527,14 @@ const enumTypeEnforcement = {
4477
4527
  const annotation = param.typeAnnotation.typeAnnotation;
4478
4528
 
4479
4529
  if (annotation && annotation.type === "TSTypeLiteral") {
4480
- annotation.members.forEach((member) => {
4481
- if (member.type === "TSPropertySignature" && member.key?.type === "Identifier") {
4482
- const propName = member.key.name;
4483
- const typeInfo = extractTypeInfoHandler(member);
4530
+ processTypeLiteralMembersHandler(annotation.members);
4531
+ }
4484
4532
 
4485
- if (typeInfo) {
4486
- typeAnnotatedVars.set(propName, typeInfo);
4487
- }
4533
+ // Handle intersection types: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: ButtonVariantType }
4534
+ if (annotation && annotation.type === "TSIntersectionType") {
4535
+ annotation.types.forEach((intersectionType) => {
4536
+ if (intersectionType.type === "TSTypeLiteral") {
4537
+ processTypeLiteralMembersHandler(intersectionType.members);
4488
4538
  }
4489
4539
  });
4490
4540
  }
@@ -9565,6 +9615,29 @@ const noEmptyLinesInFunctionParams = {
9565
9615
  });
9566
9616
  }
9567
9617
  }
9618
+
9619
+ // Check inside ObjectPattern params for empty lines between destructured props
9620
+ params.forEach((param) => {
9621
+ if (param.type === "ObjectPattern" && param.properties.length > 1) {
9622
+ for (let i = 0; i < param.properties.length - 1; i += 1) {
9623
+ const current = param.properties[i];
9624
+ const next = param.properties[i + 1];
9625
+
9626
+ if (next.loc.start.line - current.loc.end.line > 1) {
9627
+ const commaToken = sourceCode.getTokenAfter(current, (t) => t.value === ",");
9628
+
9629
+ context.report({
9630
+ fix: (fixer) => fixer.replaceTextRange(
9631
+ [commaToken.range[1], next.range[0]],
9632
+ "\n" + " ".repeat(next.loc.start.column),
9633
+ ),
9634
+ message: "No empty lines between destructured properties",
9635
+ node: next,
9636
+ });
9637
+ }
9638
+ }
9639
+ }
9640
+ });
9568
9641
  };
9569
9642
 
9570
9643
  return {
@@ -14227,6 +14300,191 @@ const componentPropsInlineType = {
14227
14300
  }
14228
14301
  }
14229
14302
 
14303
+ // Handle intersection types: ButtonHTMLAttributes<HTMLButtonElement> & { prop: Type }
14304
+ if (typeAnnotation.type === "TSIntersectionType" && isComponent) {
14305
+ const types = typeAnnotation.types;
14306
+
14307
+ // Check & operators are on same line as previous type
14308
+ for (let i = 0; i < types.length - 1; i += 1) {
14309
+ const currentType = types[i];
14310
+ const nextType = types[i + 1];
14311
+
14312
+ const ampersandToken = sourceCode.getTokenAfter(currentType, (t) => t.value === "&");
14313
+
14314
+ if (ampersandToken && ampersandToken.loc.start.line !== currentType.loc.end.line) {
14315
+ context.report({
14316
+ fix: (fixer) => fixer.replaceTextRange(
14317
+ [currentType.range[1], ampersandToken.range[1]],
14318
+ " &",
14319
+ ),
14320
+ message: "\"&\" must be on same line as previous type",
14321
+ node: ampersandToken,
14322
+ });
14323
+ }
14324
+
14325
+ // { should be on same line as &
14326
+ if (nextType.type === "TSTypeLiteral" && ampersandToken) {
14327
+ const openBrace = sourceCode.getFirstToken(nextType);
14328
+
14329
+ if (openBrace && openBrace.loc.start.line !== ampersandToken.loc.end.line) {
14330
+ context.report({
14331
+ fix: (fixer) => fixer.replaceTextRange(
14332
+ [ampersandToken.range[1], openBrace.range[0]],
14333
+ " ",
14334
+ ),
14335
+ message: "Opening brace must be on same line as \"&\"",
14336
+ node: openBrace,
14337
+ });
14338
+ }
14339
+ }
14340
+ }
14341
+
14342
+ // Find TSTypeLiteral in the intersection and apply formatting rules
14343
+ const typeLiteral = types.find((t) => t.type === "TSTypeLiteral");
14344
+
14345
+ if (typeLiteral) {
14346
+ const members = typeLiteral.members;
14347
+
14348
+ // Get the base indentation from the component declaration
14349
+ const componentLine = sourceCode.lines[node.loc.start.line - 1];
14350
+ const baseIndent = componentLine.match(/^\s*/)[0];
14351
+ const propIndent = baseIndent + " ";
14352
+
14353
+ // Get opening and closing brace tokens
14354
+ const openBraceToken = sourceCode.getFirstToken(typeLiteral);
14355
+ const closeBraceToken = sourceCode.getLastToken(typeLiteral);
14356
+
14357
+ // For multiple members, first member should be on new line after opening brace
14358
+ if (members.length > 1 && members[0]) {
14359
+ const firstMember = members[0];
14360
+
14361
+ if (firstMember.loc.start.line === openBraceToken.loc.end.line) {
14362
+ context.report({
14363
+ fix: (fixer) => fixer.replaceTextRange(
14364
+ [openBraceToken.range[1], firstMember.range[0]],
14365
+ "\n" + propIndent,
14366
+ ),
14367
+ message: "First props type property must be on a new line when there are multiple properties",
14368
+ node: firstMember,
14369
+ });
14370
+ }
14371
+ }
14372
+
14373
+ // Check closing brace position - should be on its own line for multiple members
14374
+ if (members.length > 1 && closeBraceToken) {
14375
+ const lastMember = members[members.length - 1];
14376
+
14377
+ if (closeBraceToken.loc.start.line === lastMember.loc.end.line) {
14378
+ context.report({
14379
+ fix: (fixer) => fixer.replaceTextRange(
14380
+ [lastMember.range[1], closeBraceToken.range[0]],
14381
+ "\n" + baseIndent,
14382
+ ),
14383
+ message: "Closing brace must be on its own line when there are multiple properties",
14384
+ node: closeBraceToken,
14385
+ });
14386
+ }
14387
+ }
14388
+
14389
+ // Check each member for formatting
14390
+ members.forEach((member, index) => {
14391
+ const memberText = sourceCode.getText(member);
14392
+
14393
+ // Check property ends with comma, not semicolon
14394
+ if (memberText.trimEnd().endsWith(";")) {
14395
+ context.report({
14396
+ fix: (fixer) => {
14397
+ const lastChar = memberText.lastIndexOf(";");
14398
+ const absolutePos = member.range[0] + lastChar;
14399
+
14400
+ return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
14401
+ },
14402
+ message: "Props type properties must end with comma (,) not semicolon (;)",
14403
+ node: member,
14404
+ });
14405
+ }
14406
+
14407
+ // If more than one member, check each is on its own line
14408
+ if (members.length > 1 && index > 0) {
14409
+ const prevMember = members[index - 1];
14410
+
14411
+ if (member.loc.start.line === prevMember.loc.end.line) {
14412
+ context.report({
14413
+ fix: (fixer) => {
14414
+ let commaToken = sourceCode.getTokenAfter(prevMember);
14415
+
14416
+ while (commaToken && commaToken.value !== "," && commaToken.range[0] < member.range[0]) {
14417
+ commaToken = sourceCode.getTokenAfter(commaToken);
14418
+ }
14419
+
14420
+ const insertPoint = commaToken && commaToken.value === "," ? commaToken.range[1] : prevMember.range[1];
14421
+
14422
+ return fixer.replaceTextRange(
14423
+ [insertPoint, member.range[0]],
14424
+ "\n" + propIndent,
14425
+ );
14426
+ },
14427
+ message: "Each props type property must be on its own line when there are multiple properties",
14428
+ node: member,
14429
+ });
14430
+ }
14431
+
14432
+ // Check for empty lines between properties
14433
+ if (member.loc.start.line - prevMember.loc.end.line > 1) {
14434
+ context.report({
14435
+ fix: (fixer) => {
14436
+ const textBetween = sourceCode.getText().slice(
14437
+ prevMember.range[1],
14438
+ member.range[0],
14439
+ );
14440
+ const newText = textBetween.replace(/\n\s*\n/g, "\n");
14441
+
14442
+ return fixer.replaceTextRange(
14443
+ [prevMember.range[1], member.range[0]],
14444
+ newText,
14445
+ );
14446
+ },
14447
+ message: "No empty lines allowed between props type properties",
14448
+ node: member,
14449
+ });
14450
+ }
14451
+ }
14452
+ });
14453
+
14454
+ // Check that last member has trailing comma
14455
+ if (members.length > 0) {
14456
+ const lastMember = members[members.length - 1];
14457
+ const lastMemberText = sourceCode.getText(lastMember);
14458
+
14459
+ if (!lastMemberText.trimEnd().endsWith(",")) {
14460
+ context.report({
14461
+ fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
14462
+ message: "Last props type property must have trailing comma",
14463
+ node: lastMember,
14464
+ });
14465
+ }
14466
+ }
14467
+
14468
+ // Check for empty lines before closing brace
14469
+ if (members.length > 0 && closeBraceToken) {
14470
+ const lastMember = members[members.length - 1];
14471
+
14472
+ if (closeBraceToken.loc.start.line - lastMember.loc.end.line > 1) {
14473
+ context.report({
14474
+ fix: (fixer) => fixer.replaceTextRange(
14475
+ [lastMember.range[1], closeBraceToken.range[0]],
14476
+ "\n" + baseIndent,
14477
+ ),
14478
+ message: "No empty line before closing brace in props type",
14479
+ node: closeBraceToken,
14480
+ });
14481
+ }
14482
+ }
14483
+ }
14484
+
14485
+ return;
14486
+ }
14487
+
14230
14488
  // Check if type is a reference (TSTypeReference) instead of inline (TSTypeLiteral)
14231
14489
  // Only enforce inline types for React components - regular functions can use interface/type references
14232
14490
  if (typeAnnotation.type === "TSTypeReference") {
@@ -14429,6 +14687,20 @@ const componentPropsInlineType = {
14429
14687
  }
14430
14688
  }
14431
14689
  });
14690
+
14691
+ // Check that last member has trailing comma
14692
+ if (members.length > 0) {
14693
+ const lastMember = members[members.length - 1];
14694
+ const lastMemberText = sourceCode.getText(lastMember);
14695
+
14696
+ if (!lastMemberText.trimEnd().endsWith(",")) {
14697
+ context.report({
14698
+ fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
14699
+ message: "Last props type property must have trailing comma",
14700
+ node: lastMember,
14701
+ });
14702
+ }
14703
+ }
14432
14704
  }
14433
14705
  };
14434
14706
 
@@ -16766,19 +17038,24 @@ const enumFormat = {
16766
17038
  }
16767
17039
 
16768
17040
  // Check member ends with comma, not semicolon
16769
- const memberText = sourceCode.getText(member);
17041
+ // Skip last member when multiple members - handled by combined check below
17042
+ const isLastMember = index === members.length - 1;
16770
17043
 
16771
- if (memberText.trimEnd().endsWith(";")) {
16772
- context.report({
16773
- fix(fixer) {
16774
- const lastChar = memberText.lastIndexOf(";");
16775
- const absolutePos = member.range[0] + lastChar;
17044
+ if (!isLastMember || members.length === 1) {
17045
+ const memberText = sourceCode.getText(member);
16776
17046
 
16777
- return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
16778
- },
16779
- message: "Enum members must end with comma (,) not semicolon (;)",
16780
- node: member,
16781
- });
17047
+ if (memberText.trimEnd().endsWith(";")) {
17048
+ context.report({
17049
+ fix(fixer) {
17050
+ const lastChar = memberText.lastIndexOf(";");
17051
+ const absolutePos = member.range[0] + lastChar;
17052
+
17053
+ return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
17054
+ },
17055
+ message: "Enum members must end with comma (,) not semicolon (;)",
17056
+ node: member,
17057
+ });
17058
+ }
16782
17059
  }
16783
17060
 
16784
17061
  // Check formatting for multiple members
@@ -16828,6 +17105,67 @@ const enumFormat = {
16828
17105
  }
16829
17106
  }
16830
17107
  });
17108
+
17109
+ // Check closing brace position and trailing comma/semicolon (for multiple members)
17110
+ if (members.length > 1) {
17111
+ const lastMemberText = sourceCode.getText(lastMember);
17112
+ const trimmedText = lastMemberText.trimEnd();
17113
+ // Check both: text ends with comma OR there's a comma token after the member
17114
+ const tokenAfterLast = sourceCode.getTokenAfter(lastMember);
17115
+ const hasTrailingComma = trimmedText.endsWith(",") || (tokenAfterLast && tokenAfterLast.value === ",");
17116
+ const hasTrailingSemicolon = trimmedText.endsWith(";");
17117
+ const braceOnSameLine = closeBraceToken && closeBraceToken.loc.start.line === lastMember.loc.end.line;
17118
+
17119
+ // Handle semicolon on last member (needs replacement with comma)
17120
+ if (hasTrailingSemicolon) {
17121
+ const lastSemicolon = lastMemberText.lastIndexOf(";");
17122
+ const absolutePos = lastMember.range[0] + lastSemicolon;
17123
+
17124
+ if (braceOnSameLine) {
17125
+ // Both semicolon and brace issues - fix together
17126
+ context.report({
17127
+ fix: (fixer) => fixer.replaceTextRange(
17128
+ [absolutePos, closeBraceToken.range[0]],
17129
+ ",\n" + baseIndent,
17130
+ ),
17131
+ message: "Last enum member must end with comma and closing brace must be on its own line",
17132
+ node: lastMember,
17133
+ });
17134
+ } else {
17135
+ // Just semicolon issue
17136
+ context.report({
17137
+ fix: (fixer) => fixer.replaceTextRange([absolutePos, absolutePos + 1], ","),
17138
+ message: "Enum members must end with comma (,) not semicolon (;)",
17139
+ node: lastMember,
17140
+ });
17141
+ }
17142
+ } else if (!hasTrailingComma && braceOnSameLine) {
17143
+ // Both missing comma and brace issues - fix together
17144
+ context.report({
17145
+ fix: (fixer) => fixer.replaceTextRange(
17146
+ [lastMember.range[1], closeBraceToken.range[0]],
17147
+ ",\n" + baseIndent,
17148
+ ),
17149
+ message: "Last enum member must have trailing comma and closing brace must be on its own line",
17150
+ node: lastMember,
17151
+ });
17152
+ } else if (!hasTrailingComma) {
17153
+ context.report({
17154
+ fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
17155
+ message: "Last enum member must have trailing comma",
17156
+ node: lastMember,
17157
+ });
17158
+ } else if (braceOnSameLine) {
17159
+ context.report({
17160
+ fix: (fixer) => fixer.replaceTextRange(
17161
+ [lastMember.range[1], closeBraceToken.range[0]],
17162
+ "\n" + baseIndent,
17163
+ ),
17164
+ message: "Closing brace must be on its own line",
17165
+ node: closeBraceToken,
17166
+ });
17167
+ }
17168
+ }
16831
17169
  },
16832
17170
  };
16833
17171
  },
@@ -17011,19 +17349,24 @@ const interfaceFormat = {
17011
17349
  }
17012
17350
 
17013
17351
  // Check property ends with comma, not semicolon
17014
- const memberText = sourceCode.getText(member);
17352
+ // Skip last member when multiple members - handled by combined check below
17353
+ const isLastMember = index === members.length - 1;
17015
17354
 
17016
- if (memberText.trimEnd().endsWith(";")) {
17017
- context.report({
17018
- fix(fixer) {
17019
- const lastChar = memberText.lastIndexOf(";");
17020
- const absolutePos = member.range[0] + lastChar;
17355
+ if (!isLastMember || members.length === 1) {
17356
+ const memberText = sourceCode.getText(member);
17021
17357
 
17022
- return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
17023
- },
17024
- message: "Interface properties must end with comma (,) not semicolon (;)",
17025
- node: member,
17026
- });
17358
+ if (memberText.trimEnd().endsWith(";")) {
17359
+ context.report({
17360
+ fix(fixer) {
17361
+ const lastChar = memberText.lastIndexOf(";");
17362
+ const absolutePos = member.range[0] + lastChar;
17363
+
17364
+ return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
17365
+ },
17366
+ message: "Interface properties must end with comma (,) not semicolon (;)",
17367
+ node: member,
17368
+ });
17369
+ }
17027
17370
  }
17028
17371
 
17029
17372
  // Check formatting for multiple members
@@ -17073,6 +17416,67 @@ const interfaceFormat = {
17073
17416
  }
17074
17417
  }
17075
17418
  });
17419
+
17420
+ // Check closing brace position and trailing comma/semicolon (for multiple members)
17421
+ if (members.length > 1) {
17422
+ const lastMemberText = sourceCode.getText(lastMember);
17423
+ const trimmedText = lastMemberText.trimEnd();
17424
+ // Check both: text ends with comma OR there's a comma token after the member
17425
+ const tokenAfterLast = sourceCode.getTokenAfter(lastMember);
17426
+ const hasTrailingComma = trimmedText.endsWith(",") || (tokenAfterLast && tokenAfterLast.value === ",");
17427
+ const hasTrailingSemicolon = trimmedText.endsWith(";");
17428
+ const braceOnSameLine = closeBraceToken.loc.start.line === lastMember.loc.end.line;
17429
+
17430
+ // Handle semicolon on last member (needs replacement with comma)
17431
+ if (hasTrailingSemicolon) {
17432
+ const lastSemicolon = lastMemberText.lastIndexOf(";");
17433
+ const absolutePos = lastMember.range[0] + lastSemicolon;
17434
+
17435
+ if (braceOnSameLine) {
17436
+ // Both semicolon and brace issues - fix together
17437
+ context.report({
17438
+ fix: (fixer) => fixer.replaceTextRange(
17439
+ [absolutePos, closeBraceToken.range[0]],
17440
+ ",\n" + baseIndent,
17441
+ ),
17442
+ message: "Last interface property must end with comma and closing brace must be on its own line",
17443
+ node: lastMember,
17444
+ });
17445
+ } else {
17446
+ // Just semicolon issue
17447
+ context.report({
17448
+ fix: (fixer) => fixer.replaceTextRange([absolutePos, absolutePos + 1], ","),
17449
+ message: "Interface properties must end with comma (,) not semicolon (;)",
17450
+ node: lastMember,
17451
+ });
17452
+ }
17453
+ } else if (!hasTrailingComma && braceOnSameLine) {
17454
+ // Both missing comma and brace issues - fix together
17455
+ context.report({
17456
+ fix: (fixer) => fixer.replaceTextRange(
17457
+ [lastMember.range[1], closeBraceToken.range[0]],
17458
+ ",\n" + baseIndent,
17459
+ ),
17460
+ message: "Last interface property must have trailing comma and closing brace must be on its own line",
17461
+ node: lastMember,
17462
+ });
17463
+ } else if (!hasTrailingComma) {
17464
+ context.report({
17465
+ fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
17466
+ message: "Last interface property must have trailing comma",
17467
+ node: lastMember,
17468
+ });
17469
+ } else if (braceOnSameLine) {
17470
+ context.report({
17471
+ fix: (fixer) => fixer.replaceTextRange(
17472
+ [lastMember.range[1], closeBraceToken.range[0]],
17473
+ "\n" + baseIndent,
17474
+ ),
17475
+ message: "Closing brace must be on its own line",
17476
+ node: closeBraceToken,
17477
+ });
17478
+ }
17479
+ }
17076
17480
  },
17077
17481
  };
17078
17482
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
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",