eslint-plugin-code-style 1.7.6 → 1.8.1
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 +33 -0
- package/README.md +102 -28
- package/index.d.ts +2 -0
- package/index.js +678 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.8.1] - 2026-02-03
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`function-naming-convention`** - Add `handleXxx` → `xxxHandler` auto-fix (converts `handleClick` to `clickHandler` instead of `handleClickHandler`)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## [1.8.0] - 2026-02-03
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **New Rule: `no-hardcoded-strings`** - Enforce importing strings from constants/strings modules instead of hardcoding them inline. Promotes maintainability, consistency, and easier internationalization.
|
|
23
|
+
- Detects hardcoded strings in JSX text content, attributes, and component logic
|
|
24
|
+
- Configurable `ignoreAttributes`, `extraIgnoreAttributes`, `ignorePatterns` options
|
|
25
|
+
- Automatically ignores technical strings (CSS units, URLs, paths, identifiers, etc.)
|
|
26
|
+
- Valid import paths: `@/constants`, `@/strings`, `@/@constants`, `@/@strings`, `@/data/constants`, `@/data/strings`
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **`absolute-imports-only`** - Add `strings`, `@constants`, `@strings` to default allowed folders
|
|
31
|
+
- **`module-index-exports`** - Add `strings`, `@constants`, `@strings` to default module folders
|
|
32
|
+
|
|
33
|
+
### Stats
|
|
34
|
+
|
|
35
|
+
- Total Rules: 70 (was 69)
|
|
36
|
+
- Auto-fixable: 63 rules 🔧
|
|
37
|
+
- Report-only: 7 rules (was 6)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
10
41
|
## [1.7.6] - 2026-02-02
|
|
11
42
|
|
|
12
43
|
### Changed
|
|
@@ -1138,6 +1169,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1138
1169
|
|
|
1139
1170
|
---
|
|
1140
1171
|
|
|
1172
|
+
[1.8.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.8.0...v1.8.1
|
|
1173
|
+
[1.8.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.6...v1.8.0
|
|
1141
1174
|
[1.7.6]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.5...v1.7.6
|
|
1142
1175
|
[1.7.5]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.4...v1.7.5
|
|
1143
1176
|
[1.7.4]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.3...v1.7.4
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
**A powerful ESLint plugin for enforcing consistent code formatting and style rules in React/JSX projects.**
|
|
21
21
|
|
|
22
|
-
*
|
|
22
|
+
*70 rules (63 auto-fixable) to keep your codebase clean and consistent*
|
|
23
23
|
|
|
24
24
|
</div>
|
|
25
25
|
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
## 🎯 Why This Plugin?
|
|
29
29
|
|
|
30
|
-
This plugin provides **
|
|
30
|
+
This plugin provides **70 custom rules** (63 auto-fixable) for code formatting. Built for **ESLint v9 flat configs**.
|
|
31
31
|
|
|
32
32
|
> **Note:** ESLint [deprecated 79 formatting rules](https://eslint.org/blog/2023/10/deprecating-formatting-rules/) in v8.53.0. Our recommended configs use `@stylistic/eslint-plugin` as the replacement for these deprecated rules.
|
|
33
33
|
|
|
@@ -36,7 +36,7 @@ This plugin provides **69 custom rules** (63 auto-fixable) for code formatting.
|
|
|
36
36
|
- **Works alongside existing tools** — Complements ESLint's built-in rules and packages like eslint-plugin-react, eslint-plugin-import, etc
|
|
37
37
|
- **Self-sufficient rules** — Each rule handles complete formatting independently
|
|
38
38
|
- **Consistency at scale** — Reduces code-style differences between team members by enforcing uniform formatting across your projects
|
|
39
|
-
- **Highly automated** — 63 of
|
|
39
|
+
- **Highly automated** — 63 of 70 rules support auto-fix with `eslint --fix`
|
|
40
40
|
|
|
41
41
|
When combined with ESLint's native rules and other popular plugins, this package helps create a complete code style solution that keeps your codebase clean and consistent.
|
|
42
42
|
|
|
@@ -236,6 +236,7 @@ rules: {
|
|
|
236
236
|
"code-style/no-empty-lines-in-jsx": "error",
|
|
237
237
|
"code-style/no-empty-lines-in-objects": "error",
|
|
238
238
|
"code-style/no-empty-lines-in-switch-cases": "error",
|
|
239
|
+
"code-style/no-hardcoded-strings": "error",
|
|
239
240
|
"code-style/no-inline-type-definitions": "error",
|
|
240
241
|
"code-style/object-property-per-line": "error",
|
|
241
242
|
"code-style/object-property-value-brace": "error",
|
|
@@ -259,7 +260,7 @@ rules: {
|
|
|
259
260
|
|
|
260
261
|
## 📖 Rules Categories
|
|
261
262
|
|
|
262
|
-
> **
|
|
263
|
+
> **70 rules total** — 63 with auto-fix 🔧, 7 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
|
|
263
264
|
>
|
|
264
265
|
> **Legend:** 🔧 Auto-fixable with `eslint --fix` • ⚙️ Customizable options
|
|
265
266
|
|
|
@@ -299,7 +300,7 @@ rules: {
|
|
|
299
300
|
| **Function Rules** | |
|
|
300
301
|
| `function-call-spacing` | No space between function name and `(`: `fn()` not `fn ()` 🔧 |
|
|
301
302
|
| `function-declaration-style` | Auto-fix for `func-style`: converts function declarations to arrow expressions 🔧 |
|
|
302
|
-
| `function-naming-convention` | Functions use camelCase, start with verb
|
|
303
|
+
| `function-naming-convention` | Functions use camelCase, start with verb, end with Handler suffix; handleXxx → xxxHandler 🔧 |
|
|
303
304
|
| `function-object-destructure` | Non-component functions: use typed params (not destructured), destructure in body; report dot notation access 🔧 |
|
|
304
305
|
| `function-params-per-line` | When multiline, each param on own line with consistent indentation 🔧 |
|
|
305
306
|
| `no-empty-lines-in-function-params` | No empty lines between parameters or after `(`/before `)` 🔧 |
|
|
@@ -348,6 +349,8 @@ rules: {
|
|
|
348
349
|
| `typescript-definition-location` | Enforce TypeScript definitions (interfaces, types, enums) to be in designated folders ⚙️ |
|
|
349
350
|
| **React Rules** | |
|
|
350
351
|
| `react-code-order` | Enforce consistent ordering in components and hooks: props destructure → refs → state → redux → router → context → custom hooks → derived → memo → callback → handlers → effects → return 🔧 |
|
|
352
|
+
| **String Rules** | |
|
|
353
|
+
| `no-hardcoded-strings` | Enforce importing strings from constants/strings modules instead of hardcoding them ⚙️ |
|
|
351
354
|
| **Variable Rules** | |
|
|
352
355
|
| `variable-naming-convention` | camelCase for all variables and constants, PascalCase for components, `use` prefix for hooks 🔧 |
|
|
353
356
|
|
|
@@ -1316,33 +1319,37 @@ function isAuthenticated(): boolean {
|
|
|
1316
1319
|
|
|
1317
1320
|
**What it does:** Enforces naming conventions for functions:
|
|
1318
1321
|
- **camelCase** required
|
|
1319
|
-
- **Verb prefix**
|
|
1320
|
-
- **
|
|
1322
|
+
- **Verb prefix** required (get, set, fetch, is, has, can, should, click, submit, etc.)
|
|
1323
|
+
- **Handler suffix** required (all functions must end with `Handler`)
|
|
1324
|
+
- **Auto-fixes** `handleXxx` to `xxxHandler` (avoids redundant `handleClickHandler`)
|
|
1325
|
+
- **Auto-fixes** PascalCase to camelCase for verb-prefixed functions
|
|
1321
1326
|
|
|
1322
|
-
**Why use it:** Function names should describe actions. Verb prefixes make the purpose immediately clear.
|
|
1327
|
+
**Why use it:** Function names should describe actions. Verb prefixes make the purpose immediately clear, and consistent Handler suffix makes event handlers easy to identify.
|
|
1323
1328
|
|
|
1324
1329
|
```javascript
|
|
1325
|
-
// ✅ Good —
|
|
1326
|
-
function
|
|
1327
|
-
function
|
|
1328
|
-
function
|
|
1329
|
-
function
|
|
1330
|
-
function
|
|
1331
|
-
function
|
|
1332
|
-
function
|
|
1333
|
-
|
|
1334
|
-
const fetchUsers = async () => {};
|
|
1335
|
-
const submitHandler = () => {};
|
|
1330
|
+
// ✅ Good — verb prefix + Handler suffix
|
|
1331
|
+
function getUserDataHandler() {}
|
|
1332
|
+
function setUserNameHandler(name) {}
|
|
1333
|
+
function clickHandler() {}
|
|
1334
|
+
function submitHandler() {}
|
|
1335
|
+
function isValidEmailHandler(email) {}
|
|
1336
|
+
function hasPermissionHandler(user) {}
|
|
1337
|
+
function canAccessHandler(resource) {}
|
|
1338
|
+
const fetchUsersHandler = async () => {};
|
|
1336
1339
|
|
|
1337
|
-
// ❌ Bad —
|
|
1338
|
-
function
|
|
1339
|
-
function
|
|
1340
|
-
function
|
|
1341
|
-
function valid(email) {}
|
|
1340
|
+
// ❌ Bad (auto-fixed) — handleXxx → xxxHandler
|
|
1341
|
+
function handleClick() {} // → clickHandler
|
|
1342
|
+
function handleSubmit() {} // → submitHandler
|
|
1343
|
+
function handleChange() {} // → changeHandler
|
|
1342
1344
|
|
|
1343
|
-
// ❌ Bad —
|
|
1344
|
-
function
|
|
1345
|
-
function
|
|
1345
|
+
// ❌ Bad (auto-fixed) — missing Handler suffix
|
|
1346
|
+
function getUserData() {} // → getUserDataHandler
|
|
1347
|
+
function setUserName() {} // → setUserNameHandler
|
|
1348
|
+
function fetchUsers() {} // → fetchUsersHandler
|
|
1349
|
+
|
|
1350
|
+
// ❌ Bad (auto-fixed) — PascalCase to camelCase
|
|
1351
|
+
function GetUserData() {} // → getUserDataHandler
|
|
1352
|
+
function FetchStatus() {} // → fetchStatusHandler
|
|
1346
1353
|
```
|
|
1347
1354
|
|
|
1348
1355
|
---
|
|
@@ -3278,6 +3285,73 @@ const YetAnotherBad = ({ title }) => {
|
|
|
3278
3285
|
|
|
3279
3286
|
<br />
|
|
3280
3287
|
|
|
3288
|
+
## 📝 String Rules
|
|
3289
|
+
|
|
3290
|
+
### `no-hardcoded-strings`
|
|
3291
|
+
|
|
3292
|
+
**What it does:** Enforces that user-facing strings should be imported from constants/strings modules rather than hardcoded inline. This promotes maintainability, consistency, and enables easier internationalization.
|
|
3293
|
+
|
|
3294
|
+
**Why use it:** Hardcoded strings scattered throughout your codebase are hard to maintain, translate, and keep consistent. Centralizing strings in constants makes them easy to find, update, and potentially translate.
|
|
3295
|
+
|
|
3296
|
+
**Options:**
|
|
3297
|
+
|
|
3298
|
+
| Option | Type | Default | Description |
|
|
3299
|
+
|--------|------|---------|-------------|
|
|
3300
|
+
| `ignoreAttributes` | `string[]` | See below | JSX attributes to ignore (replaces defaults) |
|
|
3301
|
+
| `extraIgnoreAttributes` | `string[]` | `[]` | Additional JSX attributes to ignore (extends defaults) |
|
|
3302
|
+
| `ignorePatterns` | `string[]` | `[]` | Regex patterns for strings to ignore |
|
|
3303
|
+
|
|
3304
|
+
**Default ignored attributes:** `className`, `id`, `type`, `name`, `href`, `src`, `alt`, `role`, `style`, `key`, `data-*`, `aria-*`, and many more HTML/SVG attributes.
|
|
3305
|
+
|
|
3306
|
+
**Default ignored patterns:** Empty strings, single characters, CSS units (`px`, `em`, `%`), colors, URLs, paths, file extensions, MIME types, UUIDs, dates, camelCase/snake_case identifiers, HTTP methods, and other technical strings.
|
|
3307
|
+
|
|
3308
|
+
```javascript
|
|
3309
|
+
// ✅ Good — strings imported from constants
|
|
3310
|
+
import { BUTTON_LABEL, ERROR_MESSAGE, welcomeText } from "@/constants";
|
|
3311
|
+
import { FORM_LABELS } from "@/strings";
|
|
3312
|
+
|
|
3313
|
+
const Component = () => (
|
|
3314
|
+
<div>
|
|
3315
|
+
<button>{BUTTON_LABEL}</button>
|
|
3316
|
+
<span>{ERROR_MESSAGE}</span>
|
|
3317
|
+
<p>{welcomeText}</p>
|
|
3318
|
+
</div>
|
|
3319
|
+
);
|
|
3320
|
+
|
|
3321
|
+
const getMessage = () => ERROR_MESSAGE;
|
|
3322
|
+
|
|
3323
|
+
// ✅ Good — technical strings are allowed
|
|
3324
|
+
<input type="text" className="input-field" />
|
|
3325
|
+
<a href="/dashboard">Link</a>
|
|
3326
|
+
const url = `/api/users/${id}`;
|
|
3327
|
+
const size = "100px";
|
|
3328
|
+
|
|
3329
|
+
// ❌ Bad — hardcoded user-facing strings
|
|
3330
|
+
<button>Submit Form</button>
|
|
3331
|
+
<span>Something went wrong</span>
|
|
3332
|
+
const message = "Welcome to the application";
|
|
3333
|
+
return "User not found";
|
|
3334
|
+
```
|
|
3335
|
+
|
|
3336
|
+
**Configuration example:**
|
|
3337
|
+
|
|
3338
|
+
```javascript
|
|
3339
|
+
// Allow more attributes, add custom ignore patterns
|
|
3340
|
+
"code-style/no-hardcoded-strings": ["error", {
|
|
3341
|
+
extraIgnoreAttributes: ["tooltip", "placeholder"],
|
|
3342
|
+
ignorePatterns: ["^TODO:", "^FIXME:"]
|
|
3343
|
+
}]
|
|
3344
|
+
```
|
|
3345
|
+
|
|
3346
|
+
**Valid import paths for constants:**
|
|
3347
|
+
- `@/constants` or `@/@constants`
|
|
3348
|
+
- `@/strings` or `@/@strings`
|
|
3349
|
+
- `@/data/constants` or `@/data/strings`
|
|
3350
|
+
|
|
3351
|
+
---
|
|
3352
|
+
|
|
3353
|
+
<br />
|
|
3354
|
+
|
|
3281
3355
|
## 📝 Variable Rules
|
|
3282
3356
|
|
|
3283
3357
|
### `variable-naming-convention`
|
|
@@ -3311,7 +3385,7 @@ const UseAuth = () => {}; // hooks should be camelCase
|
|
|
3311
3385
|
|
|
3312
3386
|
## 🔧 Auto-fixing
|
|
3313
3387
|
|
|
3314
|
-
63 of
|
|
3388
|
+
63 of 70 rules support auto-fixing. Run ESLint with the `--fix` flag:
|
|
3315
3389
|
|
|
3316
3390
|
```bash
|
|
3317
3391
|
# Fix all files in src directory
|
package/index.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export type RuleNames =
|
|
|
59
59
|
| "code-style/no-empty-lines-in-jsx"
|
|
60
60
|
| "code-style/no-empty-lines-in-objects"
|
|
61
61
|
| "code-style/no-empty-lines-in-switch-cases"
|
|
62
|
+
| "code-style/no-hardcoded-strings"
|
|
62
63
|
| "code-style/object-property-per-line"
|
|
63
64
|
| "code-style/object-property-value-brace"
|
|
64
65
|
| "code-style/object-property-value-format"
|
|
@@ -150,6 +151,7 @@ interface PluginRules {
|
|
|
150
151
|
"no-empty-lines-in-jsx": Rule.RuleModule;
|
|
151
152
|
"no-empty-lines-in-objects": Rule.RuleModule;
|
|
152
153
|
"no-empty-lines-in-switch-cases": Rule.RuleModule;
|
|
154
|
+
"no-hardcoded-strings": Rule.RuleModule;
|
|
153
155
|
"object-property-per-line": Rule.RuleModule;
|
|
154
156
|
"object-property-value-brace": Rule.RuleModule;
|
|
155
157
|
"object-property-value-format": Rule.RuleModule;
|
package/index.js
CHANGED
|
@@ -1996,19 +1996,21 @@ const functionDeclarationStyle = {
|
|
|
1996
1996
|
*
|
|
1997
1997
|
* Description:
|
|
1998
1998
|
* Function names should follow naming conventions: camelCase,
|
|
1999
|
-
* starting with a verb, and
|
|
2000
|
-
* Auto-fixes PascalCase functions
|
|
1999
|
+
* starting with a verb, and ending with "Handler" suffix.
|
|
2000
|
+
* Auto-fixes PascalCase functions to camelCase.
|
|
2001
|
+
* Auto-fixes handleXxx to xxxHandler (avoids "handleClickHandler").
|
|
2001
2002
|
*
|
|
2002
2003
|
* ✓ Good:
|
|
2003
|
-
* function
|
|
2004
|
-
* function
|
|
2005
|
-
* function
|
|
2006
|
-
* const submitHandler = () => {}
|
|
2004
|
+
* function getUserDataHandler() {}
|
|
2005
|
+
* function clickHandler() {}
|
|
2006
|
+
* function isValidEmailHandler() {}
|
|
2007
|
+
* const submitHandler = () => {};
|
|
2007
2008
|
*
|
|
2008
2009
|
* ✗ Bad (auto-fixed):
|
|
2009
|
-
* function GetUserData() {}
|
|
2010
|
-
*
|
|
2011
|
-
* function
|
|
2010
|
+
* function GetUserData() {} // → getUserDataHandler
|
|
2011
|
+
* function handleClick() {} // → clickHandler (not handleClickHandler)
|
|
2012
|
+
* function getUserData() {} // → getUserDataHandler
|
|
2013
|
+
* const FetchStatus = () => {} // → fetchStatusHandler
|
|
2012
2014
|
*/
|
|
2013
2015
|
const functionNamingConvention = {
|
|
2014
2016
|
create(context) {
|
|
@@ -2071,8 +2073,8 @@ const functionNamingConvention = {
|
|
|
2071
2073
|
// Performance
|
|
2072
2074
|
"debounce", "throttle", "memoize", "cache", "batch", "queue", "defer", "delay",
|
|
2073
2075
|
"schedule", "preload", "prefetch", "lazy",
|
|
2074
|
-
// Events
|
|
2075
|
-
"
|
|
2076
|
+
// Events (note: "handle" is NOT included - handleXxx is auto-fixed to xxxHandler)
|
|
2077
|
+
"on", "click", "change", "input", "press", "drag", "drop",
|
|
2076
2078
|
"hover", "enter", "leave", "touch", "swipe", "pinch", "tap",
|
|
2077
2079
|
// Comparison
|
|
2078
2080
|
"compare", "diff", "equal", "differ", "overlap", "intersect", "union", "exclude",
|
|
@@ -2212,10 +2214,57 @@ const functionNamingConvention = {
|
|
|
2212
2214
|
|
|
2213
2215
|
const hasVerbPrefix = startsWithVerbHandler(name);
|
|
2214
2216
|
const hasHandlerSuffix = endsWithHandler(name);
|
|
2217
|
+
const startsWithHandle = /^handle[A-Z]/.test(name);
|
|
2218
|
+
|
|
2219
|
+
// Special case: handleXxx -> xxxHandler (to avoid handleClickHandler)
|
|
2220
|
+
if (startsWithHandle && !hasHandlerSuffix) {
|
|
2221
|
+
const identifierNode = node.id || node.parent.id;
|
|
2222
|
+
// Remove "handle" prefix and add "Handler" suffix: handleClick -> clickHandler
|
|
2223
|
+
const baseName = name.slice(6); // Remove "handle"
|
|
2224
|
+
const newName = baseName[0].toLowerCase() + baseName.slice(1) + "Handler";
|
|
2225
|
+
|
|
2226
|
+
context.report({
|
|
2227
|
+
fix(fixer) {
|
|
2228
|
+
const scope = context.sourceCode
|
|
2229
|
+
? context.sourceCode.getScope(node)
|
|
2230
|
+
: context.getScope();
|
|
2231
|
+
|
|
2232
|
+
const variable = scope.variables.find((v) => v.name === name)
|
|
2233
|
+
|| (scope.upper && scope.upper.variables.find((v) => v.name === name));
|
|
2234
|
+
|
|
2235
|
+
if (!variable) return fixer.replaceText(identifierNode, newName);
|
|
2236
|
+
|
|
2237
|
+
const fixes = [];
|
|
2238
|
+
const fixedRanges = new Set();
|
|
2239
|
+
|
|
2240
|
+
const addFixHandler = (nodeToFix) => {
|
|
2241
|
+
const rangeKey = `${nodeToFix.range[0]}-${nodeToFix.range[1]}`;
|
|
2242
|
+
|
|
2243
|
+
if (!fixedRanges.has(rangeKey)) {
|
|
2244
|
+
fixedRanges.add(rangeKey);
|
|
2245
|
+
fixes.push(fixer.replaceText(nodeToFix, newName));
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
|
|
2249
|
+
variable.defs.forEach((def) => {
|
|
2250
|
+
addFixHandler(def.name);
|
|
2251
|
+
});
|
|
2252
|
+
|
|
2253
|
+
variable.references.forEach((ref) => {
|
|
2254
|
+
addFixHandler(ref.identifier);
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
return fixes;
|
|
2258
|
+
},
|
|
2259
|
+
message: `Function "${name}" should be "${newName}" (handleXxx → xxxHandler to avoid redundant "handleXxxHandler")`,
|
|
2260
|
+
node: identifierNode,
|
|
2261
|
+
});
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2215
2264
|
|
|
2216
2265
|
if (!hasVerbPrefix && !hasHandlerSuffix) {
|
|
2217
2266
|
context.report({
|
|
2218
|
-
message: `Function "${name}" should start with a verb (get, set, fetch,
|
|
2267
|
+
message: `Function "${name}" should start with a verb (get, set, fetch, etc.) AND end with "Handler" (e.g., getDataHandler, clickHandler)`,
|
|
2219
2268
|
node: node.id || node.parent.id,
|
|
2220
2269
|
});
|
|
2221
2270
|
} else if (!hasVerbPrefix) {
|
|
@@ -4738,6 +4787,8 @@ const absoluteImportsOnly = {
|
|
|
4738
4787
|
|
|
4739
4788
|
// Default allowed folders
|
|
4740
4789
|
const defaultAllowedFolders = [
|
|
4790
|
+
"@constants",
|
|
4791
|
+
"@strings",
|
|
4741
4792
|
"actions",
|
|
4742
4793
|
"apis",
|
|
4743
4794
|
"assets",
|
|
@@ -4764,6 +4815,7 @@ const absoluteImportsOnly = {
|
|
|
4764
4815
|
"schemas",
|
|
4765
4816
|
"services",
|
|
4766
4817
|
"store",
|
|
4818
|
+
"strings",
|
|
4767
4819
|
"styles",
|
|
4768
4820
|
"theme",
|
|
4769
4821
|
"thunks",
|
|
@@ -5402,6 +5454,8 @@ const moduleIndexExports = {
|
|
|
5402
5454
|
|
|
5403
5455
|
// Default module folders
|
|
5404
5456
|
const defaultModuleFolders = [
|
|
5457
|
+
"@constants",
|
|
5458
|
+
"@strings",
|
|
5405
5459
|
"actions",
|
|
5406
5460
|
"apis",
|
|
5407
5461
|
"assets",
|
|
@@ -5428,6 +5482,7 @@ const moduleIndexExports = {
|
|
|
5428
5482
|
"schemas",
|
|
5429
5483
|
"services",
|
|
5430
5484
|
"store",
|
|
5485
|
+
"strings",
|
|
5431
5486
|
"styles",
|
|
5432
5487
|
"theme",
|
|
5433
5488
|
"thunks",
|
|
@@ -12640,6 +12695,614 @@ const stringPropertySpacing = {
|
|
|
12640
12695
|
},
|
|
12641
12696
|
};
|
|
12642
12697
|
|
|
12698
|
+
/**
|
|
12699
|
+
* ───────────────────────────────────────────────────────────────
|
|
12700
|
+
* Rule: No Hardcoded Strings
|
|
12701
|
+
* ───────────────────────────────────────────────────────────────
|
|
12702
|
+
*
|
|
12703
|
+
* Description:
|
|
12704
|
+
* Enforces that user-facing strings should be imported from
|
|
12705
|
+
* constants/strings modules rather than hardcoded inline.
|
|
12706
|
+
* This promotes maintainability, consistency, and enables
|
|
12707
|
+
* easier internationalization.
|
|
12708
|
+
*
|
|
12709
|
+
* Options:
|
|
12710
|
+
* { ignoreAttributes: ["className", "id", ...] } - JSX attributes to ignore (replaces defaults)
|
|
12711
|
+
* { extraIgnoreAttributes: ["tooltip", ...] } - Additional JSX attributes to ignore (extends defaults)
|
|
12712
|
+
* { ignorePatterns: [/^[A-Z_]+$/, ...] } - Regex patterns for strings to ignore
|
|
12713
|
+
*
|
|
12714
|
+
* ✓ Good:
|
|
12715
|
+
* import { BUTTON_LABEL, ERROR_MESSAGE } from "@/constants";
|
|
12716
|
+
* import { welcomeText } from "@/strings";
|
|
12717
|
+
*
|
|
12718
|
+
* <button>{BUTTON_LABEL}</button>
|
|
12719
|
+
* <span>{ERROR_MESSAGE}</span>
|
|
12720
|
+
* const message = welcomeText;
|
|
12721
|
+
*
|
|
12722
|
+
* ✗ Bad:
|
|
12723
|
+
* <button>Submit</button>
|
|
12724
|
+
* <span>Something went wrong</span>
|
|
12725
|
+
* const message = "Welcome to the app";
|
|
12726
|
+
* return "User not found";
|
|
12727
|
+
*/
|
|
12728
|
+
const noHardcodedStrings = {
|
|
12729
|
+
create(context) {
|
|
12730
|
+
const options = context.options[0] || {};
|
|
12731
|
+
|
|
12732
|
+
// JSX attributes that commonly contain non-translatable values
|
|
12733
|
+
const defaultIgnoreAttributes = [
|
|
12734
|
+
"accept",
|
|
12735
|
+
"acceptCharset",
|
|
12736
|
+
"accessKey",
|
|
12737
|
+
"action",
|
|
12738
|
+
"align",
|
|
12739
|
+
"allow",
|
|
12740
|
+
"allowFullScreen",
|
|
12741
|
+
"alt", // Often needs translation but sometimes contains technical descriptions
|
|
12742
|
+
"as",
|
|
12743
|
+
"async",
|
|
12744
|
+
"autoCapitalize",
|
|
12745
|
+
"autoComplete",
|
|
12746
|
+
"autoCorrect",
|
|
12747
|
+
"autoFocus",
|
|
12748
|
+
"autoPlay",
|
|
12749
|
+
"capture",
|
|
12750
|
+
"cellPadding",
|
|
12751
|
+
"cellSpacing",
|
|
12752
|
+
"charSet",
|
|
12753
|
+
"className",
|
|
12754
|
+
"classNames",
|
|
12755
|
+
"colSpan",
|
|
12756
|
+
"contentEditable",
|
|
12757
|
+
"controls",
|
|
12758
|
+
"controlsList",
|
|
12759
|
+
"coords",
|
|
12760
|
+
"crossOrigin",
|
|
12761
|
+
"d", // SVG path data
|
|
12762
|
+
"data",
|
|
12763
|
+
"data-*",
|
|
12764
|
+
"dateTime",
|
|
12765
|
+
"decoding",
|
|
12766
|
+
"default",
|
|
12767
|
+
"defer",
|
|
12768
|
+
"dir",
|
|
12769
|
+
"disabled",
|
|
12770
|
+
"download",
|
|
12771
|
+
"draggable",
|
|
12772
|
+
"encType",
|
|
12773
|
+
"enterKeyHint",
|
|
12774
|
+
"fill", // SVG
|
|
12775
|
+
"fillRule", // SVG
|
|
12776
|
+
"for",
|
|
12777
|
+
"form",
|
|
12778
|
+
"formAction",
|
|
12779
|
+
"formEncType",
|
|
12780
|
+
"formMethod",
|
|
12781
|
+
"formNoValidate",
|
|
12782
|
+
"formTarget",
|
|
12783
|
+
"frameBorder",
|
|
12784
|
+
"headers",
|
|
12785
|
+
"height",
|
|
12786
|
+
"hidden",
|
|
12787
|
+
"high",
|
|
12788
|
+
"href",
|
|
12789
|
+
"hrefLang",
|
|
12790
|
+
"htmlFor",
|
|
12791
|
+
"httpEquiv",
|
|
12792
|
+
"icon",
|
|
12793
|
+
"id",
|
|
12794
|
+
"imagesizes",
|
|
12795
|
+
"imagesrcset",
|
|
12796
|
+
"inputMode",
|
|
12797
|
+
"integrity",
|
|
12798
|
+
"is",
|
|
12799
|
+
"itemID",
|
|
12800
|
+
"itemProp",
|
|
12801
|
+
"itemRef",
|
|
12802
|
+
"itemScope",
|
|
12803
|
+
"itemType",
|
|
12804
|
+
"key",
|
|
12805
|
+
"keyParams",
|
|
12806
|
+
"keyType",
|
|
12807
|
+
"kind",
|
|
12808
|
+
"lang",
|
|
12809
|
+
"list",
|
|
12810
|
+
"loading",
|
|
12811
|
+
"loop",
|
|
12812
|
+
"low",
|
|
12813
|
+
"marginHeight",
|
|
12814
|
+
"marginWidth",
|
|
12815
|
+
"max",
|
|
12816
|
+
"maxLength",
|
|
12817
|
+
"media",
|
|
12818
|
+
"mediaGroup",
|
|
12819
|
+
"method",
|
|
12820
|
+
"min",
|
|
12821
|
+
"minLength",
|
|
12822
|
+
"multiple",
|
|
12823
|
+
"muted",
|
|
12824
|
+
"name",
|
|
12825
|
+
"noModule",
|
|
12826
|
+
"noValidate",
|
|
12827
|
+
"nonce",
|
|
12828
|
+
"open",
|
|
12829
|
+
"optimum",
|
|
12830
|
+
"pattern",
|
|
12831
|
+
"ping",
|
|
12832
|
+
"playsInline",
|
|
12833
|
+
"poster",
|
|
12834
|
+
"preload",
|
|
12835
|
+
"profile",
|
|
12836
|
+
"radioGroup",
|
|
12837
|
+
"readOnly",
|
|
12838
|
+
"referrerPolicy",
|
|
12839
|
+
"rel",
|
|
12840
|
+
"required",
|
|
12841
|
+
"reversed",
|
|
12842
|
+
"role",
|
|
12843
|
+
"rowSpan",
|
|
12844
|
+
"rows",
|
|
12845
|
+
"sandbox",
|
|
12846
|
+
"scope",
|
|
12847
|
+
"scoped",
|
|
12848
|
+
"scrolling",
|
|
12849
|
+
"seamless",
|
|
12850
|
+
"selected",
|
|
12851
|
+
"shape",
|
|
12852
|
+
"sizes",
|
|
12853
|
+
"slot",
|
|
12854
|
+
"span",
|
|
12855
|
+
"spellCheck",
|
|
12856
|
+
"src",
|
|
12857
|
+
"srcDoc",
|
|
12858
|
+
"srcLang",
|
|
12859
|
+
"srcSet",
|
|
12860
|
+
"start",
|
|
12861
|
+
"step",
|
|
12862
|
+
"stroke", // SVG
|
|
12863
|
+
"strokeWidth", // SVG
|
|
12864
|
+
"style",
|
|
12865
|
+
"summary",
|
|
12866
|
+
"tabIndex",
|
|
12867
|
+
"target",
|
|
12868
|
+
"testId",
|
|
12869
|
+
"transform", // SVG
|
|
12870
|
+
"translate",
|
|
12871
|
+
"type",
|
|
12872
|
+
"useMap",
|
|
12873
|
+
"value",
|
|
12874
|
+
"viewBox", // SVG
|
|
12875
|
+
"width",
|
|
12876
|
+
"wmode",
|
|
12877
|
+
"wrap",
|
|
12878
|
+
"xmlns",
|
|
12879
|
+
];
|
|
12880
|
+
|
|
12881
|
+
const ignoreAttributes = options.ignoreAttributes
|
|
12882
|
+
|| [...defaultIgnoreAttributes, ...(options.extraIgnoreAttributes || [])];
|
|
12883
|
+
|
|
12884
|
+
// Patterns for strings that are likely technical/non-translatable
|
|
12885
|
+
const technicalPatterns = [
|
|
12886
|
+
// Empty or whitespace only
|
|
12887
|
+
/^\s*$/,
|
|
12888
|
+
// Single characters
|
|
12889
|
+
/^.$/,
|
|
12890
|
+
// CSS units and values
|
|
12891
|
+
/^-?\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc|deg|rad|turn|s|ms|fr)?$/,
|
|
12892
|
+
// Colors (hex, rgb, hsl)
|
|
12893
|
+
/^#[0-9a-fA-F]{3,8}$/,
|
|
12894
|
+
/^(rgb|rgba|hsl|hsla)\(.+\)$/,
|
|
12895
|
+
// URLs and paths
|
|
12896
|
+
/^(https?:\/\/|\/\/|\/|\.\/|\.\.\/)/,
|
|
12897
|
+
// Data URLs
|
|
12898
|
+
/^data:/,
|
|
12899
|
+
// Email pattern check (not full validation)
|
|
12900
|
+
/^mailto:/,
|
|
12901
|
+
// Tel pattern
|
|
12902
|
+
/^tel:/,
|
|
12903
|
+
// File extensions
|
|
12904
|
+
/^\.[a-zA-Z0-9]+$/,
|
|
12905
|
+
// MIME types
|
|
12906
|
+
/^[a-z]+\/[a-z0-9.+-]+$/,
|
|
12907
|
+
// UUIDs
|
|
12908
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
12909
|
+
// Date formats (ISO, common patterns)
|
|
12910
|
+
/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/,
|
|
12911
|
+
// Time formats
|
|
12912
|
+
/^\d{1,2}:\d{2}(:\d{2})?(\s?(AM|PM|am|pm))?$/,
|
|
12913
|
+
// JSON keys (camelCase, snake_case, SCREAMING_SNAKE_CASE)
|
|
12914
|
+
/^[a-z][a-zA-Z0-9]*$/,
|
|
12915
|
+
/^[a-z][a-z0-9_]*$/,
|
|
12916
|
+
/^[A-Z][A-Z0-9_]*$/,
|
|
12917
|
+
// Common technical strings
|
|
12918
|
+
/^(true|false|null|undefined|NaN|Infinity)$/,
|
|
12919
|
+
// HTTP methods
|
|
12920
|
+
/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|CONNECT|TRACE)$/,
|
|
12921
|
+
// Content types
|
|
12922
|
+
/^application\//,
|
|
12923
|
+
// Environment variables pattern
|
|
12924
|
+
/^[A-Z][A-Z0-9_]*$/,
|
|
12925
|
+
// Query parameters
|
|
12926
|
+
/^[a-z][a-zA-Z0-9_]*=/,
|
|
12927
|
+
// CSS property-like
|
|
12928
|
+
/^[a-z]+(-[a-z]+)*$/,
|
|
12929
|
+
// Numbers with separators
|
|
12930
|
+
/^[\d,._]+$/,
|
|
12931
|
+
// Semantic version
|
|
12932
|
+
/^\d+\.\d+\.\d+/,
|
|
12933
|
+
// Common separators
|
|
12934
|
+
/^[,;:|•·\-–—/\\]+$/,
|
|
12935
|
+
// HTML entities
|
|
12936
|
+
/^&[a-z]+;$/,
|
|
12937
|
+
// Punctuation only
|
|
12938
|
+
/^[.!?,;:'"()\[\]{}]+$/,
|
|
12939
|
+
];
|
|
12940
|
+
|
|
12941
|
+
const extraIgnorePatterns = (options.ignorePatterns || []).map((p) => {
|
|
12942
|
+
if (typeof p === "string") return new RegExp(p);
|
|
12943
|
+
|
|
12944
|
+
return p;
|
|
12945
|
+
});
|
|
12946
|
+
|
|
12947
|
+
const allIgnorePatterns = [...technicalPatterns, ...extraIgnorePatterns];
|
|
12948
|
+
|
|
12949
|
+
// Check if a string matches any ignore pattern
|
|
12950
|
+
const shouldIgnoreStringHandler = (str) => allIgnorePatterns.some((pattern) => pattern.test(str));
|
|
12951
|
+
|
|
12952
|
+
// Check if we're inside a constants/strings file
|
|
12953
|
+
const isConstantsFileHandler = () => {
|
|
12954
|
+
const filename = context.filename || context.getFilename();
|
|
12955
|
+
const normalizedPath = filename.replace(/\\/g, "/").toLowerCase();
|
|
12956
|
+
|
|
12957
|
+
// Check if file is in constants/strings folders
|
|
12958
|
+
return /\/(constants|strings|@constants|@strings)(\/|\.)/i.test(normalizedPath)
|
|
12959
|
+
|| /\/data\/(constants|strings)/i.test(normalizedPath);
|
|
12960
|
+
};
|
|
12961
|
+
|
|
12962
|
+
// Check if the string is from an imported constant
|
|
12963
|
+
const importedConstantsHandler = new Set();
|
|
12964
|
+
|
|
12965
|
+
// Track which identifiers come from constants imports
|
|
12966
|
+
const trackImportsHandler = (node) => {
|
|
12967
|
+
const importPath = node.source.value;
|
|
12968
|
+
|
|
12969
|
+
if (typeof importPath !== "string") return;
|
|
12970
|
+
|
|
12971
|
+
// Check if import is from constants/strings
|
|
12972
|
+
const isFromConstants = /@?\/?(@?constants|@?strings|data\/constants|data\/strings)/i
|
|
12973
|
+
.test(importPath);
|
|
12974
|
+
|
|
12975
|
+
if (isFromConstants) {
|
|
12976
|
+
node.specifiers.forEach((spec) => {
|
|
12977
|
+
if (spec.local && spec.local.name) {
|
|
12978
|
+
importedConstantsHandler.add(spec.local.name);
|
|
12979
|
+
}
|
|
12980
|
+
});
|
|
12981
|
+
}
|
|
12982
|
+
};
|
|
12983
|
+
|
|
12984
|
+
// Check if a node is a reference to an imported constant
|
|
12985
|
+
const isImportedConstantHandler = (node) => {
|
|
12986
|
+
if (node.type === "Identifier") {
|
|
12987
|
+
return importedConstantsHandler.has(node.name);
|
|
12988
|
+
}
|
|
12989
|
+
|
|
12990
|
+
if (node.type === "MemberExpression") {
|
|
12991
|
+
// Check if the object is an imported constant (e.g., STRINGS.welcome)
|
|
12992
|
+
if (node.object.type === "Identifier") {
|
|
12993
|
+
return importedConstantsHandler.has(node.object.name);
|
|
12994
|
+
}
|
|
12995
|
+
}
|
|
12996
|
+
|
|
12997
|
+
return false;
|
|
12998
|
+
};
|
|
12999
|
+
|
|
13000
|
+
// Check if we're in a component, hook, or utility function
|
|
13001
|
+
const isInRelevantContextHandler = (node) => {
|
|
13002
|
+
let current = node.parent;
|
|
13003
|
+
|
|
13004
|
+
while (current) {
|
|
13005
|
+
// Check for function declarations/expressions
|
|
13006
|
+
if (
|
|
13007
|
+
current.type === "FunctionDeclaration"
|
|
13008
|
+
|| current.type === "FunctionExpression"
|
|
13009
|
+
|| current.type === "ArrowFunctionExpression"
|
|
13010
|
+
) {
|
|
13011
|
+
// Get function name if available
|
|
13012
|
+
let funcName = null;
|
|
13013
|
+
|
|
13014
|
+
if (current.id && current.id.name) {
|
|
13015
|
+
funcName = current.id.name;
|
|
13016
|
+
} else if (
|
|
13017
|
+
current.parent
|
|
13018
|
+
&& current.parent.type === "VariableDeclarator"
|
|
13019
|
+
&& current.parent.id
|
|
13020
|
+
&& current.parent.id.name
|
|
13021
|
+
) {
|
|
13022
|
+
funcName = current.parent.id.name;
|
|
13023
|
+
}
|
|
13024
|
+
|
|
13025
|
+
if (funcName) {
|
|
13026
|
+
// React components (PascalCase)
|
|
13027
|
+
if (/^[A-Z]/.test(funcName)) return true;
|
|
13028
|
+
|
|
13029
|
+
// Custom hooks (useXxx)
|
|
13030
|
+
if (/^use[A-Z]/.test(funcName)) return true;
|
|
13031
|
+
|
|
13032
|
+
// Utility/helper functions (common patterns)
|
|
13033
|
+
if (/Handler$|Helper$|Util$|Utils$/i.test(funcName)) return true;
|
|
13034
|
+
|
|
13035
|
+
// Any function that returns JSX is a component
|
|
13036
|
+
// (This is checked via JSX detection below)
|
|
13037
|
+
}
|
|
13038
|
+
|
|
13039
|
+
return true; // Check all functions for now
|
|
13040
|
+
}
|
|
13041
|
+
|
|
13042
|
+
// Check for JSX - if we're in JSX, we're in a component
|
|
13043
|
+
if (
|
|
13044
|
+
current.type === "JSXElement"
|
|
13045
|
+
|| current.type === "JSXFragment"
|
|
13046
|
+
) {
|
|
13047
|
+
return true;
|
|
13048
|
+
}
|
|
13049
|
+
|
|
13050
|
+
current = current.parent;
|
|
13051
|
+
}
|
|
13052
|
+
|
|
13053
|
+
return false;
|
|
13054
|
+
};
|
|
13055
|
+
|
|
13056
|
+
// Check if string is in an object that looks like constants definition
|
|
13057
|
+
const isInConstantsObjectHandler = (node) => {
|
|
13058
|
+
let current = node.parent;
|
|
13059
|
+
|
|
13060
|
+
while (current) {
|
|
13061
|
+
if (current.type === "VariableDeclarator") {
|
|
13062
|
+
const varName = current.id && current.id.name;
|
|
13063
|
+
|
|
13064
|
+
// Check if variable name suggests it's a constants object
|
|
13065
|
+
if (varName && /^[A-Z][A-Z0-9_]*$|CONSTANTS?|STRINGS?|MESSAGES?|LABELS?|TEXTS?/i.test(varName)) {
|
|
13066
|
+
return true;
|
|
13067
|
+
}
|
|
13068
|
+
}
|
|
13069
|
+
|
|
13070
|
+
// Check for export const CONSTANT_NAME = "value"
|
|
13071
|
+
if (current.type === "ExportNamedDeclaration") {
|
|
13072
|
+
return true;
|
|
13073
|
+
}
|
|
13074
|
+
|
|
13075
|
+
current = current.parent;
|
|
13076
|
+
}
|
|
13077
|
+
|
|
13078
|
+
return false;
|
|
13079
|
+
};
|
|
13080
|
+
|
|
13081
|
+
// Skip if we're in a constants file
|
|
13082
|
+
if (isConstantsFileHandler()) {
|
|
13083
|
+
return {};
|
|
13084
|
+
}
|
|
13085
|
+
|
|
13086
|
+
return {
|
|
13087
|
+
ImportDeclaration: trackImportsHandler,
|
|
13088
|
+
|
|
13089
|
+
// Check JSX text content
|
|
13090
|
+
JSXText(node) {
|
|
13091
|
+
const text = node.value.trim();
|
|
13092
|
+
|
|
13093
|
+
if (!text || shouldIgnoreStringHandler(text)) return;
|
|
13094
|
+
|
|
13095
|
+
// Check if it looks like user-facing text (contains letters and spaces)
|
|
13096
|
+
if (!/[a-zA-Z]/.test(text)) return;
|
|
13097
|
+
|
|
13098
|
+
context.report({
|
|
13099
|
+
message: `Hardcoded string "${text.substring(0, 30)}${text.length > 30 ? "..." : ""}" should be imported from constants/strings module`,
|
|
13100
|
+
node,
|
|
13101
|
+
});
|
|
13102
|
+
},
|
|
13103
|
+
|
|
13104
|
+
// Check JSX expression containers with string literals
|
|
13105
|
+
JSXExpressionContainer(node) {
|
|
13106
|
+
const { expression } = node;
|
|
13107
|
+
|
|
13108
|
+
// Skip if it's a reference to an imported constant
|
|
13109
|
+
if (isImportedConstantHandler(expression)) return;
|
|
13110
|
+
|
|
13111
|
+
// Check string literals
|
|
13112
|
+
if (expression.type === "Literal" && typeof expression.value === "string") {
|
|
13113
|
+
const str = expression.value;
|
|
13114
|
+
|
|
13115
|
+
if (shouldIgnoreStringHandler(str)) return;
|
|
13116
|
+
|
|
13117
|
+
// Check if it looks like user-facing text
|
|
13118
|
+
if (!/[a-zA-Z]/.test(str)) return;
|
|
13119
|
+
|
|
13120
|
+
context.report({
|
|
13121
|
+
message: `Hardcoded string "${str.substring(0, 30)}${str.length > 30 ? "..." : ""}" should be imported from constants/strings module`,
|
|
13122
|
+
node: expression,
|
|
13123
|
+
});
|
|
13124
|
+
}
|
|
13125
|
+
|
|
13126
|
+
// Check template literals
|
|
13127
|
+
if (expression.type === "TemplateLiteral") {
|
|
13128
|
+
expression.quasis.forEach((quasi) => {
|
|
13129
|
+
const str = quasi.value.cooked || quasi.value.raw;
|
|
13130
|
+
|
|
13131
|
+
if (shouldIgnoreStringHandler(str)) return;
|
|
13132
|
+
|
|
13133
|
+
// Check if it contains user-facing text (more than just variable placeholders)
|
|
13134
|
+
if (!/[a-zA-Z]{2,}/.test(str)) return;
|
|
13135
|
+
|
|
13136
|
+
// Skip if it looks like a path or URL pattern
|
|
13137
|
+
if (/^[/.]|https?:\/\//.test(str)) return;
|
|
13138
|
+
|
|
13139
|
+
context.report({
|
|
13140
|
+
message: `Hardcoded string in template literal "${str.substring(0, 30)}${str.length > 30 ? "..." : ""}" should be imported from constants/strings module`,
|
|
13141
|
+
node: quasi,
|
|
13142
|
+
});
|
|
13143
|
+
});
|
|
13144
|
+
}
|
|
13145
|
+
},
|
|
13146
|
+
|
|
13147
|
+
// Check JSX attributes
|
|
13148
|
+
JSXAttribute(node) {
|
|
13149
|
+
if (!node.value) return;
|
|
13150
|
+
|
|
13151
|
+
// Get attribute name
|
|
13152
|
+
const attrName = node.name.name || (node.name.namespace && `${node.name.namespace.name}:${node.name.name.name}`);
|
|
13153
|
+
|
|
13154
|
+
// Skip ignored attributes
|
|
13155
|
+
if (ignoreAttributes.includes(attrName)) return;
|
|
13156
|
+
|
|
13157
|
+
// Handle data-* attributes
|
|
13158
|
+
if (attrName && attrName.startsWith("data-")) return;
|
|
13159
|
+
|
|
13160
|
+
// Handle aria-* attributes
|
|
13161
|
+
if (attrName && attrName.startsWith("aria-")) return;
|
|
13162
|
+
|
|
13163
|
+
// Check string literal values
|
|
13164
|
+
if (node.value.type === "Literal" && typeof node.value.value === "string") {
|
|
13165
|
+
const str = node.value.value;
|
|
13166
|
+
|
|
13167
|
+
if (shouldIgnoreStringHandler(str)) return;
|
|
13168
|
+
|
|
13169
|
+
// Check if it looks like user-facing text (contains letters and multiple words or is long)
|
|
13170
|
+
if (!/[a-zA-Z]/.test(str)) return;
|
|
13171
|
+
|
|
13172
|
+
if (str.split(/\s+/).length < 2 && str.length < 10) return;
|
|
13173
|
+
|
|
13174
|
+
context.report({
|
|
13175
|
+
message: `Hardcoded string "${str.substring(0, 30)}${str.length > 30 ? "..." : ""}" in attribute "${attrName}" should be imported from constants/strings module`,
|
|
13176
|
+
node: node.value,
|
|
13177
|
+
});
|
|
13178
|
+
}
|
|
13179
|
+
|
|
13180
|
+
// Check expression containers
|
|
13181
|
+
if (node.value.type === "JSXExpressionContainer") {
|
|
13182
|
+
const { expression } = node.value;
|
|
13183
|
+
|
|
13184
|
+
// Skip if it's a reference to an imported constant
|
|
13185
|
+
if (isImportedConstantHandler(expression)) return;
|
|
13186
|
+
|
|
13187
|
+
if (expression.type === "Literal" && typeof expression.value === "string") {
|
|
13188
|
+
const str = expression.value;
|
|
13189
|
+
|
|
13190
|
+
if (shouldIgnoreStringHandler(str)) return;
|
|
13191
|
+
|
|
13192
|
+
if (!/[a-zA-Z]/.test(str)) return;
|
|
13193
|
+
|
|
13194
|
+
if (str.split(/\s+/).length < 2 && str.length < 10) return;
|
|
13195
|
+
|
|
13196
|
+
context.report({
|
|
13197
|
+
message: `Hardcoded string "${str.substring(0, 30)}${str.length > 30 ? "..." : ""}" in attribute "${attrName}" should be imported from constants/strings module`,
|
|
13198
|
+
node: expression,
|
|
13199
|
+
});
|
|
13200
|
+
}
|
|
13201
|
+
}
|
|
13202
|
+
},
|
|
13203
|
+
|
|
13204
|
+
// Check string literals in component/hook/utility logic
|
|
13205
|
+
Literal(node) {
|
|
13206
|
+
// Only check string literals
|
|
13207
|
+
if (typeof node.value !== "string") return;
|
|
13208
|
+
|
|
13209
|
+
const str = node.value;
|
|
13210
|
+
|
|
13211
|
+
// Skip if it matches ignore patterns
|
|
13212
|
+
if (shouldIgnoreStringHandler(str)) return;
|
|
13213
|
+
|
|
13214
|
+
// Skip if not in relevant context
|
|
13215
|
+
if (!isInRelevantContextHandler(node)) return;
|
|
13216
|
+
|
|
13217
|
+
// Skip if in a constants definition object
|
|
13218
|
+
if (isInConstantsObjectHandler(node)) return;
|
|
13219
|
+
|
|
13220
|
+
// Skip JSX (handled separately)
|
|
13221
|
+
if (node.parent.type === "JSXAttribute" || node.parent.type === "JSXExpressionContainer") return;
|
|
13222
|
+
|
|
13223
|
+
// Skip import/export sources
|
|
13224
|
+
if (node.parent.type === "ImportDeclaration" || node.parent.type === "ExportNamedDeclaration" || node.parent.type === "ExportAllDeclaration") return;
|
|
13225
|
+
|
|
13226
|
+
// Skip object property keys
|
|
13227
|
+
if (node.parent.type === "Property" && node.parent.key === node) return;
|
|
13228
|
+
|
|
13229
|
+
// Skip if it doesn't look like user-facing text
|
|
13230
|
+
if (!/[a-zA-Z]/.test(str)) return;
|
|
13231
|
+
|
|
13232
|
+
// Require at least 2 words or be reasonably long to be considered user-facing
|
|
13233
|
+
if (str.split(/\s+/).length < 2 && str.length < 15) return;
|
|
13234
|
+
|
|
13235
|
+
context.report({
|
|
13236
|
+
message: `Hardcoded string "${str.substring(0, 30)}${str.length > 30 ? "..." : ""}" should be imported from constants/strings module`,
|
|
13237
|
+
node,
|
|
13238
|
+
});
|
|
13239
|
+
},
|
|
13240
|
+
|
|
13241
|
+
// Check template literals in component/hook/utility logic
|
|
13242
|
+
TemplateLiteral(node) {
|
|
13243
|
+
// Skip if in JSX (handled separately)
|
|
13244
|
+
if (node.parent.type === "JSXExpressionContainer") return;
|
|
13245
|
+
|
|
13246
|
+
// Skip if not in relevant context
|
|
13247
|
+
if (!isInRelevantContextHandler(node)) return;
|
|
13248
|
+
|
|
13249
|
+
// Skip if in a constants definition
|
|
13250
|
+
if (isInConstantsObjectHandler(node)) return;
|
|
13251
|
+
|
|
13252
|
+
// Check each quasi (static part)
|
|
13253
|
+
node.quasis.forEach((quasi) => {
|
|
13254
|
+
const str = quasi.value.cooked || quasi.value.raw;
|
|
13255
|
+
|
|
13256
|
+
if (shouldIgnoreStringHandler(str)) return;
|
|
13257
|
+
|
|
13258
|
+
// Check if it contains substantial user-facing text
|
|
13259
|
+
if (!/[a-zA-Z]{3,}/.test(str)) return;
|
|
13260
|
+
|
|
13261
|
+
// Skip if it looks like a path, URL, or query
|
|
13262
|
+
if (/^[/.]|^https?:\/\/|^[?&]/.test(str)) return;
|
|
13263
|
+
|
|
13264
|
+
// Skip interpolation-heavy templates (more expressions than text)
|
|
13265
|
+
if (node.expressions.length > node.quasis.length) return;
|
|
13266
|
+
|
|
13267
|
+
context.report({
|
|
13268
|
+
message: `Hardcoded string in template literal "${str.substring(0, 30)}${str.length > 30 ? "..." : ""}" should be imported from constants/strings module`,
|
|
13269
|
+
node: quasi,
|
|
13270
|
+
});
|
|
13271
|
+
});
|
|
13272
|
+
},
|
|
13273
|
+
};
|
|
13274
|
+
},
|
|
13275
|
+
meta: {
|
|
13276
|
+
docs: {
|
|
13277
|
+
description: "Enforce importing strings from constants/strings modules instead of hardcoding them",
|
|
13278
|
+
},
|
|
13279
|
+
schema: [
|
|
13280
|
+
{
|
|
13281
|
+
additionalProperties: false,
|
|
13282
|
+
properties: {
|
|
13283
|
+
extraIgnoreAttributes: {
|
|
13284
|
+
description: "Additional JSX attributes to ignore (extends defaults)",
|
|
13285
|
+
items: { type: "string" },
|
|
13286
|
+
type: "array",
|
|
13287
|
+
},
|
|
13288
|
+
ignoreAttributes: {
|
|
13289
|
+
description: "JSX attributes to ignore (replaces defaults)",
|
|
13290
|
+
items: { type: "string" },
|
|
13291
|
+
type: "array",
|
|
13292
|
+
},
|
|
13293
|
+
ignorePatterns: {
|
|
13294
|
+
description: "Regex patterns for strings to ignore",
|
|
13295
|
+
items: { type: "string" },
|
|
13296
|
+
type: "array",
|
|
13297
|
+
},
|
|
13298
|
+
},
|
|
13299
|
+
type: "object",
|
|
13300
|
+
},
|
|
13301
|
+
],
|
|
13302
|
+
type: "suggestion",
|
|
13303
|
+
},
|
|
13304
|
+
};
|
|
13305
|
+
|
|
12643
13306
|
/**
|
|
12644
13307
|
* ───────────────────────────────────────────────────────────────
|
|
12645
13308
|
* Rule: Variable Naming Convention
|
|
@@ -17819,6 +18482,9 @@ export default {
|
|
|
17819
18482
|
// Type/Enum rules
|
|
17820
18483
|
"enum-type-enforcement": enumTypeEnforcement,
|
|
17821
18484
|
|
|
18485
|
+
// String rules
|
|
18486
|
+
"no-hardcoded-strings": noHardcodedStrings,
|
|
18487
|
+
|
|
17822
18488
|
// Variable rules
|
|
17823
18489
|
"variable-naming-convention": variableNamingConvention,
|
|
17824
18490
|
},
|
package/package.json
CHANGED