eslint-plugin-code-style 1.11.2 → 1.11.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 (3) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/index.js +237 -7
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.11.3] - 2026-02-04
11
+
12
+ ### Fixed
13
+
14
+ - **`no-hardcoded-strings`**
15
+ - Skip CSS values in template literals assigned to style-related variables (e.g., `const lineGradient = \`linear-gradient(...)\``)
16
+ - Flag exported hardcoded strings like `export const tokenKey = "auth_token"` (non-SCREAMING_SNAKE_CASE exports)
17
+ - Skip HTML input types in default parameters (e.g., `type = "text"`)
18
+ - Smarter single-word classification: all lowercase (e.g., `"loading"`) → keyword/enum, capitalized (e.g., `"Loading"`) → UI string
19
+ - Descriptive error messages: UI strings → `@/strings or @/constants`, keywords → `@/data or @/enums`
20
+
21
+ - **`opening-brackets-same-line`**
22
+ - Collapse simple JSX logical expressions (≤2 operands, ≤80 chars) to single line
23
+ - Ensure closing `}` is on its own line for multiline logical expressions with 3+ operands
24
+
25
+ ---
26
+
10
27
  ## [1.11.2] - 2026-02-04
11
28
 
12
29
  ### Fixed
@@ -1472,6 +1489,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1472
1489
 
1473
1490
  ---
1474
1491
 
1492
+ [1.11.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.2...v1.11.3
1475
1493
  [1.11.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.1...v1.11.2
1476
1494
  [1.11.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.11.0...v1.11.1
1477
1495
  [1.11.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.10.3...v1.11.0
package/index.js CHANGED
@@ -13251,8 +13251,75 @@ const openingBracketsSameLine = {
13251
13251
  return;
13252
13252
  }
13253
13253
 
13254
- // Case 5: LogicalExpression - ensure { and expression start are on same line
13254
+ // Case 5: LogicalExpression - handle based on complexity
13255
13255
  if (expression.type === "LogicalExpression") {
13256
+ // Count total operands in the logical expression
13257
+ const countOperandsHandler = (n) => {
13258
+ if (n.type === "LogicalExpression") {
13259
+ return countOperandsHandler(n.left) + countOperandsHandler(n.right);
13260
+ }
13261
+
13262
+ return 1;
13263
+ };
13264
+
13265
+ const operandCount = countOperandsHandler(expression);
13266
+ const expressionText = sourceCode.getText(expression);
13267
+ const isMultiLine = expression.loc.start.line !== expression.loc.end.line;
13268
+
13269
+ // Simple expression (2 operands, <= 80 chars) - collapse to single line
13270
+ if (operandCount <= 2 && expressionText.length <= 80) {
13271
+ const collapsedText = expressionText.replace(/\s*\n\s*/g, " ");
13272
+
13273
+ if (isMultiLine || openBrace.loc.end.line !== expression.loc.start.line
13274
+ || expression.loc.end.line !== closeBrace.loc.start.line) {
13275
+ context.report({
13276
+ fix: (fixer) => fixer.replaceTextRange(
13277
+ [openBrace.range[1], closeBrace.range[0]],
13278
+ collapsedText,
13279
+ ),
13280
+ message: "Simple logical expression should be on a single line",
13281
+ node: expression,
13282
+ });
13283
+ }
13284
+
13285
+ return;
13286
+ }
13287
+
13288
+ // Complex expression (3+ operands) - closing } should be on its own line
13289
+ if (operandCount >= 3 && isMultiLine) {
13290
+ // Ensure opening { and expression start are on same line
13291
+ if (openBrace.loc.end.line !== expression.loc.start.line) {
13292
+ context.report({
13293
+ fix: (fixer) => fixer.replaceTextRange(
13294
+ [openBrace.range[1], expression.range[0]],
13295
+ "",
13296
+ ),
13297
+ message: "Opening brace and logical expression should be on the same line",
13298
+ node: expression,
13299
+ });
13300
+
13301
+ return;
13302
+ }
13303
+
13304
+ // Ensure closing } is on its own line after multiline expression
13305
+ if (expression.loc.end.line === closeBrace.loc.start.line) {
13306
+ // Get the indentation from the line with the opening brace
13307
+ const openBraceLine = sourceCode.lines[openBrace.loc.start.line - 1];
13308
+ const indent = openBraceLine.match(/^\s*/)[0];
13309
+
13310
+ context.report({
13311
+ fix: (fixer) => fixer.replaceTextRange(
13312
+ [expression.range[1], closeBrace.range[0]],
13313
+ "\n" + indent,
13314
+ ),
13315
+ message: "Closing brace should be on its own line for multiline logical expression",
13316
+ node: closeBrace,
13317
+ });
13318
+ }
13319
+
13320
+ return;
13321
+ }
13322
+
13256
13323
  // First, ensure { and expression start are on same line
13257
13324
  if (openBrace.loc.end.line !== expression.loc.start.line) {
13258
13325
  context.report({
@@ -14633,12 +14700,148 @@ const noHardcodedStrings = {
14633
14700
  return false;
14634
14701
  };
14635
14702
 
14636
- // Get descriptive error message - unified message for all hardcoded strings
14703
+ // CSS/style-related variable name patterns
14704
+ const styleVariablePatterns = [
14705
+ /gradient/i,
14706
+ /transform/i,
14707
+ /animation/i,
14708
+ /transition/i,
14709
+ /color/i,
14710
+ /background/i,
14711
+ /border/i,
14712
+ /shadow/i,
14713
+ /filter/i,
14714
+ /clip/i,
14715
+ /mask/i,
14716
+ /font/i,
14717
+ /^style/i,
14718
+ /Style$/i,
14719
+ /css/i,
14720
+ ];
14721
+
14722
+ // Check if template literal content looks like CSS value
14723
+ const isCssValueHandler = (str) => {
14724
+ // CSS functions: linear-gradient, radial-gradient, rotate, translate, etc.
14725
+ 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)) {
14726
+ return true;
14727
+ }
14728
+
14729
+ // Color values
14730
+ if (/^(#[0-9a-fA-F]{3,8}|rgb|rgba|hsl|hsla)\(/i.test(str)) return true;
14731
+
14732
+ // CSS value with units
14733
+ if (/^\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax|deg|rad|turn|s|ms|fr)\s*/.test(str)) return true;
14734
+
14735
+ return false;
14736
+ };
14737
+
14738
+ // Check if a template literal is assigned to a style-related variable
14739
+ const isStyleVariableAssignmentHandler = (node) => {
14740
+ let current = node.parent;
14741
+
14742
+ while (current) {
14743
+ if (current.type === "VariableDeclarator" && current.id && current.id.name) {
14744
+ const varName = current.id.name;
14745
+
14746
+ // Check if variable name matches style patterns
14747
+ if (styleVariablePatterns.some((pattern) => pattern.test(varName))) {
14748
+ return true;
14749
+ }
14750
+ }
14751
+
14752
+ // Check for property assignment like: const styles = { gradient: `...` }
14753
+ if (current.type === "Property" && current.key) {
14754
+ const propName = current.key.name || (current.key.value && String(current.key.value));
14755
+
14756
+ if (propName && styleVariablePatterns.some((pattern) => pattern.test(propName))) {
14757
+ return true;
14758
+ }
14759
+ }
14760
+
14761
+ current = current.parent;
14762
+ }
14763
+
14764
+ return false;
14765
+ };
14766
+
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
+ // Check if this is a module-level exported string that should be flagged
14783
+ const isExportedHardcodedStringHandler = (node) => {
14784
+ let current = node.parent;
14785
+ let depth = 0;
14786
+
14787
+ while (current) {
14788
+ depth++;
14789
+
14790
+ // Check for export const name = "value" pattern (NOT in function)
14791
+ if (current.type === "ExportNamedDeclaration" && depth <= 3) {
14792
+ const declaration = current.declaration;
14793
+
14794
+ if (declaration && declaration.type === "VariableDeclaration") {
14795
+ const declarator = declaration.declarations[0];
14796
+
14797
+ if (declarator && declarator.id && declarator.id.name) {
14798
+ const varName = declarator.id.name;
14799
+
14800
+ // Skip SCREAMING_SNAKE_CASE - these are intentional constants
14801
+ if (/^[A-Z][A-Z0-9_]*$/.test(varName)) return false;
14802
+
14803
+ // Skip constants-like variable names
14804
+ if (/^(constants?|strings?|messages?|labels?|texts?|data)$/i.test(varName)) return false;
14805
+
14806
+ // This is an exported string that looks like a hardcoded value (e.g., tokenKey)
14807
+ return true;
14808
+ }
14809
+ }
14810
+ }
14811
+
14812
+ // Stop if we hit a function - we're inside a function, not module-level
14813
+ if (
14814
+ current.type === "FunctionDeclaration"
14815
+ || current.type === "FunctionExpression"
14816
+ || current.type === "ArrowFunctionExpression"
14817
+ ) {
14818
+ return false;
14819
+ }
14820
+
14821
+ current = current.parent;
14822
+ }
14823
+
14824
+ return false;
14825
+ };
14826
+
14827
+ // Get descriptive error message based on string type
14637
14828
  const getErrorMessageHandler = (str, context = "") => {
14638
14829
  const truncatedStr = str.length > 30 ? `${str.substring(0, 30)}...` : str;
14639
14830
  const contextPart = context ? ` in ${context}` : "";
14640
14831
 
14641
- return `Hardcoded string "${truncatedStr}"${contextPart} should be imported from @/data, @/strings, @/constants, or @/enums`;
14832
+ // Single word detection:
14833
+ // - All lowercase (e.g., "loading", "submit") → keyword/enum/data
14834
+ // - Starts with capital (e.g., "Loading", "Submit") → UI string
14835
+ // - Has spaces or multiple words → UI string
14836
+ const isSingleWord = !/\s/.test(str) && str.length <= 30;
14837
+ const isAllLowercase = /^[a-z_]+$/.test(str);
14838
+
14839
+ 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")`;
14841
+ }
14842
+
14843
+ // 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")`;
14642
14845
  };
14643
14846
 
14644
14847
  // Check if a string matches any ignore pattern
@@ -14963,13 +15166,31 @@ const noHardcodedStrings = {
14963
15166
 
14964
15167
  const str = node.value;
14965
15168
 
14966
- // Skip if it matches ignore patterns
14967
- if (shouldIgnoreStringHandler(str)) return;
14968
-
14969
15169
  // Skip if inside a style object (style={{ transform: "..." }})
14970
15170
  if (isInsideStyleObjectHandler(node)) return;
14971
15171
 
14972
- // Skip if not in relevant context
15172
+ // Skip input type default params (e.g., type = "text")
15173
+ if (isInputTypeDefaultParamHandler(node)) return;
15174
+
15175
+ // Check for exported hardcoded strings (e.g., export const tokenKey = "auth_token")
15176
+ // These should be flagged even at module level, regardless of whether the value
15177
+ // looks "technical" - the point is exposing hardcoded strings in exports
15178
+ if (isExportedHardcodedStringHandler(node)) {
15179
+ // Skip if it doesn't look like user-facing text
15180
+ if (!/[a-zA-Z]/.test(str)) return;
15181
+
15182
+ context.report({
15183
+ message: getErrorMessageHandler(str, "exported constant"),
15184
+ node,
15185
+ });
15186
+
15187
+ return;
15188
+ }
15189
+
15190
+ // Skip if it matches ignore patterns (for strings inside functions)
15191
+ if (shouldIgnoreStringHandler(str)) return;
15192
+
15193
+ // Skip if not in relevant context (must be inside a function)
14973
15194
  if (!isInRelevantContextHandler(node)) return;
14974
15195
 
14975
15196
  // Skip if in a constants definition object
@@ -15001,6 +15222,15 @@ const noHardcodedStrings = {
15001
15222
  // Skip if inside a style object (style={{ background: `...` }})
15002
15223
  if (isInsideStyleObjectHandler(node)) return;
15003
15224
 
15225
+ // Skip if assigned to a style-related variable with CSS value
15226
+ // e.g., const lineGradient = `linear-gradient(...)`
15227
+ if (isStyleVariableAssignmentHandler(node)) {
15228
+ // Get full template content to check if it's CSS
15229
+ const fullContent = node.quasis.map((q) => q.value.cooked || q.value.raw).join("");
15230
+
15231
+ if (isCssValueHandler(fullContent)) return;
15232
+ }
15233
+
15004
15234
  // Skip if not in relevant context
15005
15235
  if (!isInRelevantContextHandler(node)) return;
15006
15236
 
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.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",