eslint-plugin-code-style 1.9.6 → 1.10.0
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 +57 -1
- package/index.d.ts +2 -0
- package/index.js +208 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.10.0] - 2026-02-03
|
|
11
|
+
|
|
12
|
+
**New Rule: logical-expression-multiline + Enhanced no-hardcoded-strings**
|
|
13
|
+
|
|
14
|
+
**Version Range:** v1.9.1 → v1.10.0
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
**New Rules (1)**
|
|
19
|
+
|
|
20
|
+
- **`logical-expression-multiline`** - Enforce multiline formatting for logical expressions with more than maxOperands (default: 3) 🔧
|
|
21
|
+
- Handles variable declarations: `const err = a || b || c || d || e;`
|
|
22
|
+
- Handles return statements, assignments, and other contexts
|
|
23
|
+
- Skips if/ternary conditions (handled by other rules)
|
|
24
|
+
- Auto-fixes to put each operand on its own line with operator at start
|
|
25
|
+
|
|
26
|
+
### Enhanced
|
|
27
|
+
|
|
28
|
+
- **`no-hardcoded-strings`** - Major improvements:
|
|
29
|
+
- Remove single-word string length limitations (now detects all single-word hardcoded strings)
|
|
30
|
+
- Add validation strings: `empty`, `invalid`, `missing`, `optional`, `required`, `valid`
|
|
31
|
+
- Add auth state strings: `anonymous`, `authenticated`, `authed`, `authorized`, `denied`, `forbidden`, etc.
|
|
32
|
+
- Add more status strings: `done`, `finished`, `inprogress`, `queued`, `ready`, `running`, etc.
|
|
33
|
+
- Skip UI component patterns in JSX attributes: `variant="ghost"`, `size="md"`, etc.
|
|
34
|
+
- Skip Tailwind CSS class strings: `"px-5 py-3 w-full"`, `"hover:bg-primary"`, etc.
|
|
35
|
+
- Make technical patterns stricter to avoid false negatives
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **`no-hardcoded-strings`** - Fix detection of strings inside exported components
|
|
40
|
+
- **`no-hardcoded-strings`** - Fix Tailwind detection being too broad (now requires actual Tailwind syntax)
|
|
41
|
+
|
|
42
|
+
### Stats
|
|
43
|
+
|
|
44
|
+
- Total Rules: 72 (was 71)
|
|
45
|
+
- Auto-fixable: 65 rules 🔧 (was 64)
|
|
46
|
+
- Report-only: 7 rules
|
|
47
|
+
|
|
48
|
+
**Full Changelog:** [v1.9.1...v1.10.0](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.1...v1.10.0)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## [1.9.7] - 2026-02-03
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **`no-hardcoded-strings`** - Fix Tailwind detection being too broad:
|
|
57
|
+
- Previously: `"john"`, `"not found"` were incorrectly skipped as Tailwind classes
|
|
58
|
+
- Now: Only strings with Tailwind syntax (hyphens, colons, slashes, brackets) are skipped
|
|
59
|
+
- Regular strings like `const name = "john"` are now properly detected
|
|
60
|
+
- Tailwind classes like `"px-5 py-3 w-full"` still correctly skipped
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
10
64
|
## [1.9.6] - 2026-02-03
|
|
11
65
|
|
|
12
66
|
### Enhanced
|
|
@@ -27,7 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
27
81
|
### Fixed
|
|
28
82
|
|
|
29
83
|
- **`no-hardcoded-strings`** - Fix bug where strings inside exported components were incorrectly skipped:
|
|
30
|
-
- Previously: `export const Component = () => { const name = "
|
|
84
|
+
- Previously: `export const Component = () => { const name = "john" }` was not detected
|
|
31
85
|
- Now: Strings inside functions are properly detected regardless of export status
|
|
32
86
|
- Only direct constant exports are skipped: `export const MESSAGE = "value"` or `export const DATA = { key: "value" }`
|
|
33
87
|
|
|
@@ -1325,6 +1379,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1325
1379
|
|
|
1326
1380
|
---
|
|
1327
1381
|
|
|
1382
|
+
[1.10.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.0...v1.10.0
|
|
1383
|
+
[1.9.7]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.6...v1.9.7
|
|
1328
1384
|
[1.9.6]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.5...v1.9.6
|
|
1329
1385
|
[1.9.5]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.4...v1.9.5
|
|
1330
1386
|
[1.9.4]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.9.3...v1.9.4
|
package/index.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export type RuleNames =
|
|
|
50
50
|
| "code-style/jsx-simple-element-one-line"
|
|
51
51
|
| "code-style/jsx-string-value-trim"
|
|
52
52
|
| "code-style/jsx-ternary-format"
|
|
53
|
+
| "code-style/logical-expression-multiline"
|
|
53
54
|
| "code-style/member-expression-bracket-spacing"
|
|
54
55
|
| "code-style/module-index-exports"
|
|
55
56
|
| "code-style/multiline-if-conditions"
|
|
@@ -143,6 +144,7 @@ interface PluginRules {
|
|
|
143
144
|
"jsx-simple-element-one-line": Rule.RuleModule;
|
|
144
145
|
"jsx-string-value-trim": Rule.RuleModule;
|
|
145
146
|
"jsx-ternary-format": Rule.RuleModule;
|
|
147
|
+
"logical-expression-multiline": Rule.RuleModule;
|
|
146
148
|
"member-expression-bracket-spacing": Rule.RuleModule;
|
|
147
149
|
"module-index-exports": Rule.RuleModule;
|
|
148
150
|
"multiline-if-conditions": Rule.RuleModule;
|
package/index.js
CHANGED
|
@@ -5208,6 +5208,179 @@ const ternaryConditionMultiline = {
|
|
|
5208
5208
|
},
|
|
5209
5209
|
};
|
|
5210
5210
|
|
|
5211
|
+
/**
|
|
5212
|
+
* ───────────────────────────────────────────────────────────────
|
|
5213
|
+
* Rule: Logical Expression Multiline
|
|
5214
|
+
* ───────────────────────────────────────────────────────────────
|
|
5215
|
+
*
|
|
5216
|
+
* Description:
|
|
5217
|
+
* Enforce multiline formatting for logical expressions with more
|
|
5218
|
+
* than maxOperands. This applies to variable declarations, return
|
|
5219
|
+
* statements, assignment expressions, and other contexts.
|
|
5220
|
+
*
|
|
5221
|
+
* Options:
|
|
5222
|
+
* { maxOperands: 3 } - Maximum operands on single line (default: 3)
|
|
5223
|
+
*
|
|
5224
|
+
* ✓ Good:
|
|
5225
|
+
* const err = data.error
|
|
5226
|
+
* || data.message
|
|
5227
|
+
* || data.status
|
|
5228
|
+
* || data.fallback;
|
|
5229
|
+
*
|
|
5230
|
+
* ✗ Bad:
|
|
5231
|
+
* const err = data.error || data.message || data.status || data.fallback;
|
|
5232
|
+
*/
|
|
5233
|
+
const logicalExpressionMultiline = {
|
|
5234
|
+
create(context) {
|
|
5235
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
5236
|
+
const options = context.options[0] || {};
|
|
5237
|
+
const maxOperands = options.maxOperands !== undefined ? options.maxOperands : 3;
|
|
5238
|
+
|
|
5239
|
+
// Check if node is wrapped in parentheses
|
|
5240
|
+
const isParenthesizedHandler = (node) => {
|
|
5241
|
+
const tokenBefore = sourceCode.getTokenBefore(node);
|
|
5242
|
+
const tokenAfter = sourceCode.getTokenAfter(node);
|
|
5243
|
+
|
|
5244
|
+
if (!tokenBefore || !tokenAfter) return false;
|
|
5245
|
+
|
|
5246
|
+
return tokenBefore.value === "(" && tokenAfter.value === ")";
|
|
5247
|
+
};
|
|
5248
|
+
|
|
5249
|
+
// Collect all operands from a logical expression (flattening non-parenthesized ones)
|
|
5250
|
+
const collectOperandsHandler = (node) => {
|
|
5251
|
+
const operands = [];
|
|
5252
|
+
|
|
5253
|
+
const collectHelperHandler = (n) => {
|
|
5254
|
+
if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
|
|
5255
|
+
collectHelperHandler(n.left);
|
|
5256
|
+
collectHelperHandler(n.right);
|
|
5257
|
+
} else {
|
|
5258
|
+
operands.push(n);
|
|
5259
|
+
}
|
|
5260
|
+
};
|
|
5261
|
+
|
|
5262
|
+
collectHelperHandler(node);
|
|
5263
|
+
|
|
5264
|
+
return operands;
|
|
5265
|
+
};
|
|
5266
|
+
|
|
5267
|
+
// Get the operator between two operands
|
|
5268
|
+
const getOperatorHandler = (leftNode, rightNode) => {
|
|
5269
|
+
const tokens = sourceCode.getTokensBetween(leftNode, rightNode);
|
|
5270
|
+
const opToken = tokens.find((t) => t.value === "||" || t.value === "&&" || t.value === "??" || t.value === "|" || t.value === "&");
|
|
5271
|
+
|
|
5272
|
+
return opToken ? opToken.value : "||";
|
|
5273
|
+
};
|
|
5274
|
+
|
|
5275
|
+
// Check if the expression is already multiline
|
|
5276
|
+
const isMultilineHandler = (node) => node.loc.start.line !== node.loc.end.line;
|
|
5277
|
+
|
|
5278
|
+
// Check if we're inside an if condition or ternary test (handled by other rules)
|
|
5279
|
+
const isInIfOrTernaryHandler = (node) => {
|
|
5280
|
+
let current = node.parent;
|
|
5281
|
+
|
|
5282
|
+
while (current) {
|
|
5283
|
+
if (current.type === "IfStatement" && current.test === node) return true;
|
|
5284
|
+
|
|
5285
|
+
if (current.type === "ConditionalExpression" && current.test === node) return true;
|
|
5286
|
+
|
|
5287
|
+
// Stop if we hit a statement or declaration boundary
|
|
5288
|
+
if (current.type.includes("Statement") || current.type.includes("Declaration")) return false;
|
|
5289
|
+
|
|
5290
|
+
current = current.parent;
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
return false;
|
|
5294
|
+
};
|
|
5295
|
+
|
|
5296
|
+
// Handle logical expression
|
|
5297
|
+
const checkLogicalExpressionHandler = (node) => {
|
|
5298
|
+
// Only process top-level logical expressions (not nested ones)
|
|
5299
|
+
if (node.parent.type === "LogicalExpression" && !isParenthesizedHandler(node)) return;
|
|
5300
|
+
|
|
5301
|
+
// Skip if this is in an if condition or ternary test (handled by other rules)
|
|
5302
|
+
if (isInIfOrTernaryHandler(node)) return;
|
|
5303
|
+
|
|
5304
|
+
// Collect all operands
|
|
5305
|
+
const operands = collectOperandsHandler(node);
|
|
5306
|
+
|
|
5307
|
+
// If operands count is within threshold, skip
|
|
5308
|
+
if (operands.length <= maxOperands) return;
|
|
5309
|
+
|
|
5310
|
+
// Check if already properly multiline
|
|
5311
|
+
if (isMultilineHandler(node)) {
|
|
5312
|
+
// Check if each operand is on its own line
|
|
5313
|
+
let allOnOwnLines = true;
|
|
5314
|
+
|
|
5315
|
+
for (let i = 1; i < operands.length; i++) {
|
|
5316
|
+
if (operands[i].loc.start.line === operands[i - 1].loc.end.line) {
|
|
5317
|
+
allOnOwnLines = false;
|
|
5318
|
+
break;
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5322
|
+
if (allOnOwnLines) return;
|
|
5323
|
+
}
|
|
5324
|
+
|
|
5325
|
+
// Report and fix
|
|
5326
|
+
context.report({
|
|
5327
|
+
fix(fixer) {
|
|
5328
|
+
// Build the formatted expression
|
|
5329
|
+
const firstOperandText = sourceCode.getText(operands[0]);
|
|
5330
|
+
|
|
5331
|
+
// Get the line containing the first operand
|
|
5332
|
+
const firstToken = sourceCode.getFirstToken(operands[0]);
|
|
5333
|
+
const lineStart = sourceCode.text.lastIndexOf("\n", firstToken.range[0]) + 1;
|
|
5334
|
+
const lineContent = sourceCode.text.slice(lineStart, firstToken.range[0]);
|
|
5335
|
+
|
|
5336
|
+
// Extract only the whitespace indentation from the line
|
|
5337
|
+
const indentMatch = lineContent.match(/^(\s*)/);
|
|
5338
|
+
const baseIndent = indentMatch ? indentMatch[1] : "";
|
|
5339
|
+
|
|
5340
|
+
// Build each line: first operand, then operator + operand for each subsequent
|
|
5341
|
+
const lines = [firstOperandText];
|
|
5342
|
+
|
|
5343
|
+
for (let i = 1; i < operands.length; i++) {
|
|
5344
|
+
const operator = getOperatorHandler(operands[i - 1], operands[i]);
|
|
5345
|
+
const operandText = sourceCode.getText(operands[i]);
|
|
5346
|
+
lines.push(`${baseIndent} ${operator} ${operandText}`);
|
|
5347
|
+
}
|
|
5348
|
+
|
|
5349
|
+
// Get the full range of the expression
|
|
5350
|
+
const fullText = lines.join("\n");
|
|
5351
|
+
|
|
5352
|
+
return fixer.replaceText(node, fullText);
|
|
5353
|
+
},
|
|
5354
|
+
message: `Logical expression with ${operands.length} operands should be on multiple lines (max: ${maxOperands})`,
|
|
5355
|
+
node,
|
|
5356
|
+
});
|
|
5357
|
+
};
|
|
5358
|
+
|
|
5359
|
+
return {
|
|
5360
|
+
LogicalExpression: checkLogicalExpressionHandler,
|
|
5361
|
+
};
|
|
5362
|
+
},
|
|
5363
|
+
meta: {
|
|
5364
|
+
docs: { description: "Enforce multiline formatting for logical expressions with more than maxOperands" },
|
|
5365
|
+
fixable: "code",
|
|
5366
|
+
schema: [
|
|
5367
|
+
{
|
|
5368
|
+
additionalProperties: false,
|
|
5369
|
+
properties: {
|
|
5370
|
+
maxOperands: {
|
|
5371
|
+
default: 3,
|
|
5372
|
+
description: "Maximum operands to keep on single line (default: 3)",
|
|
5373
|
+
minimum: 1,
|
|
5374
|
+
type: "integer",
|
|
5375
|
+
},
|
|
5376
|
+
},
|
|
5377
|
+
type: "object",
|
|
5378
|
+
},
|
|
5379
|
+
],
|
|
5380
|
+
type: "layout",
|
|
5381
|
+
},
|
|
5382
|
+
};
|
|
5383
|
+
|
|
5211
5384
|
/**
|
|
5212
5385
|
* ───────────────────────────────────────────────────────────────
|
|
5213
5386
|
* Rule: Empty Line After Block
|
|
@@ -14109,6 +14282,26 @@ const noHardcodedStrings = {
|
|
|
14109
14282
|
// Tailwind/CSS class pattern - matches individual class names
|
|
14110
14283
|
const tailwindClassPattern = /^-?[a-z]+(-[a-z0-9]+)*(\/\d+)?$|^-?[a-z]+(-[a-z0-9]+)*-\[.+\]$|^[a-z]+:[a-z][-a-z0-9/[\]]*$/;
|
|
14111
14284
|
|
|
14285
|
+
// Known single-word Tailwind utilities (no hyphen required)
|
|
14286
|
+
const singleWordTailwindUtilities = new Set([
|
|
14287
|
+
// Display
|
|
14288
|
+
"block", "contents", "flex", "flow", "grid", "hidden", "inline", "table",
|
|
14289
|
+
// Position
|
|
14290
|
+
"absolute", "fixed", "relative", "static", "sticky",
|
|
14291
|
+
// Visibility
|
|
14292
|
+
"collapse", "invisible", "visible",
|
|
14293
|
+
// Typography
|
|
14294
|
+
"antialiased", "capitalize", "italic", "lowercase", "ordinal", "overline",
|
|
14295
|
+
"subpixel", "truncate", "underline", "uppercase",
|
|
14296
|
+
// Layout
|
|
14297
|
+
"container", "isolate",
|
|
14298
|
+
// Misc
|
|
14299
|
+
"resize", "snap", "touch", "select", "pointer", "transition", "animate",
|
|
14300
|
+
"filter", "backdrop", "transform", "appearance", "cursor", "outline",
|
|
14301
|
+
"ring", "shadow", "opacity", "blur", "invert", "sepia", "grayscale",
|
|
14302
|
+
"hue", "saturate", "brightness", "contrast",
|
|
14303
|
+
]);
|
|
14304
|
+
|
|
14112
14305
|
// Check if a string contains only CSS/Tailwind class names
|
|
14113
14306
|
const isTailwindClassStringHandler = (str) => {
|
|
14114
14307
|
// Split by whitespace and filter empty strings
|
|
@@ -14117,23 +14310,31 @@ const noHardcodedStrings = {
|
|
|
14117
14310
|
// Must have at least one token
|
|
14118
14311
|
if (tokens.length === 0) return false;
|
|
14119
14312
|
|
|
14313
|
+
// Must have at least one token with Tailwind-like syntax (hyphen, colon, slash, or brackets)
|
|
14314
|
+
// to be considered a Tailwind class string
|
|
14315
|
+
const hasTailwindSyntax = tokens.some((token) =>
|
|
14316
|
+
token.includes("-") || token.includes(":") || token.includes("/") || token.includes("["));
|
|
14317
|
+
|
|
14318
|
+
if (!hasTailwindSyntax) return false;
|
|
14319
|
+
|
|
14120
14320
|
// Check if all tokens look like CSS classes
|
|
14121
14321
|
return tokens.every((token) => {
|
|
14122
14322
|
// Skip template literal expressions placeholders if any
|
|
14123
14323
|
if (token.includes("${")) return true;
|
|
14124
14324
|
|
|
14125
|
-
//
|
|
14325
|
+
// Known single-word Tailwind utilities
|
|
14326
|
+
if (singleWordTailwindUtilities.has(token)) return true;
|
|
14327
|
+
|
|
14328
|
+
// Common Tailwind patterns - MUST have hyphen, colon, slash, or brackets
|
|
14126
14329
|
return (
|
|
14127
|
-
//
|
|
14128
|
-
/^-?[a-z]+(-[a-z0-9]+)
|
|
14129
|
-
// With fractions: w-1/2, -translate-y-1/2
|
|
14330
|
+
// Kebab-case: w-5, p-4, pr-12, text-2xl, gap-4, bg-white, text-error
|
|
14331
|
+
/^-?[a-z]+(-[a-z0-9]+)+$/.test(token)
|
|
14332
|
+
// With fractions: w-1/2, -translate-y-1/2, bg-black/50
|
|
14130
14333
|
|| /^-?[a-z]+(-[a-z0-9]+)*\/\d+$/.test(token)
|
|
14131
14334
|
// With modifiers: hover:bg-primary, focus:ring-2, sm:flex
|
|
14132
14335
|
|| /^[a-z0-9]+:[a-z][-a-z0-9/[\]]*$/.test(token)
|
|
14133
14336
|
// Arbitrary values: w-[100px], bg-[#ff0000]
|
|
14134
14337
|
|| /^-?[a-z]+(-[a-z]+)*-?\[.+\]$/.test(token)
|
|
14135
|
-
// Single word utilities: flex, hidden, block
|
|
14136
|
-
|| /^[a-z]+$/.test(token)
|
|
14137
14338
|
);
|
|
14138
14339
|
});
|
|
14139
14340
|
};
|
|
@@ -19890,6 +20091,7 @@ export default {
|
|
|
19890
20091
|
"empty-line-after-block": emptyLineAfterBlock,
|
|
19891
20092
|
"if-else-spacing": ifElseSpacing,
|
|
19892
20093
|
"if-statement-format": ifStatementFormat,
|
|
20094
|
+
"logical-expression-multiline": logicalExpressionMultiline,
|
|
19893
20095
|
"multiline-if-conditions": multilineIfConditions,
|
|
19894
20096
|
"no-empty-lines-in-switch-cases": noEmptyLinesInSwitchCases,
|
|
19895
20097
|
"ternary-condition-multiline": ternaryConditionMultiline,
|
package/package.json
CHANGED