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 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
- *69 rules (63 auto-fixable) to keep your codebase clean and consistent*
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 **69 custom rules** (63 auto-fixable) for code formatting. Built for **ESLint v9 flat configs**.
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 69 rules support auto-fix with `eslint --fix`
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
- > **69 rules total** — 63 with auto-fix 🔧, 6 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
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 (get/set/handle/is/has), handlers end with Handler 🔧 |
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** recommended (get, set, handle, is, has, can, should, etc.)
1320
- - **Event handlers** can use `handle` prefix or `Handler` suffix
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 — clear verb prefixes
1326
- function getUserData() {}
1327
- function setUserName(name) {}
1328
- function handleClick() {}
1329
- function handleSubmit() {}
1330
- function isValidEmail(email) {}
1331
- function hasPermission(user) {}
1332
- function canAccess(resource) {}
1333
- function shouldUpdate(props) {}
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 — no verb, unclear purpose
1338
- function userData() {}
1339
- function userName(name) {}
1340
- function click() {}
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 — wrong case
1344
- function GetUserData() {}
1345
- function get_user_data() {}
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 69 rules support auto-fixing. Run ESLint with the `--fix` flag:
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 handlers ending with "Handler".
2000
- * Auto-fixes PascalCase functions that start with verbs to camelCase.
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 getUserData() {}
2004
- * function handleClick() {}
2005
- * function isValidEmail() {}
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() {} // → getUserData
2010
- * const FetchStatus = () => {} // → fetchStatus
2011
- * function user_data() {}
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
- "handle", "on", "click", "change", "input", "press", "drag", "drop",
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, handle, etc.) AND end with "Handler" (e.g., getDataHandler, handleClickHandler)`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.7.6",
3
+ "version": "1.8.1",
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",