diff --git a/docs/rules/prefer-immutable-types.md b/docs/rules/prefer-immutable-types.md index 202946f70..6e8bdb77e 100644 --- a/docs/rules/prefer-immutable-types.md +++ b/docs/rules/prefer-immutable-types.md @@ -240,9 +240,15 @@ type Options = { }; suggestions?: { - ReadonlyShallow?: Array>; - ReadonlyDeep?: Array>; - Immutable?: Array>; + ReadonlyShallow?: Array< + Array<{ pattern: string; replace: string; message?: string }> + >; + ReadonlyDeep?: Array< + Array<{ pattern: string; replace: string; message?: string }> + >; + Immutable?: Array< + Array<{ pattern: string; replace: string; message?: string }> + >; }; }; ``` @@ -262,14 +268,19 @@ const defaults = { pattern: "^([_$a-zA-Z\\xA0-\\uFFFF][_$a-zA-Z0-9\\xA0-\\uFFFF]*\\[\\])$", replace: "readonly $1", + message: "Prepend with readonly.", }, { pattern: "^(Array|Map|Set)<(.+)>$", replace: "Readonly$1<$2>", + message: "Use Readonly$1 instead of $1.", }, + ], + [ { pattern: "^(.+)$", replace: "Readonly<$1>", + message: "Surround with Readonly.", }, ], ], diff --git a/src/rules/prefer-immutable-types.ts b/src/rules/prefer-immutable-types.ts index 4ca41f27a..e7ab569dc 100644 --- a/src/rules/prefer-immutable-types.ts +++ b/src/rules/prefer-immutable-types.ts @@ -69,6 +69,8 @@ type FixerConfigRaw = { replace: string; }; +type SuggestionsConfigRaw = Array; + type FixerConfigRawMap = Partial< Record< "ReadonlyShallow" | "ReadonlyDeep" | "Immutable", @@ -79,7 +81,7 @@ type FixerConfigRawMap = Partial< type SuggestionConfigRawMap = Partial< Record< "ReadonlyShallow" | "ReadonlyDeep" | "Immutable", - FixerConfigRaw[][] | undefined + SuggestionsConfigRaw[] | undefined > >; @@ -88,7 +90,7 @@ type FixerConfig = { replace: string; }; -type SuggestionsConfig = FixerConfig[]; +type SuggestionsConfig = Array; /** * The options this rule can take. @@ -205,6 +207,7 @@ const suggestionsSchema: JSONSchema4 = { properties: { pattern: { type: "string" }, replace: { type: "string" }, + message: { type: "string" }, }, additionalProperties: false, }, @@ -275,14 +278,19 @@ const defaultOptions: Options = [ pattern: "^([_$a-zA-Z\\xA0-\\uFFFF][_$a-zA-Z0-9\\xA0-\\uFFFF]*\\[\\])$", replace: "readonly $1", + message: "Prepend with readonly.", }, { pattern: "^(Array|Map|Set)<(.+)>$", replace: "Readonly$1<$2>", + message: "Use Readonly$1 instead of $1.", }, + ], + [ { pattern: "^(.+)$", replace: "Readonly<$1>", + message: "Surround with Readonly.", }, ], ], @@ -303,6 +311,8 @@ const errorMessages = { propertyImmutability: 'Property should have an immutability of at least "{{ expected }}" (actual: "{{ actual }}").', propertyModifier: "Property should have a readonly modifier.", + propertyModifierSuggestion: "Add readonly modifier.", + userDefined: "{{ message }}", } as const; /** @@ -331,7 +341,7 @@ type Descriptor = RuleResult< type AllFixers = { fix: ReportFixFunction | null; - suggestionFixers: ReportFixFunction[] | null; + suggestionFixers: Array<{ fix: ReportFixFunction; message: string }> | null; }; /** @@ -383,14 +393,27 @@ function getConfiguredSuggestionFixers( suggestionsConfigs: SuggestionsConfig[], ) { return suggestionsConfigs - .map((configs): NonNullable | null => { - const config = configs.find((c) => c.pattern.test(text)); - if (config === undefined) { - return null; - } - return (fixer) => - fixer.replaceText(node, text.replace(config.pattern, config.replace)); - }) + .map( + ( + configs, + ): { fix: NonNullable; message: string } | null => { + const config = configs.find((c) => c.pattern.test(text)); + if (config === undefined) { + return null; + } + return { + fix: (fixer) => + fixer.replaceText( + node, + text.replace(config.pattern, config.replace), + ), + message: + config.message === undefined + ? `Replace with: ${text.replace(config.pattern, config.replace)}` + : text.replace(config.pattern, config.message), + }; + }, + ) .filter(isDefined); } @@ -504,17 +527,16 @@ function getParameterTypeViolations( const parameterProperty = isTSParameterProperty(param); if (parameterProperty && !param.readonly) { - const messageId = "propertyModifier"; const fix: NonNullable | null = (fixer) => fixer.insertTextBefore(param.parameter, "readonly "); return { node: param, - messageId, + messageId: "propertyModifier", fix: fixerConfigs === false ? null : fix, suggest: [ { - messageId, + messageId: "propertyModifierSuggestion", fix, }, ], @@ -564,21 +586,20 @@ function getParameterTypeViolations( suggestionsConfigs, ); - const messageId = "parameter"; - const data = { - actual: Immutability[immutability], - expected: Immutability[enforcement], - }; - return { node: actualParam, - messageId, - data, + messageId: "parameter", + data: { + actual: Immutability[immutability], + expected: Immutability[enforcement], + }, fix, suggest: - suggestionFixers?.map((fix) => ({ - messageId, - data, + suggestionFixers?.map(({ fix, message }) => ({ + messageId: "userDefined", + data: { + message, + }, fix, })) ?? null, }; @@ -658,22 +679,21 @@ function getReturnTypeViolations( suggestionsConfigs, ); - const messageId = "returnType"; - const data = { - actual: Immutability[immutability], - expected: Immutability[enforcement], - }; - return [ { node: node.returnType, - messageId, - data, + messageId: "returnType", + data: { + actual: Immutability[immutability], + expected: Immutability[enforcement], + }, fix, suggest: - suggestionFixers?.map((fix) => ({ - messageId, - data, + suggestionFixers?.map(({ fix, message }) => ({ + messageId: "userDefined", + data: { + message, + }, fix, })) ?? null, }, @@ -713,22 +733,21 @@ function getReturnTypeViolations( suggestionsConfigs, ); - const messageId = "returnType"; - const data = { - actual: Immutability[immutability], - expected: Immutability[enforcement], - }; - return [ { node: hasID(node) && node.id !== null ? node.id : node, - messageId, - data, + messageId: "returnType", + data: { + actual: Immutability[immutability], + expected: Immutability[enforcement], + }, fix, suggest: - suggestionFixers?.map((fix) => ({ - messageId, - data, + suggestionFixers?.map(({ fix, message }) => ({ + messageId: "userDefined", + data: { + message, + }, fix, })) ?? null, }, @@ -807,17 +826,16 @@ function checkVariable( const fix: NonNullable | null = (fixer) => fixer.insertTextBefore(node.key, "readonly "); - const messageId = "propertyModifier"; return { context, descriptors: [ { node, - messageId, + messageId: "propertyModifier", fix: rawFixerConfig === undefined ? null : fix, suggest: [ { - messageId, + messageId: "propertyModifierSuggestion", fix, }, ], @@ -912,9 +930,11 @@ function checkVariable( data, fix, suggest: - suggestionFixers?.map((fix) => ({ - messageId, - data, + suggestionFixers?.map(({ fix, message }) => ({ + messageId: "userDefined", + data: { + message, + }, fix, })) ?? null, }; diff --git a/tests/rules/prefer-immutable-types/ts/parameters/invalid.ts b/tests/rules/prefer-immutable-types/ts/parameters/invalid.ts index a5377dd68..275caeb41 100644 --- a/tests/rules/prefer-immutable-types/ts/parameters/invalid.ts +++ b/tests/rules/prefer-immutable-types/ts/parameters/invalid.ts @@ -46,7 +46,8 @@ const tests: Array< column: 14, suggestions: [ { - messageId: "parameter", + messageId: "userDefined", + data: { message: "Surround with Readonly." }, output: "function foo(arg1: Readonly<{ foo: string }>, arg2: { foo: number }) {}", }, @@ -59,7 +60,8 @@ const tests: Array< column: 37, suggestions: [ { - messageId: "parameter", + messageId: "userDefined", + data: { message: "Surround with Readonly." }, output: "function foo(arg1: { foo: string }, arg2: Readonly<{ foo: number }>) {}", }, @@ -179,7 +181,7 @@ const tests: Array< column: 5, suggestions: [ { - messageId: "propertyModifier", + messageId: "propertyModifierSuggestion", output: dedent` class Klass { constructor ( @@ -199,7 +201,7 @@ const tests: Array< column: 5, suggestions: [ { - messageId: "propertyModifier", + messageId: "propertyModifierSuggestion", output: dedent` class Klass { constructor ( @@ -219,7 +221,7 @@ const tests: Array< column: 5, suggestions: [ { - messageId: "propertyModifier", + messageId: "propertyModifierSuggestion", output: dedent` class Klass { constructor ( @@ -245,7 +247,8 @@ const tests: Array< column: 46, suggestions: [ { - messageId: "parameter", + messageId: "userDefined", + data: { message: "Surround with Readonly." }, output: "function foo(arg0: { foo: string | number }, arg1: Readonly<{ foo: string | number }>): arg0 is { foo: number } {}", }, @@ -295,7 +298,8 @@ const tests: Array< column: 14, suggestions: [ { - messageId: "parameter", + messageId: "userDefined", + data: { message: "Replace with: ReadonlyDeep<{ foo: string }>" }, output: "function foo(arg1: ReadonlyDeep<{ foo: string }>) {}", }, ], @@ -329,7 +333,10 @@ const tests: Array< column: 14, suggestions: [ { - messageId: "parameter", + messageId: "userDefined", + data: { + message: "Replace with: ReadonlyDeep<{ foo: { bar: string } }>", + }, output: "function foo(arg1: ReadonlyDeep<{ foo: { bar: string } }>) {}", }, @@ -357,7 +364,10 @@ const tests: Array< column: 14, suggestions: [ { - messageId: "parameter", + messageId: "userDefined", + data: { + message: "Use ReadonlyArray instead of Array.", + }, output: dedent` function foo(arg: ReadonlyArray) {} function foo(arg: string[]) {} @@ -369,6 +379,22 @@ const tests: Array< function foo(arg: ReadonlyMap) {} `, }, + { + messageId: "userDefined", + data: { + message: "Surround with Readonly.", + }, + output: dedent` + function foo(arg: Readonly>) {} + function foo(arg: string[]) {} + function foo(arg: Set) {} + function foo(arg: Map) {} + function foo(arg: ReadonlyArray) {} + function foo(arg: readonly string[]) {} + function foo(arg: ReadonlySet) {} + function foo(arg: ReadonlyMap) {} + `, + }, ], }, { @@ -378,7 +404,10 @@ const tests: Array< column: 14, suggestions: [ { - messageId: "parameter", + messageId: "userDefined", + data: { + message: "Prepend with readonly.", + }, output: dedent` function foo(arg: Array) {} function foo(arg: readonly string[]) {} @@ -390,6 +419,22 @@ const tests: Array< function foo(arg: ReadonlyMap) {} `, }, + { + messageId: "userDefined", + data: { + message: "Surround with Readonly.", + }, + output: dedent` + function foo(arg: Array) {} + function foo(arg: Readonly) {} + function foo(arg: Set) {} + function foo(arg: Map) {} + function foo(arg: ReadonlyArray) {} + function foo(arg: readonly string[]) {} + function foo(arg: ReadonlySet) {} + function foo(arg: ReadonlyMap) {} + `, + }, ], }, { @@ -399,7 +444,10 @@ const tests: Array< column: 14, suggestions: [ { - messageId: "parameter", + messageId: "userDefined", + data: { + message: "Use ReadonlySet instead of Set.", + }, output: dedent` function foo(arg: Array) {} function foo(arg: string[]) {} @@ -411,6 +459,22 @@ const tests: Array< function foo(arg: ReadonlyMap) {} `, }, + { + messageId: "userDefined", + data: { + message: "Surround with Readonly.", + }, + output: dedent` + function foo(arg: Array) {} + function foo(arg: string[]) {} + function foo(arg: Readonly>) {} + function foo(arg: Map) {} + function foo(arg: ReadonlyArray) {} + function foo(arg: readonly string[]) {} + function foo(arg: ReadonlySet) {} + function foo(arg: ReadonlyMap) {} + `, + }, ], }, { @@ -420,7 +484,10 @@ const tests: Array< column: 14, suggestions: [ { - messageId: "parameter", + messageId: "userDefined", + data: { + message: "Use ReadonlyMap instead of Map.", + }, output: dedent` function foo(arg: Array) {} function foo(arg: string[]) {} @@ -432,6 +499,22 @@ const tests: Array< function foo(arg: ReadonlyMap) {} `, }, + { + messageId: "userDefined", + data: { + message: "Surround with Readonly.", + }, + output: dedent` + function foo(arg: Array) {} + function foo(arg: string[]) {} + function foo(arg: Set) {} + function foo(arg: Readonly>) {} + function foo(arg: ReadonlyArray) {} + function foo(arg: readonly string[]) {} + function foo(arg: ReadonlySet) {} + function foo(arg: ReadonlyMap) {} + `, + }, ], }, ], diff --git a/tests/rules/prefer-immutable-types/ts/return-types/invalid.ts b/tests/rules/prefer-immutable-types/ts/return-types/invalid.ts index 238528b99..114710584 100644 --- a/tests/rules/prefer-immutable-types/ts/return-types/invalid.ts +++ b/tests/rules/prefer-immutable-types/ts/return-types/invalid.ts @@ -68,7 +68,10 @@ const tests: Array< column: 26, suggestions: [ { - messageId: "returnType", + messageId: "userDefined", + data: { + message: "Surround with Readonly.", + }, output: dedent` function foo(arg: number): Readonly<{ foo: string }>; function foo(arg: string): Readonly<{ foo: number }>; @@ -85,7 +88,10 @@ const tests: Array< column: 27, suggestions: [ { - messageId: "returnType", + messageId: "userDefined", + data: { + message: "Surround with Readonly.", + }, output: dedent` function foo(arg: number): { foo: string }; function foo(arg: string): Readonly<{ foo: number }>; diff --git a/tests/rules/prefer-immutable-types/ts/variables/invalid.ts b/tests/rules/prefer-immutable-types/ts/variables/invalid.ts index c724f0dcb..44ef78f82 100644 --- a/tests/rules/prefer-immutable-types/ts/variables/invalid.ts +++ b/tests/rules/prefer-immutable-types/ts/variables/invalid.ts @@ -65,7 +65,10 @@ const tests: Array< column: 7, suggestions: [ { - messageId: "variable", + messageId: "userDefined", + data: { + message: "Surround with Readonly.", + }, output: dedent` const foo: Readonly<{ foo: string }> = {} as any, bar: Readonly<{ foo: number }> = {} as any; @@ -168,7 +171,7 @@ const tests: Array< column: 3, suggestions: [ { - messageId: "propertyModifier", + messageId: "propertyModifierSuggestion", output: dedent` class Klass { readonly foo: number; @@ -187,7 +190,7 @@ const tests: Array< column: 3, suggestions: [ { - messageId: "propertyModifier", + messageId: "propertyModifierSuggestion", output: dedent` class Klass { foo: number; @@ -206,7 +209,7 @@ const tests: Array< column: 3, suggestions: [ { - messageId: "propertyModifier", + messageId: "propertyModifierSuggestion", output: dedent` class Klass { foo: number; @@ -225,7 +228,7 @@ const tests: Array< column: 3, suggestions: [ { - messageId: "propertyModifier", + messageId: "propertyModifierSuggestion", output: dedent` class Klass { foo: number;