Repository: biomejs/biome (language: rust)

Issue:
💅 `useNamingConvention` forbids the `readonly` modifier for `indexParameter` selector kind
### Environment information

```bash
CLI:
  Version:                      1.9.3
  Color support:                true

Platform:
  CPU Architecture:             x86_64
  OS:                           windows

Environment:
  BIOME_LOG_PATH:               unset
  BIOME_LOG_PREFIX_NAME:        unset
  BIOME_CONFIG_PATH:            unset
  NO_COLOR:                     unset
  TERM:                         unset
  JS_RUNTIME_VERSION:           "v20.18.0"
  JS_RUNTIME_NAME:              "node"
  NODE_PACKAGE_MANAGER:         unset

Biome Configuration:
  Error:                        The `readonly` modifier cannot be used with the `index parameter` kind.
  Status:                       Loaded with errors
  Formatter disabled:           false
  Linter disabled:              false
  Organize imports disabled:    false
  VCS disabled:                 true

Linter:
  JavaScript enabled:           true
  JSON enabled:                 true
  CSS enabled:                  true
  GraphQL enabled:              false
  Recommended:                  false
  All:                          false
  Enabled rules:
  style/useNamingConvention

Workspace:
  Open Documents:               0
```


### Rule name

style/useNamingConvention

### Playground link

https://github.com/sepruko/biome-repro/tree/style/use-naming-convention-0

### Expected result

When configured to ensure index parameters with the `readonly` modifier are of specific formats and/or patterns, it should successfully parse its configuration file and emit the correct diagnostics.

The [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures) clearly includes an example showing an index parameter marked as `readonly`, and the official `tsc` compiler has no issue handling this code.

### Code of Conduct

- [X] I agree to follow Biome's Code of Conduct


Discussion / hints:
The configuration you provided has errors, the rage output confirms it.

Can you please make sure to fix that?
Hello @sepruko, please provide a minimal reproduction. You can use one of the following options:

- Provide a link to [our playground](https://biomejs.dev/playground), if it's applicable.
- Provide a link to GitHub repository. To easily create a reproduction, you can use our interactive CLI via `npm create @biomejs/biome-reproduction`

Issues marked with `S-Needs repro` will be **closed** if they have **no activity within 3 days**.
> The configuration you provided has errors, the rage output confirms it.
> 
> Can you please make sure to fix that?

The error is the problem, it is complaining that the `readonly` modifier is incompatible with the `indexParameter` kind when [TypeScript itself says it is, in fact, not](https://github.com/microsoft/TypeScript/blob/main/tests/cases/compiler/propertyAccessOfReadonlyIndexSignature.ts).

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 393a3a5ce8e9..cb178ec18aa9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -166,6 +166,10 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b
 
  Contributed by @fireairforce
 
+ - [useNamingConvention](https://biomejs.dev/linter/rules/use-naming-convention/) now allows configuring conventions for readonly index signatures.
+
+  Contributed by @sepruko
+
 - [noDuplicateCustomProperties](https://biomejs.dev/linter/rules/no-duplicate-custom-properties/) now correctly handles custom properties and ignores non-custom properties.
   Previously, the rule incorrectly reported duplicates for all properties, including non-custom ones. Contributed by @togami2864
 
diff --git a/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs b/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs
index ca7ccf3cc432..821962d96cd3 100644
--- a/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs
+++ b/crates/biome_js_analyze/src/lint/style/use_naming_convention.rs
@@ -21,8 +21,8 @@ use biome_js_syntax::{
     AnyJsVariableDeclaration, AnyTsTypeMember, JsFileSource, JsIdentifierBinding,
     JsLiteralExportName, JsLiteralMemberName, JsMethodModifierList, JsPrivateClassMemberName,
     JsPropertyModifierList, JsSyntaxKind, JsSyntaxToken, JsVariableDeclarator, JsVariableKind,
-    Modifier, TsIdentifierBinding, TsLiteralEnumMemberName, TsMethodSignatureModifierList,
-    TsPropertySignatureModifierList, TsTypeParameterName,
+    Modifier, TsIdentifierBinding, TsIndexSignatureModifierList, TsLiteralEnumMemberName,
+    TsMethodSignatureModifierList, TsPropertySignatureModifierList, TsTypeParameterName,
 };
 use biome_rowan::{
     declare_node_union, AstNode, BatchMutationExt, SyntaxResult, TextRange, TextSize,
@@ -1092,7 +1092,10 @@ impl Selector {
             }
         }
         if self.modifiers.contains(Modifier::Readonly)
-            && !matches!(self.kind, Kind::ClassProperty | Kind::TypeProperty)
+            && !matches!(
+                self.kind,
+                Kind::ClassProperty | Kind::IndexParameter | Kind::TypeProperty
+            )
         {
             return Err(InvalidSelector::UnsupportedModifiers(
                 self.kind,
@@ -1236,7 +1239,9 @@ impl Selector {
             | AnyJsClassMember::TsConstructorSignatureClassMember(_)
             | AnyJsClassMember::JsEmptyClassMember(_)
             | AnyJsClassMember::JsStaticInitializationBlockClassMember(_) => return None,
-            AnyJsClassMember::TsIndexSignatureClassMember(_) => Kind::IndexParameter.into(),
+            AnyJsClassMember::TsIndexSignatureClassMember(getter) => {
+                Selector::with_modifiers(Kind::IndexParameter, getter.modifiers())
+            }
             AnyJsClassMember::JsGetterClassMember(getter) => {
                 Selector::with_modifiers(Kind::ClassGetter, getter.modifiers())
             }
@@ -1290,7 +1295,17 @@ impl Selector {
             | AnyJsBindingDeclaration::JsRestParameter(_) => Some(Kind::FunctionParameter.into()),
             AnyJsBindingDeclaration::JsCatchDeclaration(_) => Some(Kind::CatchParameter.into()),
             AnyJsBindingDeclaration::TsPropertyParameter(_) => Some(Kind::ClassProperty.into()),
-            AnyJsBindingDeclaration::TsIndexSignatureParameter(_) => Some(Kind::IndexParameter.into()),
+            AnyJsBindingDeclaration::TsIndexSignatureParameter(member_name) => {
+                if let Some(member) = member_name.parent::<>() {
+                    Selector::from_class_member(&member)
+                } else if let Some(member) = member_name.parent::<AnyTsTypeMember>() {
+                    Selector::from_type_member(&member)
+                } else if let Some(member) = member_name.parent::<AnyJsObjectMember>() {
+                    Selector::from_object_member(&member)
+                } else {
+                    Some(Kind::IndexParameter.into())
+                }
+            },
             AnyJsBindingDeclaration::JsNamespaceImportSpecifier(_) => Some(Selector::with_scope(Kind::ImportNamespace, Scope::Global)),
             AnyJsBindingDeclaration::JsFunctionDeclaration(_)
             | AnyJsBindingDeclaration::JsFunctionExpression(_)
@@ -1384,7 +1399,13 @@ impl Selector {
             AnyTsTypeMember::JsBogusMember(_)
             | AnyTsTypeMember::TsCallSignatureTypeMember(_)
             | AnyTsTypeMember::TsConstructSignatureTypeMember(_) => None,
-            AnyTsTypeMember::TsIndexSignatureTypeMember(_) => Some(Kind::IndexParameter.into()),
+            AnyTsTypeMember::TsIndexSignatureTypeMember(property) => {
+                Some(if property.readonly_token().is_some() {
+                    Selector::with_modifiers(Kind::IndexParameter, Modifier::Readonly)
+                } else {
+                    Kind::IndexParameter.into()
+                })
+            }
             AnyTsTypeMember::TsGetterSignatureTypeMember(_) => Some(Kind::TypeGetter.into()),
             AnyTsTypeMember::TsMethodSignatureTypeMember(_) => Some(Kind::TypeMethod.into()),
             AnyTsTypeMember::TsPropertySignatureTypeMember(property) => {
@@ -1778,6 +1799,11 @@ impl From<JsPropertyModifierList> for Modifiers {
         Modifiers((&value).into())
     }
 }
+impl From<TsIndexSignatureModifierList> for Modifiers {
+    fn from(value: TsIndexSignatureModifierList) -> Self {
+        Modifiers((&value).into())
+    }
+}
 impl From<TsMethodSignatureModifierList> for Modifiers {
     fn from(value: TsMethodSignatureModifierList) -> Self {
         Modifiers((&value).into())
diff --git a/crates/biome_js_syntax/src/modifier_ext.rs b/crates/biome_js_syntax/src/modifier_ext.rs
index f39c703cfd41..e10339dd33d0 100644
--- a/crates/biome_js_syntax/src/modifier_ext.rs
+++ b/crates/biome_js_syntax/src/modifier_ext.rs
@@ -4,7 +4,8 @@ use crate::{
     AnyJsMethodModifier, AnyJsPropertyModifier, AnyTsIndexSignatureModifier,
     AnyTsMethodSignatureModifier, AnyTsPropertyParameterModifier, AnyTsPropertySignatureModifier,
     AnyTsTypeParameterModifier, JsMethodModifierList, JsPropertyModifierList, JsSyntaxKind,
-    TsAccessibilityModifier, TsMethodSignatureModifierList, TsPropertySignatureModifierList,
+    TsAccessibilityModifier, TsIndexSignatureModifierList, TsMethodSignatureModifierList,
+    TsPropertySignatureModifierList,
 };
 
 /// Helpful data structure to make the order of modifiers predictable inside the formatter
@@ -174,6 +175,14 @@ impl From<&JsPropertyModifierList> for enumflags2::BitFlags<Modifier> {
             .fold(Self::empty(), |acc, m| acc | m)
     }
 }
+impl From<&TsIndexSignatureModifierList> for enumflags2::BitFlags<Modifier> {
+    fn from(value: &TsIndexSignatureModifierList) -> Self {
+        value
+            .into_iter()
+            .map(|m| Modifier::from(&m))
+            .fold(Self::empty(), |acc, m| acc | m)
+    }
+}
 impl From<&TsPropertySignatureModifierList> for enumflags2::BitFlags<Modifier> {
     fn from(value: &TsPropertySignatureModifierList) -> Self {
         value


Test patch:
diff --git a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidIndexParameter.ts b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidIndexParameter.ts
index ac87505b22b0..8d4c9a47cade 100644
--- a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidIndexParameter.ts
+++ b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidIndexParameter.ts
@@ -4,4 +4,12 @@ export interface X {
     [CONSTANT_CASE: number]: unknown
 
     [snake_case: symbol]: unknown
+}
+
+export interface Y {
+    readonly [PascalCase: string]: unknown
+
+    readonly [CONSTANT_CASE: number]: unknown
+
+    readonly [snake_case: symbol]: unknown
 }
\ No newline at end of file
diff --git a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidIndexParameter.ts.snap b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidIndexParameter.ts.snap
index a24c9277437a..a23d2fb5573b 100644
--- a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidIndexParameter.ts.snap
+++ b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/invalidIndexParameter.ts.snap
@@ -11,6 +11,14 @@ export interface X {
 
     [snake_case: symbol]: unknown
 }
+
+export interface Y {
+    readonly [PascalCase: string]: unknown
+
+    readonly [CONSTANT_CASE: number]: unknown
+
+    readonly [snake_case: symbol]: unknown
+}
 ```
 
 # Diagnostics
@@ -27,11 +35,11 @@ invalidIndexParameter.ts:2:6 lint/style/useNamingConvention  FIXABLE  ━━━
   
   i Safe fix: Rename this symbol in camelCase.
   
-    1 1 │   export interface X {
-    2   │ - ····[PascalCase:·string]:·unknown
-      2 │ + ····[pascalCase:·string]:·unknown
-    3 3 │   
-    4 4 │       [CONSTANT_CASE: number]: unknown
+     1  1 │   export interface X {
+     2    │ - ····[PascalCase:·string]:·unknown
+        2 │ + ····[pascalCase:·string]:·unknown
+     3  3 │   
+     4  4 │       [CONSTANT_CASE: number]: unknown
   
 
 ```
@@ -50,12 +58,12 @@ invalidIndexParameter.ts:4:6 lint/style/useNamingConvention  FIXABLE  ━━━
   
   i Safe fix: Rename this symbol in camelCase.
   
-    2 2 │       [PascalCase: string]: unknown
-    3 3 │   
-    4   │ - ····[CONSTANT_CASE:·number]:·unknown
-      4 │ + ····[constantCase:·number]:·unknown
-    5 5 │   
-    6 6 │       [snake_case: symbol]: unknown
+     2  2 │       [PascalCase: string]: unknown
+     3  3 │   
+     4    │ - ····[CONSTANT_CASE:·number]:·unknown
+        4 │ + ····[constantCase:·number]:·unknown
+     5  5 │   
+     6  6 │       [snake_case: symbol]: unknown
   
 
 ```
@@ -70,14 +78,85 @@ invalidIndexParameter.ts:6:6 lint/style/useNamingConvention  FIXABLE  ━━━
   > 6 │     [snake_case: symbol]: unknown
       │      ^^^^^^^^^^
     7 │ }
+    8 │ 
+  
+  i Safe fix: Rename this symbol in camelCase.
+  
+     4  4 │       [CONSTANT_CASE: number]: unknown
+     5  5 │   
+     6    │ - ····[snake_case:·symbol]:·unknown
+        6 │ + ····[snakeCase:·symbol]:·unknown
+     7  7 │   }
+     8  8 │   
+  
+
+```
+
+```
+invalidIndexParameter.ts:10:15 lint/style/useNamingConvention  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This index parameter name should be in camelCase.
+  
+     9 │ export interface Y {
+  > 10 │     readonly [PascalCase: string]: unknown
+       │               ^^^^^^^^^^
+    11 │ 
+    12 │     readonly [CONSTANT_CASE: number]: unknown
+  
+  i Safe fix: Rename this symbol in camelCase.
+  
+     8  8 │   
+     9  9 │   export interface Y {
+    10    │ - ····readonly·[PascalCase:·string]:·unknown
+       10 │ + ····readonly·[pascalCase:·string]:·unknown
+    11 11 │   
+    12 12 │       readonly [CONSTANT_CASE: number]: unknown
+  
+
+```
+
+```
+invalidIndexParameter.ts:12:15 lint/style/useNamingConvention  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This index parameter name should be in camelCase.
+  
+    10 │     readonly [PascalCase: string]: unknown
+    11 │ 
+  > 12 │     readonly [CONSTANT_CASE: number]: unknown
+       │               ^^^^^^^^^^^^^
+    13 │ 
+    14 │     readonly [snake_case: symbol]: unknown
+  
+  i Safe fix: Rename this symbol in camelCase.
+  
+    10 10 │       readonly [PascalCase: string]: unknown
+    11 11 │   
+    12    │ - ····readonly·[CONSTANT_CASE:·number]:·unknown
+       12 │ + ····readonly·[constantCase:·number]:·unknown
+    13 13 │   
+    14 14 │       readonly [snake_case: symbol]: unknown
+  
+
+```
+
+```
+invalidIndexParameter.ts:14:15 lint/style/useNamingConvention  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This index parameter name should be in camelCase.
+  
+    12 │     readonly [CONSTANT_CASE: number]: unknown
+    13 │ 
+  > 14 │     readonly [snake_case: symbol]: unknown
+       │               ^^^^^^^^^^
+    15 │ }
   
   i Safe fix: Rename this symbol in camelCase.
   
-    4 4 │       [CONSTANT_CASE: number]: unknown
-    5 5 │   
-    6   │ - ····[snake_case:·symbol]:·unknown
-      6 │ + ····[snakeCase:·symbol]:·unknown
-    7 7 │   }
+    12 12 │       readonly [CONSTANT_CASE: number]: unknown
+    13 13 │   
+    14    │ - ····readonly·[snake_case:·symbol]:·unknown
+       14 │ + ····readonly·[snakeCase:·symbol]:·unknown
+    15 15 │   }
   
 
 ```
diff --git a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validIndexParameter.ts b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validIndexParameter.ts
index 3cb9b1d49c63..57c8fce96666 100644
--- a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validIndexParameter.ts
+++ b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validIndexParameter.ts
@@ -4,4 +4,12 @@ export interface X {
     [index: number]: unknown
 
     [specialSymbol: symbol]: unknown
+}
+
+export interface Y {
+    readonly [s: string]: unknown;
+
+    readonly [index: number]: unknown;
+
+    readonly [specialSymbol: symbol]: unknown;
 }
\ No newline at end of file
diff --git a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validIndexParameter.ts.snap b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validIndexParameter.ts.snap
index 857714ec7281..c501d2b68da6 100644
--- a/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validIndexParameter.ts.snap
+++ b/crates/biome_js_analyze/tests/specs/style/useNamingConvention/validIndexParameter.ts.snap
@@ -11,6 +11,12 @@ export interface X {
 
     [specialSymbol: symbol]: unknown
 }
-```
 
+export interface Y {
+    readonly [s: string]: unknown;
+
+    readonly [index: number]: unknown;
 
+    readonly [specialSymbol: symbol]: unknown;
+}
+```