eslint-plugin-code-style 1.11.2 → 1.11.4

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 +34 -0
  2. package/index.js +396 -7
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.11.4] - 2026-02-04
11
+
12
+ ### Fixed
13
+
14
+ - **`opening-brackets-same-line`**
15
+ - Collapse JSX elements with simple children to single line (e.g., `<span>{strings.label}</span>`)
16
+ - Handle simple LogicalExpression children (e.g., `<p>{user?.email || fallback}</p>`)
17
+
18
+ - **`jsx-children-on-new-line`** / **`jsx-element-child-new-line`**
19
+ - Recognize simple LogicalExpression (≤2 operands) as simple children
20
+ - Recognize ChainExpression (optional chaining like `user?.name`) as simple expression
21
+ - Prevent circular fix conflicts with `opening-brackets-same-line`
22
+
23
+ ---
24
+
25
+ ## [1.11.3] - 2026-02-04
26
+
27
+ ### Fixed
28
+
29
+ - **`no-hardcoded-strings`**
30
+ - Skip CSS values in template literals assigned to style-related variables (e.g., `const lineGradient = \`linear-gradient(...)\``)
31
+ - Flag exported hardcoded strings like `export const tokenKey = "auth_token"` (non-SCREAMING_SNAKE_CASE exports)
32
+ - Skip HTML input types in default parameters (e.g., `type = "text"`)
33
+ - Smarter single-word classification: all lowercase (e.g., `"loading"`) → keyword/enum, capitalized (e.g., `"Loading"`) → UI string
34
+ - Descriptive error messages: UI strings → `@/strings or @/constants`, keywords → `@/data or @/enums`
35
+
36
+ - **`opening-brackets-same-line`**
37
+ - Collapse simple JSX logical expressions (≤2 operands, ≤80 chars) to single line
38
+ - Ensure closing `}` is on its own line for multiline logical expressions with 3+ operands
39
+
40
+ ---
41
+
10
42
  ## [1.11.2] - 2026-02-04
11
43
 
12
44
  ### Fixed
@@ -1472,6 +1504,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1472
1504
 
1473
1505
  ---
1474
1506
 
1507
+ [1.11.4]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.3...v1.11.4
1508
+ [1.11.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.2...v1.11.3
1475
1509
  [1.11.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.1...v1.11.2
1476
1510
  [1.11.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.0...v1.11.1
1477
1511
  [1.11.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.3...v1.11.0
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;
@@ -13251,8 +13368,117 @@ const openingBracketsSameLine = {
13251
13368
  return;
13252
13369
  }
13253
13370
 
13254
- // Case 5: LogicalExpression - ensure { and expression start are on same line
13371
+ // Case 5: LogicalExpression - handle based on complexity
13255
13372
  if (expression.type === "LogicalExpression") {
13373
+ // Count total operands in the logical expression
13374
+ const countOperandsHandler = (n) => {
13375
+ if (n.type === "LogicalExpression") {
13376
+ return countOperandsHandler(n.left) + countOperandsHandler(n.right);
13377
+ }
13378
+
13379
+ return 1;
13380
+ };
13381
+
13382
+ const operandCount = countOperandsHandler(expression);
13383
+ const expressionText = sourceCode.getText(expression);
13384
+ const isMultiLine = expression.loc.start.line !== expression.loc.end.line;
13385
+
13386
+ // Simple expression (2 operands, <= 80 chars) - collapse to single line
13387
+ if (operandCount <= 2 && expressionText.length <= 80) {
13388
+ const collapsedText = expressionText.replace(/\s*\n\s*/g, " ");
13389
+
13390
+ if (isMultiLine || openBrace.loc.end.line !== expression.loc.start.line
13391
+ || expression.loc.end.line !== closeBrace.loc.start.line) {
13392
+ context.report({
13393
+ fix: (fixer) => fixer.replaceTextRange(
13394
+ [openBrace.range[1], closeBrace.range[0]],
13395
+ collapsedText,
13396
+ ),
13397
+ message: "Simple logical expression should be on a single line",
13398
+ node: expression,
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
+ }
13442
+ }
13443
+
13444
+ return;
13445
+ }
13446
+
13447
+ // Complex expression (3+ operands) - closing } should be on its own line
13448
+ if (operandCount >= 3 && isMultiLine) {
13449
+ // Ensure opening { and expression start are on same line
13450
+ if (openBrace.loc.end.line !== expression.loc.start.line) {
13451
+ context.report({
13452
+ fix: (fixer) => fixer.replaceTextRange(
13453
+ [openBrace.range[1], expression.range[0]],
13454
+ "",
13455
+ ),
13456
+ message: "Opening brace and logical expression should be on the same line",
13457
+ node: expression,
13458
+ });
13459
+
13460
+ return;
13461
+ }
13462
+
13463
+ // Ensure closing } is on its own line after multiline expression
13464
+ if (expression.loc.end.line === closeBrace.loc.start.line) {
13465
+ // Get the indentation from the line with the opening brace
13466
+ const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
13467
+ const indent = openBraceLine.match(/^\s*/)[0];
13468
+
13469
+ context.report({
13470
+ fix: (fixer) => fixer.replaceTextRange(
13471
+ [expression.range[1], closeBrace.range[0]],
13472
+ "\n" + indent,
13473
+ ),
13474
+ message: "Closing brace should be on its own line for multiline logical expression",
13475
+ node: closeBrace,
13476
+ });
13477
+ }
13478
+
13479
+ return;
13480
+ }
13481
+
13256
13482
  // First, ensure { and expression start are on same line
13257
13483
  if (openBrace.loc.end.line !== expression.loc.start.line) {
13258
13484
  context.report({
@@ -14633,12 +14859,148 @@ const noHardcodedStrings = {
14633
14859
  return false;
14634
14860
  };
14635
14861
 
14636
- // Get descriptive error message - unified message for all hardcoded strings
14862
+ // CSS/style-related variable name patterns
14863
+ const styleVariablePatterns = [
14864
+ /gradient/i,
14865
+ /transform/i,
14866
+ /animation/i,
14867
+ /transition/i,
14868
+ /color/i,
14869
+ /background/i,
14870
+ /border/i,
14871
+ /shadow/i,
14872
+ /filter/i,
14873
+ /clip/i,
14874
+ /mask/i,
14875
+ /font/i,
14876
+ /^style/i,
14877
+ /Style$/i,
14878
+ /css/i,
14879
+ ];
14880
+
14881
+ // Check if template literal content looks like CSS value
14882
+ const isCssValueHandler = (str) => {
14883
+ // CSS functions: linear-gradient, radial-gradient, rotate, translate, etc.
14884
+ if (/^(linear-gradient|radial-gradient|conic-gradient|repeating-linear-gradient|repeating-radial-gradient|rotate|translate|translateX|translateY|translateZ|translate3d|scale|scaleX|scaleY|scaleZ|scale3d|skew|skewX|skewY|matrix|matrix3d|perspective|calc|var|clamp|min|max|cubic-bezier|steps|url)\(/i.test(str)) {
14885
+ return true;
14886
+ }
14887
+
14888
+ // Color values
14889
+ if (/^(#[0-9a-fA-F]{3,8}|rgb|rgba|hsl|hsla)\(/i.test(str)) return true;
14890
+
14891
+ // CSS value with units
14892
+ if (/^\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax|deg|rad|turn|s|ms|fr)\s*/.test(str)) return true;
14893
+
14894
+ return false;
14895
+ };
14896
+
14897
+ // Check if a template literal is assigned to a style-related variable
14898
+ const isStyleVariableAssignmentHandler = (node) => {
14899
+ let current = node.parent;
14900
+
14901
+ while (current) {
14902
+ if (current.type === "VariableDeclarator" && current.id && current.id.name) {
14903
+ const varName = current.id.name;
14904
+
14905
+ // Check if variable name matches style patterns
14906
+ if (styleVariablePatterns.some((pattern) => pattern.test(varName))) {
14907
+ return true;
14908
+ }
14909
+ }
14910
+
14911
+ // Check for property assignment like: const styles = { gradient: `...` }
14912
+ if (current.type === "Property" && current.key) {
14913
+ const propName = current.key.name || (current.key.value && String(current.key.value));
14914
+
14915
+ if (propName && styleVariablePatterns.some((pattern) => pattern.test(propName))) {
14916
+ return true;
14917
+ }
14918
+ }
14919
+
14920
+ current = current.parent;
14921
+ }
14922
+
14923
+ return false;
14924
+ };
14925
+
14926
+ // Check if string is in a default parameter for input type
14927
+ const isInputTypeDefaultParamHandler = (node) => {
14928
+ // Check if we're in an AssignmentPattern (default param)
14929
+ if (node.parent && node.parent.type === "AssignmentPattern") {
14930
+ const assignPattern = node.parent;
14931
+
14932
+ // Check if the parameter name is "type"
14933
+ if (assignPattern.left && assignPattern.left.type === "Identifier" && assignPattern.left.name === "type") {
14934
+ return true;
14935
+ }
14936
+ }
14937
+
14938
+ return false;
14939
+ };
14940
+
14941
+ // Check if this is a module-level exported string that should be flagged
14942
+ const isExportedHardcodedStringHandler = (node) => {
14943
+ let current = node.parent;
14944
+ let depth = 0;
14945
+
14946
+ while (current) {
14947
+ depth++;
14948
+
14949
+ // Check for export const name = "value" pattern (NOT in function)
14950
+ if (current.type === "ExportNamedDeclaration" && depth <= 3) {
14951
+ const declaration = current.declaration;
14952
+
14953
+ if (declaration && declaration.type === "VariableDeclaration") {
14954
+ const declarator = declaration.declarations[0];
14955
+
14956
+ if (declarator && declarator.id && declarator.id.name) {
14957
+ const varName = declarator.id.name;
14958
+
14959
+ // Skip SCREAMING_SNAKE_CASE - these are intentional constants
14960
+ if (/^[A-Z][A-Z0-9_]*$/.test(varName)) return false;
14961
+
14962
+ // Skip constants-like variable names
14963
+ if (/^(constants?|strings?|messages?|labels?|texts?|data)$/i.test(varName)) return false;
14964
+
14965
+ // This is an exported string that looks like a hardcoded value (e.g., tokenKey)
14966
+ return true;
14967
+ }
14968
+ }
14969
+ }
14970
+
14971
+ // Stop if we hit a function - we're inside a function, not module-level
14972
+ if (
14973
+ current.type === "FunctionDeclaration"
14974
+ || current.type === "FunctionExpression"
14975
+ || current.type === "ArrowFunctionExpression"
14976
+ ) {
14977
+ return false;
14978
+ }
14979
+
14980
+ current = current.parent;
14981
+ }
14982
+
14983
+ return false;
14984
+ };
14985
+
14986
+ // Get descriptive error message based on string type
14637
14987
  const getErrorMessageHandler = (str, context = "") => {
14638
14988
  const truncatedStr = str.length > 30 ? `${str.substring(0, 30)}...` : str;
14639
14989
  const contextPart = context ? ` in ${context}` : "";
14640
14990
 
14641
- return `Hardcoded string "${truncatedStr}"${contextPart} should be imported from @/data, @/strings, @/constants, or @/enums`;
14991
+ // Single word detection:
14992
+ // - All lowercase (e.g., "loading", "submit") → keyword/enum/data
14993
+ // - Starts with capital (e.g., "Loading", "Submit") → UI string
14994
+ // - Has spaces or multiple words → UI string
14995
+ const isSingleWord = !/\s/.test(str) && str.length <= 30;
14996
+ const isAllLowercase = /^[a-z_]+$/.test(str);
14997
+
14998
+ if (isSingleWord && isAllLowercase) {
14999
+ return `Hardcoded data keyword or enum "${truncatedStr}"${contextPart} should be imported from @/data or @/enums (e.g., import { StatusEnum } from "@/enums")`;
15000
+ }
15001
+
15002
+ // UI string: starts with capital, has spaces, or multiple words
15003
+ return `Hardcoded UI string "${truncatedStr}"${contextPart} should be imported from @/strings or @/constants or @/@strings or @/@constants (e.g., import { strings } from "@/strings")`;
14642
15004
  };
14643
15005
 
14644
15006
  // Check if a string matches any ignore pattern
@@ -14963,13 +15325,31 @@ const noHardcodedStrings = {
14963
15325
 
14964
15326
  const str = node.value;
14965
15327
 
14966
- // Skip if it matches ignore patterns
14967
- if (shouldIgnoreStringHandler(str)) return;
14968
-
14969
15328
  // Skip if inside a style object (style={{ transform: "..." }})
14970
15329
  if (isInsideStyleObjectHandler(node)) return;
14971
15330
 
14972
- // Skip if not in relevant context
15331
+ // Skip input type default params (e.g., type = "text")
15332
+ if (isInputTypeDefaultParamHandler(node)) return;
15333
+
15334
+ // Check for exported hardcoded strings (e.g., export const tokenKey = "auth_token")
15335
+ // These should be flagged even at module level, regardless of whether the value
15336
+ // looks "technical" - the point is exposing hardcoded strings in exports
15337
+ if (isExportedHardcodedStringHandler(node)) {
15338
+ // Skip if it doesn't look like user-facing text
15339
+ if (!/[a-zA-Z]/.test(str)) return;
15340
+
15341
+ context.report({
15342
+ message: getErrorMessageHandler(str, "exported constant"),
15343
+ node,
15344
+ });
15345
+
15346
+ return;
15347
+ }
15348
+
15349
+ // Skip if it matches ignore patterns (for strings inside functions)
15350
+ if (shouldIgnoreStringHandler(str)) return;
15351
+
15352
+ // Skip if not in relevant context (must be inside a function)
14973
15353
  if (!isInRelevantContextHandler(node)) return;
14974
15354
 
14975
15355
  // Skip if in a constants definition object
@@ -15001,6 +15381,15 @@ const noHardcodedStrings = {
15001
15381
  // Skip if inside a style object (style={{ background: `...` }})
15002
15382
  if (isInsideStyleObjectHandler(node)) return;
15003
15383
 
15384
+ // Skip if assigned to a style-related variable with CSS value
15385
+ // e.g., const lineGradient = `linear-gradient(...)`
15386
+ if (isStyleVariableAssignmentHandler(node)) {
15387
+ // Get full template content to check if it's CSS
15388
+ const fullContent = node.quasis.map((q) => q.value.cooked || q.value.raw).join("");
15389
+
15390
+ if (isCssValueHandler(fullContent)) return;
15391
+ }
15392
+
15004
15393
  // Skip if not in relevant context
15005
15394
  if (!isInRelevantContextHandler(node)) return;
15006
15395
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.11.2",
3
+ "version": "1.11.4",
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",