eslint-plugin-code-style 1.7.1 → 1.7.3

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 (4) hide show
  1. package/CHANGELOG.md +35 -2
  2. package/README.md +236 -0
  3. package/index.js +430 -68
  4. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,14 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.7.3] - 2026-02-02
11
+
12
+ ### Fixed
13
+
14
+ - **`ternary-condition-multiline`** - Fix `?`/`:` on own line without value; collapse simple ternaries to single line when they fit
15
+ - **`no-empty-lines-in-function-params`** - Detect empty lines after opening `{` and before closing `}` in ObjectPattern params
16
+ - **`empty-line-after-block`** - Skip consecutive if statements (already handled by `if-else-spacing`)
17
+ - **`classname-multiline`** - Fix closing backtick alignment for return statements
18
+
19
+ ### Documentation
20
+
21
+ - Add 6 missing rules to README detailed documentation
22
+ - Add 7 missing rules to README Quick Start example
23
+ - Update rule counts from 66 to 69 across all documentation files
24
+ - Update AGENTS.md Tailwind section with actual rules and comparison with `tailwindcss/classnames-order`
25
+ - Add README multi-section update warnings to AGENTS.md rule modification checklists
26
+
27
+ ### Added
28
+
29
+ - **`manage-rule` skill** - New skill for adding, editing, or removing ESLint rules with complete workflow
30
+
31
+ ---
32
+
33
+ ## [1.7.2] - 2026-02-02
34
+
35
+ ### Fixed
36
+
37
+ - **`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
38
+ - **`interface-format`** - Same fix as enum-format for trailing comma detection
39
+
40
+ ---
41
+
10
42
  ## [1.7.1] - 2026-02-02
11
43
 
12
44
  ### Fixed
13
45
 
14
46
  - **`no-empty-lines-in-function-params`** - Detect empty lines between destructured properties inside ObjectPattern params
15
47
  - **`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
16
- - **`enum-type-enforcement`** - Fix argument passed to extractTypeInfoHandler for TSPropertySignature members (was passing `member`, now correctly passes `member.typeAnnotation`)
17
- - **`ternary-condition-multiline`** - Improve simple ternary prefix calculation to use actual declaration text instead of column positions; add check for `?` on same line as multiline condition end
48
+ - **`enum-type-enforcement`** - Handle TSIntersectionType to track typed props; fix extractTypeInfoHandler argument for TSPropertySignature members
49
+ - **`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
18
50
 
19
51
  ---
20
52
 
@@ -1075,6 +1107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1075
1107
 
1076
1108
  ---
1077
1109
 
1110
+ [1.7.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.1...v1.7.2
1078
1111
  [1.7.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.0...v1.7.1
1079
1112
  [1.7.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.6...v1.7.0
1080
1113
  [1.6.6]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.5...v1.6.6
package/README.md CHANGED
@@ -190,14 +190,18 @@ rules: {
190
190
  "code-style/arrow-function-simplify": "error",
191
191
  "code-style/assignment-value-same-line": "error",
192
192
  "code-style/block-statement-newlines": "error",
193
+ "code-style/class-naming-convention": "error",
193
194
  "code-style/classname-dynamic-at-end": "error",
194
195
  "code-style/classname-multiline": "error",
195
196
  "code-style/classname-no-extra-spaces": "error",
197
+ "code-style/classname-order": "error",
196
198
  "code-style/comment-format": "error",
197
199
  "code-style/component-props-destructure": "error",
198
200
  "code-style/component-props-inline-type": "error",
199
201
  "code-style/curried-arrow-same-line": "error",
202
+ "code-style/empty-line-after-block": "error",
200
203
  "code-style/enum-format": "error",
204
+ "code-style/enum-type-enforcement": "error",
201
205
  "code-style/export-format": "error",
202
206
  "code-style/function-arguments-format": "error",
203
207
  "code-style/function-call-spacing": "error",
@@ -212,6 +216,7 @@ rules: {
212
216
  "code-style/import-format": "error",
213
217
  "code-style/import-source-spacing": "error",
214
218
  "code-style/index-export-style": "error",
219
+ "code-style/index-exports-only": "error",
215
220
  "code-style/interface-format": "error",
216
221
  "code-style/jsx-children-on-new-line": "error",
217
222
  "code-style/jsx-closing-bracket-spacing": "error",
@@ -231,6 +236,7 @@ rules: {
231
236
  "code-style/no-empty-lines-in-jsx": "error",
232
237
  "code-style/no-empty-lines-in-objects": "error",
233
238
  "code-style/no-empty-lines-in-switch-cases": "error",
239
+ "code-style/no-inline-type-definitions": "error",
234
240
  "code-style/object-property-per-line": "error",
235
241
  "code-style/object-property-value-brace": "error",
236
242
  "code-style/object-property-value-format": "error",
@@ -239,6 +245,7 @@ rules: {
239
245
  "code-style/simple-call-single-line": "error",
240
246
  "code-style/single-argument-on-one-line": "error",
241
247
  "code-style/string-property-spacing": "error",
248
+ "code-style/ternary-condition-multiline": "error",
242
249
  "code-style/type-annotation-spacing": "error",
243
250
  "code-style/type-format": "error",
244
251
  "code-style/typescript-definition-location": "error",
@@ -881,6 +888,37 @@ dispatch(
881
888
 
882
889
  <br />
883
890
 
891
+ ## 🏛️ Class Rules
892
+
893
+ ### `class-naming-convention`
894
+
895
+ **What it does:** Enforces that class declarations must end with "Class" suffix. This distinguishes class definitions from other PascalCase names like React components or type definitions.
896
+
897
+ **Why use it:** Clear naming conventions prevent confusion between classes, components, and types. The "Class" suffix immediately identifies the construct.
898
+
899
+ ```javascript
900
+ // ✅ Good — class ends with "Class"
901
+ class ApiServiceClass {
902
+ constructor() {}
903
+ fetch() {}
904
+ }
905
+
906
+ class UserRepositoryClass {
907
+ save(user) {}
908
+ }
909
+
910
+ // ❌ Bad — missing "Class" suffix
911
+ class ApiService {
912
+ constructor() {}
913
+ }
914
+
915
+ class UserRepository {
916
+ save(user) {}
917
+ }
918
+ ```
919
+
920
+ <br />
921
+
884
922
  ## 🔀 Control Flow Rules
885
923
 
886
924
  ### `block-statement-newlines`
@@ -916,6 +954,38 @@ for (const item of items) { process(item);
916
954
 
917
955
  ---
918
956
 
957
+ ### `empty-line-after-block`
958
+
959
+ **What it does:** Requires an empty line between a closing brace `}` of a block statement (if, try, for, while, etc.) and the next statement, unless the next statement is part of the same construct (else, catch, finally).
960
+
961
+ **Why use it:** Visual separation between logical blocks improves code readability and makes the structure clearer.
962
+
963
+ > **Note:** Consecutive if statements are handled by `if-else-spacing` rule.
964
+
965
+ ```javascript
966
+ // ✅ Good — empty line after block
967
+ if (condition) {
968
+ doSomething();
969
+ }
970
+
971
+ const x = 1;
972
+
973
+ // ✅ Good — else is part of same construct (no empty line needed)
974
+ if (condition) {
975
+ doSomething();
976
+ } else {
977
+ doOther();
978
+ }
979
+
980
+ // ❌ Bad — no empty line after block
981
+ if (condition) {
982
+ doSomething();
983
+ }
984
+ const x = 1;
985
+ ```
986
+
987
+ ---
988
+
919
989
  ### `if-else-spacing`
920
990
 
921
991
  **What it does:** Enforces proper spacing between if statements and if-else chains:
@@ -1137,6 +1207,39 @@ switch (status) {
1137
1207
  }
1138
1208
  ```
1139
1209
 
1210
+ ---
1211
+
1212
+ ### `ternary-condition-multiline`
1213
+
1214
+ **What it does:** Enforces consistent ternary formatting:
1215
+ - Simple ternaries (≤3 operands in condition) collapse to single line if they fit
1216
+ - Complex ternaries (>3 operands) expand to multiline with each operand on its own line
1217
+
1218
+ **Why use it:** Long ternary conditions on a single line are hard to read. Breaking complex conditions into multiple lines makes them scannable.
1219
+
1220
+ **Options:**
1221
+
1222
+ | Option | Type | Default | Description |
1223
+ |--------|------|---------|-------------|
1224
+ | `maxOperands` | `integer` | `3` | Maximum operands to keep on single line |
1225
+ | `maxLineLength` | `integer` | `120` | Maximum line length for single-line ternaries |
1226
+
1227
+ ```javascript
1228
+ // ✅ Good — simple condition on single line
1229
+ const x = a && b && c ? "yes" : "no";
1230
+
1231
+ // ✅ Good — complex condition multiline (>3 operands)
1232
+ const style = variant === "ghost"
1233
+ || variant === "ghost-danger"
1234
+ || variant === "muted"
1235
+ || variant === "primary"
1236
+ ? "transparent"
1237
+ : "solid";
1238
+
1239
+ // ❌ Bad — complex condition crammed on one line
1240
+ const style = variant === "ghost" || variant === "ghost-danger" || variant === "muted" || variant === "primary" ? "transparent" : "solid";
1241
+ ```
1242
+
1140
1243
  <br />
1141
1244
 
1142
1245
  ## ⚡ Function Rules
@@ -1719,6 +1822,28 @@ export {
1719
1822
 
1720
1823
  ---
1721
1824
 
1825
+ ### `index-exports-only`
1826
+
1827
+ **What it does:** Index files (`index.ts`, `index.tsx`, `index.js`, `index.jsx`) should only contain imports and re-exports, not any code definitions. All definitions (types, interfaces, functions, variables, classes) should be moved to separate files.
1828
+
1829
+ **Why use it:** Index files should be "barrels" that aggregate exports from a module. Mixing definitions with re-exports makes the codebase harder to navigate and can cause circular dependency issues.
1830
+
1831
+ ```javascript
1832
+ // ✅ Good — index.ts with only imports and re-exports
1833
+ export { Button } from "./Button";
1834
+ export { helper } from "./utils";
1835
+ export type { ButtonProps } from "./types";
1836
+ export * from "./constants";
1837
+
1838
+ // ❌ Bad — index.ts with code definitions
1839
+ export type ButtonVariant = "primary" | "secondary"; // Move to types.ts
1840
+ export interface ButtonProps { ... } // Move to types.ts
1841
+ export const CONSTANT = "value"; // Move to constants.ts
1842
+ export function helper() { ... } // Move to utils.ts
1843
+ ```
1844
+
1845
+ ---
1846
+
1722
1847
  ### `module-index-exports`
1723
1848
 
1724
1849
  **What it does:** Ensures module folders have index files that export all their contents, creating a proper public API for each module.
@@ -1897,6 +2022,49 @@ const buttonClasses = ` flex items-center ${className} `;
1897
2022
 
1898
2023
  ---
1899
2024
 
2025
+ ### `classname-order`
2026
+
2027
+ **What it does:** Enforces Tailwind CSS class ordering in variables, object properties, and return statements. Uses smart detection to identify Tailwind class strings.
2028
+
2029
+ **Why use it:** This rule complements the official `tailwindcss/classnames-order` plugin by handling areas it doesn't cover:
2030
+ - **`tailwindcss/classnames-order`** — Handles JSX `className` attributes directly
2031
+ - **`classname-order`** — Handles class strings in variables, object properties, and return statements
2032
+
2033
+ Both rules should be enabled together for complete Tailwind class ordering coverage.
2034
+
2035
+ **Order enforced:** layout (flex, grid) → positioning → sizing (w, h) → spacing (p, m) → typography (text, font) → colors (bg, text) → effects (shadow, opacity) → transitions → states (hover, focus)
2036
+
2037
+ ```javascript
2038
+ // ✅ Good — classes in correct order (variable)
2039
+ const buttonClasses = "flex items-center px-4 py-2 text-white bg-blue-500 hover:bg-blue-600";
2040
+
2041
+ // ✅ Good — classes in correct order (object property)
2042
+ const variants = {
2043
+ primary: "flex items-center bg-blue-500 hover:bg-blue-600",
2044
+ secondary: "flex items-center bg-gray-500 hover:bg-gray-600",
2045
+ };
2046
+
2047
+ // ✅ Good — classes in correct order (return statement)
2048
+ const getInputStyles = () => {
2049
+ return "border-error text-error placeholder-error/50 focus:border-error";
2050
+ };
2051
+
2052
+ // ❌ Bad — hover state before base color (variable)
2053
+ const buttonClasses = "flex items-center hover:bg-blue-600 bg-blue-500";
2054
+
2055
+ // ❌ Bad — unordered classes (object property)
2056
+ const variants = {
2057
+ primary: "hover:bg-blue-600 bg-blue-500 flex items-center",
2058
+ };
2059
+
2060
+ // ❌ Bad — unordered classes (return statement)
2061
+ const getInputStyles = () => {
2062
+ return "focus:border-error text-error border-error";
2063
+ };
2064
+ ```
2065
+
2066
+ ---
2067
+
1900
2068
  ### `jsx-children-on-new-line`
1901
2069
 
1902
2070
  **What it does:** When a JSX element has multiple children, ensures each child is on its own line with proper indentation.
@@ -2696,6 +2864,38 @@ export enum UserStatusEnum {
2696
2864
 
2697
2865
  ---
2698
2866
 
2867
+ ### `enum-type-enforcement`
2868
+
2869
+ **What it does:** When a variable or parameter has a type ending in `Type` (like `ButtonVariantType`), enforces using the corresponding enum (`ButtonVariantEnum.VALUE`) instead of string literals.
2870
+
2871
+ **Why use it:** Using enum values instead of string literals provides type safety, autocompletion, and prevents typos. Changes to enum values automatically propagate.
2872
+
2873
+ ```javascript
2874
+ // ✅ Good — using enum values
2875
+ const Button = ({
2876
+ variant = ButtonVariantEnum.PRIMARY,
2877
+ }: {
2878
+ variant?: ButtonVariantType,
2879
+ }) => { ... };
2880
+
2881
+ if (variant === ButtonVariantEnum.GHOST) {
2882
+ // ...
2883
+ }
2884
+
2885
+ // ❌ Bad — using string literals
2886
+ const Button = ({
2887
+ variant = "primary", // Should use ButtonVariantEnum.PRIMARY
2888
+ }: {
2889
+ variant?: ButtonVariantType,
2890
+ }) => { ... };
2891
+
2892
+ if (variant === "ghost") { // Should use ButtonVariantEnum.GHOST
2893
+ // ...
2894
+ }
2895
+ ```
2896
+
2897
+ ---
2898
+
2699
2899
  ### `interface-format`
2700
2900
 
2701
2901
  **What it does:** Enforces consistent formatting for TypeScript interfaces:
@@ -2739,6 +2939,42 @@ export interface UserInterface {
2739
2939
 
2740
2940
  ---
2741
2941
 
2942
+ ### `no-inline-type-definitions`
2943
+
2944
+ **What it does:** Reports when function parameters have inline union types that are too complex (too many members or too long). These should be extracted to a named type in a types file.
2945
+
2946
+ **Why use it:** Complex inline types make function signatures hard to read. Named types are reusable, self-documenting, and easier to maintain.
2947
+
2948
+ **Options:**
2949
+
2950
+ | Option | Type | Default | Description |
2951
+ |--------|------|---------|-------------|
2952
+ | `maxUnionMembers` | `integer` | `2` | Maximum union members before requiring extraction |
2953
+ | `maxLength` | `integer` | `50` | Maximum character length before requiring extraction |
2954
+
2955
+ ```javascript
2956
+ // ✅ Good — type extracted to separate file
2957
+ // types.ts
2958
+ export type ButtonVariantType = "primary" | "muted" | "danger";
2959
+
2960
+ // Button.tsx
2961
+ import { ButtonVariantType } from "./types";
2962
+ export const Button = ({
2963
+ variant,
2964
+ }: {
2965
+ variant?: ButtonVariantType,
2966
+ }) => { ... };
2967
+
2968
+ // ❌ Bad — complex inline union type
2969
+ export const Button = ({
2970
+ variant,
2971
+ }: {
2972
+ variant?: "primary" | "muted" | "danger", // Extract to named type
2973
+ }) => { ... };
2974
+ ```
2975
+
2976
+ ---
2977
+
2742
2978
  ### `type-format`
2743
2979
 
2744
2980
  **What it does:** Enforces consistent formatting for TypeScript type aliases:
package/index.js CHANGED
@@ -4030,10 +4030,31 @@ const ternaryConditionMultiline = {
4030
4030
  return false;
4031
4031
  };
4032
4032
 
4033
+ // Check if ? or : is on its own line without its value
4034
+ const isOperatorOnOwnLineHandler = (node) => {
4035
+ const questionToken = sourceCode.getTokenAfter(node.test, (t) => t.value === "?");
4036
+ const colonToken = sourceCode.getTokenAfter(node.consequent, (t) => t.value === ":");
4037
+
4038
+ // Check if ? is on different line than consequent start
4039
+ if (questionToken && node.consequent.loc.start.line !== questionToken.loc.start.line) {
4040
+ return true;
4041
+ }
4042
+
4043
+ // Check if : is on different line than alternate start
4044
+ if (colonToken && node.alternate.loc.start.line !== colonToken.loc.start.line) {
4045
+ return true;
4046
+ }
4047
+
4048
+ return false;
4049
+ };
4050
+
4033
4051
  // Handle simple ternaries - collapse to single line if they fit
4034
4052
  const handleSimpleTernaryHandler = (node) => {
4035
- // Skip if already on single line
4036
- if (node.loc.start.line === node.loc.end.line) return false;
4053
+ const isOnSingleLine = node.loc.start.line === node.loc.end.line;
4054
+ const hasOperatorOnOwnLine = isOperatorOnOwnLineHandler(node);
4055
+
4056
+ // Skip if already on single line and no formatting issues
4057
+ if (isOnSingleLine && !hasOperatorOnOwnLine) return false;
4037
4058
 
4038
4059
  // Skip nested ternaries
4039
4060
  if (node.consequent.type === "ConditionalExpression" || node.alternate.type === "ConditionalExpression") {
@@ -4049,7 +4070,7 @@ const ternaryConditionMultiline = {
4049
4070
  const singleLineText = getTernarySingleLineHandler(node);
4050
4071
  const indent = getLineIndentHandler(node);
4051
4072
 
4052
- // Check if the parent needs prefix text (like "const x = ")
4073
+ // Check if the parent needs prefix text (like "const x = " or "key: ")
4053
4074
  let prefixLength = 0;
4054
4075
  const parent = node.parent;
4055
4076
 
@@ -4066,6 +4087,11 @@ const ternaryConditionMultiline = {
4066
4087
  const leftText = sourceCode.getText(parent.left);
4067
4088
 
4068
4089
  prefixLength = leftText.length + 3; // left + " = "
4090
+ } else if (parent && parent.type === "Property" && parent.value === node) {
4091
+ // Object property: key: ternary
4092
+ const keyText = sourceCode.getText(parent.key);
4093
+
4094
+ prefixLength = keyText.length + 2; // key + ": "
4069
4095
  }
4070
4096
 
4071
4097
  // Check if single line would fit
@@ -4092,7 +4118,7 @@ const ternaryConditionMultiline = {
4092
4118
  const testEndLine = test.loc.end.line;
4093
4119
  const isMultiLine = testStartLine !== testEndLine;
4094
4120
 
4095
- // ≤maxOperands operands: keep condition on single line
4121
+ // ≤maxOperands operands: keep condition on single line, and try to collapse whole ternary
4096
4122
  if (operands.length <= maxOperands) {
4097
4123
  const firstOperandStartLine = operands[0].loc.start.line;
4098
4124
  const allOperandsStartOnSameLine = operands.every(
@@ -4103,29 +4129,82 @@ const ternaryConditionMultiline = {
4103
4129
  (op) => isBinaryExpressionSplitHandler(op),
4104
4130
  );
4105
4131
 
4106
- if (!allOperandsStartOnSameLine || hasSplitBinaryExpression) {
4107
- context.report({
4108
- fix: (fixer) => {
4109
- const buildSameLineHandler = (n) => {
4110
- if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
4111
- const leftText = buildSameLineHandler(n.left);
4112
- const rightText = buildSameLineHandler(n.right);
4132
+ // Check if ? or : is on its own line without its value
4133
+ const hasOperatorOnOwnLine = isOperatorOnOwnLineHandler(node);
4113
4134
 
4114
- return `${leftText} ${n.operator} ${rightText}`;
4115
- }
4135
+ // Check if ternary is multiline (could be collapsed)
4136
+ const isTernaryMultiline = node.loc.start.line !== node.loc.end.line;
4116
4137
 
4117
- if (n.type === "BinaryExpression" && isBinaryExpressionSplitHandler(n)) {
4118
- return buildBinaryExpressionSingleLineHandler(n);
4119
- }
4138
+ // Helper to build single line condition
4139
+ const buildSameLineHandler = (n) => {
4140
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
4141
+ const leftText = buildSameLineHandler(n.left);
4142
+ const rightText = buildSameLineHandler(n.right);
4120
4143
 
4121
- return getSourceTextWithGroupsHandler(n);
4122
- };
4144
+ return `${leftText} ${n.operator} ${rightText}`;
4145
+ }
4123
4146
 
4124
- return fixer.replaceText(test, buildSameLineHandler(test));
4125
- },
4126
- message: `Ternary conditions with ≤${maxOperands} operands should be single line`,
4127
- node: test,
4128
- });
4147
+ if (n.type === "BinaryExpression" && isBinaryExpressionSplitHandler(n)) {
4148
+ return buildBinaryExpressionSingleLineHandler(n);
4149
+ }
4150
+
4151
+ return getSourceTextWithGroupsHandler(n);
4152
+ };
4153
+
4154
+ // Check if whole ternary can fit on one line
4155
+ const singleLineText = getTernarySingleLineHandler(node);
4156
+ const indent = getLineIndentHandler(node);
4157
+
4158
+ // Calculate prefix length for context
4159
+ let prefixLength = 0;
4160
+ const parent = node.parent;
4161
+
4162
+ if (parent && parent.type === "VariableDeclarator" && parent.init === node) {
4163
+ const varDecl = parent.parent;
4164
+ const declKeyword = varDecl ? sourceCode.getFirstToken(varDecl).value : "const";
4165
+ const varName = parent.id.name || sourceCode.getText(parent.id);
4166
+
4167
+ prefixLength = declKeyword.length + 1 + varName.length + 3;
4168
+ } else if (parent && parent.type === "AssignmentExpression" && parent.right === node) {
4169
+ const leftText = sourceCode.getText(parent.left);
4170
+
4171
+ prefixLength = leftText.length + 3;
4172
+ } else if (parent && parent.type === "Property" && parent.value === node) {
4173
+ const keyText = sourceCode.getText(parent.key);
4174
+
4175
+ prefixLength = keyText.length + 2;
4176
+ }
4177
+
4178
+ const totalLength = indent + prefixLength + singleLineText.length + 1;
4179
+ const canFitOnOneLine = totalLength <= maxLineLength;
4180
+
4181
+ // Skip if branches have complex objects
4182
+ const hasComplexBranches = hasComplexObjectHandler(node.consequent) || hasComplexObjectHandler(node.alternate);
4183
+
4184
+ // Skip nested ternaries
4185
+ const hasNestedTernary = node.consequent.type === "ConditionalExpression" || node.alternate.type === "ConditionalExpression";
4186
+
4187
+ // Determine if we need to fix anything
4188
+ const needsConditionFix = !allOperandsStartOnSameLine || hasSplitBinaryExpression;
4189
+ const needsTernaryCollapse = isTernaryMultiline && canFitOnOneLine && !hasComplexBranches && !hasNestedTernary;
4190
+ const needsOperatorFix = hasOperatorOnOwnLine;
4191
+
4192
+ if (needsConditionFix || needsTernaryCollapse || needsOperatorFix) {
4193
+ // If whole ternary can fit on one line, collapse it
4194
+ if (canFitOnOneLine && !hasComplexBranches && !hasNestedTernary) {
4195
+ context.report({
4196
+ fix: (fixer) => fixer.replaceText(node, singleLineText),
4197
+ message: `Ternary with ≤${maxOperands} operands should be on single line when it fits`,
4198
+ node,
4199
+ });
4200
+ } else if (needsConditionFix) {
4201
+ // Otherwise just fix the condition to be on single line
4202
+ context.report({
4203
+ fix: (fixer) => fixer.replaceText(test, buildSameLineHandler(test)),
4204
+ message: `Ternary conditions with ≤${maxOperands} operands should be single line`,
4205
+ node: test,
4206
+ });
4207
+ }
4129
4208
  }
4130
4209
 
4131
4210
  return;
@@ -4155,6 +4234,29 @@ const ternaryConditionMultiline = {
4155
4234
  isCorrectionNeeded = true;
4156
4235
  }
4157
4236
  }
4237
+
4238
+ // Check for empty lines before ? (between condition and ?)
4239
+ if (!isCorrectionNeeded) {
4240
+ const questionToken = sourceCode.getTokenAfter(test, (t) => t.value === "?");
4241
+
4242
+ if (questionToken && questionToken.loc.start.line > test.loc.end.line + 1) {
4243
+ isCorrectionNeeded = true;
4244
+ }
4245
+ }
4246
+
4247
+ // Check for empty lines before : (between consequent and :)
4248
+ if (!isCorrectionNeeded) {
4249
+ const colonToken = sourceCode.getTokenAfter(node.consequent, (t) => t.value === ":");
4250
+
4251
+ if (colonToken && colonToken.loc.start.line > node.consequent.loc.end.line + 1) {
4252
+ isCorrectionNeeded = true;
4253
+ }
4254
+ }
4255
+
4256
+ // Check if ? or : is on its own line without its value
4257
+ if (!isCorrectionNeeded && isOperatorOnOwnLineHandler(node)) {
4258
+ isCorrectionNeeded = true;
4259
+ }
4158
4260
  }
4159
4261
 
4160
4262
  if (isCorrectionNeeded) {
@@ -4164,7 +4266,6 @@ const ternaryConditionMultiline = {
4164
4266
  const lineText = sourceCode.lines[node.loc.start.line - 1];
4165
4267
  const baseIndent = lineText.match(/^\s*/)[0];
4166
4268
  const conditionIndent = baseIndent + " ";
4167
- const branchIndent = baseIndent + " ";
4168
4269
 
4169
4270
  const buildMultilineHandler = (n) => {
4170
4271
  if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
@@ -4180,7 +4281,9 @@ const ternaryConditionMultiline = {
4180
4281
  const consequentText = sourceCode.getText(node.consequent);
4181
4282
  const alternateText = sourceCode.getText(node.alternate);
4182
4283
 
4183
- const newText = `\n${conditionIndent}${buildMultilineHandler(test)}\n${branchIndent}? ${consequentText}\n${branchIndent}: ${alternateText}`;
4284
+ // No leading newline - keep first operand on same line as whatever precedes it
4285
+ // Use conditionIndent for ? and : to align with || operators
4286
+ const newText = `${buildMultilineHandler(test)}\n${conditionIndent}? ${consequentText}\n${conditionIndent}: ${alternateText}`;
4184
4287
 
4185
4288
  return fixer.replaceText(node, newText);
4186
4289
  },
@@ -4243,6 +4346,8 @@ const ternaryConditionMultiline = {
4243
4346
  * statement (if, try, for, while, etc.) and the next statement,
4244
4347
  * unless the next statement is part of the same construct (else, catch, finally).
4245
4348
  *
4349
+ * Note: Consecutive if statements are handled by if-else-spacing rule.
4350
+ *
4246
4351
  * ✓ Good:
4247
4352
  * if (condition) {
4248
4353
  * doSomething();
@@ -4323,6 +4428,11 @@ const emptyLineAfterBlock = {
4323
4428
  // Get the next statement
4324
4429
  const nextStmt = grandparent.body[stmtIndex + 1];
4325
4430
 
4431
+ // Skip consecutive if statements - handled by if-else-spacing rule
4432
+ if (parent.type === "IfStatement" && nextStmt.type === "IfStatement") {
4433
+ return;
4434
+ }
4435
+
4326
4436
  // Get the actual end of the current statement
4327
4437
  const currentEndLine = getStatementEndLineHandler(parent);
4328
4438
  const nextStartLine = nextStmt.loc.start.line;
@@ -4481,6 +4591,20 @@ const enumTypeEnforcement = {
4481
4591
  return null;
4482
4592
  };
4483
4593
 
4594
+ // Helper to process TSTypeLiteral members
4595
+ const processTypeLiteralMembersHandler = (members) => {
4596
+ members.forEach((member) => {
4597
+ if (member.type === "TSPropertySignature" && member.key?.type === "Identifier") {
4598
+ const propName = member.key.name;
4599
+ const typeInfo = extractTypeInfoHandler(member.typeAnnotation);
4600
+
4601
+ if (typeInfo) {
4602
+ typeAnnotatedVars.set(propName, typeInfo);
4603
+ }
4604
+ }
4605
+ });
4606
+ };
4607
+
4484
4608
  // Track type-annotated parameters in function/component definitions
4485
4609
  const trackTypedParamsHandler = (params) => {
4486
4610
  params.forEach((param) => {
@@ -4489,14 +4613,14 @@ const enumTypeEnforcement = {
4489
4613
  const annotation = param.typeAnnotation.typeAnnotation;
4490
4614
 
4491
4615
  if (annotation && annotation.type === "TSTypeLiteral") {
4492
- annotation.members.forEach((member) => {
4493
- if (member.type === "TSPropertySignature" && member.key?.type === "Identifier") {
4494
- const propName = member.key.name;
4495
- const typeInfo = extractTypeInfoHandler(member.typeAnnotation);
4616
+ processTypeLiteralMembersHandler(annotation.members);
4617
+ }
4496
4618
 
4497
- if (typeInfo) {
4498
- typeAnnotatedVars.set(propName, typeInfo);
4499
- }
4619
+ // Handle intersection types: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: ButtonVariantType }
4620
+ if (annotation && annotation.type === "TSIntersectionType") {
4621
+ annotation.types.forEach((intersectionType) => {
4622
+ if (intersectionType.type === "TSTypeLiteral") {
4623
+ processTypeLiteralMembersHandler(intersectionType.members);
4500
4624
  }
4501
4625
  });
4502
4626
  }
@@ -7474,12 +7598,16 @@ const classNameNoExtraSpaces = {
7474
7598
  *
7475
7599
  * Description:
7476
7600
  * Enforce Tailwind CSS class ordering in class string variables,
7477
- * object properties, and return statements. Complements
7478
- * tailwindcss/classnames-order by handling cases it doesn't cover.
7601
+ * object properties, and return statements. This rule complements
7602
+ * tailwindcss/classnames-order by handling areas it doesn't cover.
7479
7603
  * Uses smart detection: checks if values look like Tailwind classes.
7480
7604
  *
7481
- * Note: This rule does NOT check JSX className attributes directly,
7482
- * as those should be handled by tailwindcss/classnames-order.
7605
+ * Coverage Division:
7606
+ * - tailwindcss/classnames-order: Handles JSX className attributes
7607
+ * - classname-order (this rule): Handles variables, object properties,
7608
+ * and return statements containing Tailwind class strings
7609
+ *
7610
+ * Both rules should be enabled together for complete coverage.
7483
7611
  *
7484
7612
  * ✓ Good:
7485
7613
  * const variants = { primary: "bg-blue-500 hover:bg-blue-600" };
@@ -7808,6 +7936,11 @@ const classNameMultiline = {
7808
7936
  return getLineIndent(current);
7809
7937
  }
7810
7938
 
7939
+ // For return statements, use the return keyword's indentation
7940
+ if (current.type === "ReturnStatement") {
7941
+ return getLineIndent(current);
7942
+ }
7943
+
7811
7944
  current = current.parent;
7812
7945
  }
7813
7946
 
@@ -9496,6 +9629,8 @@ const noEmptyLinesInFunctionCalls = {
9496
9629
  * Description:
9497
9630
  * Function parameter lists should not contain empty lines
9498
9631
  * between parameters or after opening/before closing parens.
9632
+ * Also checks inside ObjectPattern (destructuring) params for
9633
+ * empty lines after {, before }, or between properties.
9499
9634
  *
9500
9635
  * ✓ Good:
9501
9636
  * function test(
@@ -9503,12 +9638,23 @@ const noEmptyLinesInFunctionCalls = {
9503
9638
  * param2,
9504
9639
  * ) {}
9505
9640
  *
9641
+ * const Button = ({
9642
+ * children,
9643
+ * className,
9644
+ * }) => {};
9645
+ *
9506
9646
  * ✗ Bad:
9507
9647
  * function test(
9508
9648
  * param1,
9509
9649
  *
9510
9650
  * param2,
9511
9651
  * ) {}
9652
+ *
9653
+ * const Button = ({
9654
+ *
9655
+ * children,
9656
+ * className,
9657
+ * }) => {};
9512
9658
  */
9513
9659
  const noEmptyLinesInFunctionParams = {
9514
9660
  create(context) {
@@ -9580,24 +9726,64 @@ const noEmptyLinesInFunctionParams = {
9580
9726
 
9581
9727
  // Check inside ObjectPattern params for empty lines between destructured props
9582
9728
  params.forEach((param) => {
9583
- if (param.type === "ObjectPattern" && param.properties.length > 1) {
9584
- for (let i = 0; i < param.properties.length - 1; i += 1) {
9585
- const current = param.properties[i];
9586
- const next = param.properties[i + 1];
9729
+ if (param.type === "ObjectPattern" && param.properties.length > 0) {
9730
+ const firstProp = param.properties[0];
9731
+ const lastProp = param.properties[param.properties.length - 1];
9587
9732
 
9588
- if (next.loc.start.line - current.loc.end.line > 1) {
9589
- const commaToken = sourceCode.getTokenAfter(current, (t) => t.value === ",");
9733
+ // Find the opening brace of ObjectPattern
9734
+ const openBrace = sourceCode.getFirstToken(param);
9590
9735
 
9736
+ if (openBrace && openBrace.value === "{") {
9737
+ // Check for empty line after opening brace
9738
+ if (firstProp.loc.start.line - openBrace.loc.end.line > 1) {
9591
9739
  context.report({
9592
9740
  fix: (fixer) => fixer.replaceTextRange(
9593
- [commaToken.range[1], next.range[0]],
9594
- "\n" + " ".repeat(next.loc.start.column),
9741
+ [openBrace.range[1], firstProp.range[0]],
9742
+ "\n" + " ".repeat(firstProp.loc.start.column),
9595
9743
  ),
9596
- message: "No empty lines between destructured properties",
9597
- node: next,
9744
+ message: "No empty line after opening brace in destructuring",
9745
+ node: firstProp,
9598
9746
  });
9599
9747
  }
9600
9748
  }
9749
+
9750
+ // Find the closing brace of ObjectPattern
9751
+ const closeBrace = sourceCode.getLastToken(param);
9752
+
9753
+ if (closeBrace && closeBrace.value === "}") {
9754
+ // Check for empty line before closing brace
9755
+ if (closeBrace.loc.start.line - lastProp.loc.end.line > 1) {
9756
+ context.report({
9757
+ fix: (fixer) => fixer.replaceTextRange(
9758
+ [lastProp.range[1], closeBrace.range[0]],
9759
+ "\n" + " ".repeat(closeBrace.loc.start.column),
9760
+ ),
9761
+ message: "No empty line before closing brace in destructuring",
9762
+ node: lastProp,
9763
+ });
9764
+ }
9765
+ }
9766
+
9767
+ // Check for empty lines between properties
9768
+ if (param.properties.length > 1) {
9769
+ for (let i = 0; i < param.properties.length - 1; i += 1) {
9770
+ const current = param.properties[i];
9771
+ const next = param.properties[i + 1];
9772
+
9773
+ if (next.loc.start.line - current.loc.end.line > 1) {
9774
+ const commaToken = sourceCode.getTokenAfter(current, (t) => t.value === ",");
9775
+
9776
+ context.report({
9777
+ fix: (fixer) => fixer.replaceTextRange(
9778
+ [commaToken.range[1], next.range[0]],
9779
+ "\n" + " ".repeat(next.loc.start.column),
9780
+ ),
9781
+ message: "No empty lines between destructured properties",
9782
+ node: next,
9783
+ });
9784
+ }
9785
+ }
9786
+ }
9601
9787
  }
9602
9788
  });
9603
9789
  };
@@ -14412,6 +14598,36 @@ const componentPropsInlineType = {
14412
14598
  }
14413
14599
  }
14414
14600
  });
14601
+
14602
+ // Check that last member has trailing comma
14603
+ if (members.length > 0) {
14604
+ const lastMember = members[members.length - 1];
14605
+ const lastMemberText = sourceCode.getText(lastMember);
14606
+
14607
+ if (!lastMemberText.trimEnd().endsWith(",")) {
14608
+ context.report({
14609
+ fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
14610
+ message: "Last props type property must have trailing comma",
14611
+ node: lastMember,
14612
+ });
14613
+ }
14614
+ }
14615
+
14616
+ // Check for empty lines before closing brace
14617
+ if (members.length > 0 && closeBraceToken) {
14618
+ const lastMember = members[members.length - 1];
14619
+
14620
+ if (closeBraceToken.loc.start.line - lastMember.loc.end.line > 1) {
14621
+ context.report({
14622
+ fix: (fixer) => fixer.replaceTextRange(
14623
+ [lastMember.range[1], closeBraceToken.range[0]],
14624
+ "\n" + baseIndent,
14625
+ ),
14626
+ message: "No empty line before closing brace in props type",
14627
+ node: closeBraceToken,
14628
+ });
14629
+ }
14630
+ }
14415
14631
  }
14416
14632
 
14417
14633
  return;
@@ -14619,6 +14835,20 @@ const componentPropsInlineType = {
14619
14835
  }
14620
14836
  }
14621
14837
  });
14838
+
14839
+ // Check that last member has trailing comma
14840
+ if (members.length > 0) {
14841
+ const lastMember = members[members.length - 1];
14842
+ const lastMemberText = sourceCode.getText(lastMember);
14843
+
14844
+ if (!lastMemberText.trimEnd().endsWith(",")) {
14845
+ context.report({
14846
+ fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
14847
+ message: "Last props type property must have trailing comma",
14848
+ node: lastMember,
14849
+ });
14850
+ }
14851
+ }
14622
14852
  }
14623
14853
  };
14624
14854
 
@@ -16956,19 +17186,24 @@ const enumFormat = {
16956
17186
  }
16957
17187
 
16958
17188
  // Check member ends with comma, not semicolon
16959
- const memberText = sourceCode.getText(member);
17189
+ // Skip last member when multiple members - handled by combined check below
17190
+ const isLastMember = index === members.length - 1;
16960
17191
 
16961
- if (memberText.trimEnd().endsWith(";")) {
16962
- context.report({
16963
- fix(fixer) {
16964
- const lastChar = memberText.lastIndexOf(";");
16965
- const absolutePos = member.range[0] + lastChar;
17192
+ if (!isLastMember || members.length === 1) {
17193
+ const memberText = sourceCode.getText(member);
16966
17194
 
16967
- return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
16968
- },
16969
- message: "Enum members must end with comma (,) not semicolon (;)",
16970
- node: member,
16971
- });
17195
+ if (memberText.trimEnd().endsWith(";")) {
17196
+ context.report({
17197
+ fix(fixer) {
17198
+ const lastChar = memberText.lastIndexOf(";");
17199
+ const absolutePos = member.range[0] + lastChar;
17200
+
17201
+ return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
17202
+ },
17203
+ message: "Enum members must end with comma (,) not semicolon (;)",
17204
+ node: member,
17205
+ });
17206
+ }
16972
17207
  }
16973
17208
 
16974
17209
  // Check formatting for multiple members
@@ -17018,6 +17253,67 @@ const enumFormat = {
17018
17253
  }
17019
17254
  }
17020
17255
  });
17256
+
17257
+ // Check closing brace position and trailing comma/semicolon (for multiple members)
17258
+ if (members.length > 1) {
17259
+ const lastMemberText = sourceCode.getText(lastMember);
17260
+ const trimmedText = lastMemberText.trimEnd();
17261
+ // Check both: text ends with comma OR there's a comma token after the member
17262
+ const tokenAfterLast = sourceCode.getTokenAfter(lastMember);
17263
+ const hasTrailingComma = trimmedText.endsWith(",") || (tokenAfterLast && tokenAfterLast.value === ",");
17264
+ const hasTrailingSemicolon = trimmedText.endsWith(";");
17265
+ const braceOnSameLine = closeBraceToken && closeBraceToken.loc.start.line === lastMember.loc.end.line;
17266
+
17267
+ // Handle semicolon on last member (needs replacement with comma)
17268
+ if (hasTrailingSemicolon) {
17269
+ const lastSemicolon = lastMemberText.lastIndexOf(";");
17270
+ const absolutePos = lastMember.range[0] + lastSemicolon;
17271
+
17272
+ if (braceOnSameLine) {
17273
+ // Both semicolon and brace issues - fix together
17274
+ context.report({
17275
+ fix: (fixer) => fixer.replaceTextRange(
17276
+ [absolutePos, closeBraceToken.range[0]],
17277
+ ",\n" + baseIndent,
17278
+ ),
17279
+ message: "Last enum member must end with comma and closing brace must be on its own line",
17280
+ node: lastMember,
17281
+ });
17282
+ } else {
17283
+ // Just semicolon issue
17284
+ context.report({
17285
+ fix: (fixer) => fixer.replaceTextRange([absolutePos, absolutePos + 1], ","),
17286
+ message: "Enum members must end with comma (,) not semicolon (;)",
17287
+ node: lastMember,
17288
+ });
17289
+ }
17290
+ } else if (!hasTrailingComma && braceOnSameLine) {
17291
+ // Both missing comma and brace issues - fix together
17292
+ context.report({
17293
+ fix: (fixer) => fixer.replaceTextRange(
17294
+ [lastMember.range[1], closeBraceToken.range[0]],
17295
+ ",\n" + baseIndent,
17296
+ ),
17297
+ message: "Last enum member must have trailing comma and closing brace must be on its own line",
17298
+ node: lastMember,
17299
+ });
17300
+ } else if (!hasTrailingComma) {
17301
+ context.report({
17302
+ fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
17303
+ message: "Last enum member must have trailing comma",
17304
+ node: lastMember,
17305
+ });
17306
+ } else if (braceOnSameLine) {
17307
+ context.report({
17308
+ fix: (fixer) => fixer.replaceTextRange(
17309
+ [lastMember.range[1], closeBraceToken.range[0]],
17310
+ "\n" + baseIndent,
17311
+ ),
17312
+ message: "Closing brace must be on its own line",
17313
+ node: closeBraceToken,
17314
+ });
17315
+ }
17316
+ }
17021
17317
  },
17022
17318
  };
17023
17319
  },
@@ -17201,19 +17497,24 @@ const interfaceFormat = {
17201
17497
  }
17202
17498
 
17203
17499
  // Check property ends with comma, not semicolon
17204
- const memberText = sourceCode.getText(member);
17500
+ // Skip last member when multiple members - handled by combined check below
17501
+ const isLastMember = index === members.length - 1;
17205
17502
 
17206
- if (memberText.trimEnd().endsWith(";")) {
17207
- context.report({
17208
- fix(fixer) {
17209
- const lastChar = memberText.lastIndexOf(";");
17210
- const absolutePos = member.range[0] + lastChar;
17503
+ if (!isLastMember || members.length === 1) {
17504
+ const memberText = sourceCode.getText(member);
17211
17505
 
17212
- return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
17213
- },
17214
- message: "Interface properties must end with comma (,) not semicolon (;)",
17215
- node: member,
17216
- });
17506
+ if (memberText.trimEnd().endsWith(";")) {
17507
+ context.report({
17508
+ fix(fixer) {
17509
+ const lastChar = memberText.lastIndexOf(";");
17510
+ const absolutePos = member.range[0] + lastChar;
17511
+
17512
+ return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
17513
+ },
17514
+ message: "Interface properties must end with comma (,) not semicolon (;)",
17515
+ node: member,
17516
+ });
17517
+ }
17217
17518
  }
17218
17519
 
17219
17520
  // Check formatting for multiple members
@@ -17263,6 +17564,67 @@ const interfaceFormat = {
17263
17564
  }
17264
17565
  }
17265
17566
  });
17567
+
17568
+ // Check closing brace position and trailing comma/semicolon (for multiple members)
17569
+ if (members.length > 1) {
17570
+ const lastMemberText = sourceCode.getText(lastMember);
17571
+ const trimmedText = lastMemberText.trimEnd();
17572
+ // Check both: text ends with comma OR there's a comma token after the member
17573
+ const tokenAfterLast = sourceCode.getTokenAfter(lastMember);
17574
+ const hasTrailingComma = trimmedText.endsWith(",") || (tokenAfterLast && tokenAfterLast.value === ",");
17575
+ const hasTrailingSemicolon = trimmedText.endsWith(";");
17576
+ const braceOnSameLine = closeBraceToken.loc.start.line === lastMember.loc.end.line;
17577
+
17578
+ // Handle semicolon on last member (needs replacement with comma)
17579
+ if (hasTrailingSemicolon) {
17580
+ const lastSemicolon = lastMemberText.lastIndexOf(";");
17581
+ const absolutePos = lastMember.range[0] + lastSemicolon;
17582
+
17583
+ if (braceOnSameLine) {
17584
+ // Both semicolon and brace issues - fix together
17585
+ context.report({
17586
+ fix: (fixer) => fixer.replaceTextRange(
17587
+ [absolutePos, closeBraceToken.range[0]],
17588
+ ",\n" + baseIndent,
17589
+ ),
17590
+ message: "Last interface property must end with comma and closing brace must be on its own line",
17591
+ node: lastMember,
17592
+ });
17593
+ } else {
17594
+ // Just semicolon issue
17595
+ context.report({
17596
+ fix: (fixer) => fixer.replaceTextRange([absolutePos, absolutePos + 1], ","),
17597
+ message: "Interface properties must end with comma (,) not semicolon (;)",
17598
+ node: lastMember,
17599
+ });
17600
+ }
17601
+ } else if (!hasTrailingComma && braceOnSameLine) {
17602
+ // Both missing comma and brace issues - fix together
17603
+ context.report({
17604
+ fix: (fixer) => fixer.replaceTextRange(
17605
+ [lastMember.range[1], closeBraceToken.range[0]],
17606
+ ",\n" + baseIndent,
17607
+ ),
17608
+ message: "Last interface property must have trailing comma and closing brace must be on its own line",
17609
+ node: lastMember,
17610
+ });
17611
+ } else if (!hasTrailingComma) {
17612
+ context.report({
17613
+ fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
17614
+ message: "Last interface property must have trailing comma",
17615
+ node: lastMember,
17616
+ });
17617
+ } else if (braceOnSameLine) {
17618
+ context.report({
17619
+ fix: (fixer) => fixer.replaceTextRange(
17620
+ [lastMember.range[1], closeBraceToken.range[0]],
17621
+ "\n" + baseIndent,
17622
+ ),
17623
+ message: "Closing brace must be on its own line",
17624
+ node: closeBraceToken,
17625
+ });
17626
+ }
17627
+ }
17266
17628
  },
17267
17629
  };
17268
17630
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
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",