From 7a0a79078382768eec6a467b6be81854469b8dbd Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Mon, 1 Apr 2024 15:14:44 +1300 Subject: [PATCH] feat(type-declaration-immutability): add support for in-editor suggestions fix #797 --- README.md | 2 +- docs/rules/type-declaration-immutability.md | 8 ++- src/rules/type-declaration-immutability.ts | 75 ++++++++++++++++++++- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 083dd970d..1d0c32861 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ The [below section](#rules) gives details on which rules are enabled by each rul | [no-let](docs/rules/no-let.md) | Disallow mutable variables. | ☑️ ✅ 🔒 ![badge-no-mutations][] | | | | | | | | [prefer-immutable-types](docs/rules/prefer-immutable-types.md) | Require function parameters to be typed as certain immutability | ☑️ ✅ 🔒 ![badge-no-mutations][] | | | 🔧 | 💡 | 💭 | | | [prefer-readonly-type](docs/rules/prefer-readonly-type.md) | Prefer readonly types over mutable types. | | | | 🔧 | | 💭 | ❌ | -| [type-declaration-immutability](docs/rules/type-declaration-immutability.md) | Enforce the immutability of types based on patterns. | ☑️ ✅ 🔒 ![badge-no-mutations][] | | | 🔧 | | 💭 | | +| [type-declaration-immutability](docs/rules/type-declaration-immutability.md) | Enforce the immutability of types based on patterns. | ☑️ ✅ 🔒 ![badge-no-mutations][] | | | 🔧 | 💡 | 💭 | | ### No Other Paradigms diff --git a/docs/rules/type-declaration-immutability.md b/docs/rules/type-declaration-immutability.md index 0968d6690..0c0dffed4 100644 --- a/docs/rules/type-declaration-immutability.md +++ b/docs/rules/type-declaration-immutability.md @@ -2,7 +2,7 @@ 💼 This rule is enabled in the following configs: ☑️ `lite`, `no-mutations`, ✅ `recommended`, 🔒 `strict`. -🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). +🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). 💭 This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). @@ -86,6 +86,7 @@ type Options = { | { pattern: string; replace: string } | Array<{ pattern: string; replace: string }> | false; + suggestions?: Array<{ pattern: string; replace: string }> | false; }>; ignoreInterfaces: boolean; ignoreIdentifierPattern: string[] | string; @@ -102,6 +103,7 @@ const defaults = { immutability: "Immutable", comparator: "AtLeast", fixer: false, + suggestions: false, }, ], ignoreInterfaces: false, @@ -186,6 +188,10 @@ immutability. This can be thought of as `<`, `<=`, `==`, `>=` or `>`. Configure the fixer for this rule to work with your setup. If not set, or set to `false`, the fixer will be disabled. +#### `suggestions` + +Configure any suggestions for this rule to work with your setup. + ### `ignoreInterfaces` A boolean to specify whether interfaces should be exempt from these rules. diff --git a/src/rules/type-declaration-immutability.ts b/src/rules/type-declaration-immutability.ts index ad54923f6..d80f2db12 100644 --- a/src/rules/type-declaration-immutability.ts +++ b/src/rules/type-declaration-immutability.ts @@ -56,6 +56,8 @@ type FixerConfig = { replace: string; }; +type SuggestionsConfig = FixerConfig[]; + /** * The options this rule can take. */ @@ -71,6 +73,7 @@ type Options = [ | RuleEnforcementComparator | keyof typeof RuleEnforcementComparator; fixer?: FixerConfigRaw | FixerConfigRaw[] | false; + suggestions?: FixerConfigRaw[] | false; }>; ignoreInterfaces: boolean; }, @@ -107,6 +110,26 @@ const fixerSchema: JSONSchema4 = { ], }; +const suggestionsSchema: JSONSchema4 = { + oneOf: [ + { + type: "boolean", + enum: [false], + }, + { + type: "array", + items: { + type: "object", + properties: { + pattern: { type: "string" }, + replace: { type: "string" }, + }, + additionalProperties: false, + }, + }, + ], +}; + /** * The schema for the rule options. */ @@ -138,6 +161,7 @@ const schema: JSONSchema4[] = [ enum: Object.values(RuleEnforcementComparator), }, fixer: fixerSchema, + suggestions: suggestionsSchema, }, required: ["identifiers", "immutability"], additionalProperties: false, @@ -195,6 +219,7 @@ const meta: NamedCreateRuleCustomMeta = { }, messages: errorMessages, fixable: "code", + hasSuggestions: true, schema, }; @@ -206,6 +231,7 @@ export type ImmutabilityRule = { immutability: Immutability; comparator: RuleEnforcementComparator; fixers: FixerConfig[] | false; + suggestions: SuggestionsConfig | false; }; type Descriptor = RuleResult< @@ -245,11 +271,20 @@ function getRules(options: Readonly): ImmutabilityRule[] { pattern: new RegExp(r.pattern, "us"), })); + const suggestions = + rule.suggestions === undefined || rule.suggestions === false + ? false + : rule.suggestions.map((r) => ({ + ...r, + pattern: new RegExp(r.pattern, "us"), + })); + return { identifiers, immutability, comparator, fixers, + suggestions, }; }); } @@ -297,6 +332,27 @@ function getConfiguredFixer( fixer.replaceText(node, text.replace(config.pattern, config.replace)); } +/** + * Get the suggestions that uses the user config. + */ +function getConfiguredSuggestions( + node: T, + context: Readonly>, + configs: FixerConfig[], + messageId: keyof typeof errorMessages, +): NonNullable | null { + const text = context.sourceCode.getText(node); + const matchingConfig = configs.filter((c) => c.pattern.test(text)); + if (matchingConfig.length === 0) { + return null; + } + return matchingConfig.map((config) => ({ + fix: (fixer) => + fixer.replaceText(node, text.replace(config.pattern, config.replace)), + messageId, + })); +} + /** * Compare the actual immutability to the expected immutability. */ @@ -337,24 +393,37 @@ function getResults( }; } + const messageId = RuleEnforcementComparator[ + rule.comparator + ] as keyof typeof RuleEnforcementComparator; + const fix = rule.fixers === false || isTSInterfaceDeclaration(node) ? null : getConfiguredFixer(node.typeAnnotation, context, rule.fixers); + const suggest = + rule.suggestions === false || isTSInterfaceDeclaration(node) + ? null + : getConfiguredSuggestions( + node.typeAnnotation, + context, + rule.suggestions, + messageId, + ); + return { context, descriptors: [ { node: node.id, - messageId: RuleEnforcementComparator[ - rule.comparator - ] as keyof typeof RuleEnforcementComparator, + messageId, data: { actual: Immutability[immutability], expected: Immutability[rule.immutability], }, fix, + suggest, }, ], };