eslint-plugin-code-style 1.11.3 → 1.11.7

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 +53 -0
  2. package/index.js +171 -35
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.11.7] - 2026-02-04
11
+
12
+ ### Changed
13
+
14
+ - **`no-hardcoded-strings`**
15
+ - Remove UI component pattern exemption - ALL enum-like attribute values are now flagged (e.g., `variant="primary"`, `size="large"`, `color="danger"`)
16
+ - Enforce consistent use of enums for component props to prevent typos
17
+
18
+ ---
19
+
20
+ ## [1.11.6] - 2026-02-04
21
+
22
+ ### Changed
23
+
24
+ - **`no-hardcoded-strings`**
25
+ - Flag `type` attribute in JSX elements (e.g., `<input type="text" />`) - should use enums to prevent typos
26
+ - Remove `type` from default ignored attributes list
27
+ - Remove "text" from UI component pattern (conflicts with input type)
28
+ - Update error message for JSX attributes: "should be imported from @/enums (preferred) or @/data to prevent typos"
29
+
30
+ ---
31
+
32
+ ## [1.11.5] - 2026-02-04
33
+
34
+ ### Fixed
35
+
36
+ - **`no-hardcoded-strings`**
37
+ - Flag hardcoded strings in component default params (e.g., `type = "text"`, `variant = "ghost"`)
38
+ - Flag hardcoded strings in ternary expressions (e.g., `showPassword ? "text" : "password"`)
39
+ - Remove overly broad HTML input type exemption from general string checks
40
+ - Remove "text" from CSS cursor pattern (conflicts with common input type usage)
41
+
42
+ ---
43
+
44
+ ## [1.11.4] - 2026-02-04
45
+
46
+ ### Fixed
47
+
48
+ - **`opening-brackets-same-line`**
49
+ - Collapse JSX elements with simple children to single line (e.g., `<span>{strings.label}</span>`)
50
+ - Handle simple LogicalExpression children (e.g., `<p>{user?.email || fallback}</p>`)
51
+
52
+ - **`jsx-children-on-new-line`** / **`jsx-element-child-new-line`**
53
+ - Recognize simple LogicalExpression (≤2 operands) as simple children
54
+ - Recognize ChainExpression (optional chaining like `user?.name`) as simple expression
55
+ - Prevent circular fix conflicts with `opening-brackets-same-line`
56
+
57
+ ---
58
+
10
59
  ## [1.11.3] - 2026-02-04
11
60
 
12
61
  ### Fixed
@@ -1489,6 +1538,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1489
1538
 
1490
1539
  ---
1491
1540
 
1541
+ [1.11.7]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.6...v1.11.7
1542
+ [1.11.6]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.5...v1.11.6
1543
+ [1.11.5]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.4...v1.11.5
1544
+ [1.11.4]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.3...v1.11.4
1492
1545
  [1.11.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.2...v1.11.3
1493
1546
  [1.11.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.1...v1.11.2
1494
1547
  [1.11.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.0...v1.11.1
package/index.js CHANGED
@@ -7683,6 +7683,12 @@ const jsxChildrenOnNewLine = {
7683
7683
 
7684
7684
  if (expr.type === "Identifier") return true;
7685
7685
  if (expr.type === "Literal") return true;
7686
+
7687
+ // Handle ChainExpression (optional chaining like user?.name)
7688
+ if (expr.type === "ChainExpression") {
7689
+ return isSimpleExpressionHandler(expr.expression);
7690
+ }
7691
+
7686
7692
  if (expr.type === "MemberExpression") {
7687
7693
  // Allow nested member expressions like row.original.currency or row[field]
7688
7694
  let current = expr;
@@ -7700,6 +7706,11 @@ const jsxChildrenOnNewLine = {
7700
7706
  current = current.object;
7701
7707
  }
7702
7708
 
7709
+ // Handle ChainExpression at the end of the chain
7710
+ if (current.type === "ChainExpression") {
7711
+ return isSimpleExpressionHandler(current.expression);
7712
+ }
7713
+
7703
7714
  return current.type === "Identifier";
7704
7715
  }
7705
7716
 
@@ -7719,6 +7730,32 @@ const jsxChildrenOnNewLine = {
7719
7730
  return false;
7720
7731
  }
7721
7732
 
7733
+ // Allow simple LogicalExpression (2 operands with simple left/right)
7734
+ if (expr.type === "LogicalExpression") {
7735
+ // Count operands - if more than 2, not simple
7736
+ const countOperands = (n) => {
7737
+ if (n.type === "LogicalExpression") {
7738
+ return countOperands(n.left) + countOperands(n.right);
7739
+ }
7740
+
7741
+ return 1;
7742
+ };
7743
+
7744
+ if (countOperands(expr) > 2) return false;
7745
+
7746
+ // Check if left and right are simple
7747
+ const isSimpleSide = (n) => {
7748
+ if (n.type === "Identifier") return true;
7749
+ if (n.type === "Literal") return true;
7750
+ if (n.type === "MemberExpression") return isSimpleExpressionHandler(n);
7751
+ if (n.type === "ChainExpression" && n.expression) return isSimpleSide(n.expression);
7752
+
7753
+ return false;
7754
+ };
7755
+
7756
+ return isSimpleSide(expr.left) && isSimpleSide(expr.right);
7757
+ }
7758
+
7722
7759
  return false;
7723
7760
  };
7724
7761
 
@@ -7944,6 +7981,12 @@ const jsxElementChildNewLine = {
7944
7981
 
7945
7982
  if (expr.type === "Identifier") return true;
7946
7983
  if (expr.type === "Literal") return true;
7984
+
7985
+ // Handle ChainExpression (optional chaining like user?.name)
7986
+ if (expr.type === "ChainExpression") {
7987
+ return isSimpleExpressionHandler(expr.expression);
7988
+ }
7989
+
7947
7990
  if (expr.type === "MemberExpression") {
7948
7991
  // Allow nested member expressions like row.original.currency or row[field]
7949
7992
  let current = expr;
@@ -7961,6 +8004,11 @@ const jsxElementChildNewLine = {
7961
8004
  current = current.object;
7962
8005
  }
7963
8006
 
8007
+ // Handle ChainExpression at the end of the chain
8008
+ if (current.type === "ChainExpression") {
8009
+ return isSimpleExpressionHandler(current.expression);
8010
+ }
8011
+
7964
8012
  return current.type === "Identifier";
7965
8013
  }
7966
8014
 
@@ -7980,6 +8028,32 @@ const jsxElementChildNewLine = {
7980
8028
  return false;
7981
8029
  }
7982
8030
 
8031
+ // Allow simple LogicalExpression (2 operands with simple left/right)
8032
+ if (expr.type === "LogicalExpression") {
8033
+ // Count operands - if more than 2, not simple
8034
+ const countOperands = (n) => {
8035
+ if (n.type === "LogicalExpression") {
8036
+ return countOperands(n.left) + countOperands(n.right);
8037
+ }
8038
+
8039
+ return 1;
8040
+ };
8041
+
8042
+ if (countOperands(expr) > 2) return false;
8043
+
8044
+ // Check if left and right are simple
8045
+ const isSimpleSide = (n) => {
8046
+ if (n.type === "Identifier") return true;
8047
+ if (n.type === "Literal") return true;
8048
+ if (n.type === "MemberExpression") return isSimpleExpressionHandler(n);
8049
+ if (n.type === "ChainExpression" && n.expression) return isSimpleSide(n.expression);
8050
+
8051
+ return false;
8052
+ };
8053
+
8054
+ return isSimpleSide(expr.left) && isSimpleSide(expr.right);
8055
+ }
8056
+
7983
8057
  return false;
7984
8058
  };
7985
8059
 
@@ -13117,6 +13191,49 @@ const openingBracketsSameLine = {
13117
13191
  message: "Simple expression should be on single line in JSX attribute",
13118
13192
  node: expression,
13119
13193
  });
13194
+
13195
+ return;
13196
+ }
13197
+
13198
+ // Check if parent JSX element should be collapsed to single line
13199
+ // e.g., <span>\n {strings.label}\n</span> → <span>{strings.label}</span>
13200
+ const parent = node.parent;
13201
+
13202
+ if (parent && parent.type === "JSXElement") {
13203
+ const children = parent.children.filter(
13204
+ (child) => !(child.type === "JSXText" && /^\s*$/.test(child.value)),
13205
+ );
13206
+
13207
+ // Only collapse if this expression is the only meaningful child
13208
+ if (children.length === 1 && children[0] === node) {
13209
+ const openingTag = parent.openingElement;
13210
+ const closingTag = parent.closingElement;
13211
+
13212
+ if (closingTag) {
13213
+ const openTagEnd = openingTag.loc.end.line;
13214
+ const closeTagStart = closingTag.loc.start.line;
13215
+
13216
+ // Check if element spans multiple lines but content is simple
13217
+ if (openTagEnd !== closeTagStart) {
13218
+ const openTagText = sourceCode.getText(openingTag);
13219
+ const closeTagText = sourceCode.getText(closingTag);
13220
+ const expressionText = sourceCode.getText(node);
13221
+ const collapsedLength = openTagText.length + expressionText.length + closeTagText.length;
13222
+
13223
+ // Only collapse if total length is reasonable
13224
+ if (collapsedLength <= 120) {
13225
+ context.report({
13226
+ fix: (fixer) => fixer.replaceTextRange(
13227
+ [openingTag.range[1], closingTag.range[0]],
13228
+ expressionText,
13229
+ ),
13230
+ message: "JSX element with simple expression should be on single line",
13231
+ node: parent,
13232
+ });
13233
+ }
13234
+ }
13235
+ }
13236
+ }
13120
13237
  }
13121
13238
 
13122
13239
  return;
@@ -13280,6 +13397,48 @@ const openingBracketsSameLine = {
13280
13397
  message: "Simple logical expression should be on a single line",
13281
13398
  node: expression,
13282
13399
  });
13400
+
13401
+ return;
13402
+ }
13403
+
13404
+ // Check if parent JSX element should be collapsed to single line
13405
+ const parent = node.parent;
13406
+
13407
+ if (parent && parent.type === "JSXElement") {
13408
+ const children = parent.children.filter(
13409
+ (child) => !(child.type === "JSXText" && /^\s*$/.test(child.value)),
13410
+ );
13411
+
13412
+ // Only collapse if this expression is the only meaningful child
13413
+ if (children.length === 1 && children[0] === node) {
13414
+ const openingTag = parent.openingElement;
13415
+ const closingTag = parent.closingElement;
13416
+
13417
+ if (closingTag) {
13418
+ const openTagEnd = openingTag.loc.end.line;
13419
+ const closeTagStart = closingTag.loc.start.line;
13420
+
13421
+ // Check if element spans multiple lines but content is simple
13422
+ if (openTagEnd !== closeTagStart) {
13423
+ const openTagText = sourceCode.getText(openingTag);
13424
+ const closeTagText = sourceCode.getText(closingTag);
13425
+ const collapsedExpr = "{" + collapsedText + "}";
13426
+ const collapsedLength = openTagText.length + collapsedExpr.length + closeTagText.length;
13427
+
13428
+ // Only collapse if total length is reasonable
13429
+ if (collapsedLength <= 120) {
13430
+ context.report({
13431
+ fix: (fixer) => fixer.replaceTextRange(
13432
+ [openingTag.range[1], closingTag.range[0]],
13433
+ collapsedExpr,
13434
+ ),
13435
+ message: "JSX element with simple logical expression should be on single line",
13436
+ node: parent,
13437
+ });
13438
+ }
13439
+ }
13440
+ }
13441
+ }
13283
13442
  }
13284
13443
 
13285
13444
  return;
@@ -14402,7 +14561,7 @@ const noHardcodedStrings = {
14402
14561
  "textDecoration", // SVG
14403
14562
  "transform", // SVG
14404
14563
  "translate",
14405
- "type",
14564
+ // "type" removed - should use enums for input/button types to prevent typos
14406
14565
  "vectorEffect", // SVG
14407
14566
  "useMap",
14408
14567
  "value",
@@ -14487,8 +14646,8 @@ const noHardcodedStrings = {
14487
14646
  /^[a-zA-Z]+\d*[_a-zA-Z0-9]*(_[a-zA-Z0-9]+)+$/,
14488
14647
  // Color names (CSS named colors used in SVG)
14489
14648
  /^(white|black|red|green|blue|yellow|orange|purple|pink|brown|gray|grey|cyan|magenta|transparent)$/i,
14490
- // CSS cursor values
14491
- /^(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|text|vertical-text|alias|copy|move|no-drop|not-allowed|grab|grabbing|all-scroll|col-resize|row-resize|n-resize|e-resize|s-resize|w-resize|ne-resize|nw-resize|se-resize|sw-resize|ew-resize|ns-resize|nesw-resize|nwse-resize|zoom-in|zoom-out)$/,
14649
+ // CSS cursor values (excluding "text" as it conflicts with input type)
14650
+ /^(auto|default|none|context-menu|help|pointer|progress|wait|cell|crosshair|vertical-text|alias|copy|move|no-drop|not-allowed|grab|grabbing|all-scroll|col-resize|row-resize|n-resize|e-resize|s-resize|w-resize|ne-resize|nw-resize|se-resize|sw-resize|ew-resize|ns-resize|nesw-resize|nwse-resize|zoom-in|zoom-out)$/,
14492
14651
  // CSS display/visibility values
14493
14652
  /^(block|inline|inline-block|flex|inline-flex|grid|inline-grid|flow-root|contents|table|table-row|table-cell|list-item|none|visible|hidden|collapse)$/,
14494
14653
  // CSS position values
@@ -14642,9 +14801,6 @@ const noHardcodedStrings = {
14642
14801
  });
14643
14802
  };
14644
14803
 
14645
- // UI component patterns - only ignored in JSX attributes, not in logic
14646
- const uiComponentPattern = /^(primary|secondary|tertiary|ghost|outline|link|muted|danger|warning|info|success|error|default|subtle|solid|soft|plain|flat|elevated|filled|tonal|text|contained|standard|xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|xxs|xxl|small|medium|large|tiny|huge|compact|comfortable|spacious|left|right|center|top|bottom|start|end|middle|baseline|stretch|between|around|evenly|horizontal|vertical|row|column|inline|block|flex|grid|auto|none|hidden|visible|static|relative|absolute|fixed|sticky|on|off|hover|focus|click|blur|always|never)$/;
14647
-
14648
14804
  // HTML input types - standard browser input types, not hardcoded strings
14649
14805
  const htmlInputTypes = new Set([
14650
14806
  "button",
@@ -14764,21 +14920,6 @@ const noHardcodedStrings = {
14764
14920
  return false;
14765
14921
  };
14766
14922
 
14767
- // Check if string is in a default parameter for input type
14768
- const isInputTypeDefaultParamHandler = (node) => {
14769
- // Check if we're in an AssignmentPattern (default param)
14770
- if (node.parent && node.parent.type === "AssignmentPattern") {
14771
- const assignPattern = node.parent;
14772
-
14773
- // Check if the parameter name is "type"
14774
- if (assignPattern.left && assignPattern.left.type === "Identifier" && assignPattern.left.name === "type") {
14775
- return true;
14776
- }
14777
- }
14778
-
14779
- return false;
14780
- };
14781
-
14782
14923
  // Check if this is a module-level exported string that should be flagged
14783
14924
  const isExportedHardcodedStringHandler = (node) => {
14784
14925
  let current = node.parent;
@@ -14836,19 +14977,23 @@ const noHardcodedStrings = {
14836
14977
  const isSingleWord = !/\s/.test(str) && str.length <= 30;
14837
14978
  const isAllLowercase = /^[a-z_]+$/.test(str);
14838
14979
 
14980
+ // For JSX attributes (type, variant, etc.), prefer enums to prevent typos
14981
+ const isJsxAttribute = context.includes("attribute");
14982
+
14839
14983
  if (isSingleWord && isAllLowercase) {
14840
- return `Hardcoded data keyword or enum "${truncatedStr}"${contextPart} should be imported from @/data or @/enums (e.g., import { StatusEnum } from "@/enums")`;
14984
+ if (isJsxAttribute) {
14985
+ return `Hardcoded "${truncatedStr}"${contextPart} should be imported from @/enums (preferred) or @/data to prevent typos (e.g., import { InputTypeEnum } from "@/enums")`;
14986
+ }
14987
+
14988
+ return `Hardcoded "${truncatedStr}"${contextPart} should be imported from @/enums (preferred) or @/data (e.g., import { StatusEnum } from "@/enums")`;
14841
14989
  }
14842
14990
 
14843
14991
  // UI string: starts with capital, has spaces, or multiple words
14844
- return `Hardcoded UI string "${truncatedStr}"${contextPart} should be imported from @/strings or @/constants or @/@strings or @/@constants (e.g., import { strings } from "@/strings")`;
14992
+ return `Hardcoded UI string "${truncatedStr}"${contextPart} should be imported from @/strings or @/constants (e.g., import { strings } from "@/strings")`;
14845
14993
  };
14846
14994
 
14847
14995
  // Check if a string matches any ignore pattern
14848
14996
  const shouldIgnoreStringHandler = (str) => {
14849
- // Skip HTML input types (text, password, email, etc.)
14850
- if (isHtmlInputTypeHandler(str)) return true;
14851
-
14852
14997
  // Skip Tailwind/CSS class strings
14853
14998
  if (isTailwindClassStringHandler(str)) return true;
14854
14999
 
@@ -15120,9 +15265,6 @@ const noHardcodedStrings = {
15120
15265
  if (node.value.type === "Literal" && typeof node.value.value === "string") {
15121
15266
  const str = node.value.value;
15122
15267
 
15123
- // Skip UI component patterns in JSX attributes (variant, size, position props)
15124
- if (uiComponentPattern.test(str)) return;
15125
-
15126
15268
  if (shouldIgnoreStringHandler(str)) return;
15127
15269
 
15128
15270
  // Check if it looks like user-facing text
@@ -15144,9 +15286,6 @@ const noHardcodedStrings = {
15144
15286
  if (expression.type === "Literal" && typeof expression.value === "string") {
15145
15287
  const str = expression.value;
15146
15288
 
15147
- // Skip UI component patterns in JSX attributes (variant, size, position props)
15148
- if (uiComponentPattern.test(str)) return;
15149
-
15150
15289
  if (shouldIgnoreStringHandler(str)) return;
15151
15290
 
15152
15291
  if (!/[a-zA-Z]/.test(str)) return;
@@ -15169,9 +15308,6 @@ const noHardcodedStrings = {
15169
15308
  // Skip if inside a style object (style={{ transform: "..." }})
15170
15309
  if (isInsideStyleObjectHandler(node)) return;
15171
15310
 
15172
- // Skip input type default params (e.g., type = "text")
15173
- if (isInputTypeDefaultParamHandler(node)) return;
15174
-
15175
15311
  // Check for exported hardcoded strings (e.g., export const tokenKey = "auth_token")
15176
15312
  // These should be flagged even at module level, regardless of whether the value
15177
15313
  // looks "technical" - the point is exposing hardcoded strings in exports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.11.3",
3
+ "version": "1.11.7",
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",