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.
- package/CHANGELOG.md +18 -0
- package/index.js +237 -7
- 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 -
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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