diff --git a/.eslintrc.json b/.eslintrc.json index 12616a7d2..eeb9f087a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -59,6 +59,13 @@ "extends": ["plugin:eslint-plugin/recommended"], "rules": {} }, + { + "files": ["**/*.md/**"], + "rules": { + "@typescript-eslint/array-type": "off", + "functional/no-mixed-type": "off" + } + }, // FIXME: This override is defined in the upsteam; it shouldn't need to be redefined here. Why? { "files": ["./**/*.md/**"], diff --git a/README.md b/README.md index d11d0a496..b89e56ad3 100644 --- a/README.md +++ b/README.md @@ -186,12 +186,13 @@ The [below section](#supported-rules) gives details on which rules are enabled b :see_no_evil: = `no-mutations` Ruleset. -| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | -| -------------------------------------------------------------- | -------------------------------------------------------------------------- | :---------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: | -| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: | -| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| [`no-method-signature`](./docs/rules/no-method-signature.md) | Enforce property signatures with readonly modifiers over method signatures | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`prefer-readonly-type`](./docs/rules/prefer-readonly-type.md) | Use readonly types and readonly modifiers where possible | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: | +| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | +| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | :---------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: | +| [`immutable-data`](./docs/rules/immutable-data.md) | Disallow mutating objects and arrays | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :blue_heart: | +| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`no-method-signature`](./docs/rules/no-method-signature.md) | Enforce property signatures with readonly modifiers over method signatures | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| [`prefer-readonly-type`](./docs/rules/prefer-readonly-type.md) | Use readonly types and readonly modifiers where possible | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :thought_balloon: | +| [`prefer-readonly-return-types`](./docs/rules/prefer-readonly-return-types.md) | Enforce use of readonly types for function return values | | | | | :thought_balloon: | ### No Object-Orientation Rules diff --git a/docs/rules/prefer-readonly-return-types.md b/docs/rules/prefer-readonly-return-types.md new file mode 100644 index 000000000..939c5ca4e --- /dev/null +++ b/docs/rules/prefer-readonly-return-types.md @@ -0,0 +1,268 @@ +# Requires that function return values are typed as readonly (prefer-readonly-return-types) + +This rules work just like [prefer-readonly-parameter-types](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md). + +This rule should only be used in a purely functional environment to help ensure that values are not being mutated. + +## Rule Details + +This rule allows you to enforce that function return values resolve to a readonly type. + +Examples of **incorrect** code for this rule: + + + +```ts +/* eslint functional/prefer-readonly-type-declaration: "error" */ + +function array1(): string[] {} // array is not readonly +function array2(): readonly string[][] {} // array element is not readonly +function array3(): [string, number] {} // tuple is not readonly +function array4(): readonly [string[], number] {} // tuple element is not readonly +// the above examples work the same if you use ReadonlyArray instead + +function object1(): { prop: string } {} // property is not readonly +function object2(): { readonly prop: string; prop2: string } {} // not all properties are readonly +function object3(): { readonly prop: { prop2: string } } {} // nested property is not readonly +// the above examples work the same if you use Readonly instead + +interface CustomArrayType extends ReadonlyArray { + prop: string; // note: this property is mutable +} +function custom1(): CustomArrayType {} + +interface CustomFunction { + (): void; + prop: string; // note: this property is mutable +} +function custom2(): CustomFunction {} + +function union(): string[] | ReadonlyArray {} // not all types are readonly + +// rule also checks function types +interface Foo { + (): string[]; +} +interface Foo { + new (): string[]; +} +const x = { foo(): string[]; }; +function foo(): string[]; +type Foo = () => string[]; +interface Foo { + foo(): string[]; +} +``` + +Examples of **correct** code for this rule: + +```ts +/* eslint functional/prefer-readonly-return-types: "error" */ + +function array1(): readonly string[] {} +function array2(): readonly (readonly string[])[] {} +function array3(): readonly [string, number] {} +function array4(): readonly [readonly string[], number] {} +// the above examples work the same if you use ReadonlyArray instead + +function object1(): { readonly prop: string } {} +function object2(): { readonly prop: string; readonly prop2: string } {} +function object3(): { readonly prop: { readonly prop2: string } } {} +// the above examples work the same if you use Readonly instead + +interface CustomArrayType extends ReadonlyArray { + readonly prop: string; +} +function custom1(): Readonly {} +// interfaces that extend the array types are not considered arrays, and thus must be made readonly. + +interface CustomFunction { + (): void; + readonly prop: string; +} +function custom2(): CustomFunction {} + +function union(): readonly string[] | ReadonlyArray {} + +function primitive1(): string {} +function primitive2(): number {} +function primitive3(): boolean {} +function primitive4(): unknown {} +function primitive5(): null {} +function primitive6(): undefined {} +function primitive7(): any {} +function primitive8(): never {} +function primitive9(): number | string | undefined {} + +function fnSig(): () => void {} + +enum Foo { A, B } +function enum1(): Foo {} + +function symb1(): symbol {} +const customSymbol = Symbol('a'); +function symb2(): typeof customSymbol {} + +// function types +interface Foo { + (): readonly string[]; +} +interface Foo { + new (): readonly string[]; +} +const x = { foo(): readonly string[]; }; +function foo(): readonly string[]; +type Foo = () => readonly string[]; +interface Foo { + foo(): readonly string[]; +} +``` + +The default options: + +```ts +const defaults = { + allowLocalMutation: false, + ignoreClass: false, + ignoreCollections: false, + ignoreInferredTypes: false, + ignoreInterface: false, + treatMethodsAsReadonly: false, +} +``` + +### `treatMethodsAsReadonly` + +This option allows you to treat all mutable methods as though they were readonly. This may be desirable in when you are never reassigning methods. + +Examples of **incorrect** code for this rule with `{treatMethodsAsReadonly: false}`: + +```ts +type MyType = { + readonly prop: string; + method(): string; // note: this method is mutable +}; +function foo(arg: MyType) {} +``` + +Examples of **correct** code for this rule with `{treatMethodsAsReadonly: false}`: + +```ts +type MyType = Readonly<{ + prop: string; + method(): string; +}>; +function foo(): MyType {} +type MyOtherType = { + readonly prop: string; + readonly method: () => string; +}; +function bar(): MyOtherType {} +``` + +Examples of **correct** code for this rule with `{treatMethodsAsReadonly: true}`: + +```ts +type MyType = { + readonly prop: string; + method(): string; // note: this method is mutable +}; +function foo(): MyType {} +``` + +### `ignoreClass` + +If set, classes will not be checked. + +Examples of **incorrect** code for the `{ "ignoreClass": false }` option: + + + +```ts +/* eslint functional/prefer-readonly-type-declaration: ["error", { "ignoreClass": false }] */ + +class { + myprop: string; +} +``` + +Examples of **correct** code for the `{ "ignoreClass": true }` option: + +```ts +/* eslint functional/prefer-readonly-type-declaration: ["error", { "ignoreClass": true }] */ + +class { + myprop: string; +} +``` + +### `ignoreInterface` + +If set, interfaces will not be checked. + +Examples of **incorrect** code for the `{ "ignoreInterface": false }` option: + + + +```ts +/* eslint functional/prefer-readonly-type-declaration: ["error", { "ignoreInterface": false }] */ + +interface I { + myprop: string; +} +``` + +Examples of **correct** code for the `{ "ignoreInterface": true }` option: + +```ts +/* eslint functional/prefer-readonly-type-declaration: ["error", { "ignoreInterface": true }] */ + +interface I { + myprop: string; +} +``` + +### `ignoreCollections` + +If set, collections (Array, Tuple, Set, and Map) will not be required to be readonly when used outside of type aliases and interfaces. + +Examples of **incorrect** code for the `{ "ignoreCollections": false }` option: + + + +```ts +/* eslint functional/prefer-readonly-type-declaration: ["error", { "ignoreCollections": false }] */ + +const foo: number[] = []; +const bar: [string, string] = ["foo", "bar"]; +const baz: Set = new Set(); +const qux: Map = new Map(); +``` + +Examples of **correct** code for the `{ "ignoreCollections": true }` option: + +```ts +/* eslint functional/prefer-readonly-type-declaration: ["error", { "ignoreCollections": true }] */ + +const foo: number[] = []; +const bar: [string, string] = ["foo", "bar"]; +const baz: Set = new Set(); +const qux: Map = new Map(); +``` + +### `ignoreInferredTypes` + +This option allows you to ignore types that aren't explicitly specified. + +### `allowLocalMutation` + +See the [allowLocalMutation](./options/allow-local-mutation.md) docs. + +### `ignorePattern` + +Use the given regex pattern(s) to match against the type's name (for objects this is the property's name not the object's name). + +Note: If using this option to require mutable properties are marked as mutable via a naming convention (e.g. `{ "ignorePattern": "^[Mm]utable.+" }`), +type aliases and interfaces names will still need to comply with the `readonlyAliasPatterns` and `mutableAliasPatterns` options. + +See the [ignorePattern](./options/ignore-pattern.md) docs for more info. diff --git a/package.json b/package.json index 180138dd2..a4989affe 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,8 @@ "verify": "yarn build && yarn lint && yarn build-tests && yarn test-compiled && rimraf build" }, "dependencies": { - "@typescript-eslint/experimental-utils": "^5.0.0", + "@typescript-eslint/experimental-utils": "^5.9.1", + "@typescript-eslint/type-utils": "^5.9.1", "deepmerge-ts": "^2.0.1", "escape-string-regexp": "^4.0.0" }, @@ -87,8 +88,8 @@ "@types/estree": "^0.0.50", "@types/node": "16.11.0", "@types/rollup-plugin-auto-external": "^2.0.2", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", + "@typescript-eslint/eslint-plugin": "^5.9.1", + "@typescript-eslint/parser": "^5.9.1", "ava": "^3.15.0", "babel-eslint": "^10.1.0", "chalk": "^4.1.2", diff --git a/src/common/ignore-options.ts b/src/common/ignore-options.ts index 227091f30..4c31a4699 100644 --- a/src/common/ignore-options.ts +++ b/src/common/ignore-options.ts @@ -114,6 +114,20 @@ export const ignoreInterfaceOptionSchema: JSONSchema4 = { additionalProperties: false, }; +export type IgnoreInferredTypesOption = { + readonly ignoreInferredTypes: boolean; +}; + +export const ignoreInferredTypesOptionOptionSchema: JSONSchema4 = { + type: "object", + properties: { + ignoreInferredTypes: { + type: "boolean", + }, + }, + additionalProperties: false, +}; + /** * Get the identifier text of the given node. */ @@ -311,6 +325,19 @@ export function shouldIgnoreInterface( return options.ignoreInterface === true && inInterface(node); } +/** + * Should the given node be allowed base off the following rule options? + * + * - IgnoreInterfaceOption. + */ +export function shouldIgnoreInferredTypes( + node: TSESTree.TypeNode, + context: RuleContext, + options: Partial +) { + return options.ignoreInferredTypes === true && node === null; +} + /** * Should the given node be allowed base off the following rule options? * diff --git a/src/configs/all.ts b/src/configs/all.ts index 0db73d9d0..952b37034 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -21,6 +21,7 @@ const config: Linter.Config = { rules: { "functional/no-method-signature": "error", "functional/no-mixed-type": "error", + "functional/prefer-readonly-return-types": "error", "functional/prefer-readonly-type": "error", "functional/prefer-tacit": ["error", { assumeTypes: false }], "functional/no-return-void": "error", diff --git a/src/rules/index.ts b/src/rules/index.ts index 06eb7baef..ea40af84d 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -12,6 +12,7 @@ import * as noReturnVoid from "./no-return-void"; import * as noThisExpression from "./no-this-expression"; import * as noThrowStatement from "./no-throw-statement"; import * as noTryStatement from "./no-try-statement"; +import * as preferReadonlyReturnTypes from "./prefer-readonly-return-types"; import * as preferReadonlyTypes from "./prefer-readonly-type"; import * as preferTacit from "./prefer-tacit"; @@ -33,6 +34,7 @@ export const rules = { [noThisExpression.name]: noThisExpression.rule, [noThrowStatement.name]: noThrowStatement.rule, [noTryStatement.name]: noTryStatement.rule, + [preferReadonlyReturnTypes.name]: preferReadonlyReturnTypes.rule, [preferReadonlyTypes.name]: preferReadonlyTypes.rule, [preferTacit.name]: preferTacit.rule, }; diff --git a/src/rules/prefer-readonly-return-types.ts b/src/rules/prefer-readonly-return-types.ts new file mode 100644 index 000000000..12aaa133c --- /dev/null +++ b/src/rules/prefer-readonly-return-types.ts @@ -0,0 +1,135 @@ +import type { TSESTree } from "@typescript-eslint/experimental-utils"; +import type { ReadonlynessOptions } from "@typescript-eslint/type-utils"; +import { + readonlynessOptionsDefaults, + readonlynessOptionsSchema, +} from "@typescript-eslint/type-utils"; +import { deepmerge } from "deepmerge-ts"; +import type { JSONSchema4 } from "json-schema"; + +import type { + AllowLocalMutationOption, + IgnoreClassOption, + IgnoreInferredTypesOption, + IgnoreInterfaceOption, + IgnorePatternOption, +} from "~/common/ignore-options"; +import { + allowLocalMutationOptionSchema, + ignoreClassOptionSchema, + ignoreInferredTypesOptionOptionSchema, + ignoreInterfaceOptionSchema, + ignorePatternOptionSchema, + shouldIgnoreClass, + shouldIgnoreInferredTypes, + shouldIgnoreInterface, + shouldIgnoreLocalMutation, + shouldIgnorePattern, +} from "~/common/ignore-options"; +import type { RuleContext, RuleMetaData, RuleResult } from "~/util/rule"; +import { isReadonly, createRule } from "~/util/rule"; +import { isFunctionLike, isTSFunctionType } from "~/util/typeguard"; + +// The name of this rule. +export const name = "prefer-readonly-return-types" as const; + +// The options this rule can take. +type Options = AllowLocalMutationOption & + IgnoreClassOption & + IgnoreInferredTypesOption & + IgnoreInterfaceOption & + IgnorePatternOption & + ReadonlynessOptions; + +// The schema for the rule options. +const schema: JSONSchema4 = [ + deepmerge( + allowLocalMutationOptionSchema, + ignoreClassOptionSchema, + ignoreInferredTypesOptionOptionSchema, + ignoreInterfaceOptionSchema, + ignorePatternOptionSchema, + readonlynessOptionsSchema + ), +]; + +// The default options for the rule. +const defaultOptions: Options = { + ignoreClass: false, + ignoreInterface: false, + allowLocalMutation: false, + ignoreInferredTypes: false, + ...readonlynessOptionsDefaults, +}; + +// The possible error messages. +const errorMessages = { + returnTypeShouldBeReadonly: "Return type should be readonly.", +} as const; + +// The meta data for this rule. +const meta: RuleMetaData = { + type: "suggestion", + docs: { + description: "Prefer readonly return types over mutable one.", + recommended: "error", + }, + messages: errorMessages, + fixable: "code", + schema, +}; + +/** + * Check if the given TypeAnnotation violates this rule. + */ +function checkTypeAnnotation( + node: TSESTree.TSTypeAnnotation, + context: RuleContext, + options: Options +): RuleResult { + if ( + !isReturnType(node) || + shouldIgnoreInferredTypes(node.typeAnnotation, context, options) || + shouldIgnoreClass(node.typeAnnotation, context, options) || + shouldIgnoreInterface(node.typeAnnotation, context, options) || + shouldIgnoreLocalMutation(node.typeAnnotation, context, options) || + shouldIgnorePattern(node.typeAnnotation, context, options) || + isReadonly(node.typeAnnotation, context, options) + ) { + return { + context, + descriptors: [], + }; + } + + return { + context, + descriptors: [ + { + node: node.typeAnnotation, + messageId: "returnTypeShouldBeReadonly", + }, + ], + }; +} + +/** + * Is the given node a return type? + */ +function isReturnType(node: TSESTree.TSTypeAnnotation) { + return ( + node.parent !== undefined && + (isFunctionLike(node.parent) || isTSFunctionType(node.parent)) && + node.parent.returnType === node + ); +} + +// Create the rule. +export const rule = createRule( + name, + meta, + defaultOptions, + { + TSTypeAnnotation: checkTypeAnnotation, + } +); diff --git a/tests/rules/prefer-readonly-return-types/index.test.ts b/tests/rules/prefer-readonly-return-types/index.test.ts new file mode 100644 index 000000000..ae02f22c3 --- /dev/null +++ b/tests/rules/prefer-readonly-return-types/index.test.ts @@ -0,0 +1,6 @@ +import { name, rule } from "~/rules/prefer-readonly-return-types"; +import { testUsing } from "~/tests/helpers/testers"; + +import tsTests from "./ts"; + +testUsing.typescript(name, rule, tsTests); diff --git a/tests/rules/prefer-readonly-return-types/ts/index.ts b/tests/rules/prefer-readonly-return-types/ts/index.ts new file mode 100644 index 000000000..40a005f71 --- /dev/null +++ b/tests/rules/prefer-readonly-return-types/ts/index.ts @@ -0,0 +1,7 @@ +import invalid from "./invalid"; +import valid from "./valid"; + +export default { + valid, + invalid, +}; diff --git a/tests/rules/prefer-readonly-return-types/ts/invalid.ts b/tests/rules/prefer-readonly-return-types/ts/invalid.ts new file mode 100644 index 000000000..61455aa1b --- /dev/null +++ b/tests/rules/prefer-readonly-return-types/ts/invalid.ts @@ -0,0 +1,282 @@ +import dedent from "dedent"; + +import type { InvalidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + // Don't allow mutable return type. + { + code: dedent` + function foo(): Array {} + function bar(): number[] {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeReference", + line: 1, + column: 17, + }, + { + messageId: "returnTypeShouldBeReadonly", + type: "TSArrayType", + line: 2, + column: 17, + }, + ], + }, + { + code: dedent` + const foo = function(): Array {} + const bar = function(): number[] {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeReference", + line: 1, + column: 25, + }, + { + messageId: "returnTypeShouldBeReadonly", + type: "TSArrayType", + line: 2, + column: 25, + }, + ], + }, + { + code: dedent` + const foo = (): Array => {} + const bar = (): number[] => {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeReference", + line: 1, + column: 17, + }, + { + messageId: "returnTypeShouldBeReadonly", + type: "TSArrayType", + line: 2, + column: 17, + }, + ], + }, + { + code: dedent` + class Foo { + foo(): Array {} + } + class Bar { + bar(): number[] {} + } + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeReference", + line: 2, + column: 10, + }, + { + messageId: "returnTypeShouldBeReadonly", + type: "TSArrayType", + line: 5, + column: 10, + }, + ], + }, + // Interface with functions with mutable return types should fail. + { + code: dedent` + interface Foo { + a: () => string[], + } + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSArrayType", + line: 2, + column: 12, + }, + ], + }, + // Type aliases with functions with mutable return types should fail. + { + code: dedent` + type Foo = { + a: () => string[], + }; + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSArrayType", + line: 2, + column: 12, + }, + ], + }, + // Don't allow mutable return type with Type Arguments. + { + code: dedent` + function foo(): Promise> {} + function foo(): Promise {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeReference", + line: 1, + column: 17, + }, + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeReference", + line: 2, + column: 17, + }, + ], + }, + // Don't allow mutable return type with deep Type Arguments. + { + code: dedent` + type Foo = { readonly x: T; }; + function foo(): Promise>> {} + function bar(): Promise> {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeReference", + line: 2, + column: 17, + }, + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeReference", + line: 3, + column: 17, + }, + ], + }, + // Don't allow mutable Type Arguments in a tuple return type. + { + code: dedent` + function foo(): readonly [number, Array, number] {} + function bar(): readonly [number, number[], number] {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeOperator", + line: 1, + column: 17, + }, + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeOperator", + line: 2, + column: 17, + }, + ], + }, + // Don't allow mutable Union Type return type. + { + code: dedent` + function foo(): { readonly a: Array } | { readonly b: string[] } {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSUnionType", + line: 1, + column: 17, + }, + ], + }, + // Don't allow mutable Intersection Type return type. + { + code: dedent` + function foo(): { readonly a: Array } & { readonly b: string[] } {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSIntersectionType", + line: 1, + column: 17, + }, + ], + }, + // Don't allow mutable Conditional Type return type. + { + code: dedent` + function foo(): T extends Array ? string : number[] {} + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSConditionalType", + line: 1, + column: 20, + }, + ], + }, + // Mutable method signature should fail. + { + code: dedent` + type Foo = { + a: () => { + methodSignature(): string; + }, + } + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSTypeLiteral", + line: 2, + column: 12, + }, + ], + }, + { + code: dedent` + type Foo = { + a: () => { + methodSignature1(): string; + } | { + methodSignature2(): number; + }, + }; + `, + optionsSet: [[]], + errors: [ + { + messageId: "returnTypeShouldBeReadonly", + type: "TSUnionType", + line: 2, + column: 12, + }, + ], + }, +]; + +export default tests; diff --git a/tests/rules/prefer-readonly-return-types/ts/valid.ts b/tests/rules/prefer-readonly-return-types/ts/valid.ts new file mode 100644 index 000000000..9b6a44914 --- /dev/null +++ b/tests/rules/prefer-readonly-return-types/ts/valid.ts @@ -0,0 +1,153 @@ +import dedent from "dedent"; + +import type { ValidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + // Should not fail on shorthand syntax readonly array type as return type. + { + code: dedent` + function foo(): readonly number[] {} + `, + optionsSet: [[]], + }, + { + code: dedent` + const foo = (): readonly number[] => {} + `, + optionsSet: [[]], + }, + // Should not fail on longhand syntax readonly array type as return type. + { + code: dedent` + function foo(): ReadonlyArray {} + `, + optionsSet: [[]], + }, + { + code: dedent` + const foo = (): ReadonlyArray => {} + `, + optionsSet: [[]], + }, + // Allow inline immutable return type. + { + code: dedent` + function foo(bar: string): { readonly baz: number } {} + `, + optionsSet: [[]], + }, + // Interfaces with functions with immutable return types should not produce failures. + { + code: dedent` + interface Foo { + a: () => readonly string[], + } + `, + optionsSet: [[]], + }, + // Type aliases with functions with immutable return types should not produce failures. + { + code: dedent` + type Foo = { + a: () => readonly string[], + }; + `, + optionsSet: [[]], + }, + // Ignore Classes. + { + code: dedent` + class Klass { + a(): string[] {} + } + `, + optionsSet: [[{ ignoreClass: true }]], + }, + // Ignore Interfaces. + { + code: dedent` + interface Foo { + a: () => string[], + } + `, + optionsSet: [[{ ignoreInterface: true }]], + }, + // TODO: Allow Local. + // TODO: Ignore Prefix. + // TODO: Ignore Suffix. + // Readonly method signature. + { + code: dedent` + type Foo = { + a: () => Readonly<{ + methodSignature(): string; + }>, + } + `, + optionsSet: [[]], + }, + { + code: dedent` + type Foo = { + a: () => Readonly<{ + methodSignature1(): string; + }> | Readonly<{ + methodSignature2(): number; + }>, + }; + `, + optionsSet: [[]], + }, + // Mutable method signature treated as readonly. + { + code: dedent` + type Foo = { + a: () => { + methodSignature(): string; + }, + } + `, + optionsSet: [ + [ + { + treatMethodsAsReadonly: true, + }, + ], + ], + }, + { + code: dedent` + type Foo = { + a: () => { + methodSignature1(): string; + } | { + methodSignature2(): number; + }, + }; + `, + optionsSet: [ + [ + { + treatMethodsAsReadonly: true, + }, + ], + ], + }, + // Ignore inferred types. + { + code: dedent` + function foo() { + return [1, 2, 3] + } + `, + optionsSet: [ + [ + { + ignoreInferredTypes: true, + }, + ], + ], + }, +]; + +export default tests; diff --git a/yarn.lock b/yarn.lock index e3861f744..02365dbea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1508,13 +1508,14 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.0.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.3.0.tgz#a55ae72d28ffeb6badd817fe4566c9cced1f5e29" - integrity sha512-ARUEJHJrq85aaiCqez7SANeahDsJTD3AEua34EoQN9pHS6S5Bq9emcIaGGySt/4X2zSi+vF5hAH52sEen7IO7g== - dependencies: - "@typescript-eslint/experimental-utils" "5.3.0" - "@typescript-eslint/scope-manager" "5.3.0" +"@typescript-eslint/eslint-plugin@^5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.9.1.tgz#e5a86d7e1f9dc0b3df1e6d94feaf20dd838d066c" + integrity sha512-Xv9tkFlyD4MQGpJgTo6wqDqGvHIRmRgah/2Sjz1PUnJTawjHWIwBivUE9x0QtU2WVii9baYgavo/bHjrZJkqTw== + dependencies: + "@typescript-eslint/experimental-utils" "5.9.1" + "@typescript-eslint/scope-manager" "5.9.1" + "@typescript-eslint/type-utils" "5.9.1" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -1522,94 +1523,69 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@5.3.0", "@typescript-eslint/experimental-utils@^5.0.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.3.0.tgz#ee56b4957547ed2b0fc7451205e41502e664f546" - integrity sha512-NFVxYTjKj69qB0FM+piah1x3G/63WB8vCBMnlnEHUsiLzXSTWb9FmFn36FD9Zb4APKBLY3xRArOGSMQkuzTF1w== +"@typescript-eslint/experimental-utils@5.9.1", "@typescript-eslint/experimental-utils@^5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.9.1.tgz#8c407c4dd5ffe522329df6e4c9c2b52206d5f7f1" + integrity sha512-cb1Njyss0mLL9kLXgS/eEY53SZQ9sT519wpX3i+U457l2UXRDuo87hgKfgRazmu9/tQb0x2sr3Y0yrU+Zz0y+w== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.3.0" - "@typescript-eslint/types" "5.3.0" - "@typescript-eslint/typescript-estree" "5.3.0" + "@typescript-eslint/scope-manager" "5.9.1" + "@typescript-eslint/types" "5.9.1" + "@typescript-eslint/typescript-estree" "5.9.1" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/parser@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.0.0.tgz#50d1be2e0def82d73e863cceba74aeeac9973592" - integrity sha512-B6D5rmmQ14I1fdzs71eL3DAuvnPHTY/t7rQABrL9BLnx/H51Un8ox1xqYAchs0/V2trcoyxB1lMJLlrwrJCDgw== +"@typescript-eslint/parser@^5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.9.1.tgz#b114011010a87e17b3265ca715e16c76a9834cef" + integrity sha512-PLYO0AmwD6s6n0ZQB5kqPgfvh73p0+VqopQQLuNfi7Lm0EpfKyDalchpVwkE+81k5HeiRrTV/9w1aNHzjD7C4g== dependencies: - "@typescript-eslint/scope-manager" "5.0.0" - "@typescript-eslint/types" "5.0.0" - "@typescript-eslint/typescript-estree" "5.0.0" - debug "^4.3.1" - -"@typescript-eslint/scope-manager@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.0.0.tgz#aea0fb0e2480c1169a02e89d9005ac3f2835713f" - integrity sha512-5RFjdA/ain/MDUHYXdF173btOKncIrLuBmA9s6FJhzDrRAyVSA+70BHg0/MW6TE+UiKVyRtX91XpVS0gVNwVDQ== - dependencies: - "@typescript-eslint/types" "5.0.0" - "@typescript-eslint/visitor-keys" "5.0.0" + "@typescript-eslint/scope-manager" "5.9.1" + "@typescript-eslint/types" "5.9.1" + "@typescript-eslint/typescript-estree" "5.9.1" + debug "^4.3.2" -"@typescript-eslint/scope-manager@5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.3.0.tgz#97d0ccc7c9158e89e202d5e24ce6ba49052d432e" - integrity sha512-22Uic9oRlTsPppy5Tcwfj+QET5RWEnZ5414Prby465XxQrQFZ6nnm5KnXgnsAJefG4hEgMnaxTB3kNEyjdjj6A== +"@typescript-eslint/scope-manager@5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.9.1.tgz#6c27be89f1a9409f284d95dfa08ee3400166fe69" + integrity sha512-8BwvWkho3B/UOtzRyW07ffJXPaLSUKFBjpq8aqsRvu6HdEuzCY57+ffT7QoV4QXJXWSU1+7g3wE4AlgImmQ9pQ== dependencies: - "@typescript-eslint/types" "5.3.0" - "@typescript-eslint/visitor-keys" "5.3.0" - -"@typescript-eslint/types@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.0.0.tgz#25d93f6d269b2d25fdc51a0407eb81ccba60eb0f" - integrity sha512-dU/pKBUpehdEqYuvkojmlv0FtHuZnLXFBn16zsDmlFF3LXkOpkAQ2vrKc3BidIIve9EMH2zfTlxqw9XM0fFN5w== - -"@typescript-eslint/types@5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.3.0.tgz#af29fd53867c2df0028c57c36a655bd7e9e05416" - integrity sha512-fce5pG41/w8O6ahQEhXmMV+xuh4+GayzqEogN24EK+vECA3I6pUwKuLi5QbXO721EMitpQne5VKXofPonYlAQg== + "@typescript-eslint/types" "5.9.1" + "@typescript-eslint/visitor-keys" "5.9.1" -"@typescript-eslint/typescript-estree@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.0.0.tgz#bc20f413c6e572c7309dbe5fa3be027984952af3" - integrity sha512-V/6w+PPQMhinWKSn+fCiX5jwvd1vRBm7AX7SJQXEGQtwtBvjMPjaU3YTQ1ik2UF1u96X7tsB96HMnulG3eLi9Q== +"@typescript-eslint/type-utils@5.9.1", "@typescript-eslint/type-utils@^5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.9.1.tgz#c6832ffe655b9b1fec642d36db1a262d721193de" + integrity sha512-tRSpdBnPRssjlUh35rE9ug5HrUvaB9ntREy7gPXXKwmIx61TNN7+l5YKgi1hMKxo5NvqZCfYhA5FvyuJG6X6vg== dependencies: - "@typescript-eslint/types" "5.0.0" - "@typescript-eslint/visitor-keys" "5.0.0" - debug "^4.3.1" - globby "^11.0.3" - is-glob "^4.0.1" - semver "^7.3.5" + "@typescript-eslint/experimental-utils" "5.9.1" + debug "^4.3.2" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.3.0.tgz#4f68ddd46dc2983182402d2ab21fb44ad94988cf" - integrity sha512-FJ0nqcaUOpn/6Z4Jwbtf+o0valjBLkqc3MWkMvrhA2TvzFXtcclIM8F4MBEmYa2kgcI8EZeSAzwoSrIC8JYkug== +"@typescript-eslint/types@5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.9.1.tgz#1bef8f238a2fb32ebc6ff6d75020d9f47a1593c6" + integrity sha512-SsWegWudWpkZCwwYcKoDwuAjoZXnM1y2EbEerTHho19Hmm+bQ56QG4L4jrtCu0bI5STaRTvRTZmjprWlTw/5NQ== + +"@typescript-eslint/typescript-estree@5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.9.1.tgz#d5b996f49476495070d2b8dd354861cf33c005d6" + integrity sha512-gL1sP6A/KG0HwrahVXI9fZyeVTxEYV//6PmcOn1tD0rw8VhUWYeZeuWHwwhnewnvEMcHjhnJLOBhA9rK4vmb8A== dependencies: - "@typescript-eslint/types" "5.3.0" - "@typescript-eslint/visitor-keys" "5.3.0" + "@typescript-eslint/types" "5.9.1" + "@typescript-eslint/visitor-keys" "5.9.1" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/visitor-keys@5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.0.0.tgz#b789f7cd105e59bee5c0983a353942a5a48f56df" - integrity sha512-yRyd2++o/IrJdyHuYMxyFyBhU762MRHQ/bAGQeTnN3pGikfh+nEmM61XTqaDH1XDp53afZ+waXrk0ZvenoZ6xw== - dependencies: - "@typescript-eslint/types" "5.0.0" - eslint-visitor-keys "^3.0.0" - -"@typescript-eslint/visitor-keys@5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.3.0.tgz#a6258790f3b7b2547f70ed8d4a1e0c3499994523" - integrity sha512-oVIAfIQuq0x2TFDNLVavUn548WL+7hdhxYn+9j3YdJJXB7mH9dAmZNJsPDa7Jc+B9WGqoiex7GUDbyMxV0a/aw== +"@typescript-eslint/visitor-keys@5.9.1": + version "5.9.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.9.1.tgz#f52206f38128dd4f675cf28070a41596eee985b7" + integrity sha512-Xh37pNz9e9ryW4TVdwiFzmr4hloty8cFj8GTWMXh3Z8swGwyQWeCcNgF0hm6t09iZd6eiZmIf4zHedQVP6TVtg== dependencies: - "@typescript-eslint/types" "5.3.0" + "@typescript-eslint/types" "5.9.1" eslint-visitor-keys "^3.0.0" JSONStream@^1.0.4: @@ -4107,7 +4083,7 @@ globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" -globby@^11.0.0, globby@^11.0.1, globby@^11.0.3, globby@^11.0.4: +globby@^11.0.0, globby@^11.0.1, globby@^11.0.4: version "11.0.4" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==