Repository: azat-io/eslint-plugin-perfectionist (ts)

Issue:
Bug: `PerfectionistSortImports` type missing when using with `eslint-typegen`
### Describe the bug

When using `eslint-typegen`, it generates the rule option type:
```ts
'perfectionist/sort-imports'?: Linter.RuleEntry<PerfectionistSortImports>
```

However, the `PerfectionistSortImports` type itself is no longer generated. This results in a loss of type-safety for the `perfectionist/sort-imports` rule—unlike other rules such as `perfectionist/sort-exports`, which continue to generate their corresponding types correctly.

This issue did not occur in previous versions `v4.12.1`, but it does occur in `v4.12.2`. 

The issue seems to stem from a change in `rules/sort-imports.ts` where the line:
```ts
id: 'sort-imports'
```
was moved from within `schema.items` to the top-level `schema` object.

Reverting this change—i.e., moving the `id` back under `schema.items`—appears to resolve the issue. However I'm unsure whether this is the correct long-term fix or if the root cause lies elsewhere.

### Code example

```ts
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import pluginPerfectionist from 'eslint-plugin-perfectionist';
import { flatConfigsToRulesDTS } from 'eslint-typegen/core';

const dts = await flatConfigsToRulesDTS(
  [
    {
      name: 'perfectionist/rules',
      plugins: {
        perfectionist: pluginPerfectionist,
      },
      rules: {},
    },
  ],
  {
    includeAugmentation: false,
  }
);

const __dirname = path.dirname(fileURLToPath(import.meta.url));
await fs.writeFile(`${__dirname}/typegen.d.ts`, dts);
```

Same code works with previous versions or by moving the `id: 'sort-imports'` back to `schema.items`.



### ESLint version

v9.25.1

### ESLint Plugin Perfectionist version

v4.12.2

### Additional comments

_No response_

### Validations

- [x] Read the [docs](https://perfectionist.dev).
- [x] Check that there isn't [already an issue](https://github.com/azat-io/eslint-plugin-perfectionist/issues) that reports the same bug to avoid creating a duplicate.

Pull request:
fix(sort-imports): fix weak JSON schema typings generated
- Fixes #529

### Description

`sort-imports` JSON schema was recently updated to allow an array of configurations to be passed (useful when we want to match elements and not sort them with `type: 'unsorted'`).

Its JSON schema is also more complex than other rules as it has the following definitions: 
- `is-line-length` to check that if the configuration is `line-length`.
- `max-line-length-requires-line-length-type` to ensure that the `maxLineLength` option is only passed when `type` is `line-length`.

For some reason, this makes `eslint-typegen` fail to generate accurate Typescript types.

This PR removes those two JSON Schema definitions and adds a simple test to check whether the generated Typescript is correct. 

<details>
<summary>Generated TS</summary>

```ts
export type Id = {
  /**
   * Fallback sort order.
   */
  fallbackSort?: {
    /**
     * Determines whether the sorted items should be in ascending or descending order.
     */
    order?: ("asc" | "desc")
    /**
     * Specifies the sorting method.
     */
    type?: ("alphabetical" | "natural" | "line-length" | "custom" | "unsorted")
    [k: string]: unknown
  }
  /**
   * Controls how special characters should be handled before sorting.
   */
  specialCharacters?: ("remove" | "trim" | "keep")
  /**
   * Controls whether sorting should be case-sensitive or not.
   */
  ignoreCase?: boolean
  /**
   * Alphabet to use for the `custom` sort type.
   */
  alphabet?: string
  /**
   * Specifies the sorting locales.
   */
  locales?: (string | string[])
  /**
   * Determines whether the sorted items should be in ascending or descending order.
   */
  order?: ("asc" | "desc")
  /**
   * Specifies the sorting method.
   */
  type?: ("alphabetical" | "natural" | "line-length" | "custom" | "unsorted")
  customGroups?: ({
    /**
     * Specifies custom groups for value imports.
     */
    value?: {
      [k: string]: unknown
    }
    /**
     * Specifies custom groups for type imports.
     */
    type?: {
      [k: string]: unknown
    }
  } | ({
    /**
     * Specifies how new lines should be handled between members of the custom group.
     */
    newlinesInside?: ("always" | "never")
    /**
     * Fallback sort order.
     */
    fallbackSort?: {
      /**
       * Determines whether the sorted items should be in ascending or descending order.
       */
      order?: ("asc" | "desc")
      /**
       * Specifies the sorting method.
       */
      type?: ("alphabetical" | "natural" | "line-length" | "custom" | "unsorted")
      [k: string]: unknown
    }
    /**
     * Custom group name.
     */
    groupName?: string
    /**
     * Determines whether the sorted items should be in ascending or descending order.
     */
    order?: ("asc" | "desc")
    /**
     * Specifies the sorting method.
     */
    type?: ("alphabetical" | "natural" | "line-length" | "custom" | "unsorted")
    anyOf?: {
      /**
       * Modifier filters.
       */
      modifiers?: ("default" | "named" | "require" | "side-effect" | "ts-equals" | "type" | "value" | "wildcard")[]
      /**
       * Selector filter.
       */
      selector?: ("side-effect-style" | "tsconfig-path" | "side-effect" | "external" | "internal" | "builtin" | "sibling" | "subpath" | "import" | "parent" | "index" | "style" | "type")
      /**
       * Regular expression.
       */
      elementValuePattern?: (({
        pattern?: string
        flags?: string
      } | string)[] | ({
        pattern?: string
        flags?: string
      } | string))
      /**
       * Regular expression.
       */
      elementNamePattern?: (({
        pattern?: string
        flags?: string
      } | string)[] | ({
        pattern?: string
        flags?: string
      } | string))
    }[]
  } | {
    /**
     * Specifies how new lines should be handled between members of the custom group.
     */
    newlinesInside?: ("always" | "never")
    /**
     * Fallback sort order.
     */
    fallbackSort?: {
      /**
       * Determines whether the sorted items should be in ascending or descending order.
       */
      order?: ("asc" | "desc")
      /**
       * Specifies the sorting method.
       */
      type?: ("alphabetical" | "natural" | "line-length" | "custom" | "unsorted")
      [k: string]: unknown
    }
    /**
     * Custom group name.
     */
    groupName?: string
    /**
     * Determines whether the sorted items should be in ascending or descending order.
     */
    order?: ("asc" | "desc")
    /**
     * Specifies the sorting method.
     */
    type?: ("alphabetical" | "natural" | "line-length" | "custom" | "unsorted")
    /**
     * Modifier filters.
     */
    modifiers?: ("default" | "named" | "require" | "side-effect" | "ts-equals" | "type" | "value" | "wildcard")[]
    /**
     * Selector filter.
     */
    selector?: ("side-effect-style" | "tsconfig-path" | "side-effect" | "external" | "internal" | "builtin" | "sibling" | "subpath" | "import" | "parent" | "index" | "style" | "type")
    /**
     * Regular expression.
     */
    elementValuePattern?: (({
      pattern?: string
      flags?: string
    } | string)[] | ({
      pattern?: string
      flags?: string
    } | string))
    /**
     * Regular expression.
     */
    elementNamePattern?: (({
      pattern?: string
      flags?: string
    } | string)[] | ({
      pattern?: string
      flags?: string
    } | string))
  })[])
  /**
   * Specifies the maximum line length.
   */
  maxLineLength?: number
  /**
   * Controls whether side-effect imports should be sorted.
   */
  sortSideEffects?: boolean
  /**
   * Specifies the environment.
   */
  environment?: ("node" | "bun")
  /**
   * Specifies the tsConfig root directory.
   */
  tsconfigRootDir?: string
  /**
   * Allows to use comments to separate members into logical groups.
   */
  partitionByComment?: (boolean | (({
    pattern?: string
    flags?: string
  } | string)[] | ({
    pattern?: string
    flags?: string
  } | string)) | {
    block?: (boolean | (({
      pattern?: string
      flags?: string
    } | string)[] | ({
      pattern?: string
      flags?: string
    } | string)))
    line?: (boolean | (({
      pattern?: string
      flags?: string
    } | string)[] | ({
      pattern?: string
      flags?: string
    } | string)))
  })
  /**
   * Allows to use newlines to separate the nodes into logical groups.
   */
  partitionByNewLine?: boolean
  /**
   * Specifies how new lines should be handled between groups.
   */
  newlinesBetween?: ("ignore" | "always" | "never")
  /**
   * Regular expression.
   */
  internalPattern?: (({
    pattern?: string
    flags?: string
  } | string)[] | ({
    pattern?: string
    flags?: string
  } | string))
  /**
   * Specifies the order of the groups.
   */
  groups?: (string | string[] | {
    /**
     * Specifies how new lines should be handled between groups.
     */
    newlinesBetween?: ("ignore" | "always" | "never")
  })[]
}[]
```
</details>

- [x] Bug fix


Patch:
diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts
index 1d13041..74eb472 100644
--- a/rules/sort-imports.ts
+++ b/rules/sort-imports.ts
@@ -464,41 +464,9 @@ export default createEslintRule<Options, MESSAGE_ID>({
           internalPattern: regexJsonSchema,
           groups: groupsJsonSchema,
         },
-        allOf: [
-          {
-            $ref: '#/definitions/max-line-length-requires-line-length-type',
-          },
-        ],
-        dependencies: {
-          maxLineLength: ['type'],
-        },
         additionalProperties: false,
         type: 'object',
       },
-      definitions: {
-        'max-line-length-requires-line-length-type': {
-          anyOf: [
-            {
-              not: {
-                required: ['maxLineLength'],
-                type: 'object',
-              },
-              type: 'object',
-            },
-            {
-              $ref: '#/definitions/is-line-length',
-            },
-          ],
-        },
-        'is-line-length': {
-          properties: {
-            type: { enum: ['line-length'], type: 'string' },
-          },
-          required: ['type'],
-          type: 'object',
-        },
-      },
-      id: 'sort-imports',
       uniqueItems: true,
       type: 'array',
     },


Test patch:
diff --git a/test/utils/validate-rule-json-schema.ts b/test/utils/validate-rule-json-schema.ts
index 6fe0dd6..fcb51d3 100644
--- a/test/utils/validate-rule-json-schema.ts
+++ b/test/utils/validate-rule-json-schema.ts
@@ -3,6 +3,8 @@ import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'
 import { compile as compileSchemaForTs } from 'json-schema-to-typescript-lite'
 import Ajv from 'ajv-draft-04'
 
+import { commonJsonSchemas } from '../../utils/common-json-schemas'
+
 export let validateRuleJsonSchema = async (
   schemaOrSchemas: readonly JSONSchema4[] | JSONSchema4,
 ): Promise<void> => {
@@ -18,5 +20,21 @@ export let validateRuleJsonSchema = async (
 
 let validateJsonSchema = async (schema: JSONSchema4): Promise<void> => {
   new Ajv().compile(schema)
-  await compileSchemaForTs(schema, 'id')
+  await validateTsJsonSchema(schema)
+}
+
+let validateTsJsonSchema = async (schema: JSONSchema4): Promise<void> => {
+  let generatedTypescript = await compileSchemaForTs(schema, 'id')
+  for (let [commonJsonSchemaKey, commonJsonSchema] of Object.entries(
+    commonJsonSchemas,
+  )) {
+    if (
+      commonJsonSchema.description &&
+      !generatedTypescript.includes(commonJsonSchema.description)
+    ) {
+      throw new Error(
+        `TypeScript generated from JSON schema seems wrong: no description found for ${commonJsonSchemaKey}:\n${generatedTypescript}`,
+      )
+    }
+  }
 }