Repository: angular-eslint/angular-eslint
Pull request #2187: feat(eslint-plugin): introduce sort keys in type-decorator rule
Author: login: bschaeublin
Created: 2024-12-20T23:39:38Z | Merged: 2025-05-08T21:07:54Z

Fixes #797 

This PR introduces the ability to sort keys within type decorators (e.g., `@Component`, `@NgModule`, etc.) in the `type-decorator-order` rule.

**Motivation:**
This addresses issue #797, which has been open for a while. I was interested in this functionality and, since it hadn't been implemented yet, I decided to take it on as a way to learn more about ESLint rules, AST parsing, and contribute to the angular-eslint project. This is my first PR in this repository and my first time creating a new ESLint rule, so any guidance and feedback are greatly appreciated!

**Changes:**
- Added a new rule `sort-keys-in-type-decorator` to ensure consistent key ordering in type decorators such as `Component`, `Directive`, `NgModule`, and `Pipe`.
- Users can specify a custom sort order, or rely on the default sort order for certain decorators (the default sort is currently taken from the Angular CLI schematics).

**Areas for Discussion/Improvement:**
I'd love to get feedback on the following points to ensure this PR aligns well with the project's goals and coding style:

1.  **Default Sorts from Angular CLI Schematics:**
    - The current implementation uses default sorts for some decorators based on the order generated by Angular CLI schematics.
    - [x] after discussing we can expect few changes in that matter, but added substitutions (template/templateUrl, etc).

2.  **Using Enums vs. Hardcoded Decorator Sets:**
    -   Currently, the rule uses a finite, hardcoded set of decorators. It might be more maintainable to use existing enums like `AngularClassDecorators`.
    -  Is there a way to use enums that can be reflected effectively in the rule's schema? I wasn't sure what is the right way to go.
    - [x] the current proposal is to hardcode the supported decorators in the configuration, as it would be not much of a benefit. Together with another approach, allowing for an unlimited number of properties, rather than a fixed set.  While this avoids the need to regularly update the known properties for each decorator, it does mean we'll lose better schema support during configuration. This could be discussed or changed if needed.

I'm eager to learn and improve this PR based on your feedback. Thanks in advance for your time and help!

Linked issue #797: [sort-keys-in-type-decorator] Auto sort keys in angular decorators
**Description and reproduction of the issue**

```ts
@Component({
    styleUrls: ['./money.style.less'],
    selector: 'money',
    changeDetection: ChangeDetectionStrategy.OnPush,
    templateUrl: './money.template.html',
})
```

It would be really cool if we could sort the keys automatically

`eslint . --fix`

```ts
@Component({
    selector: 'money',
    templateUrl: './money.template.html',
    styleUrls: ['./money.style.less'],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
```

```JSON
{
  "rules": {
    "@angular-eslint/sort-keys-in-type-decorator": [
       {
          "Component": ["selector", "templateUrl", "styleUrls", "changeDetection"]
       }
    ]
  }
}
```



Diff:
diff --git a/packages/angular-eslint/src/configs/ts-all.ts b/packages/angular-eslint/src/configs/ts-all.ts
index 2be622fa7..fa71b3684 100644
--- a/packages/angular-eslint/src/configs/ts-all.ts
+++ b/packages/angular-eslint/src/configs/ts-all.ts
@@ -50,6 +50,7 @@ export default (
       '@angular-eslint/require-lifecycle-on-prototype': 'error',
       '@angular-eslint/require-localize-metadata': 'error',
       '@angular-eslint/runtime-localize': 'error',
+      '@angular-eslint/sort-keys-in-type-decorator': 'error',
       '@angular-eslint/sort-lifecycle-methods': 'error',
       '@angular-eslint/use-component-selector': 'error',
       '@angular-eslint/use-component-view-encapsulation': 'error',
diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index 1058e5c6d..bade7d022 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -73,6 +73,7 @@ Please see https://github.com/angular-eslint/angular-eslint for full usage instr
 | [`relative-url-prefix`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/relative-url-prefix.md) | The ./ and ../ prefix is standard syntax for relative URLs; don't depend on Angular's current ability to do without that prefix. See more at https://angular.dev/style-guide#style-05-04 |  |  |  |
 | [`require-localize-metadata`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/require-localize-metadata.md) | Ensures that $localize tagged messages contain helpful metadata to aid with translations. |  |  |  |
 | [`runtime-localize`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/runtime-localize.md) | Ensures that $localize tagged messages can use runtime-loaded translations. |  |  |  |
+| [`sort-keys-in-type-decorator`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/sort-keys-in-type-decorator.md) | Ensures that keys in type decorators (Component, Directive, NgModule, Pipe) are sorted in a consistent order |  | :wrench: |  |
 | [`use-component-selector`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/use-component-selector.md) | Component selector must be declared |  |  |  |
 | [`use-component-view-encapsulation`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/use-component-view-encapsulation.md) | Disallows using `ViewEncapsulation.None` |  |  | :bulb: |
 | [`use-injectable-provided-in`](https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/docs/rules/use-injectable-provided-in.md) | Using the `providedIn` property makes `Injectables` tree-shakable |  |  | :bulb: |
diff --git a/packages/eslint-plugin/docs/rules/sort-keys-in-type-decorator.md b/packages/eslint-plugin/docs/rules/sort-keys-in-type-decorator.md
new file mode 100644
index 000000000..d4793e734
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/sort-keys-in-type-decorator.md
@@ -0,0 +1,917 @@
+<!--
+
+  DO NOT EDIT.
+
+  This markdown file was autogenerated using a mixture of the following files as the source of truth for its data:
+  - ../../src/rules/sort-keys-in-type-decorator.ts
+  - ../../tests/rules/sort-keys-in-type-decorator/cases.ts
+
+  In order to update this file, it is therefore those files which need to be updated, as well as potentially the generator script:
+  - ../../../../tools/scripts/generate-rule-docs.ts
+
+-->
+
+<br>
+
+# `@angular-eslint/sort-keys-in-type-decorator`
+
+Ensures that keys in type decorators (Component, Directive, NgModule, Pipe) are sorted in a consistent order
+
+- Type: suggestion
+- 🔧 Supports autofix (`--fix`)
+
+<br>
+
+## Rule Options
+
+The rule accepts an options object with the following properties:
+
+```ts
+interface Options {
+  /**
+   * Default: `["selector","imports","standalone","templateUrl","template","styleUrl","styleUrls","styles","encapsulation","changeDetection"]`
+   */
+  Component?: string[];
+  /**
+   * Default: `["selector","standalone"]`
+   */
+  Directive?: string[];
+  /**
+   * Default: `["declarations","imports","exports","providers","bootstrap"]`
+   */
+  NgModule?: string[];
+  /**
+   * Default: `["name","standalone"]`
+   */
+  Pipe?: string[];
+}
+
+```
+
+<br>
+
+## Usage Examples
+
+> The following examples are generated automatically from the actual unit tests within the plugin, so you can be assured that their behavior is accurate based on the current commit.
+
+<br>
+
+<details>
+<summary>❌ - Toggle examples of <strong>incorrect</strong> code for this rule</summary>
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Component": [
+          "selector",
+          "imports",
+          "standalone",
+          "templateUrl",
+          "styleUrl",
+          "encapsulation",
+          "changeDetection"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@Component({
+  changeDetection: ChangeDetectionStrategy.OnPush,
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  selector: 'app-root',
+  imports: [CommonModule],
+  standalone: true,
+  templateUrl: './app.component.html',
+  styleUrl: './app.component.css',
+  encapsulation: ViewEncapsulation.None
+})
+class Test {
+}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Directive": [
+          "selector",
+          "standalone"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@Directive({
+  standalone: true,
+  ~~~~~~~~~~~~~~~~
+  selector: '[app-test]'
+})
+class Test {
+}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "NgModule": [
+          "declarations",
+          "imports"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@NgModule({
+  imports: [CommonModule],
+  ~~~~~~~~~~~~~~~~~~~~~~~
+  declarations: [AppComponent]
+})
+class Test {
+}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Pipe": [
+          "name",
+          "standalone"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@Pipe({
+  standalone: true,
+  ~~~~~~~~~~~~~~~~
+  name: 'myPipe'
+})
+class Test {
+}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Component": [
+          "selector",
+          "imports",
+          "standalone",
+          "templateUrl",
+          "styleUrl",
+          "encapsulation",
+          "changeDetection"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@Component({
+  styleUrl: './app.component.css',
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  selector: 'app-root',
+  templateUrl: './app.component.html'
+})
+class Test {
+}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "NgModule": [
+          "declarations",
+          "imports",
+          "exports",
+          "providers",
+          "bootstrap"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@NgModule({
+  exports: [AppComponent],
+  ~~~~~~~~~~~~~~~~~~~~~~~
+  declarations: [AppComponent]
+})
+class Test {
+}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Component": [
+          "selector",
+          "styleUrl"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@Component({
+  styleUrl: './app.component.css', // Inline comment for styleUrl
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  selector: 'app-root' // Inline comment for selector
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Component": [
+          "selector",
+          "styleUrl"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@Component({
+  // Comment above styleUrl
+  styleUrl: './app.component.css',
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  // Comment above selector
+  selector: 'app-root'
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Component": [
+          "selector",
+          "styleUrl"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@Component({
+  /* This is a multi-line comment
+     above styleUrl property */
+  styleUrl: './app.component.css',
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  /* This is a multi-line comment
+     above selector property */
+  selector: 'app-root'
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Component": [
+          "selector",
+          "imports",
+          "standalone",
+          "templateUrl",
+          "styleUrl",
+          "encapsulation",
+          "changeDetection"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ❌ Invalid Code
+
+```ts
+@Component({
+  // Comment above changeDetection
+  changeDetection: ChangeDetectionStrategy.OnPush, // Inline comment for changeDetection
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+  /* Multi-line comment
+     above selector */
+  selector: 'app-root', /* Inline multi-line comment after selector */
+  // Comment above imports
+  imports: [
+    // Comment inside imports array
+    CommonModule, // Comment after CommonModule
+    FormsModule /* Comment after FormsModule */
+  ],
+  /* Comment above standalone */
+  standalone: true, // Comment after standalone
+  // Comment above templateUrl
+  templateUrl: './app.component.html',
+  /* Multi-line comment
+     above styleUrl */
+  styleUrl: './app.component.css',
+  // Comment above encapsulation
+  encapsulation: ViewEncapsulation.None /* Inline comment for encapsulation */
+})
+class Test {}
+```
+
+</details>
+
+<br>
+
+---
+
+<br>
+
+<details>
+<summary>✅ - Toggle examples of <strong>correct</strong> code for this rule</summary>
+
+<br>
+
+#### Default Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error"
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@Type({
+  a: 'a',
+  b: 'b',
+  c: 'c'
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Default Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error"
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@Type({})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Default Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error"
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@Type({
+  a: 'a'
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Component": [
+          "selector",
+          "imports",
+          "standalone",
+          "templateUrl",
+          "styleUrl",
+          "encapsulation",
+          "changeDetection"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@Component({
+  selector: 'app-root',
+  imports: [CommonModule],
+  standalone: true,
+  templateUrl: './app.component.html',
+  styleUrl: './app.component.css',
+  encapsulation: ViewEncapsulation.None,
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Directive": [
+          "selector",
+          "standalone"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@Directive({
+  selector: '[app-test]',
+  standalone: true
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "NgModule": [
+          "declarations",
+          "imports"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@NgModule({
+  declarations: [AppComponent],
+  imports: [CommonModule]
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Pipe": [
+          "name",
+          "standalone"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@Pipe({
+  name: 'myPipe',
+  standalone: true
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Component": [
+          "selector",
+          "imports",
+          "standalone",
+          "templateUrl",
+          "styleUrl",
+          "encapsulation",
+          "changeDetection"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@Component({
+  selector: 'app-root',
+  templateUrl: './app.component.html',
+  styleUrl: './app.component.css'
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "NgModule": [
+          "declarations",
+          "imports",
+          "exports",
+          "providers",
+          "bootstrap"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@NgModule({
+  declarations: [AppComponent],
+  exports: [AppComponent]
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "Component": [
+          "selector",
+          "imports",
+          "standalone",
+          "templateUrl",
+          "styleUrl",
+          "encapsulation",
+          "changeDetection"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@Component({
+  // Comment above selector
+  selector: 'app-root', // Inline comment for selector
+  /* Multi-line comment
+     above imports */
+  imports: [
+    CommonModule,
+    FormsModule
+  ], // Inline comment after imports
+  standalone: true,
+  templateUrl: './app.component.html',
+  styleUrl: './app.component.css',
+  // Comment above encapsulation
+  encapsulation: ViewEncapsulation.None,
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+class Test {}
+```
+
+<br>
+
+---
+
+<br>
+
+#### Custom Config
+
+```json
+{
+  "rules": {
+    "@angular-eslint/sort-keys-in-type-decorator": [
+      "error",
+      {
+        "NgModule": [
+          "declarations",
+          "imports",
+          "exports",
+          "providers",
+          "bootstrap"
+        ]
+      }
+    ]
+  }
+}
+```
+
+<br>
+
+#### ✅ Valid Code
+
+```ts
+@NgModule({
+  // Leading comment for declarations
+  declarations: [
+    /* Component list comment */
+    AppComponent,
+    HeaderComponent
+  ],
+  imports: [
+    CommonModule, // Common module comment
+    RouterModule /* Router module comment */
+  ],
+  /* Multi-line export comment
+     with multiple lines
+     before the property */
+  exports: [AppComponent]
+})
+class Test {}
+```
+
+</details>
+
+<br>
diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json
index 433f79d34..e34434e1a 100644
--- a/packages/eslint-plugin/src/configs/all.json
+++ b/packages/eslint-plugin/src/configs/all.json
@@ -36,6 +36,7 @@
     "@angular-eslint/require-lifecycle-on-prototype": "error",
     "@angular-eslint/require-localize-metadata": "error",
     "@angular-eslint/runtime-localize": "error",
+    "@angular-eslint/sort-keys-in-type-decorator": "error",
     "@angular-eslint/sort-lifecycle-methods": "error",
     "@angular-eslint/use-component-selector": "error",
     "@angular-eslint/use-component-view-encapsulation": "error",
diff --git a/packages/eslint-plugin/src/index.ts b/packages/eslint-plugin/src/index.ts
index 65c28d877..5c6b3824b 100644
--- a/packages/eslint-plugin/src/index.ts
+++ b/packages/eslint-plugin/src/index.ts
@@ -34,6 +34,9 @@ import noAttributeDecorator, {
 import noConflictingLifecycle, {
   RULE_NAME as noConflictingLifecycleRuleName,
 } from './rules/no-conflicting-lifecycle';
+import noDuplicatesInMetadataArrays, {
+  RULE_NAME as noDuplicatesInMetadataArraysRuleName,
+} from './rules/no-duplicates-in-metadata-arrays';
 import noEmptyLifecycleMethod, {
   RULE_NAME as noEmptyLifecycleMethodRuleName,
 } from './rules/no-empty-lifecycle-method';
@@ -100,6 +103,9 @@ import requireLocalizeMetadata, {
 import runtimeLocalize, {
   RULE_NAME as runtimeLocalizeRuleName,
 } from './rules/runtime-localize';
+import sortKeysInTypeDecorator, {
+  RULE_NAME as sortKeysInTypeDecoratorRuleName,
+} from './rules/sort-keys-in-type-decorator';
 import sortLifecycleMethods, {
   RULE_NAME as sortLifecycleMethodsRuleName,
 } from './rules/sort-lifecycle-methods';
@@ -118,9 +124,6 @@ import useLifecycleInterface, {
 import usePipeTransformInterface, {
   RULE_NAME as usePipeTransformInterfaceRuleName,
 } from './rules/use-pipe-transform-interface';
-import noDuplicatesInMetadataArrays, {
-  RULE_NAME as noDuplicatesInMetadataArraysRuleName,
-} from './rules/no-duplicates-in-metadata-arrays';
 
 export = {
   configs: {
@@ -163,6 +166,7 @@ export = {
     [requireLifecycleOnPrototypeRuleName]: requireLifecycleOnPrototype,
     [requireLocalizeMetadataRuleName]: requireLocalizeMetadata,
     [runtimeLocalizeRuleName]: runtimeLocalize,
+    [sortKeysInTypeDecoratorRuleName]: sortKeysInTypeDecorator,
     [sortLifecycleMethodsRuleName]: sortLifecycleMethods,
     [useComponentSelectorRuleName]: useComponentSelector,
     [useComponentViewEncapsulationRuleName]: useComponentViewEncapsulation,
diff --git a/packages/eslint-plugin/src/rules/sort-keys-in-type-decorator.ts b/packages/eslint-plugin/src/rules/sort-keys-in-type-decorator.ts
new file mode 100644
index 000000000..1048743c6
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/sort-keys-in-type-decorator.ts
@@ -0,0 +1,256 @@
+import { ASTUtils, CommentUtils, Selectors } from '@angular-eslint/utils';
+import { TSESLint, TSESTree } from '@typescript-eslint/utils';
+import { createESLintRule } from '../utils/create-eslint-rule';
+
+export type Options = [
+  {
+    [key: string]: string[];
+  },
+];
+
+export type MessageIds = 'incorrectOrder';
+
+const DEFAULT_ORDER = {
+  Component: [
+    'selector',
+    'imports',
+    'standalone',
+    'templateUrl',
+    'template',
+    'styleUrl',
+    'styleUrls',
+    'styles',
+    'encapsulation',
+    'changeDetection',
+  ],
+  Directive: ['selector', 'standalone'],
+  NgModule: ['declarations', 'imports', 'exports', 'providers', 'bootstrap'],
+  Pipe: ['name', 'standalone'],
+};
+
+export const RULE_NAME = 'sort-keys-in-type-decorator';
+
+export default createESLintRule<Options, MessageIds>({
+  name: RULE_NAME,
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Ensures that keys in type decorators (Component, Directive, NgModule, Pipe) are sorted in a consistent order',
+    },
+    fixable: 'code',
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          Component: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+          Directive: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+          NgModule: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+          Pipe: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+        },
+        additionalProperties: false,
+      },
+    ],
+    messages: {
+      incorrectOrder:
+        'Keys in @{{decorator}} decorator should be ordered: {{expectedOrder}}',
+    },
+  },
+  defaultOptions: [DEFAULT_ORDER],
+  create(
+    context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
+    [orderConfig]: Readonly<Options>,
+  ) {
+    function checkContext(
+      node: TSESTree.Decorator,
+      decoratorName: string,
+    ): void {
+      const expectedOrder = orderConfig[decoratorName];
+      if (!expectedOrder) {
+        return;
+      }
+
+      const argument = ASTUtils.getDecoratorArgument(node);
+      if (!argument) {
+        return;
+      }
+
+      const properties = ASTUtils.getDecoratorProperties(node);
+      if (properties.length <= 1) {
+        return;
+      }
+
+      const firstConfiguredIndex = properties.findIndex(({ key }) =>
+        expectedOrder.includes((key as TSESTree.Identifier).name),
+      );
+      const lastNonConfiguredIndex = properties.findIndex(
+        ({ key }) => !expectedOrder.includes((key as TSESTree.Identifier).name),
+      );
+
+      if (
+        firstConfiguredIndex !== -1 &&
+        lastNonConfiguredIndex !== -1 &&
+        lastNonConfiguredIndex < firstConfiguredIndex
+      ) {
+        createInvalidSortRuleForDecorator(
+          context,
+          decoratorName,
+          expectedOrder,
+          properties,
+          properties[lastNonConfiguredIndex],
+        );
+        return;
+      }
+
+      const configuredProperties = properties.filter(({ key }) =>
+        expectedOrder.includes((key as TSESTree.Identifier).name),
+      );
+
+      if (configuredProperties.length) {
+        const actualConfiguredOrder = configuredProperties.map(
+          ({ key }) => (key as TSESTree.Identifier).name,
+        );
+        const expectedConfiguredOrder = expectedOrder.filter((key: string) =>
+          actualConfiguredOrder.includes(key),
+        );
+
+        if (
+          actualConfiguredOrder.length &&
+          JSON.stringify(actualConfiguredOrder) !==
+            JSON.stringify(expectedConfiguredOrder)
+        ) {
+          const firstOutOfOrderIndex = actualConfiguredOrder.findIndex(
+            (key, index) => key !== expectedConfiguredOrder[index],
+          );
+          const outOfOrderProperty = configuredProperties[firstOutOfOrderIndex];
+
+          createInvalidSortRuleForDecorator(
+            context,
+            decoratorName,
+            expectedOrder,
+            properties,
+            outOfOrderProperty,
+          );
+        }
+      }
+    }
+
+    return {
+      [Selectors.COMPONENT_CLASS_DECORATOR](node: TSESTree.Decorator) {
+        checkContext(node, ASTUtils.AngularClassDecorators.Component);
+      },
+      [Selectors.DIRECTIVE_CLASS_DECORATOR](node: TSESTree.Decorator) {
+        checkContext(node, ASTUtils.AngularClassDecorators.Directive);
+      },
+      [Selectors.INJECTABLE_CLASS_DECORATOR](node: TSESTree.Decorator) {
+        checkContext(node, ASTUtils.AngularClassDecorators.Injectable);
+      },
+      [Selectors.MODULE_CLASS_DECORATOR](node: TSESTree.Decorator) {
+        checkContext(node, ASTUtils.AngularClassDecorators.NgModule);
+      },
+      [Selectors.PIPE_CLASS_DECORATOR](node: TSESTree.Decorator) {
+        checkContext(node, ASTUtils.AngularClassDecorators.Pipe);
+      },
+    };
+  },
+});
+
+function createInvalidSortRuleForDecorator(
+  context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
+  decoratorName: string,
+  expectedOrder: string[],
+  properties: TSESTree.Property[],
+  node: TSESTree.Property,
+): void {
+  const presentProps = properties.map(
+    (prop) => (prop.key as TSESTree.Identifier).name,
+  );
+
+  const relevantExpectedOrder = expectedOrder.filter((propName) =>
+    presentProps.includes(propName),
+  );
+
+  const data = {
+    decorator: decoratorName,
+    expectedOrder: relevantExpectedOrder.join(', '),
+  };
+
+  reportAndFix(
+    context,
+    node,
+    'incorrectOrder',
+    data,
+    properties,
+    expectedOrder,
+    node.parent as TSESTree.Expression,
+  );
+}
+
+function reportAndFix(
+  context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
+  node: TSESTree.Property,
+  messageId: MessageIds,
+  data: { decorator: string; expectedOrder: string },
+  properties: TSESTree.Property[],
+  expectedOrder: string[],
+  objectExpression: TSESTree.Expression,
+): void {
+  const sourceCode = context.sourceCode;
+
+  context.report({
+    node,
+    messageId,
+    data,
+    fix(fixer) {
+      const indentation = CommentUtils.getObjectIndentation(
+        sourceCode,
+        objectExpression,
+      );
+
+      const propNames = properties.map(
+        (p) => (p.key as TSESTree.Identifier).name,
+      );
+      const filteredOrder = expectedOrder.filter((name) =>
+        propNames.includes(name),
+      );
+
+      const propInfoMap = CommentUtils.extractPropertyComments(
+        sourceCode,
+        properties,
+        objectExpression,
+        indentation,
+      );
+
+      const sortedText = CommentUtils.buildSortedPropertiesWithComments(
+        filteredOrder,
+        propInfoMap,
+        indentation,
+      );
+
+      return fixer.replaceText(
+        objectExpression,
+        `{\n${sortedText}\n${indentation.slice(0, -2)}}`,
+      );
+    },
+  });
+}
diff --git a/packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/cases.ts b/packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/cases.ts
new file mode 100644
index 000000000..4201c14e2
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/cases.ts
@@ -0,0 +1,606 @@
+import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/test-utils';
+import type {
+  InvalidTestCase,
+  ValidTestCase,
+} from '@typescript-eslint/rule-tester';
+import type {
+  MessageIds,
+  Options,
+} from '../../../src/rules/sort-keys-in-type-decorator';
+
+export const valid: readonly ValidTestCase<Options>[] = [
+  {
+    code: `
+    @Type({
+      a: 'a',
+      b: 'b',
+      c: 'c'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({})
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({
+      a: 'a'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root', // Inline comment for selector
+        /* Multi-line comment 
+           above imports */
+        imports: [
+          CommonModule,
+          FormsModule
+        ], // Inline comment after imports
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        // Leading comment for declarations
+        declarations: [
+          /* Component list comment */
+          AppComponent,
+          HeaderComponent
+        ],
+        imports: [
+          CommonModule, // Common module comment
+          RouterModule /* Router module comment */
+        ],
+        /* Multi-line export comment
+           with multiple lines
+           before the property */
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+];
+
+export const invalid: readonly InvalidTestCase<MessageIds, Options>[] = [
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Component decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Component({
+        changeDetection: ChangeDetectionStrategy.OnPush,
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Directive decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Directive({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        selector: '[app-test]'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Directive',
+      expectedOrder: 'selector, standalone',
+    },
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when NgModule decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @NgModule({
+        imports: [CommonModule],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, imports',
+    },
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Pipe decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Pipe({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        name: 'myPipe'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Pipe',
+      expectedOrder: 'name, standalone',
+    },
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should fail when partial properties are not in correct order',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        templateUrl: './app.component.html'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, templateUrl, styleUrl',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when partial NgModule properties are not in correct order',
+    annotatedSource: `
+      @NgModule({
+        exports: [AppComponent],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, exports',
+    },
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should handle basic inline comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css', // Inline comment for styleUrl
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root' // Inline comment for selector
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root', // Inline comment for selector
+        styleUrl: './app.component.css' // Inline comment for styleUrl
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve leading comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above styleUrl
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        // Comment above selector
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root',
+        // Comment above styleUrl
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve multi-line comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root',
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should properly handle comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush, // Inline comment for changeDetection
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None /* Inline comment for encapsulation */
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None, /* Inline comment for encapsulation */
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush // Inline comment for changeDetection
+      })
+      class Test {}
+    `,
+  }),
+];
diff --git a/packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/spec.ts b/packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/spec.ts
new file mode 100644
index 000000000..be8e7fb7d
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/spec.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '@angular-eslint/test-utils';
+import rule, {
+  RULE_NAME,
+} from '../../../src/rules/sort-keys-in-type-decorator';
+import { invalid, valid } from './cases';
+
+const ruleTester = new RuleTester();
+
+ruleTester.run(RULE_NAME, rule, {
+  valid,
+  invalid,
+});
diff --git a/packages/utils/src/eslint-plugin/ast-utils.ts b/packages/utils/src/eslint-plugin/ast-utils.ts
index 24af11c0b..e5f375b96 100644
--- a/packages/utils/src/eslint-plugin/ast-utils.ts
+++ b/packages/utils/src/eslint-plugin/ast-utils.ts
@@ -442,13 +442,19 @@ export function getPropertyDefinitionName({
   );
 }
 
+export const getDecoratorProperties = (
+  decorator: TSESTree.Decorator,
+): TSESTree.Property[] => {
+  return getDecoratorArgument(decorator)?.properties.filter(isProperty) ?? [];
+};
+
 export const getDecoratorProperty = (
   decorator: TSESTree.Decorator,
   name: string,
 ): TSESTree.Property | undefined => {
-  return getDecoratorArgument(decorator)
-    ?.properties.filter(isProperty)
-    .find(({ key }) => ASTUtils.isIdentifier(key) && key.name === name);
+  return getDecoratorProperties(decorator).find(
+    ({ key }) => ASTUtils.isIdentifier(key) && key.name === name,
+  );
 };
 
 export const getDecoratorPropertyValue = (
diff --git a/packages/utils/src/eslint-plugin/comment-utils.ts b/packages/utils/src/eslint-plugin/comment-utils.ts
new file mode 100644
index 000000000..e303fda31
--- /dev/null
+++ b/packages/utils/src/eslint-plugin/comment-utils.ts
@@ -0,0 +1,128 @@
+import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
+
+export interface PropInfo {
+  name: string;
+  leadingComments: string[];
+  value: string;
+  trailingComments: string[];
+}
+
+export function extractPropertyComments(
+  sourceCode: Readonly<TSESLint.SourceCode>,
+  properties: TSESTree.Property[],
+  objectExpression: TSESTree.Expression,
+  indentation: string,
+): Map<string, PropInfo> {
+  const allComments = sourceCode.getAllComments();
+  const processedCommentRanges = new Set<string>();
+  const propInfoMap = new Map<string, PropInfo>();
+  const commentLineMap = new Map<number, TSESTree.Comment[]>();
+
+  for (const comment of allComments) {
+    const line = sourceCode.getLocFromIndex(comment.range[0]).line;
+    if (!commentLineMap.has(line)) {
+      commentLineMap.set(line, []);
+    }
+    commentLineMap.get(line)?.push(comment);
+  }
+
+  const makeRangeKey = (start: number, end: number) => `${start}-${end}`;
+
+  for (let i = 0; i < properties.length; i++) {
+    const prop = properties[i];
+    const name = (prop.key as TSESTree.Identifier).name;
+    const propRange = prop.range;
+    const leadingComments: string[] = [];
+    const prevPropEnd =
+      i > 0 ? properties[i - 1].range[1] : objectExpression.range[0] + 1;
+
+    for (const comment of allComments) {
+      const rangeKey = makeRangeKey(comment.range[0], comment.range[1]);
+      if (
+        comment.range[0] > prevPropEnd &&
+        comment.range[0] < propRange[0] &&
+        !processedCommentRanges.has(rangeKey)
+      ) {
+        leadingComments.push(indentation + sourceCode.getText(comment));
+        processedCommentRanges.add(rangeKey);
+      }
+    }
+
+    const propText = sourceCode.getText(prop).replace(/,\s*$/, '');
+    const trailingComments: string[] = [];
+    const propEndLine = sourceCode.getLocFromIndex(propRange[1]).line;
+
+    if (commentLineMap.has(propEndLine)) {
+      const commentsOnLine = commentLineMap.get(propEndLine) || [];
+      for (const comment of commentsOnLine) {
+        const rangeKey = makeRangeKey(comment.range[0], comment.range[1]);
+        if (
+          comment.range[0] > propRange[1] &&
+          !processedCommentRanges.has(rangeKey)
+        ) {
+          const spaceBefore = sourceCode
+            .getText()
+            .substring(propRange[1], comment.range[0])
+            .replace(/,/g, '');
+
+          trailingComments.push(spaceBefore + sourceCode.getText(comment));
+          processedCommentRanges.add(rangeKey);
+        }
+      }
+    }
+
+    propInfoMap.set(name, {
+      name,
+      leadingComments,
+      value: propText,
+      trailingComments,
+    });
+  }
+
+  return propInfoMap;
+}
+
+export function buildSortedPropertiesWithComments(
+  filteredOrder: string[],
+  propInfoMap: Map<string, PropInfo>,
+  indentation: string,
+): string {
+  const sortedParts: string[] = [];
+
+  for (let i = 0; i < filteredOrder.length; i++) {
+    const propName = filteredOrder[i];
+    const info = propInfoMap.get(propName);
+
+    if (info) {
+      if (info.leadingComments.length > 0) {
+        sortedParts.push(...info.leadingComments);
+      }
+
+      const isLast = i === filteredOrder.length - 1;
+      let finalPropText = indentation + info.value;
+
+      if (!isLast) {
+        finalPropText += ',';
+      }
+
+      if (info.trailingComments.length > 0) {
+        info.trailingComments.forEach((comment) => {
+          finalPropText += comment;
+        });
+      }
+
+      sortedParts.push(finalPropText);
+    }
+  }
+
+  return sortedParts.join('\n');
+}
+
+export function getObjectIndentation(
+  sourceCode: Readonly<TSESLint.SourceCode>,
+  objectExpression: TSESTree.Expression,
+): string {
+  const objectExpressionText = sourceCode.getText(objectExpression);
+  const lines = objectExpressionText.split('\n');
+  return lines[1] ? lines[1].match(/^\s*/)?.[0] || '' : '';
+}
diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts
index 5b494d11c..34060cea3 100644
--- a/packages/utils/src/index.ts
+++ b/packages/utils/src/index.ts
@@ -12,6 +12,7 @@ export { getAriaAttributeKeys } from './eslint-plugin/get-aria-attribute-keys';
 export { getNativeEventNames } from './eslint-plugin/get-native-event-names';
 
 export * as ASTUtils from './eslint-plugin/ast-utils';
+export * as CommentUtils from './eslint-plugin/comment-utils';
 export * as RuleFixes from './eslint-plugin/rule-fixes';
 export * as Selectors from './eslint-plugin/selectors';
 export * as SelectorUtils from './eslint-plugin/selector-utils';


Reviews:
user: login: reduckted
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-01T10:29:17Z
user: login: bschaeublin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-01T16:11:00Z
user: login: bschaeublin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T00:16:06Z
user: login: JamesHenry
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:19:07Z
user: login: JamesHenry
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:19:40Z
user: login: bschaeublin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:06:41Z
user: login: JamesHenry
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T21:07:40Z

Review comments:
diff_hunk: @@ -0,0 +1,226 @@
+import { TSESLint, TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import { createESLintRule } from '../utils/create-eslint-rule';
+
+export type Options = [
+  {
+    [key: string]: string[];
+  },
+];
+
+export type MessageIds = 'incorrectOrder';
+
+const DEFAULT_ORDER = {
+  Component: [
+    'selector',
+    'imports',
+    'standalone',
+    'templateUrl',
+    'styleUrl',
+    'encapsulation',
+    'changeDetection',
+  ],
+  Directive: ['selector', 'standalone'],
+  NgModule: ['declarations', 'imports', 'exports', 'providers', 'bootstrap'],
+  Pipe: ['name', 'standalone'],
+};
+
+export const RULE_NAME = 'sort-keys-in-type-decorator';
+
+export default createESLintRule<Options, MessageIds>({
+  name: RULE_NAME,
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Ensures that keys in type decorators (Component, Directive, NgModule, Pipe) are sorted in a consistent order',
+    },
+    fixable: 'code',
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          Component: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+          Directive: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+          NgModule: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+          Pipe: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+        },
+        additionalProperties: false,
+      },
+    ],
+    messages: {
+      incorrectOrder:
+        'Keys in @{{decorator}} decorator should be ordered: {{expectedOrder}}',
+    },
+  },
+  defaultOptions: [DEFAULT_ORDER],
+  create(
+    context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
+    [orderConfig]: Readonly<Options>,
+  ) {
+    return {
+      Decorator(node: TSESTree.Decorator) {
path: packages/eslint-plugin/src/rules/sort-keys-in-type-decorator.ts
user: login: reduckted
body: I think the best way of selecting the decorators is to use the technique from `contextual-lifecycle`:
https://github.com/angular-eslint/angular-eslint/blob/79f2d5d606fae479d95a0a9a69976fbcbca8ab78/packages/eslint-plugin/src/rules/contextual-lifecycle.ts#L53-L69

This ensures that the decorators are selected the same way as they are for other rules that target the decorators.
created_at: 2025-02-01T10:26:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,226 @@
+import { TSESLint, TSESTree } from '@typescript-eslint/utils';
+import { AST_NODE_TYPES } from '@typescript-eslint/utils';
+import { createESLintRule } from '../utils/create-eslint-rule';
+
+export type Options = [
+  {
+    [key: string]: string[];
+  },
+];
+
+export type MessageIds = 'incorrectOrder';
+
+const DEFAULT_ORDER = {
+  Component: [
+    'selector',
+    'imports',
+    'standalone',
+    'templateUrl',
+    'styleUrl',
+    'encapsulation',
+    'changeDetection',
+  ],
+  Directive: ['selector', 'standalone'],
+  NgModule: ['declarations', 'imports', 'exports', 'providers', 'bootstrap'],
+  Pipe: ['name', 'standalone'],
+};
+
+export const RULE_NAME = 'sort-keys-in-type-decorator';
+
+export default createESLintRule<Options, MessageIds>({
+  name: RULE_NAME,
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'Ensures that keys in type decorators (Component, Directive, NgModule, Pipe) are sorted in a consistent order',
+    },
+    fixable: 'code',
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          Component: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+          Directive: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+          NgModule: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+          Pipe: {
+            type: 'array',
+            items: {
+              type: 'string',
+            },
+          },
+        },
+        additionalProperties: false,
+      },
+    ],
+    messages: {
+      incorrectOrder:
+        'Keys in @{{decorator}} decorator should be ordered: {{expectedOrder}}',
+    },
+  },
+  defaultOptions: [DEFAULT_ORDER],
+  create(
+    context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
+    [orderConfig]: Readonly<Options>,
+  ) {
+    return {
+      Decorator(node: TSESTree.Decorator) {
path: packages/eslint-plugin/src/rules/sort-keys-in-type-decorator.ts
user: login: bschaeublin
body: @reduckted Thank you for looking at the pr and your suggestion pointing me to the right direction. I've updated the PR reusing the pattern from `contextual-lifecycle`, as well reused some `ASTUtils` Helper methods instead of reinventing the wheel :).

If you got any other things I should take care of, let me know!
created_at: 2025-02-01T16:11:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,606 @@
+import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/test-utils';
+import type {
+  InvalidTestCase,
+  ValidTestCase,
+} from '@typescript-eslint/rule-tester';
+import type {
+  MessageIds,
+  Options,
+} from '../../../src/rules/sort-keys-in-type-decorator';
+
+export const valid: readonly ValidTestCase<Options>[] = [
+  {
+    code: `
+    @Type({
+      a: 'a',
+      b: 'b',
+      c: 'c'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({})
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({
+      a: 'a'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root', // Inline comment for selector
+        /* Multi-line comment 
+           above imports */
+        imports: [
+          CommonModule,
+          FormsModule
+        ], // Inline comment after imports
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        // Leading comment for declarations
+        declarations: [
+          /* Component list comment */
+          AppComponent,
+          HeaderComponent
+        ],
+        imports: [
+          CommonModule, // Common module comment
+          RouterModule /* Router module comment */
+        ],
+        /* Multi-line export comment
+           with multiple lines
+           before the property */
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+];
+
+export const invalid: readonly InvalidTestCase<MessageIds, Options>[] = [
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Component decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Component({
+        changeDetection: ChangeDetectionStrategy.OnPush,
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Directive decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Directive({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        selector: '[app-test]'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Directive',
+      expectedOrder: 'selector, standalone',
+    },
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when NgModule decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @NgModule({
+        imports: [CommonModule],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, imports',
+    },
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Pipe decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Pipe({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        name: 'myPipe'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Pipe',
+      expectedOrder: 'name, standalone',
+    },
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should fail when partial properties are not in correct order',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        templateUrl: './app.component.html'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, templateUrl, styleUrl',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when partial NgModule properties are not in correct order',
+    annotatedSource: `
+      @NgModule({
+        exports: [AppComponent],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, exports',
+    },
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should handle basic inline comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css', // Inline comment for styleUrl
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root' // Inline comment for selector
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root', // Inline comment for selector
+        styleUrl: './app.component.css' // Inline comment for styleUrl
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve leading comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above styleUrl
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        // Comment above selector
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root',
+        // Comment above styleUrl
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve multi-line comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root',
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should properly handle comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush, // Inline comment for changeDetection
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None /* Inline comment for encapsulation */
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None, /* Inline comment for encapsulation */
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush // Inline comment for changeDetection
+      })
+      class Test {}
path: packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/cases.ts
user: login: bschaeublin
body: This is a more complex combined test based on scenarios from your input:
- It ensures that comments above as well as inline comments get sorted correctly with their associated property (see `changeDetection`)
- It also ensures that a comma gets inserted between the property and the comment when it isn't the last property anymore to prevent syntax errors (see `encapsulation`)

All other tests above this ensure dedicated support for multiline comments, inline multi-line comments, and general autofix capabilities.

@JamesHenry Let me know what you think or if you have any other suggestions.
All added tests with maintaining the comments-structure are green currently.

By resorting the properties, newlines between properties are removed by the fixer if there are any between the properties. Let me know if we should support preserving those newlines as well - but that could be seen as a formatting concern rather than a sorting issue.
created_at: 2025-03-27T00:16:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,606 @@
+import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/test-utils';
+import type {
+  InvalidTestCase,
+  ValidTestCase,
+} from '@typescript-eslint/rule-tester';
+import type {
+  MessageIds,
+  Options,
+} from '../../../src/rules/sort-keys-in-type-decorator';
+
+export const valid: readonly ValidTestCase<Options>[] = [
+  {
+    code: `
+    @Type({
+      a: 'a',
+      b: 'b',
+      c: 'c'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({})
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({
+      a: 'a'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root', // Inline comment for selector
+        /* Multi-line comment 
+           above imports */
+        imports: [
+          CommonModule,
+          FormsModule
+        ], // Inline comment after imports
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        // Leading comment for declarations
+        declarations: [
+          /* Component list comment */
+          AppComponent,
+          HeaderComponent
+        ],
+        imports: [
+          CommonModule, // Common module comment
+          RouterModule /* Router module comment */
+        ],
+        /* Multi-line export comment
+           with multiple lines
+           before the property */
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+];
+
+export const invalid: readonly InvalidTestCase<MessageIds, Options>[] = [
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Component decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Component({
+        changeDetection: ChangeDetectionStrategy.OnPush,
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Directive decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Directive({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        selector: '[app-test]'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Directive',
+      expectedOrder: 'selector, standalone',
+    },
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when NgModule decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @NgModule({
+        imports: [CommonModule],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, imports',
+    },
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Pipe decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Pipe({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        name: 'myPipe'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Pipe',
+      expectedOrder: 'name, standalone',
+    },
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should fail when partial properties are not in correct order',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        templateUrl: './app.component.html'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, templateUrl, styleUrl',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when partial NgModule properties are not in correct order',
+    annotatedSource: `
+      @NgModule({
+        exports: [AppComponent],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, exports',
+    },
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should handle basic inline comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css', // Inline comment for styleUrl
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root' // Inline comment for selector
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root', // Inline comment for selector
+        styleUrl: './app.component.css' // Inline comment for styleUrl
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve leading comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above styleUrl
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        // Comment above selector
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root',
+        // Comment above styleUrl
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve multi-line comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root',
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should properly handle comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush, // Inline comment for changeDetection
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None /* Inline comment for encapsulation */
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None, /* Inline comment for encapsulation */
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush // Inline comment for changeDetection
+      })
+      class Test {}
path: packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/cases.ts
user: login: JamesHenry
body: Nice work!
created_at: 2025-05-08T13:19:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,606 @@
+import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/test-utils';
+import type {
+  InvalidTestCase,
+  ValidTestCase,
+} from '@typescript-eslint/rule-tester';
+import type {
+  MessageIds,
+  Options,
+} from '../../../src/rules/sort-keys-in-type-decorator';
+
+export const valid: readonly ValidTestCase<Options>[] = [
+  {
+    code: `
+    @Type({
+      a: 'a',
+      b: 'b',
+      c: 'c'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({})
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({
+      a: 'a'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root', // Inline comment for selector
+        /* Multi-line comment 
+           above imports */
+        imports: [
+          CommonModule,
+          FormsModule
+        ], // Inline comment after imports
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        // Leading comment for declarations
+        declarations: [
+          /* Component list comment */
+          AppComponent,
+          HeaderComponent
+        ],
+        imports: [
+          CommonModule, // Common module comment
+          RouterModule /* Router module comment */
+        ],
+        /* Multi-line export comment
+           with multiple lines
+           before the property */
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+];
+
+export const invalid: readonly InvalidTestCase<MessageIds, Options>[] = [
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Component decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Component({
+        changeDetection: ChangeDetectionStrategy.OnPush,
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Directive decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Directive({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        selector: '[app-test]'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Directive',
+      expectedOrder: 'selector, standalone',
+    },
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when NgModule decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @NgModule({
+        imports: [CommonModule],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, imports',
+    },
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Pipe decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Pipe({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        name: 'myPipe'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Pipe',
+      expectedOrder: 'name, standalone',
+    },
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should fail when partial properties are not in correct order',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        templateUrl: './app.component.html'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, templateUrl, styleUrl',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when partial NgModule properties are not in correct order',
+    annotatedSource: `
+      @NgModule({
+        exports: [AppComponent],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, exports',
+    },
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should handle basic inline comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css', // Inline comment for styleUrl
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root' // Inline comment for selector
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root', // Inline comment for selector
+        styleUrl: './app.component.css' // Inline comment for styleUrl
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve leading comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above styleUrl
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        // Comment above selector
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root',
+        // Comment above styleUrl
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve multi-line comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root',
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should properly handle comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush, // Inline comment for changeDetection
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None /* Inline comment for encapsulation */
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None, /* Inline comment for encapsulation */
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush // Inline comment for changeDetection
+      })
+      class Test {}
path: packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/cases.ts
user: login: JamesHenry
body: Sorry for the delay I moved across the country I live in and it's been a very busy time at work
created_at: 2025-05-08T13:19:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,606 @@
+import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/test-utils';
+import type {
+  InvalidTestCase,
+  ValidTestCase,
+} from '@typescript-eslint/rule-tester';
+import type {
+  MessageIds,
+  Options,
+} from '../../../src/rules/sort-keys-in-type-decorator';
+
+export const valid: readonly ValidTestCase<Options>[] = [
+  {
+    code: `
+    @Type({
+      a: 'a',
+      b: 'b',
+      c: 'c'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({})
+    class Test {}
+    `,
+  },
+  {
+    code: `
+    @Type({
+      a: 'a'
+    })
+    class Test {}
+    `,
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root', // Inline comment for selector
+        /* Multi-line comment 
+           above imports */
+        imports: [
+          CommonModule,
+          FormsModule
+        ], // Inline comment after imports
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+  },
+  {
+    code: `
+      @NgModule({
+        // Leading comment for declarations
+        declarations: [
+          /* Component list comment */
+          AppComponent,
+          HeaderComponent
+        ],
+        imports: [
+          CommonModule, // Common module comment
+          RouterModule /* Router module comment */
+        ],
+        /* Multi-line export comment
+           with multiple lines
+           before the property */
+        exports: [AppComponent]
+      })
+      class Test {}
+    `,
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+  },
+];
+
+export const invalid: readonly InvalidTestCase<MessageIds, Options>[] = [
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Component decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Component({
+        changeDetection: ChangeDetectionStrategy.OnPush,
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        imports: [CommonModule],
+        standalone: true,
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css',
+        encapsulation: ViewEncapsulation.None,
+        changeDetection: ChangeDetectionStrategy.OnPush
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Directive decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Directive({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        selector: '[app-test]'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Directive',
+      expectedOrder: 'selector, standalone',
+    },
+    options: [
+      {
+        Directive: ['selector', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Directive({
+        selector: '[app-test]',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when NgModule decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @NgModule({
+        imports: [CommonModule],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, imports',
+    },
+    options: [
+      {
+        NgModule: ['declarations', 'imports'],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        imports: [CommonModule]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when Pipe decorator keys are not sorted according to specified order',
+    annotatedSource: `
+      @Pipe({
+        standalone: true,
+        ~~~~~~~~~~~~~~~~
+        name: 'myPipe'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Pipe',
+      expectedOrder: 'name, standalone',
+    },
+    options: [
+      {
+        Pipe: ['name', 'standalone'],
+      },
+    ],
+    annotatedOutput: `
+      @Pipe({
+        name: 'myPipe',
+        standalone: true
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should fail when partial properties are not in correct order',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root',
+        templateUrl: './app.component.html'
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, templateUrl, styleUrl',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root',
+        templateUrl: './app.component.html',
+        styleUrl: './app.component.css'
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description:
+      'should fail when partial NgModule properties are not in correct order',
+    annotatedSource: `
+      @NgModule({
+        exports: [AppComponent],
+        ~~~~~~~~~~~~~~~~~~~~~~~
+        declarations: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'NgModule',
+      expectedOrder: 'declarations, exports',
+    },
+    options: [
+      {
+        NgModule: [
+          'declarations',
+          'imports',
+          'exports',
+          'providers',
+          'bootstrap',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @NgModule({
+        declarations: [AppComponent],
+        exports: [AppComponent]
+      })
+      class Test {
+      }
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should handle basic inline comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        styleUrl: './app.component.css', // Inline comment for styleUrl
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        selector: 'app-root' // Inline comment for selector
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        selector: 'app-root', // Inline comment for selector
+        styleUrl: './app.component.css' // Inline comment for styleUrl
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve leading comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above styleUrl
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        // Comment above selector
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        // Comment above selector
+        selector: 'app-root',
+        // Comment above styleUrl
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should preserve multi-line comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css',
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root'
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder: 'selector, styleUrl',
+    },
+    options: [
+      {
+        Component: ['selector', 'styleUrl'],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* This is a multi-line comment
+           above selector property */
+        selector: 'app-root',
+        /* This is a multi-line comment
+           above styleUrl property */
+        styleUrl: './app.component.css'
+      })
+      class Test {}
+    `,
+  }),
+  convertAnnotatedSourceToFailureCase({
+    description: 'should properly handle comments when sorting properties',
+    annotatedSource: `
+      @Component({
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush, // Inline comment for changeDetection
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None /* Inline comment for encapsulation */
+      })
+      class Test {}
+    `,
+    messageId: 'incorrectOrder',
+    data: {
+      decorator: 'Component',
+      expectedOrder:
+        'selector, imports, standalone, templateUrl, styleUrl, encapsulation, changeDetection',
+    },
+    options: [
+      {
+        Component: [
+          'selector',
+          'imports',
+          'standalone',
+          'templateUrl',
+          'styleUrl',
+          'encapsulation',
+          'changeDetection',
+        ],
+      },
+    ],
+    annotatedOutput: `
+      @Component({
+        /* Multi-line comment
+           above selector */
+        selector: 'app-root', /* Inline multi-line comment after selector */
+        // Comment above imports
+        imports: [
+          // Comment inside imports array
+          CommonModule, // Comment after CommonModule
+          FormsModule /* Comment after FormsModule */
+        ],
+        /* Comment above standalone */
+        standalone: true, // Comment after standalone
+        // Comment above templateUrl
+        templateUrl: './app.component.html',
+        /* Multi-line comment
+           above styleUrl */
+        styleUrl: './app.component.css',
+        // Comment above encapsulation
+        encapsulation: ViewEncapsulation.None, /* Inline comment for encapsulation */
+        // Comment above changeDetection
+        changeDetection: ChangeDetectionStrategy.OnPush // Inline comment for changeDetection
+      })
+      class Test {}
path: packages/eslint-plugin/tests/rules/sort-keys-in-type-decorator/cases.ts
user: login: bschaeublin
body: > Sorry for the delay I moved across the country I live in and it's been a very busy time at work

No worries - thank you for maintaining and keeping this up here :)
created_at: 2025-05-08T18:06:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: reduckted
created_at: 2025-02-03T10:41:58Z
body: It looks like the docs haven't been updated. There are three commands that you can run to update things when a new rule is added (I often forget to do this myself 😆 ):
- `pnpm run update-rule-configs`
- `pnpm run update-rule-lists`
- `pnpm run update-rule-docs`
author_association: CONTRIBUTOR
user: login: reduckted
created_at: 2025-02-03T11:18:30Z
body: > The current implementation uses default sorts for some decorators based on the order generated by Angular CLI schematics.

I think this is a good idea. If people are using the schematics to create components, then most of the properties will be in the correct order already.

> it would require us to keep these defaults updated whenever the schematics change.

Looking at the `git blame` for the component schematic, other than a recent change to the `standalone` property, the template hasn't changed in years, so I don't think there's any need to be concerned about the schematic reordering the properties. In any case, if the default order is ever changed (apart from adding new properties that come into existence), then that would be a breaking change, so I don't think the default order would be changed.

The only think I would suggest is including _every_ property in the defaults, rather than only those that are used by the schematic. For example, there is `styleUrl`, `styleUrls` and `styles`. If you enable this rule and are using `styles` instead of `styleUrl`, the that property would be moved to the bottom (I think, I'm not quite sure what happens with properties that aren't listed in the options).

> Is there a way to use enums that can be reflected effectively in the rule's schema? I wasn't sure what is the right way to go.

`AngularClassDecorators` is a non-const enum, so in theory you could dynamically build the schema, but it's a bit ugly.
```ts
schema: [
      {
        type: 'object',
        properties: Object.fromEntries(
          Object.keys(AngularClassDecorators).map((key) => [
            key,
            {
              type: 'array',
              items: {
                type: 'string',
              },
            },
          ]),
        ),
        additionalProperties: false,
      },
    ],
```
That produces an options object that looks like this:
```ts
interface Options {
  Component?: string[];
  Directive?: string[];
  Injectable?: string[];
  NgModule?: string[];
  Pipe?: string[];
}
```
So you'd have to exclude `Injectable` manually. Personally, I don't think it's worth it. What I think _might_ be better is to define the options for each decorator type as an "enum" so that you can only specify known properties. For example:
```ts
Component: {
  type: 'array',
  items: {
    type: 'string',
    enum: [
      'changeDetection',
      'viewProviders',
      'moduleId',
      'templateUrl',
      'template',
      'styleUrl',
      'styleUrls',
      'styles',
      'etc...',
    ],
  },
},
```
The only downside to this is there a higher maintenance burden - any time a new property is added to the decorators, the rule needs to be updated. However, the upside is that by defining the schema like this, and also making the `Options` type have the same shape, you'll get strongly-typed configurations, which will make it easy for consumers to know what property names they can specify for each type of decorator. That's just my personal opinion though, and @JamesHenry may have other suggestions. 😃 
author_association: CONTRIBUTOR
user: login: bschaeublin
created_at: 2025-02-03T18:17:28Z
body: > It looks like the docs haven't been updated. There are three commands that you can run to update things when a new rule is added (I often forget to do this myself 😆 ):
> 
> * `pnpm run update-rule-configs`
> * `pnpm run update-rule-lists`
> * `pnpm run update-rule-docs`

Thanks! i only saw the update-rule-docs so far :).

> The only think I would suggest is including every property in the defaults, rather than only those that are used by the schematic. For example, there is styleUrl, styleUrls and styles. If you enable this rule and are using styles instead of styleUrl, the that property would be moved to the bottom (I think, I'm not quite sure what happens with properties that aren't listed in the options).

Good catch, makes also sense in my opinion. I took a look at the [metadata](https://github.com/angular/angular/blob/main/packages/core/src/metadata/) to see for substitutions - the only one was component (having style, styleUrls, styleUIrl and templateUrl, template). I've added them as well to the default options.

Thank you also for the inputs regarding the config-schema!

> ...The only downside to this is there a higher maintenance burden - any time a new property is added to the decorators, the rule needs to be updated.

I totaly see that - so I would wait for @JamesHenry s opinion to that matter  :).
author_association: CONTRIBUTOR
user: login: nickbullock
created_at: 2025-02-06T12:17:03Z
body: hey @bschaeublin, thanks for the PR!
very useful rule, will definitely use it in my team once it's merged
author_association: NONE
user: login: bschaeublin
created_at: 2025-03-21T12:01:28Z
body: @JamesHenry I'm sorry to tag you, as I see that you always got a lot to do, but after hitting the three month mark, I thought I try my luck to get your feedback on this PR (regarding configuration, and general) after I've made some improvements with @reduckted, so that we could complete it :). 

Thank you!
author_association: CONTRIBUTOR
user: login: JamesHenry
created_at: 2025-03-22T09:20:10Z
body: I'm really sorry for the delay @bschaeublin. I have to admit it's because I instinctively strongly dislike sorting rules because of how difficult they are to maintain...

You can see some of the history here: https://github.com/angular-eslint/angular-eslint/pull/1781

Auto-fixing for sorting rules is where the problems really materialize and I see you have added it here.

The hugely problematic, but important, thing missing from your tests right now is examples of code comments and how they will be handled during auto-fixing.

I got an LLM to assist me with illustrating my point about just how difficult this is on a made up Component:

```ts
@Component({
  /* Multi-line comment above changeDetection (only on one line) */
  changeDetection: ChangeDetectionStrategy.OnPush, // Inline comment for changeDetection

  // Single-line comment above viewProviders
  viewProviders: [
    // Inline comment for first provider in viewProviders
    { provide: 'SomeToken', useValue: 'someValue' } /* Multi-line style comment inline after the object */
  ],

  // Comment above moduleId to illustrate its position
  moduleId: module.id, // Inline comment specifying moduleId

  // Single-line comment above templateUrl
  templateUrl: './foo.component.html', /* Inline multi-line comment after templateUrl */

  /* Multi-line comment above template.
     This template contains an embedded HTML comment. */
  template: `<div>
    <!-- HTML comment inside template -->
    Hello World!
  </div>`, // Inline comment after template

  // Comment above styleUrl to mark its location
  styleUrl: './foo.component.css', // Inline comment for styleUrl

  // Comments for styleUrls demonstrating inline and multi-line styles
  styleUrls: [
    './foo.component.less', // Inline comment after first styleUrl
    './foo.component.scss'  /* Inline comment after second styleUrl */
  ],

  // Comment above styles with an inline style example
  styles: [
    `/* Inline CSS comment within styles */ body { color: red; }` // Inline comment after style string
  ],

  // Comment above animations to show detailed inline annotations
  animations: [
    trigger('fade', [
      transition(':enter', [
        style({ opacity: 0 }),
        animate(500, style({ opacity: 1 }))
      ]) // Inline comment closing the transition array
    ]) // Inline comment closing the trigger
  ],

  // Comment above encapsulation
  encapsulation: ViewEncapsulation.Emulated, // Inline comment for encapsulation setting

  /* Multi-line comment above interpolation specifying the markers used */
  interpolation: ['{{', '}}'], // Inline comment for interpolation markers

  // Single-line comment above preserveWhitespaces
  preserveWhitespaces: false, // Inline comment for preserveWhitespaces

  // Single-line comment above standalone flag
  standalone: true, // Inline comment for standalone component

  // Multi-line comment above imports, demonstrating array comments
  imports: [
    CommonModule, // Inline comment for CommonModule import
    FormsModule   /* Inline comment for FormsModule import */
  ],

  // Comment above schemas with inline array entry comment
  schemas: [
    CUSTOM_ELEMENTS_SCHEMA // Inline comment for custom elements schema
  ],

  /* Multi-line comment above override selector to indicate its importance */
  'override selector': 'foo-bar', // Inline comment for override selector

  // Single-line comment above override inputs
  'override inputs': [
    'input1', // Inline comment for first input
    'input2'  /* Inline comment for second input */
  ],

  // Single-line comment above override outputs
  'override outputs': [
    'output1' // Inline comment for override output
  ],

  // Multi-line comment above override providers with inline object comments
  'override providers': [
    {
      // Inline comment inside the provider object for ServiceA
      provide: 'ServiceA',
      useClass: class ServiceAImpl {} // Inline comment after useClass declaration
    }
  ],

  // Single-line comment above override exportAs
  'override exportAs': 'foo', // Inline comment for exportAs

  // Multi-line comment above override queries to demonstrate inline property comments
  'override queries': {
    query1: { read: ElementRef } // Inline comment for query1
  },

  // Single-line comment above override host
  'override host': {
    '[attr.role]': 'button' // Inline comment for host attribute
  },

  // Single-line comment above override jit
  'override jit': true, // Inline comment for jit flag

  // Multi-line comment above override hostDirectives for clarity
  'override hostDirectives': [
    {
      // Inline comment within host directive object
      directive: class DirectiveA {},
      inputs: ['inputDirective'], // Inline comment for host directive input
      outputs: ['outputDirective'] // Inline comment for host directive output
    }
  ]
})
export class FooComponent {
  // Component implementation goes here
}
```

It's really no fun at all to maintain sorting ESLint rules 🥲 

I'll keep an open mind, though, and wait to hear back from you on how you get on auto-fixing the sorting of the above with the comments being moved to the appropriate new locations
author_association: MEMBER
user: login: bschaeublin
created_at: 2025-03-23T22:48:29Z
body: Thanks @JamesHenry for your response, I saw (to late) your comment in another PR as well, that you didn't like these kind of rules for these (proabably truly) difficult handling of comments. So if you're still open to this like you mentioned, I'd like to search for a stable and nice solution.

I think the repository [eslint-plugin-perfectionist](https://github.com/azat-io/eslint-plugin-perfectionist) (which kind soley sorting stuff), do consider comments when re-sorting properties. I saw for example that they work with ranges which contains relevant comments (top, inlinebefore and after), ex. see [getNodeRange](https://github.com/azat-io/eslint-plugin-perfectionist/blob/main/utils/get-node-range.ts#L21).

I still have to validate that, but if they solved that problem we could do it similar?
Either way I'd be happy to search for a solution.

author_association: CONTRIBUTOR
user: login: JamesHenry
created_at: 2025-03-26T17:42:47Z
body: I don't have strong opinions about how you solve it, but the challenge is there on the above snippet and whether it can be proven to be auto-fixable, the above should be your unit test input where you prove that the comments get moved appropriately. Note, formatting doesn't have to be absolutely perfect before and after, but the comments have to belong to the same exact code as before
author_association: MEMBER
user: login: nx-cloud[bot]
created_at: 2025-05-08T13:16:13Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/681d1b1161fc7c0aa94de066?utm_source=pull-request&utm_medium=comment) for commit 0312fd393c07075e221e988ba0f5b59b8498a097.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx run-many -t test` | ✅ Succeeded | 39s | [View ↗](https://cloud.nx.app/runs/cCcqSY2fsA?utm_source=pull-request&utm_medium=comment) |
| `nx run-many -t build` | ✅ Succeeded | 15s | [View ↗](https://cloud.nx.app/runs/FGhAoaXkXp?utm_source=pull-request&utm_medium=comment) |
| `nx run-many -t e2e-suite --parallel 1` | ✅ Succeeded | 27s | [View ↗](https://cloud.nx.app/runs/GOZGOw11HS?utm_source=pull-request&utm_medium=comment) |
| `nx run-many -t test --codeCoverage` | ✅ Succeeded | 57s | [View ↗](https://cloud.nx.app/runs/vDJYKJwpXq?utm_source=pull-request&utm_medium=comment) |
| `nx run-many -t build,typecheck,check-rule-docs,...` | ✅ Succeeded | 47s | [View ↗](https://cloud.nx.app/runs/4DgotfEwOC?utm_source=pull-request&utm_medium=comment) |
| `nx-cloud record -- pnpm nx sync:check` | ✅ Succeeded | 2s | [View ↗](https://cloud.nx.app/runs/E7CANo7QLs?utm_source=pull-request&utm_medium=comment) |
| `nx-cloud record -- pnpm format-check` | ✅ Succeeded | 5s | [View ↗](https://cloud.nx.app/runs/BNUYLMDXYA?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-05-08 21:02:00` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: CONTRIBUTOR
user: login: JamesHenry
created_at: 2025-05-08T13:23:37Z
body: @bschaeublin Looks like it's failing CI
author_association: MEMBER
user: login: bschaeublin
created_at: 2025-05-08T18:05:53Z
body: @JamesHenry sorry, I guess there was a merge conflict when integrating https://github.com/angular-eslint/angular-eslint/pull/2187/commits/e5b64ac0db0311cf022664919896885f6d12e991. Solved it and it seems now everything [successful](https://cloud.nx.app/runs/iKXyNz5p0C).
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-08T21:02:09Z
body: ## [Codecov](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint) Report
Attention: Patch coverage is `82.25806%` with `11 lines` in your changes missing coverage. Please review.
> Project coverage is 92.57%. Comparing base [(`f0ab5dc`)](https://app.codecov.io/gh/angular-eslint/angular-eslint/commit/f0ab5dcff1bb7af0d81e7a8fb53f37d1463771ba?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint) to head [(`0312fd3`)](https://app.codecov.io/gh/angular-eslint/angular-eslint/commit/0312fd393c07075e221e988ba0f5b59b8498a097?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint).
> Report is 3 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint) | Patch % | Lines |
|---|---|---|
| [...nt-plugin/src/rules/sort-keys-in-type-decorator.ts](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Frules%2Fsort-keys-in-type-decorator.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvcnVsZXMvc29ydC1rZXlzLWluLXR5cGUtZGVjb3JhdG9yLnRz) | 80.70% | [10 Missing and 1 partial :warning: ](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint) |

:x: Your patch status has failed because the patch coverage (82.25%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the [target](https://docs.codecov.com/docs/commit-status#target) coverage.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2187      +/-   ##
==========================================
- Coverage   92.74%   92.57%   -0.17%     
==========================================
  Files         189      191       +2     
  Lines        3816     3877      +61     
  Branches      857      865       +8     
==========================================
+ Hits         3539     3589      +50     
- Misses        214      224      +10     
- Partials       63       64       +1     
```

| [Flag](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint) | Coverage Δ | |
|---|---|---|
| [unittest](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint) | `92.57% <82.25%> (-0.17%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint) | Coverage Δ | |
|---|---|---|
| [packages/eslint-plugin/src/index.ts](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvaW5kZXgudHM=) | `100.00% <100.00%> (ø)` | |
| [...n/tests/rules/sort-keys-in-type-decorator/cases.ts](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Ftests%2Frules%2Fsort-keys-in-type-decorator%2Fcases.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi90ZXN0cy9ydWxlcy9zb3J0LWtleXMtaW4tdHlwZS1kZWNvcmF0b3IvY2FzZXMudHM=) | `100.00% <100.00%> (ø)` | |
| [...nt-plugin/src/rules/sort-keys-in-type-decorator.ts](https://app.codecov.io/gh/angular-eslint/angular-eslint/pull/2187?src=pr&el=tree&filepath=packages%2Feslint-plugin%2Fsrc%2Frules%2Fsort-keys-in-type-decorator.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=angular-eslint#diff-cGFja2FnZXMvZXNsaW50LXBsdWdpbi9zcmMvcnVsZXMvc29ydC1rZXlzLWluLXR5cGUtZGVjb3JhdG9yLnRz) | `80.70% <80.70%> (ø)` | |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: nickbullock
created_at: 2025-05-20T10:45:40Z
body: hey @JamesHenry!
could we please release this rule? 
thank you guys @bschaeublin @JamesHenry
author_association: NONE
user: login: JamesHenry
created_at: 2025-05-20T15:38:05Z
body: > We publish a canary release on every successful merge to the main branch, so **you never need to wait for a new stable version to make use of any updates.**

https://github.com/angular-eslint/angular-eslint?tab=readme-ov-file#package-versions

There is no difference between any version I will cut to latest and what has already been published there
author_association: MEMBER
user: login: dangrussell
created_at: 2025-05-26T01:10:56Z
body: Heads up: the fixer for this rule deleted my `providers` and `schemas` keys
author_association: NONE
user: login: JamesHenry
created_at: 2025-05-26T07:04:59Z
body: Already fixed https://github.com/angular-eslint/angular-eslint/pull/2456
author_association: MEMBER