From 23b45d25b252b1b0337c57ef671ae231a17f38d9 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 14 Sep 2022 18:28:20 +1200 Subject: [PATCH 001/100] build!: bump minimum supported version of node to 16.10 --- .github/workflows/ci.yml | 1 - .nvmrc | 1 + package.json | 2 +- yarn.lock | 19 +++++++++++++------ 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .nvmrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e17a487c..cfe2793f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,6 @@ jobs: os: - "ubuntu-latest" node_version: - - "14" - "16" - "18" ts_version: diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..56bfee434 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v16.10.0 diff --git a/package.json b/package.json index 34f1c4d16..bbbb08a3b 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,6 @@ }, "packageManager": "yarn@3.3.1", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=16.10.0" } } diff --git a/yarn.lock b/yarn.lock index 4dabf5e71..d50f143b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1725,7 +1725,14 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:18.11.18": +"@types/node@npm:*": + version: 18.0.0 + resolution: "@types/node@npm:18.0.0" + checksum: aab2b325727a2599f6d25ebe0dedf58c40fb66a51ce4ca9c0226ceb70fcda2d3afccdca29db5942eb48b158ee8585a274a1e3750c718bbd5399d7f41d62dfdcc + languageName: node + linkType: hard + +"@types/node@npm:18.11.18": version: 18.11.18 resolution: "@types/node@npm:18.11.18" checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d @@ -4766,7 +4773,7 @@ __metadata: "fsevents@patch:fsevents@~2.3.2#~builtin": version: 2.3.2 - resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=df0bf1" + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=18f3a7" dependencies: node-gyp: latest conditions: os=darwin @@ -8545,7 +8552,7 @@ __metadata: "resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.10.1#~builtin, resolve@patch:resolve@^1.12.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin": version: 1.22.1 - resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=c3c19d" + resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=07638b" dependencies: is-core-module: ^2.9.0 path-parse: ^1.0.7 @@ -8558,7 +8565,7 @@ __metadata: "resolve@patch:resolve@~1.19.0#~builtin": version: 1.19.0 - resolution: "resolve@patch:resolve@npm%3A1.19.0#~builtin::version=1.19.0&hash=c3c19d" + resolution: "resolve@patch:resolve@npm%3A1.19.0#~builtin::version=1.19.0&hash=07638b" dependencies: is-core-module: ^2.1.0 path-parse: ^1.0.6 @@ -9652,11 +9659,11 @@ __metadata: "typescript@patch:typescript@^4.5.5#~builtin, typescript@patch:typescript@^4.6.4#~builtin": version: 4.9.4 - resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin::version=4.9.4&hash=ad5954" + resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin::version=4.9.4&hash=a1c5e5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 1caaea6cb7f813e64345190fddc4e6c924d0b698ab81189b503763c4a18f7f5501c69362979d36e19c042d89d936443e768a78b0675690b35eb663d19e0eae71 + checksum: 37f6e2c3c5e2aa5934b85b0fddbf32eeac8b1bacf3a5b51d01946936d03f5377fe86255d4e5a4ae628fd0cd553386355ad362c57f13b4635064400f3e8e05b9d languageName: node linkType: hard From 405102be6379e7f30c8f3f0e28a43359e65a27e9 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 14 Sep 2022 18:30:47 +1200 Subject: [PATCH 002/100] build!: bump minimum supported version of TypeScript to 4.0.2 --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bbbb08a3b..0fb17cb11 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "peerDependencies": { "eslint": "^8.0.0", "tsutils": "^3.0.0", - "typescript": "^3.4.1 || ^4.0.0" + "typescript": ">=4.0.2" }, "peerDependenciesMeta": { "tsutils": { diff --git a/yarn.lock b/yarn.lock index d50f143b7..e164220d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4053,7 +4053,7 @@ __metadata: peerDependencies: eslint: ^8.0.0 tsutils: ^3.0.0 - typescript: ^3.4.1 || ^4.0.0 + typescript: ">=4.0.2" peerDependenciesMeta: tsutils: optional: true From 941e774f11ed7473167e1d37020f17e6755e6a7b Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 11 Sep 2022 08:10:29 +1200 Subject: [PATCH 003/100] feat(type-declaration-immutability): create rule --- package.json | 1 + src/configs/all.ts | 1 + src/configs/no-mutations.ts | 1 + src/rules/index.ts | 2 + src/rules/type-declaration-immutability.ts | 334 +++++++++++++++++ src/settings/immutability.ts | 212 +++++++++++ src/settings/index.ts | 1 + src/util/node-types.ts | 10 + src/util/rule.ts | 53 +++ src/util/typeguard.ts | 6 + tests/helpers/util.ts | 39 +- .../index.test.ts | 6 + .../type-declaration-immutability/ts/index.ts | 7 + .../ts/invalid.ts | 339 ++++++++++++++++++ .../type-declaration-immutability/ts/valid.ts | 162 +++++++++ tests/rules/work.test.ts | 2 + tsconfig.base.json | 1 + yarn.lock | 13 + 18 files changed, 1177 insertions(+), 13 deletions(-) create mode 100644 src/rules/type-declaration-immutability.ts create mode 100644 src/settings/immutability.ts create mode 100644 src/settings/index.ts create mode 100644 tests/rules/type-declaration-immutability/index.test.ts create mode 100644 tests/rules/type-declaration-immutability/ts/index.ts create mode 100644 tests/rules/type-declaration-immutability/ts/invalid.ts create mode 100644 tests/rules/type-declaration-immutability/ts/valid.ts diff --git a/package.json b/package.json index 0fb17cb11..bbc439bd3 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@typescript-eslint/utils": "^5.10.2", "deepmerge-ts": "^4.0.3", "escape-string-regexp": "^4.0.0", + "is-immutable-type": "^0.0.7", "semver": "^7.3.7" }, "devDependencies": { diff --git a/src/configs/all.ts b/src/configs/all.ts index 0db73d9d0..84aa0737d 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -24,6 +24,7 @@ const config: Linter.Config = { "functional/prefer-readonly-type": "error", "functional/prefer-tacit": ["error", { assumeTypes: false }], "functional/no-return-void": "error", + "functional/type-declaration-immutability": "error", }, }, ], diff --git a/src/configs/no-mutations.ts b/src/configs/no-mutations.ts index bc5cc4600..cdb807b12 100644 --- a/src/configs/no-mutations.ts +++ b/src/configs/no-mutations.ts @@ -11,6 +11,7 @@ const config: Linter.Config = { rules: { "functional/no-method-signature": "warn", "functional/prefer-readonly-type": "error", + "functional/type-declaration-immutability": "error", }, }, ], diff --git a/src/rules/index.ts b/src/rules/index.ts index 06eb7baef..2c0e308d5 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -14,6 +14,7 @@ import * as noThrowStatement from "./no-throw-statement"; import * as noTryStatement from "./no-try-statement"; import * as preferReadonlyTypes from "./prefer-readonly-type"; import * as preferTacit from "./prefer-tacit"; +import * as typeDeclarationImmutability from "./type-declaration-immutability"; /** * All of the custom rules. @@ -35,4 +36,5 @@ export const rules = { [noTryStatement.name]: noTryStatement.rule, [preferReadonlyTypes.name]: preferReadonlyTypes.rule, [preferTacit.name]: preferTacit.rule, + [typeDeclarationImmutability.name]: typeDeclarationImmutability.rule, }; diff --git a/src/rules/type-declaration-immutability.ts b/src/rules/type-declaration-immutability.ts new file mode 100644 index 000000000..b3a5e5225 --- /dev/null +++ b/src/rules/type-declaration-immutability.ts @@ -0,0 +1,334 @@ +import type { ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; +import { deepmerge } from "deepmerge-ts"; +import { Immutability } from "is-immutable-type"; +import type { JSONSchema4 } from "json-schema"; +import type { ReadonlyDeep } from "type-fest"; + +import type { IgnorePatternOption } from "~/common/ignore-options"; +import { + shouldIgnorePattern, + ignorePatternOptionSchema, +} from "~/common/ignore-options"; +import { getNodeIdentifierTexts } from "~/util/misc"; +import type { ESTypeDeclaration } from "~/util/node-types"; +import type { RuleResult } from "~/util/rule"; +import { getTypeImmutabilityOfNode, createRule } from "~/util/rule"; +import { isReadonlyArray, isTSInterfaceDeclaration } from "~/util/typeguard"; + +/** + * The name of this rule. + */ +export const name = "type-declaration-immutability" as const; + +/** + * How the actual immutability should be compared to the given immutability. + */ +export enum RuleEnforcementComparator { + Less = -2, + AtMost = -1, + Exactly = 0, + AtLeast = 1, + More = 2, +} + +/** + * The options this rule can take. + */ +type Options = ReadonlyDeep< + [ + IgnorePatternOption & { + rules?: Array<{ + identifier: string | string[]; + immutability: Exclude< + Immutability | keyof typeof Immutability, + "Unknown" + >; + comparator?: + | RuleEnforcementComparator + | keyof typeof RuleEnforcementComparator; + }>; + ignoreInterfaces: boolean; + } + ] +>; + +/** + * The schema for the rule options. + */ +const schema: JSONSchema4 = [ + { + type: "object", + properties: deepmerge(ignorePatternOptionSchema, { + rules: { + type: "array", + items: { + type: "object", + properties: { + identifier: { + type: ["string", "array"], + items: { + type: "string", + }, + }, + immutability: { + type: ["string", "number"], + enum: Object.values(Immutability).filter( + (i) => + i !== Immutability.Unknown && + i !== Immutability[Immutability.Unknown] + ), + }, + comparator: { + type: ["string", "number"], + enum: Object.values(RuleEnforcementComparator), + }, + }, + required: ["identifier", "immutability"], + additionalProperties: false, + }, + }, + ignoreInterfaces: { + type: "boolean", + }, + }), + additionalProperties: false, + }, +]; + +/** + * The default options for the rule. + */ +const defaultOptions: Options = [ + { + ignoreInterfaces: false, + }, +]; + +/** + * The possible error messages. + */ +const errorMessages = { + Less: 'This type is declare to have an immutability less than "{{ expected }}" (actual: "{{ actual }}").', + AtLeast: + 'This type is declare to have an immutability of at least "{{ expected }}" (actual: "{{ actual }}").', + Exactly: + 'This type is declare to have an immutability of exactly "{{ expected }}" (actual: "{{ actual }}").', + AtMost: + 'This type is declare to have an immutability of at most "{{ expected }}" (actual: "{{ actual }}").', + More: 'This type is declare to have an immutability more than "{{ expected }}" (actual: "{{ actual }}").', +} as const; + +/** + * The meta data for this rule. + */ +const meta: ESLintUtils.NamedCreateRuleMeta = { + type: "suggestion", + docs: { + description: "Enforce the immutability of types based on patterns.", + recommended: "error", + }, + messages: errorMessages, + schema, +}; + +/** + * A rule given by the user after being upgraded. + */ +export type ImmutabilityRule = { + identifiers: ReadonlyArray; + immutability: Immutability; + comparator: RuleEnforcementComparator; +}; + +/** + * Get the default immutability rules. + */ +function getDefaultImmutabilityRules(): ImmutabilityRule[] { + return [ + { + identifiers: [/^I?Immutable.+/u], + immutability: Immutability.Immutable, + comparator: RuleEnforcementComparator.AtLeast, + }, + { + identifiers: [/^I?ReadonlyDeep.+/u], + immutability: Immutability.ReadonlyDeep, + comparator: RuleEnforcementComparator.AtLeast, + }, + { + identifiers: [/^I?Readonly.+/u], + immutability: Immutability.ReadonlyShallow, + comparator: RuleEnforcementComparator.AtLeast, + }, + { + identifiers: [/^I?Mutable.+/u], + immutability: Immutability.Mutable, + comparator: RuleEnforcementComparator.AtMost, + }, + ]; +} + +/** + * Get all the rules that were given and upgrade them. + */ +function getRules(options: Options): ImmutabilityRule[] { + const [optionsObject] = options; + const { rules: rulesOptions } = optionsObject; + + if (rulesOptions === undefined) { + return getDefaultImmutabilityRules(); + } + + return rulesOptions.map((rule): ImmutabilityRule => { + const identifiers = isReadonlyArray(rule.identifier) + ? rule.identifier.map((id) => new RegExp(id, "u")) + : [new RegExp(rule.identifier, "u")]; + + const immutability = + typeof rule.immutability === "string" + ? Immutability[rule.immutability] + : rule.immutability; + + const comparator = + rule.comparator === undefined + ? RuleEnforcementComparator.AtLeast + : typeof rule.comparator === "string" + ? RuleEnforcementComparator[rule.comparator] + : rule.comparator; + + return { + identifiers, + immutability, + comparator, + }; + }); +} + +/** + * Find the first rule to apply to the given node. + */ +export function getRuleToApply( + node: ReadonlyDeep, + context: ReadonlyDeep< + TSESLint.RuleContext + >, + options: Options +): ImmutabilityRule | undefined { + const rules = getRules(options); + if (rules.length === 0) { + return undefined; + } + + const texts = getNodeIdentifierTexts(node, context); + + if (texts.length === 0) { + return undefined; + } + + return rules.find((rule) => + rule.identifiers.some((pattern) => texts.some((text) => pattern.test(text))) + ); +} + +/** + * Compare the actual immutability to the expected immutability. + */ +export function compareImmutability( + rule: ReadonlyDeep, + actual: Immutability +) { + switch (rule.comparator) { + case RuleEnforcementComparator.Less: + return actual < rule.immutability; + case RuleEnforcementComparator.AtMost: + return actual <= rule.immutability; + case RuleEnforcementComparator.Exactly: + return actual === rule.immutability; + case RuleEnforcementComparator.AtLeast: + return actual >= rule.immutability; + case RuleEnforcementComparator.More: + return actual > rule.immutability; + } +} + +/** + * Get the results. + */ +function getResults( + node: ReadonlyDeep, + context: ReadonlyDeep< + TSESLint.RuleContext + >, + rule: ImmutabilityRule, + immutability: Immutability +): RuleResult { + const valid = compareImmutability(rule, immutability); + if (valid) { + return { + context, + descriptors: [], + }; + } + + return { + context, + descriptors: [ + { + node: node.id, + messageId: RuleEnforcementComparator[ + rule.comparator + ] as keyof typeof RuleEnforcementComparator, + data: { + actual: Immutability[immutability], + expected: Immutability[rule.immutability], + }, + }, + ], + }; +} + +/** + * Check if the given Interface or Type Alias violates this rule. + */ +function checkTypeDeclaration( + node: ReadonlyDeep, + context: ReadonlyDeep< + TSESLint.RuleContext + >, + options: Options +): RuleResult { + const [optionsObject] = options; + const { ignoreInterfaces } = optionsObject; + if ( + shouldIgnorePattern(node, context, optionsObject) || + (ignoreInterfaces && isTSInterfaceDeclaration(node)) + ) { + return { + context, + descriptors: [], + }; + } + + const rule = getRuleToApply(node, context, options); + if (rule === undefined) { + return { + context, + descriptors: [], + }; + } + + const immutability = getTypeImmutabilityOfNode(node, context); + + return getResults(node, context, rule, immutability); +} + +// Create the rule. +export const rule = createRule( + name, + meta, + defaultOptions, + { + TSTypeAliasDeclaration: checkTypeDeclaration, + TSInterfaceDeclaration: checkTypeDeclaration, + } +); diff --git a/src/settings/immutability.ts b/src/settings/immutability.ts new file mode 100644 index 000000000..f8d6f0cda --- /dev/null +++ b/src/settings/immutability.ts @@ -0,0 +1,212 @@ +import type { SharedConfigurationSettings } from "@typescript-eslint/utils"; +import type { ImmutabilityOverrides } from "is-immutable-type"; +import { + Immutability, + getDefaultOverrides as getDefaultImmutabilityOverrides, +} from "is-immutable-type"; +import type { JSONSchema4 } from "json-schema"; +import type { ReadonlyDeep } from "type-fest"; + +import { isReadonlyArray } from "~/util/typeguard"; + +declare module "@typescript-eslint/utils" { + type OverridesSetting = { + name?: string; + pattern?: string; + to: Immutability | keyof typeof Immutability; + from?: Immutability | keyof typeof Immutability; + }; + + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-shadow + interface SharedConfigurationSettings { + immutability?: ReadonlyDeep<{ + overrides?: + | OverridesSetting[] + | { + keepDefault?: boolean; + values?: OverridesSetting[]; + }; + }>; + } +} + +/** + * The settings that have been loaded - so we don't have to reload them. + */ +const cachedSettings: WeakMap< + ReadonlyDeep, + ImmutabilityOverrides | undefined +> = new WeakMap(); + +/** + * Get the immutability overrides defined in the settings. + */ +export function getImmutabilityOverrides( + settings: ReadonlyDeep +): ImmutabilityOverrides | undefined { + if (!cachedSettings.has(settings)) { + const overrides = loadImmutabilityOverrides(settings); + + // eslint-disable-next-line functional/no-expression-statement + cachedSettings.set(settings, overrides); + return overrides; + } + return cachedSettings.get(settings); +} + +/** + * Get all the overrides and upgrade them. + */ +function loadImmutabilityOverrides( + settings: ReadonlyDeep +): ImmutabilityOverrides | undefined { + const { immutability: immutabilitySettings } = settings; + const overridesSetting = immutabilitySettings?.overrides; + + if (overridesSetting === undefined) { + return undefined; + } + + const raw = isReadonlyArray(overridesSetting) + ? overridesSetting + : overridesSetting.values ?? []; + + const upgraded = raw.map( + ({ name, pattern, to, from }) => + ({ + name, + pattern: pattern === undefined ? pattern : new RegExp(pattern, "u"), + to: typeof to !== "string" ? to : Immutability[to], + from: + from === undefined + ? undefined + : typeof from !== "string" + ? from + : Immutability[from], + } as ImmutabilityOverrides[number]) + ); + + const keepDefault = + isReadonlyArray(overridesSetting) || overridesSetting.keepDefault !== false; + + return keepDefault + ? [...getDefaultImmutabilityOverrides(), ...upgraded] + : upgraded; +} + +/** + * The schema for the rule options. + */ +export const sharedConfigurationSettingsSchema: JSONSchema4 = [ + { + type: "object", + properties: { + type: "object", + immutability: { + properties: { + overrides: { + oneOf: [ + { + type: "object", + properties: { + keepDefault: { + type: "boolean", + }, + values: { + type: "array", + items: { + oneOf: [ + { + type: "object", + properties: { + name: { + type: "string", + }, + to: { + type: ["string", "number"], + enum: Object.values(Immutability), + }, + from: { + type: ["string", "number"], + enum: Object.values(Immutability), + }, + }, + required: ["name", "to"], + additionalProperties: false, + }, + { + type: "object", + properties: { + pattern: { + type: "string", + }, + to: { + type: ["string", "number"], + enum: Object.values(Immutability), + }, + from: { + type: ["string", "number"], + enum: Object.values(Immutability), + }, + }, + required: ["pattern", "to"], + additionalProperties: false, + }, + ], + }, + }, + }, + additionalProperties: false, + }, + { + type: "array", + items: { + oneOf: [ + { + type: "object", + properties: { + name: { + type: "string", + }, + to: { + type: ["string", "number"], + enum: Object.values(Immutability), + }, + from: { + type: ["string", "number"], + enum: Object.values(Immutability), + }, + }, + required: ["name", "to"], + additionalProperties: false, + }, + { + type: "object", + properties: { + pattern: { + type: "string", + }, + to: { + type: ["string", "number"], + enum: Object.values(Immutability), + }, + from: { + type: ["string", "number"], + enum: Object.values(Immutability), + }, + }, + required: ["pattern", "to"], + additionalProperties: false, + }, + ], + }, + }, + ], + }, + }, + }, + additionalProperties: false, + }, + additionalProperties: true, + }, +]; diff --git a/src/settings/index.ts b/src/settings/index.ts new file mode 100644 index 000000000..ec763ac76 --- /dev/null +++ b/src/settings/index.ts @@ -0,0 +1 @@ +export * from "./immutability"; diff --git a/src/util/node-types.ts b/src/util/node-types.ts index 0353803ac..51cbf4eba 100644 --- a/src/util/node-types.ts +++ b/src/util/node-types.ts @@ -24,3 +24,13 @@ export type ESLoop = | TSESTree.WhileStatement; export type ESArrayTupleType = TSESTree.TSArrayType | TSESTree.TSTupleType; + +export type ESProperty = + | TSESTree.PropertyDefinition + | TSESTree.TSIndexSignature + | TSESTree.TSParameterProperty + | TSESTree.TSPropertySignature; + +export type ESTypeDeclaration = + | TSESTree.TSInterfaceDeclaration + | TSESTree.TSTypeAliasDeclaration; diff --git a/src/util/rule.ts b/src/util/rule.ts index c54ad502b..c15851137 100644 --- a/src/util/rule.ts +++ b/src/util/rule.ts @@ -5,9 +5,13 @@ import type { } from "@typescript-eslint/utils"; import { ESLintUtils } from "@typescript-eslint/utils"; import type { Rule } from "eslint"; +import type { ImmutabilityOverrides } from "is-immutable-type"; +import { getTypeImmutability, Immutability } from "is-immutable-type"; import type { ReadonlyDeep } from "type-fest"; import type { Node as TSNode, Type } from "typescript"; +import { getImmutabilityOverrides } from "~/settings"; + // eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle -- This is a special var. const __VERSION__ = "0.0.0-development"; @@ -177,6 +181,55 @@ export function getTypeOfNode< return constrained ?? nodeType; } +/** + * Get the type immutability of the the given node. + */ +export function getTypeImmutabilityOfNode< + Context extends ReadonlyDeep> +>(node: ReadonlyDeep, context: Context): Immutability; + +/** + * Get the type immutability of the the given node. + */ +export function getTypeImmutabilityOfNode( + node: ReadonlyDeep, + parserServices: ParserServices, + overrides?: ImmutabilityOverrides +): Immutability; + +export function getTypeImmutabilityOfNode< + Context extends ReadonlyDeep> +>( + node: ReadonlyDeep, + contextOrServices: Context | ParserServices, + explicitOverrides?: ImmutabilityOverrides +): Immutability { + const givenParserServices = isParserServices(contextOrServices); + + const parserServices = givenParserServices + ? contextOrServices + : getParserServices(contextOrServices); + + const overrides = givenParserServices + ? explicitOverrides + : getImmutabilityOverrides(contextOrServices.settings); + + if (parserServices === null) { + return Immutability.Unknown; + } + + const checker = parserServices.program.getTypeChecker(); + + const type = getTypeOfNode(node, parserServices); + return getTypeImmutability( + checker, + type, + overrides, + // Don't use the global cache in testing environments as it may cause errors when switching between different config options. + process.env.NODE_ENV !== "test" + ); +} + /** * Get the es tree node from the given ts node. */ diff --git a/src/util/typeguard.ts b/src/util/typeguard.ts index ed58d5990..1abb00190 100644 --- a/src/util/typeguard.ts +++ b/src/util/typeguard.ts @@ -251,6 +251,12 @@ export function isTSInterfaceBody( return node.type === AST_NODE_TYPES.TSInterfaceBody; } +export function isTSInterfaceDeclaration( + node: ReadonlyDeep +): node is ReadonlyDeep { + return node.type === AST_NODE_TYPES.TSInterfaceDeclaration; +} + export function isTSInterfaceHeritage( node: ReadonlyDeep ): node is ReadonlyDeep { diff --git a/tests/helpers/util.ts b/tests/helpers/util.ts index 6fbc43d6b..cac2d012b 100644 --- a/tests/helpers/util.ts +++ b/tests/helpers/util.ts @@ -1,4 +1,7 @@ -import type { TSESLint } from "@typescript-eslint/utils"; +import type { + SharedConfigurationSettings, + TSESLint, +} from "@typescript-eslint/utils"; import type { Rule, RuleTester as ESLintRuleTester } from "eslint"; import type { ReadonlyDeep } from "type-fest"; @@ -6,25 +9,31 @@ import ts from "~/conditional-imports/typescript"; import { filename as dummyFilename } from "./configs"; -type OptionsSet = { +type OptionsSets = { /** * The set of options this test case should pass for. */ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ readonly optionsSet: ReadonlyArray; + + /** + * The set of settings this test case should pass for. + */ + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + readonly settingsSet?: ReadonlyArray; }; export type ValidTestCase = Omit< ReadonlyDeep, - "options" + "options" | "settings" > & - OptionsSet; + OptionsSets; export type InvalidTestCase = Omit< ReadonlyDeep, - "options" + "options" | "settings" > & - OptionsSet; + OptionsSets; /** * Convert our test cases into ones eslint test runner is expecting. @@ -33,13 +42,17 @@ export function processInvalidTestCase( testCases: ReadonlyArray ): ESLintRuleTester.InvalidTestCase[] { return testCases.flatMap((testCase) => - testCase.optionsSet.map((options) => { - const { optionsSet, ...eslintTestCase } = testCase; - return { - filename: dummyFilename, - ...eslintTestCase, - options, - } as ESLintRuleTester.InvalidTestCase; + testCase.optionsSet.flatMap((options) => { + const { optionsSet, settingsSet, ...eslintTestCase } = testCase; + + return (settingsSet ?? [undefined]).map((settings) => { + return { + filename: dummyFilename, + ...eslintTestCase, + options, + settings, + } as ESLintRuleTester.InvalidTestCase; + }); }) ); } diff --git a/tests/rules/type-declaration-immutability/index.test.ts b/tests/rules/type-declaration-immutability/index.test.ts new file mode 100644 index 000000000..38c122481 --- /dev/null +++ b/tests/rules/type-declaration-immutability/index.test.ts @@ -0,0 +1,6 @@ +import { name, rule } from "~/rules/type-declaration-immutability"; +import { testUsing } from "~/tests/helpers/testers"; + +import tsTests from "./ts"; + +testUsing.typescript(name, rule, tsTests); diff --git a/tests/rules/type-declaration-immutability/ts/index.ts b/tests/rules/type-declaration-immutability/ts/index.ts new file mode 100644 index 000000000..40a005f71 --- /dev/null +++ b/tests/rules/type-declaration-immutability/ts/index.ts @@ -0,0 +1,7 @@ +import invalid from "./invalid"; +import valid from "./valid"; + +export default { + valid, + invalid, +}; diff --git a/tests/rules/type-declaration-immutability/ts/invalid.ts b/tests/rules/type-declaration-immutability/ts/invalid.ts new file mode 100644 index 000000000..06592a4e4 --- /dev/null +++ b/tests/rules/type-declaration-immutability/ts/invalid.ts @@ -0,0 +1,339 @@ +import { Immutability } from "is-immutable-type"; + +import type { InvalidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + { + code: "type ReadonlyFoo = { foo: number }", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.ReadonlyShallow], + actual: Immutability[Immutability.Mutable], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ReadonlyFoo = { readonly foo: number; bar: { baz: string; }; }", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.ReadonlyShallow], + actual: Immutability[Immutability.Mutable], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ReadonlySet = Set;", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.ReadonlyShallow], + actual: Immutability[Immutability.Mutable], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ReadonlyMap = Map;", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.ReadonlyShallow], + actual: Immutability[Immutability.Mutable], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ReadonlyDeepFoo = { readonly foo: number; readonly bar: { baz: string; }; }", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.ReadonlyDeep], + actual: Immutability[Immutability.ReadonlyShallow], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ReadonlyDeepSet = ReadonlySet<{ foo: string; }>;", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.ReadonlyDeep], + actual: Immutability[Immutability.ReadonlyShallow], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ReadonlyDeepMap = ReadonlyMap;", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.ReadonlyDeep], + actual: Immutability[Immutability.ReadonlyShallow], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ImmutableFoo = { readonly foo: number; readonly bar: { baz: string; }; }", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.Immutable], + actual: Immutability[Immutability.ReadonlyShallow], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ImmutableSet = ReadonlySet<{ readonly foo: string; }>;", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.Immutable], + actual: Immutability[Immutability.ReadonlyDeep], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ImmutableMap = ReadonlyMap;", + optionsSet: [[]], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.Immutable], + actual: Immutability[Immutability.ReadonlyDeep], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type MutableString = string", + optionsSet: [[]], + errors: [ + { + messageId: "AtMost", + data: { + expected: Immutability[Immutability.Mutable], + actual: Immutability[Immutability.Immutable], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type MutableFoo = { readonly foo: number }", + optionsSet: [[]], + errors: [ + { + messageId: "AtMost", + data: { + expected: Immutability[Immutability.Mutable], + actual: Immutability[Immutability.Immutable], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type MutableFoo = { readonly foo: number; readonly bar: { baz: string; }; }", + optionsSet: [[]], + errors: [ + { + messageId: "AtMost", + data: { + expected: Immutability[Immutability.Mutable], + actual: Immutability[Immutability.ReadonlyShallow], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type Foo = { foo: number }", + optionsSet: [ + [ + { + rules: [ + { + identifier: "Foo", + immutability: "ReadonlyDeep", + }, + ], + }, + ], + ], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.ReadonlyDeep], + actual: Immutability[Immutability.Mutable], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type ReadonlyFoo = { readonly foo: number; readonly bar: { baz: string; }; };", + optionsSet: [ + [ + { + rules: [ + { + identifier: "^I?Readonly.+", + immutability: "ReadonlyDeep", + }, + ], + }, + ], + ], + errors: [ + { + messageId: "AtLeast", + data: { + expected: Immutability[Immutability.ReadonlyDeep], + actual: Immutability[Immutability.ReadonlyShallow], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type MutableSet = Set;", + optionsSet: [[]], + settingsSet: [ + { + immutability: { + overrides: { + keepDefault: false, + }, + }, + }, + ], + errors: [ + { + messageId: "AtMost", + data: { + expected: Immutability[Immutability.Mutable], + actual: Immutability[Immutability.ReadonlyDeep], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, + { + code: "type MutableSet = Set;", + optionsSet: [[]], + settingsSet: [ + { + immutability: { + overrides: { + keepDefault: false, + values: [ + { + name: "Set", + to: Immutability.Immutable, + }, + ], + }, + }, + }, + { + immutability: { + overrides: { + keepDefault: false, + values: [ + { + name: "Set", + to: "Immutable", + }, + ], + }, + }, + }, + ], + errors: [ + { + messageId: "AtMost", + data: { + expected: Immutability[Immutability.Mutable], + actual: Immutability[Immutability.Immutable], + }, + type: "Identifier", + line: 1, + column: 6, + }, + ], + }, +]; + +export default tests; diff --git a/tests/rules/type-declaration-immutability/ts/valid.ts b/tests/rules/type-declaration-immutability/ts/valid.ts new file mode 100644 index 000000000..b6d474eec --- /dev/null +++ b/tests/rules/type-declaration-immutability/ts/valid.ts @@ -0,0 +1,162 @@ +import dedent from "dedent"; +import { Immutability } from "is-immutable-type"; + +import type { ValidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + { + code: "type ReadonlyString = string;", + optionsSet: [[]], + }, + { + code: "type ReadonlyFoo = { readonly foo: number };", + optionsSet: [[]], + }, + { + code: "type ReadonlyFoo = Readonly<{ foo: number }>;", + optionsSet: [[]], + }, + { + code: "type ReadonlyFoo = { readonly foo: number; readonly bar: { baz: string; }; };", + optionsSet: [[]], + }, + { + code: "type ReadonlySet = ReadonlySet;", + optionsSet: [[]], + }, + { + code: "type ReadonlyMap = ReadonlyMap;", + optionsSet: [[]], + }, + { + code: "type ReadonlyDeepString = string;", + optionsSet: [[]], + }, + { + code: "type ReadonlyDeepFoo = { readonly foo: number; readonly bar: { readonly baz: string; }; };", + optionsSet: [[]], + }, + { + code: "type ReadonlyDeepSet = ReadonlySet;", + optionsSet: [[]], + }, + { + code: "type ReadonlyDeepMap = ReadonlyMap;", + optionsSet: [[]], + }, + { + code: "type ReadonlyDeepSet = ReadonlySet<{ readonly foo: string; }>;", + optionsSet: [[]], + }, + { + code: "type ReadonlyDeepMap = ReadonlyMap;", + optionsSet: [[]], + }, + { + code: "type ImmutableString = string;", + optionsSet: [[]], + }, + { + code: "type ImmutableFoo = { readonly foo: number; readonly bar: { readonly baz: string; }; };", + optionsSet: [[]], + }, + { + code: "type ImmutableSet = Readonly>;", + optionsSet: [[]], + }, + { + code: "type ImmutableMap = Readonly>;", + optionsSet: [[]], + }, + { + code: "type MutableFoo = { foo: number };", + optionsSet: [[]], + }, + { + code: "type MutableFoo = { readonly foo: number; bar: { readonly baz: string; }; };", + optionsSet: [[]], + }, + { + code: "type MutableSet = Set<{ readonly foo: string; }>;", + optionsSet: [[]], + }, + { + code: "type MutableMap = Map;", + optionsSet: [[]], + }, + { + code: "type ReadonlyFoo = { foo: number };", + optionsSet: [ + [ + { + ignorePattern: "Foo", + }, + ], + ], + }, + { + code: "interface ReadonlyFoo { foo: number };", + optionsSet: [ + [ + { + ignorePattern: "Foo", + }, + ], + [ + { + ignoreInterfaces: true, + }, + ], + ], + }, + { + code: "type Foo = { readonly foo: number };", + optionsSet: [ + [ + { + rules: [ + { + identifier: "Foo", + immutability: "ReadonlyDeep", + }, + ], + }, + ], + ], + }, + { + code: "type ReadonlyFoo = { readonly foo: number; readonly bar: { readonly baz: string; }; };", + optionsSet: [ + [ + { + rules: [ + { + identifier: "^I?Readonly.+", + immutability: "ReadonlyDeep", + }, + ], + }, + ], + ], + }, + { + code: dedent` + type ReadonlyDeepFoo = ReadonlyDeep<{ foo: { bar: string; }; }>; + `, + optionsSet: [[]], + settingsSet: [ + { + immutability: { + overrides: [ + { + name: "ReadonlyDeep", + to: Immutability.ReadonlyDeep, + }, + ], + }, + }, + ], + }, +]; + +export default tests; diff --git a/tests/rules/work.test.ts b/tests/rules/work.test.ts index 213bf3f7c..bb5f8ebec 100644 --- a/tests/rules/work.test.ts +++ b/tests/rules/work.test.ts @@ -23,6 +23,7 @@ const valid: ReadonlyArray = [ // // Valid Code. // `, // optionsSet: [[]], + // settingsSet: [{}] // } ]; @@ -36,6 +37,7 @@ const invalid: ReadonlyArray = [ // // Invalid Code. // `, // optionsSet: [[]], + // settingsSet: [{}] // output: dedent` // // Fixed Code - Remove if rule doesn't have a fixer. // `, diff --git a/tsconfig.base.json b/tsconfig.base.json index 5bebdb0c1..f45a4086e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -29,6 +29,7 @@ "~/configs/*": ["src/configs/*"], "~/rules": ["src/rules"], "~/rules/*": ["src/rules/*"], + "~/settings": ["src/settings"], "~/util/*": ["src/util/*"], "~/conditional-imports/*": ["src/util/conditional-imports/*"], "~/tests/*": ["tests/*"] diff --git a/yarn.lock b/yarn.lock index e164220d6..33f8413a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4031,6 +4031,7 @@ __metadata: eslint-plugin-unicorn: ^43.0.0 espree: ^9.3.0 husky: ^8.0.0 + is-immutable-type: ^0.0.7 json-schema: ^0.4.0 jsonc-parser: ^3.0.0 lint-staged: ^13.0.0 @@ -5661,6 +5662,18 @@ __metadata: languageName: node linkType: hard +"is-immutable-type@npm:^0.0.7": + version: 0.0.7 + resolution: "is-immutable-type@npm:0.0.7" + peerDependencies: + "@typescript-eslint/type-utils": ">=5.30.5" + "@typescript-eslint/utils": ">=5.30.5" + tsutils: ">=3.21.0" + typescript: ">=4.7.4" + checksum: 52fde0fc27d9eee088e5c06d731c4bb5536ddbc192dac2dc10d4d0f397c434c76bdce7a7455213f9e3b4d493749569c34868623ff4eac4a53ab28d448a00daee + languageName: node + linkType: hard + "is-interactive@npm:^1.0.0": version: 1.0.0 resolution: "is-interactive@npm:1.0.0" From 2552d554f87c22031471a75d013d5cb3b84e2e40 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 14 Sep 2022 21:39:13 +1200 Subject: [PATCH 004/100] feat(prefer-immutable-types): create rule --- src/configs/all.ts | 1 + src/configs/no-mutations.ts | 1 + src/rules/index.ts | 2 + src/rules/prefer-immutable-parameter-types.ts | 148 ++++++++++++++ .../index.test.ts | 6 + .../ts/index.ts | 7 + .../ts/invalid.ts | 125 ++++++++++++ .../ts/valid.ts | 187 ++++++++++++++++++ 8 files changed, 477 insertions(+) create mode 100644 src/rules/prefer-immutable-parameter-types.ts create mode 100644 tests/rules/prefer-immutable-parameter-types/index.test.ts create mode 100644 tests/rules/prefer-immutable-parameter-types/ts/index.ts create mode 100644 tests/rules/prefer-immutable-parameter-types/ts/invalid.ts create mode 100644 tests/rules/prefer-immutable-parameter-types/ts/valid.ts diff --git a/src/configs/all.ts b/src/configs/all.ts index 84aa0737d..2223715b7 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-immutable-parameter-types": "error", "functional/prefer-readonly-type": "error", "functional/prefer-tacit": ["error", { assumeTypes: false }], "functional/no-return-void": "error", diff --git a/src/configs/no-mutations.ts b/src/configs/no-mutations.ts index cdb807b12..d3bfc067f 100644 --- a/src/configs/no-mutations.ts +++ b/src/configs/no-mutations.ts @@ -10,6 +10,7 @@ const config: Linter.Config = { files: ["*.ts", "*.tsx"], rules: { "functional/no-method-signature": "warn", + "functional/prefer-immutable-parameter-types": "error", "functional/prefer-readonly-type": "error", "functional/type-declaration-immutability": "error", }, diff --git a/src/rules/index.ts b/src/rules/index.ts index 2c0e308d5..075b694cf 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 preferImmutableParameterTypes from "./prefer-immutable-parameter-types"; import * as preferReadonlyTypes from "./prefer-readonly-type"; import * as preferTacit from "./prefer-tacit"; import * as typeDeclarationImmutability from "./type-declaration-immutability"; @@ -34,6 +35,7 @@ export const rules = { [noThisExpression.name]: noThisExpression.rule, [noThrowStatement.name]: noThrowStatement.rule, [noTryStatement.name]: noTryStatement.rule, + [preferImmutableParameterTypes.name]: preferImmutableParameterTypes.rule, [preferReadonlyTypes.name]: preferReadonlyTypes.rule, [preferTacit.name]: preferTacit.rule, [typeDeclarationImmutability.name]: typeDeclarationImmutability.rule, diff --git a/src/rules/prefer-immutable-parameter-types.ts b/src/rules/prefer-immutable-parameter-types.ts new file mode 100644 index 000000000..cdc1f8944 --- /dev/null +++ b/src/rules/prefer-immutable-parameter-types.ts @@ -0,0 +1,148 @@ +import type { ESLintUtils, TSESLint } from "@typescript-eslint/utils"; +import { Immutability } from "is-immutable-type"; +import type { JSONSchema4 } from "json-schema"; +import type { ReadonlyDeep } from "type-fest"; + +import type { ESFunctionType } from "~/util/node-types"; +import type { RuleResult } from "~/util/rule"; +import { getTypeImmutabilityOfNode, createRule } from "~/util/rule"; +import { isDefined, isTSParameterProperty } from "~/util/typeguard"; + +/** + * The name of this rule. + */ +export const name = "prefer-immutable-parameter-types" as const; + +/** + * The options this rule can take. + */ +type Options = ReadonlyDeep< + [ + { + enforcement: Exclude< + Immutability | keyof typeof Immutability, + "Unknown" | "Mutable" + >; + } + ] +>; + +/** + * The schema for the rule options. + */ +const schema: JSONSchema4 = [ + { + type: "object", + properties: { + enforcement: { + type: ["string", "number"], + enum: Object.values(Immutability).filter( + (i) => + i !== Immutability.Unknown && + i !== Immutability[Immutability.Unknown] && + i !== Immutability.Mutable && + i !== Immutability[Immutability.Mutable] + ), + }, + }, + additionalProperties: false, + }, +]; + +/** + * The default options for the rule. + */ +const defaultOptions: Options = [ + { + enforcement: Immutability.ReadonlyDeep, + }, +]; + +/** + * The possible error messages. + */ +const errorMessages = { + generic: + 'Parameter should have an immutability at least "{{ expected }}" (actual: "{{ actual }}").', +} as const; + +/** + * The meta data for this rule. + */ +const meta: ESLintUtils.NamedCreateRuleMeta = { + type: "suggestion", + docs: { + description: + "Require function parameters to be typed as certain immutability", + recommended: "error", + }, + messages: errorMessages, + schema, +}; + +/** + * Check if the given function node violates this rule. + */ +function checkFunction( + node: ReadonlyDeep, + context: ReadonlyDeep< + TSESLint.RuleContext + >, + options: Options +): RuleResult { + const [optionsObject] = options; + const { enforcement: rawEnforcement } = optionsObject; + + const enforcement = + typeof rawEnforcement === "string" + ? Immutability[rawEnforcement] + : rawEnforcement; + + type Descriptor = RuleResult< + keyof typeof errorMessages, + Options + >["descriptors"][number]; + + const descriptors = node.params + .map((param): Descriptor | undefined => { + const actualParam = isTSParameterProperty(param) + ? param.parameter + : param; + const immutability = getTypeImmutabilityOfNode(actualParam, context); + + return immutability >= enforcement + ? undefined + : { + node: actualParam, + messageId: "generic", + data: { + actual: Immutability[immutability], + expected: Immutability[enforcement], + }, + }; + }) + .filter(isDefined); + + return { + context, + descriptors, + }; +} + +// Create the rule. +export const rule = createRule( + name, + meta, + defaultOptions, + { + ArrowFunctionExpression: checkFunction, + FunctionDeclaration: checkFunction, + FunctionExpression: checkFunction, + TSCallSignatureDeclaration: checkFunction, + TSConstructSignatureDeclaration: checkFunction, + TSDeclareFunction: checkFunction, + TSEmptyBodyFunctionExpression: checkFunction, + TSFunctionType: checkFunction, + TSMethodSignature: checkFunction, + } +); diff --git a/tests/rules/prefer-immutable-parameter-types/index.test.ts b/tests/rules/prefer-immutable-parameter-types/index.test.ts new file mode 100644 index 000000000..43c90fa31 --- /dev/null +++ b/tests/rules/prefer-immutable-parameter-types/index.test.ts @@ -0,0 +1,6 @@ +import { name, rule } from "~/rules/prefer-immutable-parameter-types"; +import { testUsing } from "~/tests/helpers/testers"; + +import tsTests from "./ts"; + +testUsing.typescript(name, rule, tsTests); diff --git a/tests/rules/prefer-immutable-parameter-types/ts/index.ts b/tests/rules/prefer-immutable-parameter-types/ts/index.ts new file mode 100644 index 000000000..40a005f71 --- /dev/null +++ b/tests/rules/prefer-immutable-parameter-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-immutable-parameter-types/ts/invalid.ts b/tests/rules/prefer-immutable-parameter-types/ts/invalid.ts new file mode 100644 index 000000000..df80ea1fe --- /dev/null +++ b/tests/rules/prefer-immutable-parameter-types/ts/invalid.ts @@ -0,0 +1,125 @@ +import dedent from "dedent"; + +import type { InvalidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + { + code: "function foo(arg: ReadonlySet) {}", + optionsSet: [[{ enforcement: "Immutable" }]], + errors: [ + { + messageId: "generic", + type: "Identifier", + line: 1, + column: 14, + }, + ], + }, + { + code: "function foo(arg: ReadonlyMap) {}", + optionsSet: [[{ enforcement: "Immutable" }]], + errors: [ + { + messageId: "generic", + type: "Identifier", + line: 1, + column: 14, + }, + ], + }, + { + code: "function foo(arg1: { foo: string }, arg2: { foo: number }) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + errors: [ + { + messageId: "generic", + type: "Identifier", + line: 1, + column: 14, + }, + { + messageId: "generic", + type: "Identifier", + line: 1, + column: 37, + }, + ], + }, + { + code: dedent` + class Foo { + constructor( + private arg1: readonly string[], + public arg2: readonly string[], + protected arg3: readonly string[], + readonly arg4: readonly string[], + ) {} + } + `, + optionsSet: [[{ enforcement: "Immutable" }]], + errors: [ + { + messageId: "generic", + type: "Identifier", + line: 3, + column: 13, + }, + { + messageId: "generic", + type: "Identifier", + line: 4, + column: 12, + }, + { + messageId: "generic", + type: "Identifier", + line: 5, + column: 15, + }, + { + messageId: "generic", + type: "Identifier", + line: 6, + column: 14, + }, + ], + }, + { + code: dedent` + interface Foo { + (arg: readonly string[]): void; + } + `, + optionsSet: [[{ enforcement: "Immutable" }]], + errors: [ + { + messageId: "generic", + type: "Identifier", + line: 2, + column: 4, + }, + ], + }, + { + code: dedent` + interface Foo { + new (arg: readonly string[]): void; + } + `, + optionsSet: [[{ enforcement: "Immutable" }]], + errors: [ + { + messageId: "generic", + type: "Identifier", + line: 2, + column: 8, + }, + ], + }, +]; + +export default tests; diff --git a/tests/rules/prefer-immutable-parameter-types/ts/valid.ts b/tests/rules/prefer-immutable-parameter-types/ts/valid.ts new file mode 100644 index 000000000..dfa2c8f1d --- /dev/null +++ b/tests/rules/prefer-immutable-parameter-types/ts/valid.ts @@ -0,0 +1,187 @@ +import dedent from "dedent"; + +import type { ValidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + { + code: "function foo(arg: boolean) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + }, + { + code: "function foo(arg: true) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + }, + { + code: "function foo(arg: string) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + }, + { + code: "function foo(arg: 'bar') {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + }, + { + code: "function foo(arg: 'undefined') {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + }, + { + code: "function foo(arg: readonly string[]) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + settingsSet: [ + { + immutability: { + overrides: [ + { + name: "ReadonlyArray", + to: "Immutable", + }, + ], + }, + }, + ], + }, + { + code: "function foo(arg: ReadonlyArray) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(arg: readonly [string, number]) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(arg: Readonly<[string, number]>) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(arg: { readonly foo: string }) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + }, + { + code: "function foo(arg: { readonly foo: { readonly bar: number } }) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + }, + { + code: "function foo(arg: { foo(): void }) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(arg: { foo: () => void }) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(arg: ReadonlySet) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(arg: ReadonlyMap) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(arg: Readonly>) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + }, + { + code: "function foo(arg: Readonly>) {}", + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + [{ enforcement: "Immutable" }], + ], + }, + { + code: dedent` + class Foo { + constructor( + private arg1: readonly string[], + public arg2: readonly string[], + protected arg3: readonly string[], + readonly arg4: readonly string[], + ) {} + } + `, + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, + { + code: dedent` + interface Foo { + (arg: readonly string[]): void; + } + `, + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, + { + code: dedent` + interface Foo { + new (arg: readonly string[]): void; + } + `, + optionsSet: [ + [{ enforcement: "ReadonlyShallow" }], + [{ enforcement: "ReadonlyDeep" }], + ], + }, +]; + +export default tests; From d2624cd9eadf456336f4b213487dc2c43fa2b01e Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 16 Sep 2022 12:25:11 +1200 Subject: [PATCH 005/100] docs: document new rules --- .eslintrc.json | 1 + README.md | 14 +- .../rules/prefer-immutable-parameter-types.md | 195 ++++++++++++++++++ docs/rules/settings/immutability.md | 41 ++++ docs/rules/type-declaration-immutability.md | 142 +++++++++++++ 5 files changed, 387 insertions(+), 6 deletions(-) create mode 100644 docs/rules/prefer-immutable-parameter-types.md create mode 100644 docs/rules/settings/immutability.md create mode 100644 docs/rules/type-declaration-immutability.md diff --git a/.eslintrc.json b/.eslintrc.json index bd6c7fc07..22394e841 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -126,6 +126,7 @@ "functional/no-expression-statement": "off", "functional/no-let": "off", "functional/no-loop-statement": "off", + "functional/no-mixed-type": "off", "functional/no-return-void": "off", "functional/no-this-expression": "off", "functional/no-throw-statement": "off", diff --git a/README.md b/README.md index 798121699..a5308487c 100644 --- a/README.md +++ b/README.md @@ -186,12 +186,14 @@ 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-immutable-parameter-types`](./docs/rules/prefer-immutable-parameter-types.md) | Require function parameters to be typed as certain immutability | :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: | +| [`type-declaration-immutability`](./docs/rules/type-declaration-immutability.md) | Enforce the immutability of types based on patterns | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | ### No Object-Orientation Rules diff --git a/docs/rules/prefer-immutable-parameter-types.md b/docs/rules/prefer-immutable-parameter-types.md new file mode 100644 index 000000000..7f1e68892 --- /dev/null +++ b/docs/rules/prefer-immutable-parameter-types.md @@ -0,0 +1,195 @@ +# Prefer immutable parameter types over mutable ones (prefer-immutable-parameter-types) + +Although other rules can be used to ensure parameter are not mutated, it is +best to explicitly declare that the parameters are immutable. + +It is also worth noting that as immutable types are not assignable to mutable +ones, users will not be able to pass something like a readonly array to a +functional that wants a mutable array; even if the function does not actually +mutate said array. + +## Rule Details + +This rule differs from the +[@typescript-eslint/prefer-readonly-parameter-types](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md) +rule by the fact that it uses the +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type) library to +calculated immutability. This library allows for more powerful and customizable +immutability enforcements to be made. + +This rule is designed to replace the aforementioned rule. + +Examples of **incorrect** code for this rule: + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: "error" */ + +function array1(arg: string[]) {} // array is not readonly +function array2(arg: ReadonlyArray) {} // array element is not readonly +function array3(arg: [string, number]) {} // tuple is not readonly +function array4(arg: readonly [string[], number]) {} // tuple element is not readonly +// the above examples work the same if you use ReadonlyArray instead + +function object1(arg: { prop: string }) {} // property is not readonly +function object2(arg: { readonly prop: string; prop2: string }) {} // not all properties are readonly +function object3(arg: { 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(arg: CustomArrayType) {} + +interface CustomFunction { + (): void; + prop: string; // note: this property is mutable +} +function custom2(arg: CustomFunction) {} + +function union(arg: string[] | ReadonlyArray) {} // not all types are readonly + +// rule also checks function types +interface Foo1 { + (arg: string[]): void; +} +interface Foo2 { + new (arg: string[]): void; +} +const x = { foo(arg: string[]): void; }; +function foo(arg: string[]); +type Foo3 = (arg: string[]) => void; +interface Foo4 { + foo(arg: string[]): void; +} +``` + +Examples of **correct** code for this rule: + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: "error" */ + +function array1(arg: ReadonlyArray) {} +function array2(arg: ReadonlyArray>) {} +function array3(arg: readonly [string, number]) {} +function array4(arg: readonly [ReadonlyArray, number]) {} +// the above examples work the same if you use ReadonlyArray instead + +function object1(arg: { readonly prop: string }) {} +function object2(arg: { readonly prop: string; readonly prop2: string }) {} +function object3(arg: { 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(arg: 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(arg: CustomFunction) {} + +function union(arg: ReadonlyArray | ReadonlyArray) {} + +function primitive1(arg: string) {} +function primitive2(arg: number) {} +function primitive3(arg: boolean) {} +function primitive4(arg: unknown) {} +function primitive5(arg: null) {} +function primitive6(arg: undefined) {} +function primitive7(arg: any) {} +function primitive8(arg: never) {} +function primitive9(arg: string | number | undefined) {} + +function fnSig(arg: () => void) {} + +enum Foo { a, b } +function enum1(arg: Foo) {} + +function symb1(arg: symbol) {} +const customSymbol = Symbol('a'); +function symb2(arg: typeof customSymbol) {} + +// function types +interface Foo1 { + (arg: ReadonlyArray): void; +} +interface Foo2 { + new (arg: ReadonlyArray): void; +} +const x = { foo(arg: ReadonlyArray): void; }; +function foo(arg: ReadonlyArray); +type Foo3 = (arg: ReadonlyArray) => void; +interface Foo4 { + foo(arg: ReadonlyArray): void; +} +``` + +## Settings + +This rule can leverage shared settings to configure immutability settings. + +See the [immutability](./settings/immutability.md) docs. + +## Options + +This rule accepts an options object of the following type: + +```ts +type Options = { + enforcement: "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; +} +``` + +The default options: + +```ts +const defaults = { + enforcement: "ReadonlyDeep", +} +``` + +### `enforcement` + +The level of immutability that should be enforced. + +**incorrect**: + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: ["error", { "enforcement": "Immutable" }] */ + +function array(arg: ReadonlyArray) {} // ReadonlyArray is not immutable +function set(arg: ReadonlySet) {} // ReadonlySet is not immutable +function map(arg: ReadonlyMap) {} // ReadonlyMap is not immutable +``` + +**correct**: + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: ["error", { "enforcement": "Immutable" }] */ + +function set(arg: Readonly>) {} +function map(arg: Readonly>) {} +function object(arg: Readonly<{ prop: string }>) {} +``` + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: ["error", { "enforcement": "ReadonlyShallow" }] */ + +function array(arg: ReadonlyArray<{ foo: string; }>) {} +function set(arg: ReadonlySet<{ foo: string; }>) {} +function map(arg: ReadonlyMap<{ foo: string; }>) {} +function object(arg: Readonly<{ prop: { foo: string; }; }>) {} +``` diff --git a/docs/rules/settings/immutability.md b/docs/rules/settings/immutability.md new file mode 100644 index 000000000..c2ec9a416 --- /dev/null +++ b/docs/rules/settings/immutability.md @@ -0,0 +1,41 @@ +# Using the `immutability` setting + +We are using the +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type) library to +determine the immutability of types. This library can be configure for all rules +at once using a shared setting. + +## Overrides + +For details see [the overrides +section](https://github.com/RebeccaStevens/is-immutable-type#overrides) of +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type). + +### Example of configuring immutability overrides + +In this example, we are configuring +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type) to treat +any readonly array (regardless of the syntax used) as immutable in the case +where it was found to be deeply readonly. If it was only found to be shallowly +readonly, then no override will be applied. + +```jsonc +// .eslintrc.json +{ + // ... + "settings": { + "immutability": { + "overrides": [ + { + "name": "ReadonlyArray", + "to": "Immutable", + "from": "ReadonlyDeep" + } + ] + } + }, + "rules": { + // ... + } +} +``` diff --git a/docs/rules/type-declaration-immutability.md b/docs/rules/type-declaration-immutability.md new file mode 100644 index 000000000..d204685c9 --- /dev/null +++ b/docs/rules/type-declaration-immutability.md @@ -0,0 +1,142 @@ +# Enforce a level of immutability for type declaration (type-declaration-immutability) + +Require type alias declarations and interfaces that imply some level of +immutability to comply to it. + +## Rule Details + +This rule enforces rules on type immutability based on the type's name. + +For details on what the different levels of immutability mean, see [the +immutability +section](https://github.com/RebeccaStevens/is-immutable-type#immutability) of +[is-immutable-type](https://www.npmjs.com/package/is-immutable-type). + +Examples of **incorrect** code for this rule: + + + +```ts +/* eslint functional/type-declaration-immutability: "error" */ + +type ReadonlyElement = { + id: number; + data: string[]; +}; + +type ReadonlyDeepElement = Readonly<{ + id: number; + data: string[]; +}>; + +type MutableElement = Readonly<{ + id: number; + data: ReadonlyArray; +}>; +``` + +Examples of **correct** code for this rule: + + + +```ts +/* eslint functional/type-declaration-immutability: "error" */ + +type ReadonlyElement = Readonly<{ + id: number; + data: string[]; +}>; + +type ReadonlyDeepElement = Readonly<{ + id: number; + data: ReadonlyArray; +}>; + +type MutableElement = { + readonly id: number; + data: ReadonlyArray; +}; +``` + +## Settings + +This rule can leverage shared settings to configure immutability settings. + +See the [immutability](./settings/immutability.md) docs. + +## Options + +This rule accepts an options object of the following type: + +```ts +type Options = { + rules: Array<{ + identifier: string | string[]; + immutability: "Mutable" | "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; + comparator?: "Less" | "AtMost" | "Exactly" | "AtLeast" | "More"; + }>; + ignoreInterfaces: boolean; + ignorePattern: string[] | string; +} +``` + +The default options: + +```ts +const defaults = { + rules: [ + { + identifier: "I?Immutable.+", + immutability: "Immutable", + comparator: "AtLeast", + }, + { + identifier: "I?ReadonlyDeep.+", + immutability: "ReadonlyDeep", + comparator: "AtLeast", + }, + { + identifier: "I?Readonly.+", + immutability: "ReadonlyShallow", + comparator: "AtLeast", + }, + { + identifier: "I?Mutable.+", + immutability: "Mutable", + comparator: "AtMost", + }, + ], + ignoreInterfaces: false, +} +``` + +### `rules` + +An array of rules to enforce immutability by. + +These rules should be sorted by precedence as each type declaration will only +enforce the first matching rule to it. + +#### `identifier` + +A regex pattern or an array of regex patterns that are used to match against the +name of the type declarations. + +#### `immutability` + +The level of immutability to compare against. This value will be compared to the +calculated immutability using the `comparator`. + +#### `comparator` + +The comparator to use to compare the calculated immutability to the desired +immutability. This can be thought of as `<`, `<=`, `==`, `>=` or `>`. + +### `ignoreInterfaces` + +A boolean to specify whether interfaces should be exempt from these rules. +`false` by default. + +### `ignorePattern` + +See the [ignorePattern](./options/ignore-pattern.md) docs. From 82816a03f8af397c0e2de619e55515a504ff2df8 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 16 Sep 2022 13:31:41 +1200 Subject: [PATCH 006/100] feat(prefer-readonly-type): deprecated this rule --- README.md | 1 - docs/rules/prefer-readonly-type.md | 6 +++++- src/configs/all.ts | 2 +- src/configs/no-mutations.ts | 1 - src/rules/prefer-readonly-type.ts | 5 +++++ 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a5308487c..c47d321c3 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,6 @@ The [below section](#supported-rules) gives details on which rules are enabled b | [`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-immutable-parameter-types`](./docs/rules/prefer-immutable-parameter-types.md) | Require function parameters to be typed as certain immutability | :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: | | [`type-declaration-immutability`](./docs/rules/type-declaration-immutability.md) | Enforce the immutability of types based on patterns | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | ### No Object-Orientation Rules diff --git a/docs/rules/prefer-readonly-type.md b/docs/rules/prefer-readonly-type.md index 2972e1115..89cb95db8 100644 --- a/docs/rules/prefer-readonly-type.md +++ b/docs/rules/prefer-readonly-type.md @@ -1,6 +1,10 @@ # Prefer readonly types over mutable types (prefer-readonly-type) -This rule enforces use of the readonly modifier and readonly types. +## :warning: This rule is deprecated + +This rule has been replaced by +[prefer-immutable-parameter-types](./prefer-immutable-parameter-types.md) and +[type-declaration-immutability](./type-declaration-immutability.md). ## Rule Details diff --git a/src/configs/all.ts b/src/configs/all.ts index 2223715b7..6e673c717 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -22,7 +22,7 @@ const config: Linter.Config = { "functional/no-method-signature": "error", "functional/no-mixed-type": "error", "functional/prefer-immutable-parameter-types": "error", - "functional/prefer-readonly-type": "error", + "functional/prefer-property-signatures": "error", "functional/prefer-tacit": ["error", { assumeTypes: false }], "functional/no-return-void": "error", "functional/type-declaration-immutability": "error", diff --git a/src/configs/no-mutations.ts b/src/configs/no-mutations.ts index d3bfc067f..a3cd32f96 100644 --- a/src/configs/no-mutations.ts +++ b/src/configs/no-mutations.ts @@ -11,7 +11,6 @@ const config: Linter.Config = { rules: { "functional/no-method-signature": "warn", "functional/prefer-immutable-parameter-types": "error", - "functional/prefer-readonly-type": "error", "functional/type-declaration-immutability": "error", }, }, diff --git a/src/rules/prefer-readonly-type.ts b/src/rules/prefer-readonly-type.ts index 20ab69d2f..51dfdd5a7 100644 --- a/src/rules/prefer-readonly-type.ts +++ b/src/rules/prefer-readonly-type.ts @@ -112,6 +112,11 @@ const errorMessages = { * The meta data for this rule. */ const meta: ESLintUtils.NamedCreateRuleMeta = { + deprecated: true, + replacedBy: [ + "functional/prefer-immutable-parameter-types", + "functional/type-declaration-immutability", + ], type: "suggestion", docs: { description: "Prefer readonly array over mutable arrays.", From 0ebe63eacf4e1a88ccfebf4eb937d494c1c127e4 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 16 Sep 2022 13:34:38 +1200 Subject: [PATCH 007/100] chore: update default rule to import for the work test file --- tests/rules/work.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rules/work.test.ts b/tests/rules/work.test.ts index bb5f8ebec..65f7feb77 100644 --- a/tests/rules/work.test.ts +++ b/tests/rules/work.test.ts @@ -11,7 +11,7 @@ import { testUsing } from "~/tests/helpers/testers"; * Step 1. * Import the rule to test. */ -import { name, rule } from "~/rules/prefer-readonly-type"; +import { name, rule } from "~/rules/type-declaration-immutability"; /* * Step 2a. From da2259f2f592b27af9d157e82160c857621750ca Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 16 Sep 2022 13:46:13 +1200 Subject: [PATCH 008/100] feat(no-method-signature)!: rename to `prefer-property-signatures` & move it to `stylistic` ruleset --- README.md | 20 +++++++++---------- docs/rules/no-method-signature.md | 10 ++++------ src/configs/all.ts | 1 - src/configs/no-mutations.ts | 1 - src/configs/stylistic.ts | 1 + src/rules/index.ts | 4 ++-- ...ature.ts => prefer-property-signatures.ts} | 10 ++++------ .../index.test.ts | 2 +- .../ts/index.ts | 0 .../ts/invalid.ts | 0 .../ts/valid.ts | 0 11 files changed, 22 insertions(+), 27 deletions(-) rename src/rules/{no-method-signature.ts => prefer-property-signatures.ts} (86%) rename tests/rules/{no-method-signature => prefer-property-signatures}/index.test.ts (65%) rename tests/rules/{no-method-signature => prefer-property-signatures}/ts/index.ts (100%) rename tests/rules/{no-method-signature => prefer-property-signatures}/ts/invalid.ts (100%) rename tests/rules/{no-method-signature => prefer-property-signatures}/ts/valid.ts (100%) diff --git a/README.md b/README.md index c47d321c3..deb7e3fd2 100644 --- a/README.md +++ b/README.md @@ -186,13 +186,12 @@ 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-immutable-parameter-types`](./docs/rules/prefer-immutable-parameter-types.md) | Require function parameters to be typed as certain immutability | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`type-declaration-immutability`](./docs/rules/type-declaration-immutability.md) | Enforce the immutability of types based on patterns | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :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: | | | +| [`prefer-immutable-parameter-types`](./docs/rules/prefer-immutable-parameter-types.md) | Require function parameters to be typed as certain immutability | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| [`type-declaration-immutability`](./docs/rules/type-declaration-immutability.md) | Enforce the immutability of types based on patterns | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | ### No Object-Orientation Rules @@ -237,9 +236,10 @@ The [below section](#supported-rules) gives details on which rules are enabled b :see_no_evil: = `stylistic` Ruleset. -| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | -| ---------------------------------------------- | ----------------------- | :------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :----------: | -| [`prefer-tacit`](./docs/rules/prefer-tacit.md) | Tacit/Point-Free style. | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :blue_heart: | +| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | +| -------------------------------------------------------------------------- | -------------------------------------------------- | :------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: | +| [`prefer-property-signatures`](./docs/rules/prefer-property-signatures.md) | Enforce property signatures over method signatures | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| [`prefer-tacit`](./docs/rules/prefer-tacit.md) | Tacit/Point-Free style. | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :blue_heart: | ## Recommended standard rules diff --git a/docs/rules/no-method-signature.md b/docs/rules/no-method-signature.md index 19967f87d..be9f1d3eb 100644 --- a/docs/rules/no-method-signature.md +++ b/docs/rules/no-method-signature.md @@ -1,6 +1,4 @@ -# Prefer property signatures with readonly modifiers over method signatures (no-method-signature) - -Prefer property signatures with readonly modifiers over method signatures. +# Prefer property signatures over method signatures (prefer-property-signatures) ## Rule Details @@ -16,7 +14,7 @@ Examples of **incorrect** code for this rule: ```ts -/* eslint functional/no-method-signature: "error" */ +/* eslint functional/prefer-property-signatures: "error" */ type Foo = { bar(): string; @@ -28,7 +26,7 @@ Examples of **correct** code for this rule: ```ts -/* eslint functional/no-method-signature: "error" */ +/* eslint functional/prefer-property-signatures: "error" */ type Foo = { readonly bar: () => string; @@ -66,7 +64,7 @@ Examples of **incorrect** code for this rule: ```ts -/* eslint functional/no-method-signature: ["error", { "ignoreIfReadonly": false } ] */ +/* eslint functional/prefer-property-signatures: ["error", { "ignoreIfReadonly": false } ] */ type Foo = Readonly<{ bar(): string; diff --git a/src/configs/all.ts b/src/configs/all.ts index 6e673c717..dcaf39292 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -19,7 +19,6 @@ const config: Linter.Config = { { files: ["*.ts", "*.tsx"], rules: { - "functional/no-method-signature": "error", "functional/no-mixed-type": "error", "functional/prefer-immutable-parameter-types": "error", "functional/prefer-property-signatures": "error", diff --git a/src/configs/no-mutations.ts b/src/configs/no-mutations.ts index a3cd32f96..3d3693f0a 100644 --- a/src/configs/no-mutations.ts +++ b/src/configs/no-mutations.ts @@ -9,7 +9,6 @@ const config: Linter.Config = { { files: ["*.ts", "*.tsx"], rules: { - "functional/no-method-signature": "warn", "functional/prefer-immutable-parameter-types": "error", "functional/type-declaration-immutability": "error", }, diff --git a/src/configs/stylistic.ts b/src/configs/stylistic.ts index 0c30872c5..e1ca65f1d 100644 --- a/src/configs/stylistic.ts +++ b/src/configs/stylistic.ts @@ -8,6 +8,7 @@ const config: Linter.Config = { { files: ["*.ts", "*.tsx"], rules: { + "functional/prefer-property-signatures": "error", "functional/prefer-tacit": ["error", { assumeTypes: false }], }, }, diff --git a/src/rules/index.ts b/src/rules/index.ts index 075b694cf..f1d2ba501 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -5,7 +5,6 @@ import * as noConditionalStatement from "./no-conditional-statement"; import * as noExpressionStatement from "./no-expression-statement"; import * as noLet from "./no-let"; import * as noLoop from "./no-loop-statement"; -import * as noMethodSignature from "./no-method-signature"; import * as noMixedType from "./no-mixed-type"; import * as noPromiseReject from "./no-promise-reject"; import * as noReturnVoid from "./no-return-void"; @@ -13,6 +12,7 @@ import * as noThisExpression from "./no-this-expression"; import * as noThrowStatement from "./no-throw-statement"; import * as noTryStatement from "./no-try-statement"; import * as preferImmutableParameterTypes from "./prefer-immutable-parameter-types"; +import * as preferPropertySignatures from "./prefer-property-signatures"; import * as preferReadonlyTypes from "./prefer-readonly-type"; import * as preferTacit from "./prefer-tacit"; import * as typeDeclarationImmutability from "./type-declaration-immutability"; @@ -28,7 +28,6 @@ export const rules = { [noExpressionStatement.name]: noExpressionStatement.rule, [noLet.name]: noLet.rule, [noLoop.name]: noLoop.rule, - [noMethodSignature.name]: noMethodSignature.rule, [noMixedType.name]: noMixedType.rule, [noPromiseReject.name]: noPromiseReject.rule, [noReturnVoid.name]: noReturnVoid.rule, @@ -36,6 +35,7 @@ export const rules = { [noThrowStatement.name]: noThrowStatement.rule, [noTryStatement.name]: noTryStatement.rule, [preferImmutableParameterTypes.name]: preferImmutableParameterTypes.rule, + [preferPropertySignatures.name]: preferPropertySignatures.rule, [preferReadonlyTypes.name]: preferReadonlyTypes.rule, [preferTacit.name]: preferTacit.rule, [typeDeclarationImmutability.name]: typeDeclarationImmutability.rule, diff --git a/src/rules/no-method-signature.ts b/src/rules/prefer-property-signatures.ts similarity index 86% rename from src/rules/no-method-signature.ts rename to src/rules/prefer-property-signatures.ts index 1359c4087..e7335f094 100644 --- a/src/rules/no-method-signature.ts +++ b/src/rules/prefer-property-signatures.ts @@ -9,7 +9,7 @@ import { inReadonly } from "~/util/tree"; /** * The name of this rule. */ -export const name = "no-method-signature" as const; +export const name = "prefer-property-signatures" as const; /** * The options this rule can take. @@ -49,8 +49,7 @@ const defaultOptions: Options = [ * The possible error messages. */ const errorMessages = { - generic: - "Method signature is mutable, use property signature with readonly modifier instead.", + generic: "Use a property signature instead of a method signature", } as const; /** @@ -59,9 +58,8 @@ const errorMessages = { const meta: ESLintUtils.NamedCreateRuleMeta = { type: "suggestion", docs: { - description: - "Prefer property signatures with readonly modifiers over method signatures.", - recommended: "warn", + description: "Prefer property signatures over method signatures.", + recommended: false, }, messages: errorMessages, schema, diff --git a/tests/rules/no-method-signature/index.test.ts b/tests/rules/prefer-property-signatures/index.test.ts similarity index 65% rename from tests/rules/no-method-signature/index.test.ts rename to tests/rules/prefer-property-signatures/index.test.ts index 419e8b61e..eaabc3795 100644 --- a/tests/rules/no-method-signature/index.test.ts +++ b/tests/rules/prefer-property-signatures/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/no-method-signature"; +import { name, rule } from "~/rules/prefer-property-signatures"; import { testUsing } from "~/tests/helpers/testers"; import tsTests from "./ts"; diff --git a/tests/rules/no-method-signature/ts/index.ts b/tests/rules/prefer-property-signatures/ts/index.ts similarity index 100% rename from tests/rules/no-method-signature/ts/index.ts rename to tests/rules/prefer-property-signatures/ts/index.ts diff --git a/tests/rules/no-method-signature/ts/invalid.ts b/tests/rules/prefer-property-signatures/ts/invalid.ts similarity index 100% rename from tests/rules/no-method-signature/ts/invalid.ts rename to tests/rules/prefer-property-signatures/ts/invalid.ts diff --git a/tests/rules/no-method-signature/ts/valid.ts b/tests/rules/prefer-property-signatures/ts/valid.ts similarity index 100% rename from tests/rules/no-method-signature/ts/valid.ts rename to tests/rules/prefer-property-signatures/ts/valid.ts From f47e9f5cca923c1b5a0bc792e2f29ced4241a024 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Mon, 19 Sep 2022 16:07:49 +1200 Subject: [PATCH 009/100] style: disable prefer-readonly-parameter-types --- .eslintrc.json | 2 +- src/rules/prefer-tacit.ts | 1 - src/util/typeguard.ts | 41 ++++++--------------------------------- 3 files changed, 7 insertions(+), 37 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 22394e841..ec2e477e1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -38,7 +38,7 @@ "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/prefer-readonly-parameter-types": "warn", + "@typescript-eslint/prefer-readonly-parameter-types": "off", "import/no-relative-parent-imports": "error", "functional/prefer-readonly-type": "off", "node/no-unsupported-features/es-builtins": "off", diff --git a/src/rules/prefer-tacit.ts b/src/rules/prefer-tacit.ts index d93d1101f..a35a32320 100644 --- a/src/rules/prefer-tacit.ts +++ b/src/rules/prefer-tacit.ts @@ -116,7 +116,6 @@ const isTS4dot7 = */ function isCallerViolation( caller: ReadonlyDeep, - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type calleeType: Type, context: ReadonlyDeep< TSESLint.RuleContext diff --git a/src/util/typeguard.ts b/src/util/typeguard.ts index 1abb00190..4bd8c6132 100644 --- a/src/util/typeguard.ts +++ b/src/util/typeguard.ts @@ -361,25 +361,17 @@ export function isDefined(value: T | null | undefined): value is T { * TS types type guards. */ -export function isUnionType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type - type: Type -): type is UnionType { +export function isUnionType(type: Type): type is UnionType { return ts !== undefined && type.flags === ts.TypeFlags.Union; } +export function isArrayType(type: Type | null): type is ArrayType; export function isArrayType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type - type: Type | null -): type is ArrayType; -export function isArrayType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type, assumeType: false, node: null ): type is ArrayType; export function isArrayType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type | null, assumeType: boolean, node: ReadonlyDeep @@ -390,7 +382,6 @@ export function isArrayType( node: ReadonlyDeep ): boolean; export function isArrayType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type | null, assumeType = false, node: ReadonlyDeep | null = null @@ -404,17 +395,14 @@ export function isArrayType( } export function isArrayConstructorType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type | null ): type is ArrayConstructorType; export function isArrayConstructorType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type, assumeType: false, node: null ): type is ArrayConstructorType; export function isArrayConstructorType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type | null, assumeType: boolean, node: ReadonlyDeep @@ -425,7 +413,6 @@ export function isArrayConstructorType( node: ReadonlyDeep ): boolean; export function isArrayConstructorType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type | null, assumeType = false, node: ReadonlyDeep | null = null @@ -440,17 +427,14 @@ export function isArrayConstructorType( } export function isObjectConstructorType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type | null ): type is ObjectConstructorType; export function isObjectConstructorType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type, assumeType: false, node: null ): type is ObjectConstructorType; export function isObjectConstructorType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type | null, assumeType: boolean, node: ReadonlyDeep @@ -461,7 +445,6 @@ export function isObjectConstructorType( node: ReadonlyDeep ): boolean; export function isObjectConstructorType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type type: Type | null, assumeType = false, node: ReadonlyDeep | null = null @@ -475,30 +458,18 @@ export function isObjectConstructorType( type.types.some((t) => isObjectConstructorType(t, false, null)))); } -export function isNeverType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type - type: Type -): boolean { +export function isNeverType(type: Type): boolean { return ts !== undefined && type.flags === ts.TypeFlags.Never; } -export function isVoidType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type - type: Type -): boolean { +export function isVoidType(type: Type): boolean { return ts !== undefined && type.flags === ts.TypeFlags.Void; } -export function isNullType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type - type: Type -): boolean { +export function isNullType(type: Type): boolean { return ts !== undefined && type.flags === ts.TypeFlags.Null; } -export function isUndefinedType( - // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ignore TS Type - type: Type -): boolean { +export function isUndefinedType(type: Type): boolean { return ts !== undefined && type.flags === ts.TypeFlags.Undefined; } From 72aa2049413d22559b3f90c479baa66ab3312003 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Mon, 19 Sep 2022 16:11:29 +1200 Subject: [PATCH 010/100] feat!: remove `@typescript-eslint/prefer-readonly-parameter-types` from `external-recommended` --- README.md | 9 +-------- src/configs/external-recommended.ts | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index deb7e3fd2..c6c919614 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,7 @@ Without this rule, it is still possible to create `var` variables that are mutab ### [no-param-reassign](https://eslint.org/docs/rules/no-param-reassign) -Without this rule, function parameters are mutable. +Don't allow function parameters to be reassigned, they should be treated as constants. ### [prefer-const](https://eslint.org/docs/rules/prefer-const) @@ -263,13 +263,6 @@ This rule is helpful when converting from an imperative code style to a function This rule is helpful when working with classes. -### [@typescript-eslint/prefer-readonly-parameter-types](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md) - -Functional functions must not modify any data passed into them. -This rule marks mutable parameters as a violation as they prevent readonly versions of that data from being passed in. - -However, due to many 3rd-party libraries only providing mutable versions of their types, often it can not be easy to satisfy this rule. Thus by default we only enable this rule with the "warn" severity rather than "error". - ### [@typescript-eslint/switch-exhaustiveness-check](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md) Although our [no-conditional-statement](./docs/rules/no-conditional-statement.md) rule also performs this check, this rule has a fixer that will implement the unimplemented cases which can be useful. diff --git a/src/configs/external-recommended.ts b/src/configs/external-recommended.ts index 9a2e2de91..51136ac04 100644 --- a/src/configs/external-recommended.ts +++ b/src/configs/external-recommended.ts @@ -11,7 +11,6 @@ const config: Linter.Config = { files: ["*.ts", "*.tsx"], rules: { "@typescript-eslint/prefer-readonly": "error", - "@typescript-eslint/prefer-readonly-parameter-types": "warn", "@typescript-eslint/switch-exhaustiveness-check": "error", }, }, From 1e7f77acb699033d17a9b20cbd9d11e5b85fb5b8 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 21 Sep 2022 01:46:02 +1200 Subject: [PATCH 011/100] feat!: split `external-recommended` rulesets into vanilla and typescript variants --- src/configs/external-recommended.ts | 20 ------------------- .../external-typescript-recommended.ts | 18 +++++++++++++++++ src/configs/external-vanilla-recommended.ts | 11 ++++++++++ src/index.ts | 6 ++++-- 4 files changed, 33 insertions(+), 22 deletions(-) delete mode 100644 src/configs/external-recommended.ts create mode 100644 src/configs/external-typescript-recommended.ts create mode 100644 src/configs/external-vanilla-recommended.ts diff --git a/src/configs/external-recommended.ts b/src/configs/external-recommended.ts deleted file mode 100644 index 51136ac04..000000000 --- a/src/configs/external-recommended.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Linter } from "eslint"; - -const config: Linter.Config = { - rules: { - "prefer-const": "error", - "no-param-reassign": "error", - "no-var": "error", - }, - overrides: [ - { - files: ["*.ts", "*.tsx"], - rules: { - "@typescript-eslint/prefer-readonly": "error", - "@typescript-eslint/switch-exhaustiveness-check": "error", - }, - }, - ], -}; - -export default config; diff --git a/src/configs/external-typescript-recommended.ts b/src/configs/external-typescript-recommended.ts new file mode 100644 index 000000000..738f25d72 --- /dev/null +++ b/src/configs/external-typescript-recommended.ts @@ -0,0 +1,18 @@ +import { deepmerge } from "deepmerge-ts"; +import type { Linter } from "eslint"; + +import externalVanillaRecommended from "~/configs/external-vanilla-recommended"; + +const tsConfig: Linter.Config = { + rules: { + "@typescript-eslint/prefer-readonly": "error", + "@typescript-eslint/switch-exhaustiveness-check": "error", + }, +}; + +const fullConfig: Linter.Config = deepmerge( + externalVanillaRecommended, + tsConfig +); + +export default fullConfig; diff --git a/src/configs/external-vanilla-recommended.ts b/src/configs/external-vanilla-recommended.ts new file mode 100644 index 000000000..d960b48c4 --- /dev/null +++ b/src/configs/external-vanilla-recommended.ts @@ -0,0 +1,11 @@ +import type { Linter } from "eslint"; + +const config: Linter.Config = { + rules: { + "prefer-const": "error", + "no-param-reassign": "error", + "no-var": "error", + }, +}; + +export default config; diff --git a/src/index.ts b/src/index.ts index 7a5541946..56c388dab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,8 @@ import type { Linter, Rule } from "eslint"; import all from "~/configs/all"; import currying from "~/configs/currying"; -import externalRecommended from "~/configs/external-recommended"; +import externalTypeScriptRecommended from "~/configs/external-typescript-recommended"; +import externalVanillaRecommended from "~/configs/external-vanilla-recommended"; import functional from "~/configs/functional"; import functionalLite from "~/configs/functional-lite"; import noExceptions from "~/configs/no-exceptions"; @@ -26,7 +27,8 @@ const config: EslintPluginConfig = { configs: { all, recommended: functional, - "external-recommended": externalRecommended, + "external-vanilla-recommended": externalVanillaRecommended, + "external-typescript-recommended": externalTypeScriptRecommended, lite: functionalLite, off, "no-mutations": noMutations, From 019035b3079ec1b85b90c6a6b8e5561f79d0974d Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 01:55:31 +1200 Subject: [PATCH 012/100] refactor: rename ruleset files --- src/configs/{functional-lite.ts => lite.ts} | 4 ++-- src/configs/{functional.ts => recommended.ts} | 0 src/index.ts | 14 +++++++------- tests/configs.test.ts | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) rename src/configs/{functional-lite.ts => lite.ts} (82%) rename src/configs/{functional.ts => recommended.ts} (100%) diff --git a/src/configs/functional-lite.ts b/src/configs/lite.ts similarity index 82% rename from src/configs/functional-lite.ts rename to src/configs/lite.ts index d62244f71..1cef8eaee 100644 --- a/src/configs/functional-lite.ts +++ b/src/configs/lite.ts @@ -1,7 +1,7 @@ import { deepmerge } from "deepmerge-ts"; import type { Linter } from "eslint"; -import functional from "./functional"; +import recommended from "./recommended"; const overrides: Linter.Config = { rules: { @@ -18,6 +18,6 @@ const overrides: Linter.Config = { }, }; -const config: Linter.Config = deepmerge(functional, overrides); +const config: Linter.Config = deepmerge(recommended, overrides); export default config; diff --git a/src/configs/functional.ts b/src/configs/recommended.ts similarity index 100% rename from src/configs/functional.ts rename to src/configs/recommended.ts diff --git a/src/index.ts b/src/index.ts index 56c388dab..c7f6f306c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,13 +4,13 @@ import all from "~/configs/all"; import currying from "~/configs/currying"; import externalTypeScriptRecommended from "~/configs/external-typescript-recommended"; import externalVanillaRecommended from "~/configs/external-vanilla-recommended"; -import functional from "~/configs/functional"; -import functionalLite from "~/configs/functional-lite"; +import lite from "~/configs/lite"; import noExceptions from "~/configs/no-exceptions"; import noMutations from "~/configs/no-mutations"; import noObjectOrientation from "~/configs/no-object-orientation"; import noStatements from "~/configs/no-statements"; import off from "~/configs/off"; +import recommended from "~/configs/recommended"; import stylistic from "~/configs/stylistic"; import { rules } from "~/rules"; @@ -26,16 +26,16 @@ const config: EslintPluginConfig = { rules, configs: { all, - recommended: functional, + lite, + recommended, + off, "external-vanilla-recommended": externalVanillaRecommended, "external-typescript-recommended": externalTypeScriptRecommended, - lite: functionalLite, - off, - "no-mutations": noMutations, + currying, "no-exceptions": noExceptions, + "no-mutations": noMutations, "no-object-orientation": noObjectOrientation, "no-statements": noStatements, - currying, stylistic, }, }; diff --git a/tests/configs.test.ts b/tests/configs.test.ts index 4232aef26..0b0a50487 100644 --- a/tests/configs.test.ts +++ b/tests/configs.test.ts @@ -5,13 +5,13 @@ import test from "ava"; import all from "~/configs/all"; import currying from "~/configs/currying"; -import functional from "~/configs/functional"; -import functionalLite from "~/configs/functional-lite"; +import lite from "~/configs/lite"; import noExceptions from "~/configs/no-exceptions"; import noMutations from "~/configs/no-mutations"; import noObjectOrientation from "~/configs/no-object-orientation"; import noStatements from "~/configs/no-statements"; import off from "~/configs/off"; +import recommended from "~/configs/recommended"; import stylistic from "~/configs/stylistic"; import { rules } from "~/rules"; @@ -49,8 +49,8 @@ test('Config "Off" - should have all the rules "All" has but turned off', (t) => */ const configs = new Map([ [currying, "Currying"], - [functional, "Functional"], - [functionalLite, "Functional Lite"], + [recommended, "Recommended"], + [lite, "Lite"], [noExceptions, "No Exceptions"], [noMutations, "No Mutations"], [noObjectOrientation, "No Object Orientation"], From c195d8e94886b2250b6ccdaa8849445301814565 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 21 Sep 2022 01:58:17 +1200 Subject: [PATCH 013/100] feat!: update ruleset configurations --- README.md | 39 +++++++------- src/configs/all.ts | 63 +++++++++++++--------- src/configs/currying.ts | 4 +- src/configs/deprecated.ts | 11 ++++ src/configs/lite.ts | 19 +++++-- src/configs/no-exceptions.ts | 7 ++- src/configs/no-mutations.ts | 20 ++++--- src/configs/no-object-orientation.ts | 17 +++--- src/configs/no-statements.ts | 20 ++++--- src/configs/off.ts | 4 +- src/configs/recommended.ts | 19 +------ src/configs/stylistic.ts | 18 +++---- tests/configs.test.ts | 79 +++++++++++++++++----------- tests/index.test.ts | 6 ++- 14 files changed, 181 insertions(+), 145 deletions(-) create mode 100644 src/configs/deprecated.ts diff --git a/README.md b/README.md index c6c919614..8ab692cbe 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Enable rulesets via the "extends" property of your `.eslintrc` configuration fil { // ... "extends": [ - "plugin:functional/external-recommended", + "plugin:functional/external-vanilla-recommended", "plugin:functional/recommended", "plugin:functional/stylistic" ] @@ -137,7 +137,7 @@ See [@typescript-eslint/parser's README.md](https://github.com/typescript-eslint "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", - "plugin:functional/external-recommended", + "plugin:functional/external-typescript-recommended", "plugin:functional/recommended", "plugin:functional/stylistic" ] @@ -156,17 +156,18 @@ Presets: Categorized: +- **Currying** (plugin:functional/currying) +- **No Exceptions** (plugin:functional/no-exceptions) - **No Mutations** (plugin:functional/no-mutations) - **No Object Orientation** (plugin:functional/no-object-orientation) - **No Statements** (plugin:functional/no-statements) -- **No Exceptions** (plugin:functional/no-exceptions) -- **Currying** (plugin:functional/currying) - **Stylistic** (plugin:functional/stylistic) Other: - **All** (plugin:functional/all) - Enables all rules defined in this plugin. -- **External Recommended** (plugin:functional/external-recommended) - Configures recommended rules not defined by this plugin. +- **External Vanilla Recommended** (plugin:functional/external-vanilla-recommended) - Configures recommended [vanilla ESLint](https://www.npmjs.com/package/eslint) rules. +- **External Typescript Recommended** (plugin:functional/external-typescript-recommended) - Configures recommended [TypeScript ESLint](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin) rules. Enabling this ruleset will also enable the vanilla one. The [below section](#supported-rules) gives details on which rules are enabled by each ruleset. @@ -186,22 +187,22 @@ 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: | | | -| [`prefer-immutable-parameter-types`](./docs/rules/prefer-immutable-parameter-types.md) | Require function parameters to be typed as certain immutability | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`type-declaration-immutability`](./docs/rules/type-declaration-immutability.md) | Enforce the immutability of types based on patterns | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :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: | | | +| [`prefer-immutable-parameter-types`](./docs/rules/prefer-immutable-parameter-types.md) | Require parameters to be deeply readonly | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| [`type-declaration-immutability`](./docs/rules/type-declaration-immutability.md) | Enforce type immutability with patterns | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | ### No Object-Orientation Rules :see_no_evil: = `no-object-orientation` Ruleset. -| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | -| ---------------------------------------------------------- | ------------------------------------------------------------------------ | :------------------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: | -| [`no-class`](./docs/rules/no-class.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| [`no-mixed-type`](./docs/rules/no-mixed-type.md) | Restrict types so that only members of the same kind are allowed in them | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`no-this-expression`](./docs/rules/no-this-expression.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | +| ---------------------------------------------------------- | --------------------------------------------------------------------- | :------------------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: | +| [`no-class`](./docs/rules/no-class.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`no-mixed-type`](./docs/rules/no-mixed-type.md) | Disallow types from containing both callable and non-callable members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| [`no-this-expression`](./docs/rules/no-this-expression.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | ### No Statements Rules @@ -238,14 +239,14 @@ The [below section](#supported-rules) gives details on which rules are enabled b | Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | | -------------------------------------------------------------------------- | -------------------------------------------------- | :------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: | -| [`prefer-property-signatures`](./docs/rules/prefer-property-signatures.md) | Enforce property signatures over method signatures | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`prefer-tacit`](./docs/rules/prefer-tacit.md) | Tacit/Point-Free style. | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :wrench: | :blue_heart: | +| [`prefer-property-signatures`](./docs/rules/prefer-property-signatures.md) | Enforce property signatures over method signatures | :heavy_check_mark: | | | | :thought_balloon: | +| [`prefer-tacit`](./docs/rules/prefer-tacit.md) | Tacit/Point-Free style. | :heavy_check_mark: | | | :wrench: | :blue_heart: | ## Recommended standard rules In addition to the immutability rules above, there are a few standard rules that need to be enabled to achieve immutability. -These rules are all included in the _external-recommended_ rulesets. +These rules are what are included in the _external recommended_ rulesets. ### [no-var](https://eslint.org/docs/rules/no-var) diff --git a/src/configs/all.ts b/src/configs/all.ts index dcaf39292..d16dc8172 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -1,33 +1,46 @@ import type { Linter } from "eslint"; +import * as functionalParameters from "~/rules/functional-parameters"; +import * as immutableData from "~/rules/immutable-data"; +import * as noClass from "~/rules/no-class"; +import * as noConditionalStatement from "~/rules/no-conditional-statement"; +import * as noExpressionStatement from "~/rules/no-expression-statement"; +import * as noLet from "~/rules/no-let"; +import * as noLoop from "~/rules/no-loop-statement"; +import * as noMixedType from "~/rules/no-mixed-type"; +import * as noPromiseReject from "~/rules/no-promise-reject"; +import * as noReturnVoid from "~/rules/no-return-void"; +import * as noThisExpression from "~/rules/no-this-expression"; +import * as noThrowStatement from "~/rules/no-throw-statement"; +import * as noTryStatement from "~/rules/no-try-statement"; +import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; +import * as preferPropertySignatures from "~/rules/prefer-property-signatures"; +import * as preferTacit from "~/rules/prefer-tacit"; +import * as typeDeclarationImmutability from "~/rules/type-declaration-immutability"; + const config: Linter.Config = { rules: { - "functional/functional-parameters": "error", - "functional/immutable-data": "error", - "functional/no-class": "error", - "functional/no-conditional-statement": "error", - "functional/no-expression-statement": "error", - "functional/no-let": "error", - "functional/no-loop-statement": "error", - "functional/no-promise-reject": "error", - "functional/no-this-expression": "error", - "functional/no-throw-statement": "error", - "functional/no-try-statement": "error", - "functional/prefer-tacit": ["warn", { assumeTypes: { allowFixer: false } }], + [`functional/${functionalParameters.name}`]: "error", + [`functional/${immutableData.name}`]: "error", + [`functional/${noClass.name}`]: "error", + [`functional/${noConditionalStatement.name}`]: "error", + [`functional/${noExpressionStatement.name}`]: "error", + [`functional/${noLet.name}`]: "error", + [`functional/${noLoop.name}`]: "error", + [`functional/${noMixedType.name}`]: "error", + [`functional/${noPromiseReject.name}`]: "error", + [`functional/${noReturnVoid.name}`]: "error", + [`functional/${noThisExpression.name}`]: "error", + [`functional/${noThrowStatement.name}`]: "error", + [`functional/${noTryStatement.name}`]: "error", + [`functional/${preferImmutableParameterTypes.name}`]: "error", + [`functional/${preferPropertySignatures.name}`]: "error", + [`functional/${preferTacit.name}`]: [ + "warn", + { assumeTypes: { allowFixer: false } }, + ], + [`functional/${typeDeclarationImmutability.name}`]: "error", }, - overrides: [ - { - files: ["*.ts", "*.tsx"], - rules: { - "functional/no-mixed-type": "error", - "functional/prefer-immutable-parameter-types": "error", - "functional/prefer-property-signatures": "error", - "functional/prefer-tacit": ["error", { assumeTypes: false }], - "functional/no-return-void": "error", - "functional/type-declaration-immutability": "error", - }, - }, - ], }; export default config; diff --git a/src/configs/currying.ts b/src/configs/currying.ts index 856c0b2f4..154860c58 100644 --- a/src/configs/currying.ts +++ b/src/configs/currying.ts @@ -1,8 +1,10 @@ import type { Linter } from "eslint"; +import * as functionalParameters from "~/rules/functional-parameters"; + const config: Linter.Config = { rules: { - "functional/functional-parameters": "error", + [`functional/${functionalParameters.name}`]: "error", }, }; diff --git a/src/configs/deprecated.ts b/src/configs/deprecated.ts new file mode 100644 index 000000000..97be073eb --- /dev/null +++ b/src/configs/deprecated.ts @@ -0,0 +1,11 @@ +import type { Linter } from "eslint"; + +import * as preferReadonlyType from "~/rules/prefer-readonly-type"; + +const config: Linter.Config = { + rules: { + [`functional/${preferReadonlyType.name}`]: "warn", + }, +}; + +export default config; diff --git a/src/configs/lite.ts b/src/configs/lite.ts index 1cef8eaee..0656e0f84 100644 --- a/src/configs/lite.ts +++ b/src/configs/lite.ts @@ -1,20 +1,29 @@ import { deepmerge } from "deepmerge-ts"; import type { Linter } from "eslint"; +import * as functionalParameters from "~/rules/functional-parameters"; +import * as immutableData from "~/rules/immutable-data"; +import * as noConditionalStatement from "~/rules/no-conditional-statement"; +import * as noExpressionStatement from "~/rules/no-expression-statement"; +import * as noTryStatement from "~/rules/no-try-statement"; + import recommended from "./recommended"; const overrides: Linter.Config = { rules: { - "functional/immutable-data": ["error", { ignoreClass: "fieldsOnly" }], - "functional/no-conditional-statement": "off", - "functional/no-expression-statement": "off", - "functional/no-try-statement": "off", - "functional/functional-parameters": [ + [`functional/${functionalParameters.name}`]: [ "error", { enforceParameterCount: false, }, ], + [`functional/${immutableData.name}`]: [ + "error", + { ignoreClass: "fieldsOnly" }, + ], + [`functional/${noConditionalStatement.name}`]: "off", + [`functional/${noExpressionStatement.name}`]: "off", + [`functional/${noTryStatement.name}`]: "off", }, }; diff --git a/src/configs/no-exceptions.ts b/src/configs/no-exceptions.ts index bbac17993..56fd903a6 100644 --- a/src/configs/no-exceptions.ts +++ b/src/configs/no-exceptions.ts @@ -1,9 +1,12 @@ import type { Linter } from "eslint"; +import * as noThrowStatement from "~/rules/no-throw-statement"; +import * as noTryStatement from "~/rules/no-try-statement"; + const config: Linter.Config = { rules: { - "functional/no-throw-statement": "error", - "functional/no-try-statement": "error", + [`functional/${noThrowStatement.name}`]: "error", + [`functional/${noTryStatement.name}`]: "error", }, }; diff --git a/src/configs/no-mutations.ts b/src/configs/no-mutations.ts index 3d3693f0a..156c974bc 100644 --- a/src/configs/no-mutations.ts +++ b/src/configs/no-mutations.ts @@ -1,19 +1,17 @@ import type { Linter } from "eslint"; +import * as immutableData from "~/rules/immutable-data"; +import * as noLet from "~/rules/no-let"; +import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; +import * as typeDeclarationImmutability from "~/rules/type-declaration-immutability"; + const config: Linter.Config = { rules: { - "functional/no-let": "error", - "functional/immutable-data": "error", + [`functional/${immutableData.name}`]: "error", + [`functional/${noLet.name}`]: "error", + [`functional/${preferImmutableParameterTypes.name}`]: "error", + [`functional/${typeDeclarationImmutability.name}`]: "error", }, - overrides: [ - { - files: ["*.ts", "*.tsx"], - rules: { - "functional/prefer-immutable-parameter-types": "error", - "functional/type-declaration-immutability": "error", - }, - }, - ], }; export default config; diff --git a/src/configs/no-object-orientation.ts b/src/configs/no-object-orientation.ts index 4681ba459..53e53d632 100644 --- a/src/configs/no-object-orientation.ts +++ b/src/configs/no-object-orientation.ts @@ -1,18 +1,15 @@ import type { Linter } from "eslint"; +import * as noClass from "~/rules/no-class"; +import * as noMixedType from "~/rules/no-mixed-type"; +import * as noThisExpression from "~/rules/no-this-expression"; + const config: Linter.Config = { rules: { - "functional/no-this-expression": "error", - "functional/no-class": "error", + [`functional/${noClass.name}`]: "error", + [`functional/${noMixedType.name}`]: "error", + [`functional/${noThisExpression.name}`]: "error", }, - overrides: [ - { - files: ["*.ts", "*.tsx"], - rules: { - "functional/no-mixed-type": "error", - }, - }, - ], }; export default config; diff --git a/src/configs/no-statements.ts b/src/configs/no-statements.ts index 6f4d55487..6e9cd3d58 100644 --- a/src/configs/no-statements.ts +++ b/src/configs/no-statements.ts @@ -1,19 +1,17 @@ import type { Linter } from "eslint"; +import * as noConditionalStatement from "~/rules/no-conditional-statement"; +import * as noExpressionStatement from "~/rules/no-expression-statement"; +import * as noLoop from "~/rules/no-loop-statement"; +import * as noReturnVoid from "~/rules/no-return-void"; + const config: Linter.Config = { rules: { - "functional/no-expression-statement": "error", - "functional/no-conditional-statement": "error", - "functional/no-loop-statement": "error", + [`functional/${noConditionalStatement.name}`]: "error", + [`functional/${noExpressionStatement.name}`]: "error", + [`functional/${noLoop.name}`]: "error", + [`functional/${noReturnVoid.name}`]: "error", }, - overrides: [ - { - files: ["*.ts", "*.tsx"], - rules: { - "functional/no-return-void": "error", - }, - }, - ], }; export default config; diff --git a/src/configs/off.ts b/src/configs/off.ts index bf3bda5a1..6f8391952 100644 --- a/src/configs/off.ts +++ b/src/configs/off.ts @@ -1,6 +1,7 @@ import type { Linter } from "eslint"; import all from "./all"; +import deprecated from "./deprecated"; /** * Turn the given rules off. @@ -13,8 +14,7 @@ function turnRulesOff(rules: ReadonlyArray): Linter.Config["rules"] { const allRulesNames = new Set([ ...Object.keys(all.rules ?? {}), - ...(all.overrides?.flatMap((override) => Object.keys(override.rules ?? {})) ?? - []), + ...Object.keys(deprecated.rules ?? {}), ]); const config: Linter.Config = { diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 8314cba97..377cf992a 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -6,30 +6,13 @@ import noExceptions from "~/configs/no-exceptions"; import noMutations from "~/configs/no-mutations"; import noObjectOrientation from "~/configs/no-object-orientation"; import noStatements from "~/configs/no-statements"; -import stylistic from "~/configs/stylistic"; - -const overrides: Linter.Config = { - rules: { - "functional/prefer-tacit": "off", - }, - overrides: [ - { - files: ["*.ts", "*.tsx"], - rules: { - "functional/prefer-tacit": "off", - }, - }, - ], -}; const config: Linter.Config = deepmerge( currying, noMutations, noExceptions, noObjectOrientation, - noStatements, - stylistic, - overrides + noStatements ); export default config; diff --git a/src/configs/stylistic.ts b/src/configs/stylistic.ts index e1ca65f1d..152f3d21a 100644 --- a/src/configs/stylistic.ts +++ b/src/configs/stylistic.ts @@ -1,18 +1,16 @@ import type { Linter } from "eslint"; +import * as preferPropertySignatures from "~/rules/prefer-property-signatures"; +import * as preferTacit from "~/rules/prefer-tacit"; + const config: Linter.Config = { rules: { - "functional/prefer-tacit": ["warn", { assumeTypes: { allowFixer: false } }], + [`functional/${preferPropertySignatures.name}`]: "error", + [`functional/${preferTacit.name}`]: [ + "warn", + { assumeTypes: { allowFixer: false } }, + ], }, - overrides: [ - { - files: ["*.ts", "*.tsx"], - rules: { - "functional/prefer-property-signatures": "error", - "functional/prefer-tacit": ["error", { assumeTypes: false }], - }, - }, - ], }; export default config; diff --git a/tests/configs.test.ts b/tests/configs.test.ts index 0b0a50487..a0068eb5c 100644 --- a/tests/configs.test.ts +++ b/tests/configs.test.ts @@ -1,10 +1,8 @@ -/** - * @file Tests for all configs except `all`. - */ import test from "ava"; import all from "~/configs/all"; import currying from "~/configs/currying"; +import deprecated from "~/configs/deprecated"; import lite from "~/configs/lite"; import noExceptions from "~/configs/no-exceptions"; import noMutations from "~/configs/no-mutations"; @@ -19,33 +17,66 @@ const allRules = Object.values(rules); const allNonDeprecatedRules = allRules.filter( (rule) => rule.meta.deprecated !== true ); +const allDeprecatedRules = allRules.filter( + (rule) => rule.meta.deprecated === true +); test('Config "All" - should have all the non-deprecated rules', (t) => { - const configAllJSRules = Object.keys(all.rules ?? {}); - const configAllTSRules = Object.keys(all.overrides?.[0].rules ?? {}); - const configAllRules = new Set([...configAllJSRules, ...configAllTSRules]); + const configRules = Object.keys(all.rules ?? {}); + + t.is(all.overrides, undefined, "should not have any overrides"); + t.is( + configRules.length, + allNonDeprecatedRules.length, + "should have every non-deprecated rule" + ); + + for (const name of configRules) { + t.is( + Boolean(rules[name.slice("functional/".length)].meta.deprecated), + false, + `Rule "${name}" should not be deprecated.` + ); + } +}); - t.is(configAllRules.size, allNonDeprecatedRules.length); +test('Config "Deprecated" - should only have deprecated rules', (t) => { + const configRules = Object.keys(deprecated.rules ?? {}); + + t.is(deprecated.overrides, undefined, "should not have any overrides"); + t.is( + configRules.length, + allDeprecatedRules.length, + "should have every deprecated rule" + ); + + for (const name of configRules) { + t.is( + rules[name.slice("functional/".length)].meta.deprecated, + true, + `Rule "${name}" should be deprecated.` + ); + } }); -test('Config "Off" - should have all the rules "All" has but turned off', (t) => { - const configOffJSRules = Object.keys(off.rules ?? {}); - t.is(configOffJSRules.length, allNonDeprecatedRules.length); +test('Config "Off" - should have all the rules but turned off', (t) => { + const configRules = Object.keys(off.rules ?? {}); - t.is(off.overrides, undefined, '"Off" config should not have overrides'); + t.is(off.overrides, undefined, "should not have any overrides"); + t.is(configRules.length, allRules.length, "should have every rule"); for (const [name, value] of Object.entries(off.rules)) { const severity = Array.isArray(value) ? value[0] : value; t.is( severity, "off", - `Rule "${name}"" should be turned off in the off config.` + `Rule "${name}" should be turned off in the off config.` ); } }); /** - * A map of each config (except the "all" config) to it's name. + * A map of each config (except the special ones) to it's name. */ const configs = new Map([ [currying, "Currying"], @@ -59,30 +90,18 @@ const configs = new Map([ ]); for (const [config, name] of configs.entries()) { - test(`Config "${name}" - should not have any *JS* rules that the all config does not have`, (t) => { + test(`Config "${name}"`, (t) => { + t.is(config.overrides, undefined, "should not have any overrides"); + const rulesNames = Object.keys(config.rules ?? {}); if (rulesNames.length === 0) { - t.pass("no tests"); + t.fail("no rules"); } for (const rule of rulesNames) { t.not( all.rules?.[rule], undefined, - `"${rule}" should be in "${name}" config as not in "All" config` - ); - } - }); - - test(`Config "${name}" - should not have any *TS* rules that the all config does not have`, (t) => { - const rulesNames = Object.keys(config.overrides?.[0].rules ?? {}); - if (rulesNames.length === 0) { - t.pass("no tests"); - } - for (const rule of rulesNames) { - t.not( - all.overrides?.[0].rules?.[rule], - undefined, - `"${rule}" should be in "${name}" config as not in "All" config` + "should not have any rules that the `all` config does not have" ); } }); diff --git a/tests/index.test.ts b/tests/index.test.ts index f1a000989..54fb64a85 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -29,5 +29,9 @@ test("should have all the configs", (t) => { Object.prototype.hasOwnProperty.call(plugin, "configs"), 'The plugin\'s config object should have a "configs" property.' ); - t.is(configFiles.length, Object.keys(plugin.configs).length); + t.is( + configFiles.length - 1, + Object.keys(plugin.configs).length, + "should have all the configs except deprecated" + ); }); From 26424e03b1cdeba844893d219e7f950934386b72 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 22 Sep 2022 12:01:17 +1200 Subject: [PATCH 014/100] feat!: add new strict ruleset and reduce strictness of the recommended ruleset --- src/configs/recommended.ts | 41 +++++++++++++++++++++++++++----------- src/configs/strict.ts | 18 +++++++++++++++++ src/index.ts | 2 ++ tests/configs.test.ts | 2 ++ 4 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 src/configs/strict.ts diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 377cf992a..8da501ffe 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -1,18 +1,35 @@ import { deepmerge } from "deepmerge-ts"; import type { Linter } from "eslint"; -import currying from "~/configs/currying"; -import noExceptions from "~/configs/no-exceptions"; -import noMutations from "~/configs/no-mutations"; -import noObjectOrientation from "~/configs/no-object-orientation"; -import noStatements from "~/configs/no-statements"; +import * as noConditionalStatement from "~/rules/no-conditional-statement"; +import * as noLet from "~/rules/no-let"; +import * as noThrowStatement from "~/rules/no-throw-statement"; -const config: Linter.Config = deepmerge( - currying, - noMutations, - noExceptions, - noObjectOrientation, - noStatements -); +import strict from "./strict"; + +const overrides: Linter.Config = { + rules: { + [`functional/${noConditionalStatement.name}`]: [ + "error", + { + allowReturningBranches: true, + }, + ], + [`functional/${noLet.name}`]: [ + "error", + { + allowInForLoopInit: true, + }, + ], + [`functional/${noThrowStatement.name}`]: [ + "error", + { + allowInAsyncFunctions: true, + }, + ], + }, +}; + +const config: Linter.Config = deepmerge(strict, overrides); export default config; diff --git a/src/configs/strict.ts b/src/configs/strict.ts new file mode 100644 index 000000000..377cf992a --- /dev/null +++ b/src/configs/strict.ts @@ -0,0 +1,18 @@ +import { deepmerge } from "deepmerge-ts"; +import type { Linter } from "eslint"; + +import currying from "~/configs/currying"; +import noExceptions from "~/configs/no-exceptions"; +import noMutations from "~/configs/no-mutations"; +import noObjectOrientation from "~/configs/no-object-orientation"; +import noStatements from "~/configs/no-statements"; + +const config: Linter.Config = deepmerge( + currying, + noMutations, + noExceptions, + noObjectOrientation, + noStatements +); + +export default config; diff --git a/src/index.ts b/src/index.ts index c7f6f306c..b7908ce86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import noObjectOrientation from "~/configs/no-object-orientation"; import noStatements from "~/configs/no-statements"; import off from "~/configs/off"; import recommended from "~/configs/recommended"; +import strict from "~/configs/strict"; import stylistic from "~/configs/stylistic"; import { rules } from "~/rules"; @@ -28,6 +29,7 @@ const config: EslintPluginConfig = { all, lite, recommended, + strict, off, "external-vanilla-recommended": externalVanillaRecommended, "external-typescript-recommended": externalTypeScriptRecommended, diff --git a/tests/configs.test.ts b/tests/configs.test.ts index a0068eb5c..31fe7fffd 100644 --- a/tests/configs.test.ts +++ b/tests/configs.test.ts @@ -10,6 +10,7 @@ import noObjectOrientation from "~/configs/no-object-orientation"; import noStatements from "~/configs/no-statements"; import off from "~/configs/off"; import recommended from "~/configs/recommended"; +import strict from "~/configs/strict"; import stylistic from "~/configs/stylistic"; import { rules } from "~/rules"; @@ -82,6 +83,7 @@ const configs = new Map([ [currying, "Currying"], [recommended, "Recommended"], [lite, "Lite"], + [strict, "Functional Strict"], [noExceptions, "No Exceptions"], [noMutations, "No Mutations"], [noObjectOrientation, "No Object Orientation"], From b8d70a5e617a6a566deab71bdda606cfda3f5c6b Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sat, 24 Sep 2022 19:25:27 +1200 Subject: [PATCH 015/100] style: update linting --- .eslintrc.json | 57 ++++++++----------- .vscode/settings.json | 4 +- package.json | 6 +- .../{compile-tests.ts => compile-tests.mts} | 20 +++---- scripts/tsconfig.json | 7 ++- src/common/ignore-options.ts | 2 +- src/rules/functional-parameters.ts | 4 +- src/rules/no-conditional-statement.ts | 2 +- src/util/rule.ts | 2 +- src/util/typeguard.ts | 4 +- tests/common/ignore-options.test.ts | 2 +- tests/helpers/configs.ts | 2 +- tests/index.test.ts | 6 +- tests/rules/index.test.ts | 2 +- tsconfig.base.json | 2 +- typings/es.d.ts | 24 ++++++++ yarn.lock | 20 +++---- 17 files changed, 91 insertions(+), 75 deletions(-) rename scripts/{compile-tests.ts => compile-tests.mts} (77%) create mode 100644 typings/es.d.ts diff --git a/.eslintrc.json b/.eslintrc.json index ec2e477e1..82d053783 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -31,27 +31,8 @@ }, "ignorePatterns": ["/build/", "/coverage/", "/lib/", "/**/*.cjs", "/**/*.js"], "rules": { - "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-unnecessary-condition": "off", - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/prefer-readonly-parameter-types": "off", - "import/no-relative-parent-imports": "error", - "functional/prefer-readonly-type": "off", - "node/no-unsupported-features/es-builtins": "off", - "node/no-unsupported-features/es-syntax": "off", - "promise/prefer-await-to-callbacks": "off", - // only available for node >= 16.8 - "unicorn/prefer-at": "off", - // enable once supported in all our supported node versions. - "unicorn/prefer-node-protocol": "off", - // only available for node >= 15.4 - "unicorn/prefer-string-replace-all": "off", - // only available for node >= 14.8 - "unicorn/prefer-top-level-await": "off" + "import/no-relative-parent-imports": "error" }, "overrides": [ // Top level files. @@ -75,17 +56,36 @@ } }, { - "files": ["./src/util/typeguard.ts", "./tests/**/*", "./cz-adapter/**/*"], + "files": ["./src/util/typeguard.ts", "./src/util/node-types.ts"], "rules": { "jsdoc/require-jsdoc": "off" } }, + { + "files": ["./tests/**/*", "./cz-adapter/**/*"], + "rules": { + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-return": "off", + "jsdoc/require-jsdoc": "off" + } + }, + { + "files": ["./typings/**/*"], + "rules": { + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-explicit-any": "off" + } + }, // FIXME: This override is defined in the upsteam; it shouldn't need to be redefined here. Why? { "files": ["./**/*.md/**"], "parserOptions": { "project": null }, + "extends": ["plugin:markdown/recommended", "plugin:functional/off"], "rules": { "@typescript-eslint/await-thenable": "off", "@typescript-eslint/consistent-type-definitions": "off", @@ -94,12 +94,15 @@ "@typescript-eslint/naming-convention": "off", "@typescript-eslint/no-confusing-void-expression": "off", "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-floating-promises": "off", "@typescript-eslint/no-for-in-array": "off", "@typescript-eslint/no-implied-eval": "off", "@typescript-eslint/no-misused-promises": "off", "@typescript-eslint/no-throw-literal": "off", + "@typescript-eslint/no-unnecessary-condition": "off", "@typescript-eslint/no-unnecessary-type-assertion": "off", + "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-member-access": "off", @@ -120,18 +123,6 @@ "@typescript-eslint/strict-boolean-expressions": "off", "@typescript-eslint/switch-exhaustiveness-check": "off", "@typescript-eslint/unbound-method": "off", - "functional/functional-parameters": "off", - "functional/immutable-data": "off", - "functional/no-class": "off", - "functional/no-expression-statement": "off", - "functional/no-let": "off", - "functional/no-loop-statement": "off", - "functional/no-mixed-type": "off", - "functional/no-return-void": "off", - "functional/no-this-expression": "off", - "functional/no-throw-statement": "off", - "functional/no-try-statement": "off", - "functional/prefer-readonly-type": "off", "import/no-unresolved": "off", "init-declarations": "off", "jsdoc/require-jsdoc": "off", diff --git a/.vscode/settings.json b/.vscode/settings.json index e4e7d2169..aea199b3b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,11 +23,11 @@ ".markdownlintignore": "ignore" }, "[json]": { - "editor.defaultFormatter": "vscode.json-language-features", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, "[jsonc]": { - "editor.defaultFormatter": "vscode.json-language-features", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true }, "[typescript]": { diff --git a/package.json b/package.json index bbc439bd3..2554b28f1 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "check-format": "prettier --list-different \"./**/*.{md,ts}\"", "check-spelling": "cspell --config=.cspell.json \"**/*.{md,ts}\"", "compile": "rollup -c", - "compile-tests": "ts-node -P scripts/tsconfig.json scripts/compile-tests.ts", + "compile-tests": "ts-node -P scripts/tsconfig.json scripts/compile-tests.mts", "cz": "git-cz", "format": "prettier --write \"./**/*.{md,ts}\"", "prelint": "yarn build && yarn link && yarn link 'eslint-plugin-functional'", @@ -77,7 +77,7 @@ "@commitlint/config-conventional": "^17.0.0", "@google/semantic-release-replace-plugin": "^1.1.0", "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@rebeccastevens/eslint-config": "1.3.23", + "@rebeccastevens/eslint-config": "1.4.0", "@rollup/plugin-commonjs": "^24.0.0", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.0", @@ -105,7 +105,7 @@ "cross-env": "^7.0.3", "cspell": "^6.4.1", "dedent": "^0.7.0", - "eslint": "^8.8.0", + "eslint": "^8.24.0", "eslint-ava-rule-tester": "^4.0.0", "eslint-config-prettier": "^8.3.0", "eslint-import-resolver-typescript": "^3.0.0", diff --git a/scripts/compile-tests.ts b/scripts/compile-tests.mts similarity index 77% rename from scripts/compile-tests.ts rename to scripts/compile-tests.mts index b47121393..acbda0441 100644 --- a/scripts/compile-tests.ts +++ b/scripts/compile-tests.mts @@ -1,15 +1,16 @@ -import { promises as fs } from "fs"; +import { promises as fs } from "node:fs"; import * as JSONC from "jsonc-parser"; import * as tsc from "tsc-prog"; -/** - * The script. - */ -async function run() { - transpileTests(); - await Promise.all([createTestsTsConfig(), createTestsHelpersTsConfig()]); -} +transpileTests(); +await Promise.all( + /* eslint-disable unicorn/prefer-top-level-await -- See https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1919 */ [ + createTestsTsConfig(), + createTestsHelpersTsConfig(), + ] + /* eslint-enable unicorn/prefer-top-level-await */ +); /** * Transpile the tests. @@ -61,6 +62,3 @@ async function createTestsHelpersTsConfig() { "build/tests/helpers/test-tsconfig.json" ); } - -// Run the script. -void run(); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json index 034159ece..34688238a 100644 --- a/scripts/tsconfig.json +++ b/scripts/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "ES2018", - "module": "CommonJS", + "target": "ES2020", + "module": "ESNext", "lib": ["esnext"], "alwaysStrict": true, "strict": true, @@ -17,5 +17,8 @@ "newLine": "LF", "noEmitOnError": true, "removeComments": true + }, + "ts-node": { + "esm": true } } diff --git a/src/common/ignore-options.ts b/src/common/ignore-options.ts index eeb014856..887d8ea47 100644 --- a/src/common/ignore-options.ts +++ b/src/common/ignore-options.ts @@ -181,7 +181,7 @@ function accessorPatternMatch( ) : // Text matches pattern? new RegExp( - `^${escapeRegExp(pattern).replace(/\\\*/gu, ".*")}$`, + `^${escapeRegExp(pattern).replaceAll("\\*", ".*")}$`, "u" ).test(textParts[0]) && accessorPatternMatch( diff --git a/src/rules/functional-parameters.ts b/src/rules/functional-parameters.ts index 136cf292b..6a9f5bf3d 100644 --- a/src/rules/functional-parameters.ts +++ b/src/rules/functional-parameters.ts @@ -142,10 +142,10 @@ function getRestParamViolations( ): RuleResult["descriptors"] { return !allowRestParameter && node.params.length > 0 && - isRestElement(node.params[node.params.length - 1]) + isRestElement(node.params.at(-1)) ? [ { - node: node.params[node.params.length - 1], + node: node.params.at(-1), messageId: "restParam", }, ] diff --git a/src/rules/no-conditional-statement.ts b/src/rules/no-conditional-statement.ts index 878cb04be..edcc32c44 100644 --- a/src/rules/no-conditional-statement.ts +++ b/src/rules/no-conditional-statement.ts @@ -219,7 +219,7 @@ function getSwitchViolations( } if (branch.consequent.every(isBlockStatement)) { - const lastBlock = branch.consequent[branch.consequent.length - 1]; + const lastBlock = branch.consequent.at(-1); if (lastBlock.body.some(isSwitchReturningBranch)) { return false; diff --git a/src/util/rule.ts b/src/util/rule.ts index c15851137..d9a400ca5 100644 --- a/src/util/rule.ts +++ b/src/util/rule.ts @@ -285,5 +285,5 @@ function isParserServices< contextOrServices: Context | ParserServices ): contextOrServices is ParserServices { // Only context has an id property and it will always have one. - return !Object.prototype.hasOwnProperty.call(contextOrServices, "id"); + return !Object.hasOwn(contextOrServices, "id"); } diff --git a/src/util/typeguard.ts b/src/util/typeguard.ts index 4bd8c6132..43de077e2 100644 --- a/src/util/typeguard.ts +++ b/src/util/typeguard.ts @@ -344,13 +344,13 @@ export function isVariableDeclarator( export function hasID( node: ReadonlyDeep ): node is ReadonlyDeep> { - return Object.prototype.hasOwnProperty.call(node, "id"); + return Object.hasOwn(node, "id"); } export function hasKey( node: ReadonlyDeep ): node is ReadonlyDeep> { - return Object.prototype.hasOwnProperty.call(node, "key"); + return Object.hasOwn(node, "key"); } export function isDefined(value: T | null | undefined): value is T { diff --git a/tests/common/ignore-options.test.ts b/tests/common/ignore-options.test.ts index 97e362fb3..524382a12 100644 --- a/tests/common/ignore-options.test.ts +++ b/tests/common/ignore-options.test.ts @@ -1,4 +1,4 @@ -import assert from "assert"; +import assert from "node:assert"; import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; import test from "ava"; diff --git a/tests/helpers/configs.ts b/tests/helpers/configs.ts index f85c00a44..fccf5d377 100644 --- a/tests/helpers/configs.ts +++ b/tests/helpers/configs.ts @@ -1,4 +1,4 @@ -import path from "path"; +import path from "node:path"; import type { Linter } from "eslint"; diff --git a/tests/index.test.ts b/tests/index.test.ts index 54fb64a85..5980d5d46 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -2,7 +2,7 @@ * @file Tests the index file. */ -import { readdirSync } from "fs"; +import { readdirSync } from "node:fs"; import test from "ava"; @@ -18,7 +18,7 @@ const configFiles: ReadonlyArray = readdirSync("./src/configs").filter( test("should have all the rules", (t) => { t.true( - Object.prototype.hasOwnProperty.call(plugin, "rules"), + Object.hasOwn(plugin, "rules"), 'The plugin\'s config object should have a "rules" property.' ); t.is(ruleFiles.length, Object.keys(plugin.rules).length); @@ -26,7 +26,7 @@ test("should have all the rules", (t) => { test("should have all the configs", (t) => { t.true( - Object.prototype.hasOwnProperty.call(plugin, "configs"), + Object.hasOwn(plugin, "configs"), 'The plugin\'s config object should have a "configs" property.' ); t.is( diff --git a/tests/rules/index.test.ts b/tests/rules/index.test.ts index c3170a787..6d456841f 100644 --- a/tests/rules/index.test.ts +++ b/tests/rules/index.test.ts @@ -1,4 +1,4 @@ -import * as fs from "fs"; +import * as fs from "node:fs"; import test from "ava"; diff --git a/tsconfig.base.json b/tsconfig.base.json index f45a4086e..c30bc184c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,7 +20,7 @@ "sourceMap": false, "strict": true, "alwaysStrict": true, - "target": "ES2018", + "target": "ES2020", "baseUrl": ".", "paths": { "~/*": ["./*"], diff --git a/typings/es.d.ts b/typings/es.d.ts new file mode 100644 index 000000000..5a4404b44 --- /dev/null +++ b/typings/es.d.ts @@ -0,0 +1,24 @@ +declare global { + interface ObjectConstructor { + hasOwn( + object: ObjectType, + key: Key + ): object is ObjectType & Record; + } + + interface ArrayConstructor { + isArray(arg: unknown): arg is unknown[] | ReadonlyArray; + } + + interface ReadonlyArray { + at(index: U): this[U]; + at(this: readonly [...any[], R], index: -1): R; + } + + interface Array { + at(index: U): this[U]; + at(this: readonly [...any[], R], index: -1): R; + } +} + +export {}; diff --git a/yarn.lock b/yarn.lock index 33f8413a3..48848e4d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1367,9 +1367,9 @@ __metadata: languageName: node linkType: hard -"@rebeccastevens/eslint-config@npm:1.3.23": - version: 1.3.23 - resolution: "@rebeccastevens/eslint-config@npm:1.3.23" +"@rebeccastevens/eslint-config@npm:1.4.0": + version: 1.4.0 + resolution: "@rebeccastevens/eslint-config@npm:1.4.0" dependencies: deepmerge-ts: ^4.0.0 peerDependencies: @@ -1388,7 +1388,7 @@ __metadata: eslint-plugin-sonarjs: "*" eslint-plugin-tsdoc: "*" eslint-plugin-unicorn: "*" - checksum: 30305d84d983bade4d9cc81d285dd4d801703c1f41800747fa40c79f8275f841a021dfc0606eae8cb1b29d11c6c3e962ffa91a85fce13af1e3def02e79799803 + checksum: 08b7cc59e8913f47c305ff503bdde2a03506bdeece890681be92e8d3d1fdc0ce6eb0f9c5dd69fa57432dd3aa9e84eb4dd8ab917d00b17d7b1d02bdb457e6556b languageName: node linkType: hard @@ -3981,7 +3981,7 @@ __metadata: "@commitlint/config-conventional": ^17.0.0 "@google/semantic-release-replace-plugin": ^1.1.0 "@istanbuljs/nyc-config-typescript": ^1.0.2 - "@rebeccastevens/eslint-config": 1.3.23 + "@rebeccastevens/eslint-config": 1.4.0 "@rollup/plugin-commonjs": ^24.0.0 "@rollup/plugin-json": ^6.0.0 "@rollup/plugin-node-resolve": ^15.0.0 @@ -4012,7 +4012,7 @@ __metadata: dedent: ^0.7.0 deepmerge-ts: ^4.0.3 escape-string-regexp: ^4.0.0 - eslint: ^8.8.0 + eslint: ^8.24.0 eslint-ava-rule-tester: ^4.0.0 eslint-config-prettier: ^8.3.0 eslint-import-resolver-typescript: ^3.0.0 @@ -4267,9 +4267,9 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.8.0": - version: 8.31.0 - resolution: "eslint@npm:8.31.0" +"eslint@npm:^8.24.0": + version: 8.32.0 + resolution: "eslint@npm:8.32.0" dependencies: "@eslint/eslintrc": ^1.4.1 "@humanwhocodes/config-array": ^0.11.8 @@ -4312,7 +4312,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 5e5688bb864edc6b12d165849994812eefa67fb3fc44bb26f53659b63edcd8bcc68389d27cc6cc9e5b79ee22f24b6f311fa3ed047bddcafdec7d84c1b5561e4f + checksum: 23c8fb3c57291eecd9c1448faf603226a8f885022a2cd96e303459bf72e39b7f54987c6fb948f0f9eecaf7085600e6eb0663482a35ea83da12e9f9141a22b91e languageName: node linkType: hard From 4309b084272c4ab86561b63eab670ed8e22226d1 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 00:57:51 +1200 Subject: [PATCH 016/100] fixup: update ruleset configurations --- src/configs/lite.ts | 11 +++++++++-- src/configs/recommended.ts | 9 +++++++++ src/rules/prefer-immutable-parameter-types.ts | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/configs/lite.ts b/src/configs/lite.ts index 0656e0f84..cba004f90 100644 --- a/src/configs/lite.ts +++ b/src/configs/lite.ts @@ -5,7 +5,8 @@ import * as functionalParameters from "~/rules/functional-parameters"; import * as immutableData from "~/rules/immutable-data"; import * as noConditionalStatement from "~/rules/no-conditional-statement"; import * as noExpressionStatement from "~/rules/no-expression-statement"; -import * as noTryStatement from "~/rules/no-try-statement"; +import * as noReturnVoid from "~/rules/no-return-void"; +import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; import recommended from "./recommended"; @@ -23,7 +24,13 @@ const overrides: Linter.Config = { ], [`functional/${noConditionalStatement.name}`]: "off", [`functional/${noExpressionStatement.name}`]: "off", - [`functional/${noTryStatement.name}`]: "off", + [`functional/${noReturnVoid.name}`]: "error", + [`functional/${preferImmutableParameterTypes.name}`]: [ + "error", + { + enforcement: "ReadonlyShallow", + }, + ], }, }; diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 8da501ffe..aa342605a 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -4,6 +4,8 @@ import type { Linter } from "eslint"; import * as noConditionalStatement from "~/rules/no-conditional-statement"; import * as noLet from "~/rules/no-let"; import * as noThrowStatement from "~/rules/no-throw-statement"; +import * as noTryStatement from "~/rules/no-try-statement"; +import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; import strict from "./strict"; @@ -27,6 +29,13 @@ const overrides: Linter.Config = { allowInAsyncFunctions: true, }, ], + [`functional/${noTryStatement.name}`]: "off", + [`functional/${preferImmutableParameterTypes.name}`]: [ + "error", + { + enforcement: "ReadonlyDeep", + }, + ], }, }; diff --git a/src/rules/prefer-immutable-parameter-types.ts b/src/rules/prefer-immutable-parameter-types.ts index cdc1f8944..046a27c7a 100644 --- a/src/rules/prefer-immutable-parameter-types.ts +++ b/src/rules/prefer-immutable-parameter-types.ts @@ -54,7 +54,7 @@ const schema: JSONSchema4 = [ */ const defaultOptions: Options = [ { - enforcement: Immutability.ReadonlyDeep, + enforcement: Immutability.Immutable, }, ]; From 7ec10c65d1c740697ecc7ca1cf5a9b7f48ed69e2 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 00:56:50 +1200 Subject: [PATCH 017/100] feat!: rename ruleset `no-object-orientation` to `no-other-paradigms` --- .../{no-object-orientation.ts => no-other-paradigms.ts} | 0 src/configs/strict.ts | 4 ++-- src/index.ts | 4 ++-- tests/configs.test.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/configs/{no-object-orientation.ts => no-other-paradigms.ts} (100%) diff --git a/src/configs/no-object-orientation.ts b/src/configs/no-other-paradigms.ts similarity index 100% rename from src/configs/no-object-orientation.ts rename to src/configs/no-other-paradigms.ts diff --git a/src/configs/strict.ts b/src/configs/strict.ts index 377cf992a..756047ed2 100644 --- a/src/configs/strict.ts +++ b/src/configs/strict.ts @@ -4,14 +4,14 @@ import type { Linter } from "eslint"; import currying from "~/configs/currying"; import noExceptions from "~/configs/no-exceptions"; import noMutations from "~/configs/no-mutations"; -import noObjectOrientation from "~/configs/no-object-orientation"; import noStatements from "~/configs/no-statements"; +import noOtherParadigms from "~/src/configs/no-other-paradigms"; const config: Linter.Config = deepmerge( currying, noMutations, noExceptions, - noObjectOrientation, + noOtherParadigms, noStatements ); diff --git a/src/index.ts b/src/index.ts index b7908ce86..f76e76626 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import externalVanillaRecommended from "~/configs/external-vanilla-recommended"; import lite from "~/configs/lite"; import noExceptions from "~/configs/no-exceptions"; import noMutations from "~/configs/no-mutations"; -import noObjectOrientation from "~/configs/no-object-orientation"; +import noOtherParadigms from "~/configs/no-other-paradigms"; import noStatements from "~/configs/no-statements"; import off from "~/configs/off"; import recommended from "~/configs/recommended"; @@ -36,7 +36,7 @@ const config: EslintPluginConfig = { currying, "no-exceptions": noExceptions, "no-mutations": noMutations, - "no-object-orientation": noObjectOrientation, + "no-other-paradigms": noOtherParadigms, "no-statements": noStatements, stylistic, }, diff --git a/tests/configs.test.ts b/tests/configs.test.ts index 31fe7fffd..8aa8db0fe 100644 --- a/tests/configs.test.ts +++ b/tests/configs.test.ts @@ -6,7 +6,7 @@ import deprecated from "~/configs/deprecated"; import lite from "~/configs/lite"; import noExceptions from "~/configs/no-exceptions"; import noMutations from "~/configs/no-mutations"; -import noObjectOrientation from "~/configs/no-object-orientation"; +import noOtherParadigms from "~/configs/no-other-paradigms"; import noStatements from "~/configs/no-statements"; import off from "~/configs/off"; import recommended from "~/configs/recommended"; @@ -86,7 +86,7 @@ const configs = new Map([ [strict, "Functional Strict"], [noExceptions, "No Exceptions"], [noMutations, "No Mutations"], - [noObjectOrientation, "No Object Orientation"], + [noOtherParadigms, "No Other Paradigms"], [noStatements, "No Statements"], [stylistic, "Stylistic"], ]); From ebdfaf8d7e0c801584d85351fe2d172d39605328 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 03:10:13 +1300 Subject: [PATCH 018/100] chore: remove "pre" scripts --- package.json | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 2554b28f1..7dfa1daa6 100644 --- a/package.json +++ b/package.json @@ -41,18 +41,15 @@ "README.md" ], "scripts": { - "prebuild": "rimraf lib", - "build": "yarn compile", - "prebuild-tests": "rimraf build", - "build-tests": "yarn compile-tests", + "build": "rimraf lib && yarn compile", + "build-tests": "rimraf build && yarn compile-tests", "check-format": "prettier --list-different \"./**/*.{md,ts}\"", "check-spelling": "cspell --config=.cspell.json \"**/*.{md,ts}\"", "compile": "rollup -c", "compile-tests": "ts-node -P scripts/tsconfig.json scripts/compile-tests.mts", "cz": "git-cz", "format": "prettier --write \"./**/*.{md,ts}\"", - "prelint": "yarn build && yarn link && yarn link 'eslint-plugin-functional'", - "lint": "yarn lint-js && yarn lint-md", + "lint": "yarn build && yarn lint-js && yarn lint-md", "lint-js": "eslint .", "lint-md": "markdownlint \"**/*.md\" --config=.markdownlint.json --ignore-path=.markdownlintignore", "prepare": "yarn husky install", From a77f21ffb9ba9543bc3dff720b9dda00a1382420 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 01:47:27 +1200 Subject: [PATCH 019/100] docs: breakup and update readme --- .cspell.json | 1 + CONTRIBUTING.md | 23 ++++ GETTING_STARTED.md | 94 ++++++++++++++ README.md | 318 ++++++++++++++------------------------------- 4 files changed, 212 insertions(+), 224 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 GETTING_STARTED.md diff --git a/.cspell.json b/.cspell.json index 5df00838e..9cd4a2a5e 100644 --- a/.cspell.json +++ b/.cspell.json @@ -31,6 +31,7 @@ "\\(#.+?\\)" ], "words": [ + "fleur", "globstar", "IIFE", "IIFEs", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..57392d08b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contributing + +## How to + +For new features file an issue. For bugs, file an issue and optionally file a PR with a failing test. + +## How to develop + +To execute the tests run `yarn test`. + +To learn about ESLint plugin development see the [relevant section](https://eslint.org/docs/developer-guide/working-with-plugins) of the ESLint docs. You can also checkout the [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) repo which has some more information specific to TypeScript. + +In order to know which AST nodes are created for a snippet of TypeScript code you can use [AST explorer](https://astexplorer.net/) with options JavaScript and @typescript-eslint/parser. + +### Commit Messages + +> tl;dr: use `npx cz` instead of `git commit`. + +Commit messages must follow [Conventional Commit messages guidelines](https://www.conventionalcommits.org/en/v1.0.0/). You can use `npx cz` instead of `git commit` to run a interactive prompt to generate the commit message. We've customize the prompt specifically for this project. For more information see [commitizen](https://github.com/commitizen/cz-cli#readme). + +### How to publish + +Publishing is handled by [semantic release](https://github.com/semantic-release/semantic-release#readme) - there shouldn't be any need to publish manually. diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 000000000..2a4baaaee --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,94 @@ +# Getting Started + +## Installation + +### JavaScript + +```sh +# Install with npm +npm install -D eslint eslint-plugin-functional + +# Install with yarn +yarn add -D eslint eslint-plugin-functional +``` + +### TypeScript + +```sh +# Install with npm +npm install -D eslint @typescript-eslint/parser tsutils eslint-plugin-functional + +# Install with yarn +yarn add -D eslint @typescript-eslint/parser tsutils eslint-plugin-functional +``` + +## Usage + +Add `functional` to the plugins section of your `.eslintrc` configuration file. Then configure the rules you want to use under the rules section. + +```jsonc +{ + "plugins": ["functional"], + "rules": { + "functional/rule-name": "error" + } +} +``` + +There are several rulesets provided by this plugin. +[See below](#rulesets) for what they are and what rules are including in each. +Enable rulesets via the "extends" property of your `.eslintrc` configuration file. + +```jsonc +{ + // ... + "extends": [ + "plugin:functional/external-vanilla-recommended", + "plugin:functional/recommended", + "plugin:functional/stylistic" + ] +} +``` + +### With TypeScript + +Add `@typescript-eslint/parser` to the "parser" filed in your `.eslintrc` configuration file. +To use type information, you will need to specify a path to your `tsconfig.json` file in the "project" property of "parserOptions". + +```jsonc +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + } +} +``` + +See [@typescript-eslint/parser's README.md](https://github.com/typescript-eslint/typescript-eslint/tree/main/packages/parser#readme) for more information on the available parser options. + +### Example Config + +```jsonc +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json" + }, + "env": { + "es6": true + }, + "plugins": [ + "@typescript-eslint", + "functional" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:functional/external-typescript-recommended", + "plugin:functional/recommended", + "plugin:functional/stylistic" + ] +} +``` diff --git a/README.md b/README.md index 8ab692cbe..86519f68b 100644 --- a/README.md +++ b/README.md @@ -16,279 +16,149 @@ An [ESLint](http://eslint.org) plugin to disable mutation and promote functional -> :wave: If you previously used the rules in [tslint-immutable](https://www.npmjs.com/package/tslint-immutable), this package is the ESLint version of those rules. Please see the [migration guide](docs/user-guide/migrating-from-tslint.md) for how to migrate. - -## Features - -- [No mutations](#no-mutations) -- [No object-orientation](#no-object-orientation) -- [No statements](#no-statements) -- [No exceptions](#no-exceptions) -- [Currying](#currying) -- [Stylistic](#stylistic) - -### No mutations - -One aim of this project is to leverage the type system in TypeScript to enforce immutability at compile-time while still using regular objects and arrays. Additionally, this project will also aim to support disabling mutability for vanilla JavaScript where possible. - -### No object-orientation - -JavaScript is multi-paradigm, allowing both object-oriented and functional programming styles. In order to promote a functional style, the object oriented features of JavaScript need to be disabled. - -### No statements - -In functional programming everything is an expression that produces a value. JavaScript has a lot of syntax that is just statements that does not produce a value. That syntax has to be disabled to promote a functional style. - -### No exceptions - -Functional programming style does not use run-time exceptions. Instead expressions produces values to indicate errors. - -### Currying - -JavaScript functions support syntax that is not compatible with curried functions. To enable currying this syntax has to be disabled. - -### Stylistic - -Enforce code to be written in a more functional style. - -## Installation - -### JavaScript - -```sh -# Install with npm -npm install -D eslint eslint-plugin-functional - -# Install with yarn -yarn add -D eslint eslint-plugin-functional -``` - -### TypeScript - -```sh -# Install with npm -npm install -D eslint @typescript-eslint/parser tsutils eslint-plugin-functional - -# Install with yarn -yarn add -D eslint @typescript-eslint/parser tsutils eslint-plugin-functional -``` - -## Usage - -Add `functional` to the plugins section of your `.eslintrc` configuration file. Then configure the rules you want to use under the rules section. - -```jsonc -{ - "plugins": ["functional"], - "rules": { - "functional/rule-name": "error" - } -} -``` - -There are several rulesets provided by this plugin. -[See below](#rulesets) for what they are and what rules are including in each. -Enable rulesets via the "extends" property of your `.eslintrc` configuration file. - -```jsonc -{ - // ... - "extends": [ - "plugin:functional/external-vanilla-recommended", - "plugin:functional/recommended", - "plugin:functional/stylistic" - ] -} -``` +## Rulesets -### With TypeScript +The following rulesets are made available by this plugin: -Add `@typescript-eslint/parser` to the "parser" filed in your `.eslintrc` configuration file. -To use type information, you will need to specify a path to your `tsconfig.json` file in the "project" property of "parserOptions". +Presets: -```jsonc -{ - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json" - } -} -``` +- **Strict** (`plugin:functional/strict`)\ + Enforce recommended rules designed to strictly enforce functional programming. -See [@typescript-eslint/parser's README.md](https://github.com/typescript-eslint/typescript-eslint/tree/main/packages/parser#readme) for more information on the available parser options. +- **Recommended** (`plugin:functional/recommended`)\ + Has the same goal as the `strict` preset but a little more lenient, allowing for functional-like coding styles and nicer integration with non-functional 3rd-party libraries. -### Example Config +- **Lite** (`plugin:functional/lite`)\ + Good if you're new to functional programming or are converting a large codebase. -```jsonc -{ - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "tsconfig.json" - }, - "env": { - "es6": true - }, - "plugins": [ - "@typescript-eslint", - "functional" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "plugin:functional/external-typescript-recommended", - "plugin:functional/recommended", - "plugin:functional/stylistic" - ] -} -``` +Categorized: -## Rulesets +- **Currying** (`plugin:functional/currying`)\ + JavaScript functions support syntax that is not compatible with curried functions. To enforce currying, this syntax should be prevented. -The following rulesets are made available by this plugin: +- **No Exceptions** (`plugin:functional/no-exceptions`)\ + Functional programming style does not use run-time exceptions. Instead expressions produces values to indicate errors. -Presets: +- **No Mutations** (`plugin:functional/no-mutations`)\ + Prevent mutating any data as that's not functional -- **Recommended** (plugin:functional/recommended) -- **Lite** (plugin:functional/lite) -- **Off** (plugin:functional/off) +- **No Other Paradigms** (`plugin:functional/no-other-paradigms`)\ + JavaScript is multi-paradigm, allowing not only functional, but object-oriented as well as other programming styles. To promote a functional style, prevent the use of other paradigm styles. -Categorized: +- **No Statements** (`plugin:functional/no-statements`)\ + In functional programming everything is an expression that produces a value. JavaScript has a lot of syntax that is just statements that does not produce a value. That syntax has to be prevented to promote a functional style. -- **Currying** (plugin:functional/currying) -- **No Exceptions** (plugin:functional/no-exceptions) -- **No Mutations** (plugin:functional/no-mutations) -- **No Object Orientation** (plugin:functional/no-object-orientation) -- **No Statements** (plugin:functional/no-statements) -- **Stylistic** (plugin:functional/stylistic) +- **Stylistic** (`plugin:functional/stylistic`)\ + Enforce code styles that can be considered to be more functional. Other: -- **All** (plugin:functional/all) - Enables all rules defined in this plugin. -- **External Vanilla Recommended** (plugin:functional/external-vanilla-recommended) - Configures recommended [vanilla ESLint](https://www.npmjs.com/package/eslint) rules. -- **External Typescript Recommended** (plugin:functional/external-typescript-recommended) - Configures recommended [TypeScript ESLint](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin) rules. Enabling this ruleset will also enable the vanilla one. +- **All** (`plugin:functional/all`)\ + Enables all rules defined in this plugin. -The [below section](#supported-rules) gives details on which rules are enabled by each ruleset. +- **Off** (`plugin:functional/off`)\ + Disable all rules defined in this plugin. -## Supported Rules +- **External Vanilla Recommended** (`plugin:functional/external-vanilla-recommended`)\ + Configures recommended [vanilla ESLint](https://www.npmjs.com/package/eslint) rules. -**Key**: +- **External Typescript Recommended** (`plugin:functional/external-typescript-recommended`)\ + Configures recommended [TypeScript ESLint](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin) rules. Enabling this ruleset will also enable the vanilla one. -| Symbol | Meaning | -| :---------------: | -------------------------------------------------------------------------------------------------------------------------------------- | -| :hear_no_evil: | Ruleset: Lite
This ruleset is designed to enforce a somewhat functional programming code style. | -| :speak_no_evil: | Ruleset: Recommended
This ruleset is designed to enforce a functional programming code style. | -| :wrench: | Fixable
Problems found by this rule are potentially fixable with the `--fix` option. | -| :thought_balloon: | Only Available for TypeScript
The rule either requires Type Information or only works with TypeScript syntax. | -| :blue_heart: | Works better with TypeScript
Type Information will be used if available making the rule work in more cases. | +The [below section](#rules) gives details on which rules are enabled by each ruleset. -### No Mutations Rules - -:see_no_evil: = `no-mutations` Ruleset. +## Rules -| 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: | | | -| [`prefer-immutable-parameter-types`](./docs/rules/prefer-immutable-parameter-types.md) | Require parameters to be deeply readonly | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`type-declaration-immutability`](./docs/rules/type-declaration-immutability.md) | Enforce type immutability with patterns | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +### No Mutations Rules -### No Object-Orientation Rules +| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_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: | :green_circle: | | :blue_heart: | +| [`no-let`](./docs/rules/no-let.md) | Disallow mutable variables | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | :green_circle: | | | +| [`prefer-immutable-parameter-types`](./docs/rules/prefer-immutable-parameter-types.md) | Require parameters to be deeply readonly | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | :green_circle: | | :thought_balloon: | +| [`type-declaration-immutability`](./docs/rules/type-declaration-immutability.md) | Enforce type immutability with patterns | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -:see_no_evil: = `no-object-orientation` Ruleset. +### No Other Paradigms Rules -| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | -| ---------------------------------------------------------- | --------------------------------------------------------------------- | :------------------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: | -| [`no-class`](./docs/rules/no-class.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| [`no-mixed-type`](./docs/rules/no-mixed-type.md) | Disallow types from containing both callable and non-callable members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`no-this-expression`](./docs/rules/no-this-expression.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | +| ---------------------------------------------------------- | ------------------------------------------------------------------ | :----------------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | +| [`no-class`](./docs/rules/no-class.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`no-mixed-type`](./docs/rules/no-mixed-type.md) | Disallow types that contain both callable and non-callable members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| [`no-this-expression`](./docs/rules/no-this-expression.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | ### No Statements Rules -:see_no_evil: = `no-statements` Ruleset. - -| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | -| ---------------------------------------------------------------------- | ---------------------------------------------------------- | :----------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: | -| [`no-conditional-statement`](./docs/rules/no-conditional-statement.md) | Disallow conditional statements (if and switch statements) | :heavy_check_mark: | | :heavy_check_mark: | | :thought_balloon: | -| [`no-expression-statement`](./docs/rules/no-expression-statement.md) | Disallow expressions to cause side-effects | :heavy_check_mark: | | :heavy_check_mark: | | :thought_balloon: | -| [`no-loop-statement`](./docs/rules/no-loop-statement.md) | Disallow imperative loops | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| [`no-return-void`](./docs/rules/no-return-void.md) | Disallow functions that return nothing | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | +| ---------------------------------------------------------------------- | -------------------------------------------------------------- | :-----------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | +| [`no-conditional-statement`](./docs/rules/no-conditional-statement.md) | Disallow conditional statements (`if` and `switch` statements) | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | | | :thought_balloon: | +| [`no-expression-statement`](./docs/rules/no-expression-statement.md) | Disallow expressions to cause side-effects | :heavy_check_mark: | :heavy_check_mark: | | | | :thought_balloon: | +| [`no-loop-statement`](./docs/rules/no-loop-statement.md) | Disallow imperative loops | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`no-return-void`](./docs/rules/no-return-void.md) | Disallow functions that return nothing | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | ### No Exceptions Rules -:see_no_evil: = `no-exceptions` Ruleset. - -| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | -| ---------------------------------------------------------- | ----------------------------------------------------- | :----------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :----------: | -| [`no-promise-reject`](./docs/rules/no-promise-reject.md) | Disallow rejecting Promises | | | | | | -| [`no-throw-statement`](./docs/rules/no-throw-statement.md) | Disallow throwing exceptions | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| [`no-try-statement`](./docs/rules/no-try-statement.md) | Disallow try-catch[-finally] and try-finally patterns | :heavy_check_mark: | | :heavy_check_mark: | | | +| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | +| ---------------------------------------------------------- | ----------------------------------------------------- | :-----------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :----------: | +| [`no-promise-reject`](./docs/rules/no-promise-reject.md) | Disallow rejecting Promises | | | | | | | +| [`no-throw-statement`](./docs/rules/no-throw-statement.md) | Disallow throwing exceptions | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | :green_circle: | | | +| [`no-try-statement`](./docs/rules/no-try-statement.md) | Disallow try-catch[-finally] and try-finally patterns | :heavy_check_mark: | :heavy_check_mark: | | | | | ### Currying Rules -:see_no_evil: = `currying` Ruleset. - -| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | -| ---------------------------------------------------------------- | ----------------------------------------- | :-----------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :----------: | -| [`functional-parameters`](./docs/rules/functional-parameters.md) | Functions must have functional parameters | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | +| ---------------------------------------------------------------- | ----------------------------------------- | :------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :----------: | +| [`functional-parameters`](./docs/rules/functional-parameters.md) | Functions must have functional parameters | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | | | ### Stylistic Rules -:see_no_evil: = `stylistic` Ruleset. +| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | +| -------------------------------------------------------------------------- | -------------------------------------------------- | :-------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | +| [`prefer-property-signatures`](./docs/rules/prefer-property-signatures.md) | Enforce property signatures over method signatures | :heavy_check_mark: | | | | | :thought_balloon: | +| [`prefer-tacit`](./docs/rules/prefer-tacit.md) | Tacit/Point-Free style. | :heavy_check_mark: | | | | :wrench: | :blue_heart: | -| Name | Description | :see_no_evil: | :hear_no_evil: | :speak_no_evil: | :wrench: | :blue_heart: | -| -------------------------------------------------------------------------- | -------------------------------------------------- | :------------------------------------------: | :--------------------------------------: | :----------------------------------------------: | :------: | :---------------: | -| [`prefer-property-signatures`](./docs/rules/prefer-property-signatures.md) | Enforce property signatures over method signatures | :heavy_check_mark: | | | | :thought_balloon: | -| [`prefer-tacit`](./docs/rules/prefer-tacit.md) | Tacit/Point-Free style. | :heavy_check_mark: | | | :wrench: | :blue_heart: | +### Key -## Recommended standard rules +| Symbol | Meaning | +| :----------------: | -------------------------------------------------------------------------------------------------------------------------------- | +| :fleur_de_lis: | Ruleset: Current | +| :speak_no_evil: | Ruleset: Strict | +| :see_no_evil: | Ruleset: Recommended | +| :hear_no_evil: | Ruleset: Lite | +| :heavy_check_mark: | Enabled as Error | +| :green_circle: | Enabled as Error with Overrides | +| :wrench: | Fixable | +| :thought_balloon: | Only Available for TypeScript | +| :blue_heart: | Works better with TypeScript | -In addition to the immutability rules above, there are a few standard rules that need to be enabled to achieve immutability. + -These rules are what are included in the _external recommended_ rulesets. +## External Recommended Rules -### [no-var](https://eslint.org/docs/rules/no-var) +In addition to the above rules, there are a few other rules we recommended. -Without this rule, it is still possible to create `var` variables that are mutable. - -### [no-param-reassign](https://eslint.org/docs/rules/no-param-reassign) - -Don't allow function parameters to be reassigned, they should be treated as constants. - -### [prefer-const](https://eslint.org/docs/rules/prefer-const) - -This rule is helpful when converting from an imperative code style to a functional one. - -### [@typescript-eslint/prefer-readonly](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-readonly.md) - -This rule is helpful when working with classes. - -### [@typescript-eslint/switch-exhaustiveness-check](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md) - -Although our [no-conditional-statement](./docs/rules/no-conditional-statement.md) rule also performs this check, this rule has a fixer that will implement the unimplemented cases which can be useful. - -## How to contribute - -For new features file an issue. For bugs, file an issue and optionally file a PR with a failing test. - -## How to develop - -To execute the tests run `yarn test`. +These rules are what are included in the _external recommended_ rulesets. -To learn about ESLint plugin development see the [relevant section](https://eslint.org/docs/developer-guide/working-with-plugins) of the ESLint docs. You can also checkout the [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) repo which has some more information specific to TypeScript. +### Vanilla Rules -In order to know which AST nodes are created for a snippet of TypeScript code you can use [AST explorer](https://astexplorer.net/) with options JavaScript and @typescript-eslint/parser. +- [no-var](https://eslint.org/docs/rules/no-var)\ + Without this rule, it is still possible to create `var` variables that are mutable. -### Commit Messages +- [no-param-reassign](https://eslint.org/docs/rules/no-param-reassign)\ + Don't allow function parameters to be reassigned, they should be treated as constants. -> tl;dr: use `npx cz` instead of `git commit`. +- [prefer-const](https://eslint.org/docs/rules/prefer-const)\ + This rule provides a helpful fixer when converting from an imperative code style to a functional one. -Commit messages must follow [Conventional Commit messages guidelines](https://www.conventionalcommits.org/en/v1.0.0/). You can use `npx cz` instead of `git commit` to run a interactive prompt to generate the commit message. We've customize the prompt specifically for this project. For more information see [commitizen](https://github.com/commitizen/cz-cli#readme). +### Typescript Rules -### How to publish +- [@typescript-eslint/prefer-readonly](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-readonly.md)\ + This rule is helpful when working with classes. -Publishing is handled by [semantic release](https://github.com/semantic-release/semantic-release#readme) - there shouldn't be any need to publish manually. +- [@typescript-eslint/switch-exhaustiveness-check](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md)\ + Although our [no-conditional-statement](./docs/rules/no-conditional-statement.md) rule also performs this check, this rule has a fixer that will implement the unimplemented cases which can be useful. ## Prior work From 64f74ba5cf1688474cc88ffce70e8b390328f68e Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 04:03:20 +1300 Subject: [PATCH 020/100] docs: update rule docs --- docs/rules/functional-parameters.md | 14 +++++----- docs/rules/immutable-data.md | 14 +++++----- docs/rules/no-class.md | 4 +-- docs/rules/no-conditional-statement.md | 24 ++++++++++++++-- docs/rules/no-expression-statement.md | 6 ++-- docs/rules/no-let.md | 28 +++++++++++++++---- docs/rules/no-loop-statement.md | 4 +-- docs/rules/no-method-signature.md | 8 +++--- docs/rules/no-mixed-type.md | 6 ++-- docs/rules/no-promise-reject.md | 4 +-- docs/rules/no-return-void.md | 6 ++-- docs/rules/no-this-expression.md | 6 ++-- docs/rules/no-throw-statement.md | 26 ++++++++++++++--- docs/rules/no-try-statement.md | 6 ++-- .../rules/prefer-immutable-parameter-types.md | 24 ++++++++++++++-- docs/rules/prefer-readonly-type.md | 6 ++-- docs/rules/prefer-tacit.md | 6 ++-- docs/rules/type-declaration-immutability.md | 6 ++-- 18 files changed, 136 insertions(+), 62 deletions(-) diff --git a/docs/rules/functional-parameters.md b/docs/rules/functional-parameters.md index a3467b60a..ca35012a9 100644 --- a/docs/rules/functional-parameters.md +++ b/docs/rules/functional-parameters.md @@ -11,7 +11,7 @@ When it comes to functional programming, known and explicit parameters must be u Note: With an unknown number of parameters, currying functions is a lot more difficult/impossible. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -33,7 +33,7 @@ function add(...numbers) { } ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/functional-parameters: "error" */ @@ -60,7 +60,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { @@ -73,12 +73,12 @@ const defaults = { } ``` -Note: the `lite` ruleset overrides the default options to: +### Preset Overrides + +#### `lite` ```ts -const liteDefaults = { - allowRestParameter: false, - allowArgumentsKeyword: false, +const liteOptions = { enforceParameterCount: false } ``` diff --git a/docs/rules/immutable-data.md b/docs/rules/immutable-data.md index 05c321231..28cf3fb25 100644 --- a/docs/rules/immutable-data.md +++ b/docs/rules/immutable-data.md @@ -7,7 +7,7 @@ This rule prohibits syntax that mutates existing objects and arrays via assignme While requiring the `readonly` modifier forces declared types to be immutable, it won't stop assignment into or modification of untyped objects or external types declared under different rules. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -35,7 +35,7 @@ delete arr[1]; // <- Modifying an existing array is not allowed. arr.push(3); // <- Modifying an array is not allowed. ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/immutable-data: "error" */ @@ -70,7 +70,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts type Options = { @@ -80,13 +80,13 @@ type Options = { }; ``` -Note: the `lite` ruleset overrides the default options to: +### Preset Overrides + +#### `lite` ```ts -const defaults = { - assumeTypes: true, +const liteOptions = { ignoreClass: "fieldsOnly", - ignoreImmediateMutation: true, } ``` diff --git a/docs/rules/no-class.md b/docs/rules/no-class.md index 81be12635..ac425721a 100644 --- a/docs/rules/no-class.md +++ b/docs/rules/no-class.md @@ -4,7 +4,7 @@ Disallow use of the `class` keyword. ## Rule Details -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -27,7 +27,7 @@ const dogA = new Dog("Jasper", 2); console.log(`${dogA.name} is ${dogA.ageInDogYears} in dog years.`); ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/no-class: "error" */ diff --git a/docs/rules/no-conditional-statement.md b/docs/rules/no-conditional-statement.md index 6af26973c..85e6f2917 100644 --- a/docs/rules/no-conditional-statement.md +++ b/docs/rules/no-conditional-statement.md @@ -9,7 +9,7 @@ Instead consider using the [ternary operator](https://developer.mozilla.org/en-U For more background see this [blog post](https://hackernoon.com/rethinking-javascript-the-if-statement-b158a61cd6cb) and discussion in [tslint-immutable #54](https://github.com/jonaskello/tslint-immutable/issues/54). -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -24,7 +24,7 @@ if (i === 1) { } ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/no-conditional-statement: "error" */ @@ -56,7 +56,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { @@ -64,6 +64,24 @@ const defaults = { } ``` +### Preset Overrides + +#### `recommended` + +```ts +const recommendedOptions = { + allowReturningBranches: true, +} +``` + +#### `lite` + +```ts +const liteOptions = { + allowReturningBranches: true, +} +``` + ### `allowReturningBranches` #### `true` diff --git a/docs/rules/no-expression-statement.md b/docs/rules/no-expression-statement.md index 7773e9105..e6b1a1682 100644 --- a/docs/rules/no-expression-statement.md +++ b/docs/rules/no-expression-statement.md @@ -6,7 +6,7 @@ This rule checks that the value of an expression is assigned to a variable and t When you call a function and don’t use it’s return value, chances are high that it is being called for its side effect. e.g. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -32,7 +32,7 @@ array.push(3); foo(bar); ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/no-expression-statement: "error" */ @@ -59,7 +59,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { diff --git a/docs/rules/no-let.md b/docs/rules/no-let.md index c13aff50b..1141b6a44 100644 --- a/docs/rules/no-let.md +++ b/docs/rules/no-let.md @@ -6,7 +6,7 @@ This rule should be combined with ESLint's built-in `no-var` rule to enforce tha In functional programming variables should not be mutable; use `const` instead. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -25,7 +25,7 @@ for (let i = 0; i < array.length; i++) { } ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/no-let: "error" */ @@ -58,7 +58,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { @@ -67,11 +67,29 @@ const defaults = { } ``` +### Preset Overrides + +#### `recommended` + +```ts +const recommendedOptions = { + allowInForLoopInit: true, +} +``` + +#### `lite` + +```ts +const liteOptions = { + allowInForLoopInit: true, +} +``` + ### `allowInForLoopInit` If set, `let`s inside of for a loop initializer are allowed. This does not include for...of or for...in loops as they should use `const` instead. -Examples of **correct** code for this rule: +#### ✅ Correct @@ -82,7 +100,7 @@ for (let i = 0; i < array.length; i++) { } ``` -Examples of **incorrect** code for this rule: +#### ❌ Incorrect diff --git a/docs/rules/no-loop-statement.md b/docs/rules/no-loop-statement.md index 309b25eeb..0ea908528 100644 --- a/docs/rules/no-loop-statement.md +++ b/docs/rules/no-loop-statement.md @@ -9,7 +9,7 @@ Loops in JavaScript are statements so they are not a good fit for a functional p Instead consider using `map`, `reduce` or similar. For more background see this [blog post](https://hackernoon.com/rethinking-javascript-death-of-the-for-loop-c431564c84a8) and discussion in [tslint-immutable #54](https://github.com/jonaskello/tslint-immutable/issues/54). -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -35,7 +35,7 @@ for (const number of numbers) { } ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/no-loop-statement: "error" */ diff --git a/docs/rules/no-method-signature.md b/docs/rules/no-method-signature.md index be9f1d3eb..abfd170e9 100644 --- a/docs/rules/no-method-signature.md +++ b/docs/rules/no-method-signature.md @@ -9,7 +9,7 @@ Because of this any `MethodSignature` will be mutable unless wrapped in the `Rea It should be noted however that the `PropertySignature` form for declaring functions does not support overloading. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -21,7 +21,7 @@ type Foo = { }; ``` -Examples of **correct** code for this rule: +### ✅ Correct @@ -47,7 +47,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { @@ -59,7 +59,7 @@ const defaults = { If set to `false`, this option allows for the use of method signatures if they are wrapped in the `Readonly` type. -Examples of **incorrect** code for this rule: +#### ❌ Incorrect diff --git a/docs/rules/no-mixed-type.md b/docs/rules/no-mixed-type.md index 88ef13012..f3cec6c8c 100644 --- a/docs/rules/no-mixed-type.md +++ b/docs/rules/no-mixed-type.md @@ -6,7 +6,7 @@ This rule enforces that an aliased type literal or an interface only has one typ Mixing functions and data properties in the same type is a sign of object-orientation style. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -19,7 +19,7 @@ type Foo = { }; ``` -Examples of **correct** code for this rule: +### ✅ Correct ```ts /* eslint functional/no-mixed-type: "error" */ @@ -55,7 +55,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { diff --git a/docs/rules/no-promise-reject.md b/docs/rules/no-promise-reject.md index 45641fc21..c827bc812 100644 --- a/docs/rules/no-promise-reject.md +++ b/docs/rules/no-promise-reject.md @@ -10,7 +10,7 @@ You can view a `Promise` as a result object with built-in error (something like You can also view a rejected promise as something similar to an exception and as such something that does not fit with functional programming. If your view is the latter you can use the `no-promise-reject` rule to disallow rejected promises. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -26,7 +26,7 @@ async function divide(x, y) { } ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/no-promise-reject: "error" */ diff --git a/docs/rules/no-return-void.md b/docs/rules/no-return-void.md index e80f4e0ad..5d5619f5e 100644 --- a/docs/rules/no-return-void.md +++ b/docs/rules/no-return-void.md @@ -11,7 +11,7 @@ By default, this rule allows function to return `undefined` and `null`. Note: For performance reasons, this rule does not check implicit return types. We recommend using the rule [@typescript-eslint/explicit-function-return-type](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/explicit-function-return-type.md) in conjunction with this rule. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -23,7 +23,7 @@ function updateText(): void { } ``` -Examples of **correct** code for this rule: +### ✅ Correct ```ts /* eslint functional/no-return-void: "error" */ @@ -45,7 +45,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { diff --git a/docs/rules/no-this-expression.md b/docs/rules/no-this-expression.md index 4c723d75e..a04aecd4b 100644 --- a/docs/rules/no-this-expression.md +++ b/docs/rules/no-this-expression.md @@ -1,9 +1,11 @@ # Disallow this access (no-this-expression) +## Rule Details + This rule is companion rule to the [no-class](./no-class.md) rule. See the its docs for more info. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -13,7 +15,7 @@ Examples of **incorrect** code for this rule: const foo = this.value + 17; ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/no-this-expression: "error" */ diff --git a/docs/rules/no-throw-statement.md b/docs/rules/no-throw-statement.md index efdb96cc6..cdd1ba076 100644 --- a/docs/rules/no-throw-statement.md +++ b/docs/rules/no-throw-statement.md @@ -7,7 +7,7 @@ This rule disallows the `throw` keyword. Exceptions are not part of functional programming. As an alternative a function should return an error or in the case of an async function, a rejected promise. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -17,7 +17,7 @@ Examples of **incorrect** code for this rule: throw new Error("Something went wrong."); ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/no-throw-statement: "error" */ @@ -49,7 +49,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { @@ -57,12 +57,30 @@ const defaults = { } ``` +### Preset Overrides + +#### `recommended` + +```ts +const recommendedOptions = { + allowInAsyncFunctions: true, +} +``` + +#### `lite` + +```ts +const liteOptions = { + allowInAsyncFunctions: true, +} +``` + ### `allowInAsyncFunctions` If true, throw statements will be allowed within async functions.\ This essentially allows throw statements to be used as return statements for errors. -Examples of **correct** code for this rule: +#### ✅ Correct ```js /* eslint functional/no-throw-statement: ["error", { "allowInAsyncFunctions": true }] */ diff --git a/docs/rules/no-try-statement.md b/docs/rules/no-try-statement.md index 321adb3e3..4fbd815d6 100644 --- a/docs/rules/no-try-statement.md +++ b/docs/rules/no-try-statement.md @@ -6,7 +6,7 @@ This rule disallows the `try` keyword. Try statements are not part of functional programming. See [no-throw-statement](./no-throw-statement.md) for more information. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -20,7 +20,7 @@ try { } ``` -Examples of **correct** code for this rule: +### ✅ Correct ```js /* eslint functional/no-try-statement: "error" */ @@ -42,7 +42,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { diff --git a/docs/rules/prefer-immutable-parameter-types.md b/docs/rules/prefer-immutable-parameter-types.md index 7f1e68892..1733aecb3 100644 --- a/docs/rules/prefer-immutable-parameter-types.md +++ b/docs/rules/prefer-immutable-parameter-types.md @@ -19,7 +19,7 @@ immutability enforcements to be made. This rule is designed to replace the aforementioned rule. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -65,7 +65,7 @@ interface Foo4 { } ``` -Examples of **correct** code for this rule: +### ✅ Correct @@ -147,14 +147,32 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { + enforcement: "Immutable", +} +``` + +### Preset Overrides + +#### `recommended` + +```ts +const recommendedOptions = { enforcement: "ReadonlyDeep", } ``` +#### `lite` + +```ts +const liteOptions = { + enforcement: "ReadonlyShallow", +} +``` + ### `enforcement` The level of immutability that should be enforced. diff --git a/docs/rules/prefer-readonly-type.md b/docs/rules/prefer-readonly-type.md index 89cb95db8..13562dae4 100644 --- a/docs/rules/prefer-readonly-type.md +++ b/docs/rules/prefer-readonly-type.md @@ -12,7 +12,7 @@ This rule enforces use of `readonly T[]` (`ReadonlyArray`) over `T[]` (`Array The readonly modifier must appear on property signatures in interfaces, property declarations in classes, and index signatures. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -27,7 +27,7 @@ const point: Point = { x: 23, y: 44 }; point.x = 99; // This is perfectly valid. ``` -Examples of **correct** code for this rule: +### ✅ Correct ```ts /* eslint functional/prefer-readonly-type: "error" */ @@ -109,7 +109,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { diff --git a/docs/rules/prefer-tacit.md b/docs/rules/prefer-tacit.md index cd6278906..87b2a9c3e 100644 --- a/docs/rules/prefer-tacit.md +++ b/docs/rules/prefer-tacit.md @@ -7,7 +7,7 @@ This rule enforces using functions directly if they can be without wrapping them Generally there's no reason to wrap a function with a callback wrapper if it's directly called anyway. Doing so creates extra inline lambdas that slow the runtime down. -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -21,7 +21,7 @@ function f(x) { const foo = x => f(x); ``` -Examples of **correct** code for this rule: +### ✅ Correct ```ts /* eslint functional/prefer-tacit: "error" */ @@ -48,7 +48,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { diff --git a/docs/rules/type-declaration-immutability.md b/docs/rules/type-declaration-immutability.md index d204685c9..95afea91d 100644 --- a/docs/rules/type-declaration-immutability.md +++ b/docs/rules/type-declaration-immutability.md @@ -12,7 +12,7 @@ immutability section](https://github.com/RebeccaStevens/is-immutable-type#immutability) of [is-immutable-type](https://www.npmjs.com/package/is-immutable-type). -Examples of **incorrect** code for this rule: +### ❌ Incorrect @@ -35,7 +35,7 @@ type MutableElement = Readonly<{ }>; ``` -Examples of **correct** code for this rule: +### ✅ Correct @@ -80,7 +80,7 @@ type Options = { } ``` -The default options: +### Default Options ```ts const defaults = { From 2c3622281192a7a8b76a576a20c4d9e28025bdc2 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 04:04:06 +1300 Subject: [PATCH 021/100] refactor: remove useless override --- src/configs/lite.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/configs/lite.ts b/src/configs/lite.ts index cba004f90..e34dd69d0 100644 --- a/src/configs/lite.ts +++ b/src/configs/lite.ts @@ -5,7 +5,6 @@ import * as functionalParameters from "~/rules/functional-parameters"; import * as immutableData from "~/rules/immutable-data"; import * as noConditionalStatement from "~/rules/no-conditional-statement"; import * as noExpressionStatement from "~/rules/no-expression-statement"; -import * as noReturnVoid from "~/rules/no-return-void"; import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; import recommended from "./recommended"; @@ -24,7 +23,6 @@ const overrides: Linter.Config = { ], [`functional/${noConditionalStatement.name}`]: "off", [`functional/${noExpressionStatement.name}`]: "off", - [`functional/${noReturnVoid.name}`]: "error", [`functional/${preferImmutableParameterTypes.name}`]: [ "error", { From d207496c377b51d57f50c59c3d35cf438575ff17 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 04:15:18 +1300 Subject: [PATCH 022/100] chore: update is-immutable-type to 1.0 --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7dfa1daa6..173e438d7 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@typescript-eslint/utils": "^5.10.2", "deepmerge-ts": "^4.0.3", "escape-string-regexp": "^4.0.0", - "is-immutable-type": "^0.0.7", + "is-immutable-type": "^1.0.0", "semver": "^7.3.7" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 48848e4d7..7363b2aa5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4031,7 +4031,7 @@ __metadata: eslint-plugin-unicorn: ^43.0.0 espree: ^9.3.0 husky: ^8.0.0 - is-immutable-type: ^0.0.7 + is-immutable-type: ^1.0.0 json-schema: ^0.4.0 jsonc-parser: ^3.0.0 lint-staged: ^13.0.0 @@ -5662,15 +5662,15 @@ __metadata: languageName: node linkType: hard -"is-immutable-type@npm:^0.0.7": - version: 0.0.7 - resolution: "is-immutable-type@npm:0.0.7" +"is-immutable-type@npm:^1.0.0": + version: 1.0.0 + resolution: "is-immutable-type@npm:1.0.0" peerDependencies: "@typescript-eslint/type-utils": ">=5.30.5" "@typescript-eslint/utils": ">=5.30.5" tsutils: ">=3.21.0" typescript: ">=4.7.4" - checksum: 52fde0fc27d9eee088e5c06d731c4bb5536ddbc192dac2dc10d4d0f397c434c76bdce7a7455213f9e3b4d493749569c34868623ff4eac4a53ab28d448a00daee + checksum: 926ebe7ea969d7b08723a01cf91aada6a48a18062fc218d9e84c825df1241bb0ee761f2d138121d64c6367b02b8067f57600d4ef5f79c4a7b2fbd0e3b84573fd languageName: node linkType: hard From f6296bddb336ddb3613ef4588e3e63eca42aae8f Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 20:34:01 +1300 Subject: [PATCH 023/100] fixup(type-declaration-immutability): make rule more strict by default, add overrides to rulesets --- docs/rules/type-declaration-immutability.md | 30 ++++++++--- src/configs/recommended.ts | 29 ++++++++++ src/rules/type-declaration-immutability.ts | 59 +++++++-------------- 3 files changed, 72 insertions(+), 46 deletions(-) diff --git a/docs/rules/type-declaration-immutability.md b/docs/rules/type-declaration-immutability.md index 95afea91d..d4908e0c1 100644 --- a/docs/rules/type-declaration-immutability.md +++ b/docs/rules/type-declaration-immutability.md @@ -71,7 +71,7 @@ This rule accepts an options object of the following type: ```ts type Options = { rules: Array<{ - identifier: string | string[]; + identifiers: string | string[]; immutability: "Mutable" | "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; comparator?: "Less" | "AtMost" | "Exactly" | "AtLeast" | "More"; }>; @@ -86,27 +86,43 @@ type Options = { const defaults = { rules: [ { - identifier: "I?Immutable.+", + identifiers: "^(?!I?Mutable).+", immutability: "Immutable", comparator: "AtLeast", }, + ], + ignoreInterfaces: false, +} +``` + +### Preset Overrides + +#### `recommended` and `lite` + +```ts +const recommendedOptions = { + rules: [ { - identifier: "I?ReadonlyDeep.+", + identifiers: "I?Immutable.+", + immutability: "Immutable", + comparator: "AtLeast", + }, + { + identifiers: "I?ReadonlyDeep.+", immutability: "ReadonlyDeep", comparator: "AtLeast", }, { - identifier: "I?Readonly.+", + identifiers: "I?Readonly.+", immutability: "ReadonlyShallow", comparator: "AtLeast", }, { - identifier: "I?Mutable.+", + identifiers: "I?Mutable.+", immutability: "Mutable", comparator: "AtMost", }, ], - ignoreInterfaces: false, } ``` @@ -117,7 +133,7 @@ An array of rules to enforce immutability by. These rules should be sorted by precedence as each type declaration will only enforce the first matching rule to it. -#### `identifier` +#### `identifiers` A regex pattern or an array of regex patterns that are used to match against the name of the type declarations. diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index aa342605a..7e4f8a705 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -1,11 +1,13 @@ import { deepmerge } from "deepmerge-ts"; import type { Linter } from "eslint"; +import { Immutability } from "is-immutable-type"; import * as noConditionalStatement from "~/rules/no-conditional-statement"; import * as noLet from "~/rules/no-let"; import * as noThrowStatement from "~/rules/no-throw-statement"; import * as noTryStatement from "~/rules/no-try-statement"; import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; +import { RuleEnforcementComparator } from "~/rules/type-declaration-immutability"; import strict from "./strict"; @@ -36,6 +38,33 @@ const overrides: Linter.Config = { enforcement: "ReadonlyDeep", }, ], + [`functional/${preferImmutableParameterTypes.name}`]: [ + "error", + { + rules: [ + { + identifiers: [/^I?Immutable.+/u], + immutability: Immutability.Immutable, + comparator: RuleEnforcementComparator.AtLeast, + }, + { + identifiers: [/^I?ReadonlyDeep.+/u], + immutability: Immutability.ReadonlyDeep, + comparator: RuleEnforcementComparator.AtLeast, + }, + { + identifiers: [/^I?Readonly.+/u], + immutability: Immutability.ReadonlyShallow, + comparator: RuleEnforcementComparator.AtLeast, + }, + { + identifiers: [/^I?Mutable.+/u], + immutability: Immutability.Mutable, + comparator: RuleEnforcementComparator.AtMost, + }, + ], + }, + ], }, }; diff --git a/src/rules/type-declaration-immutability.ts b/src/rules/type-declaration-immutability.ts index b3a5e5225..ad1e9bee2 100644 --- a/src/rules/type-declaration-immutability.ts +++ b/src/rules/type-declaration-immutability.ts @@ -37,8 +37,8 @@ export enum RuleEnforcementComparator { type Options = ReadonlyDeep< [ IgnorePatternOption & { - rules?: Array<{ - identifier: string | string[]; + rules: Array<{ + identifiers: (string | RegExp) | Array; immutability: Exclude< Immutability | keyof typeof Immutability, "Unknown" @@ -64,7 +64,7 @@ const schema: JSONSchema4 = [ items: { type: "object", properties: { - identifier: { + identifiers: { type: ["string", "array"], items: { type: "string", @@ -83,7 +83,7 @@ const schema: JSONSchema4 = [ enum: Object.values(RuleEnforcementComparator), }, }, - required: ["identifier", "immutability"], + required: ["identifiers", "immutability"], additionalProperties: false, }, }, @@ -100,6 +100,13 @@ const schema: JSONSchema4 = [ */ const defaultOptions: Options = [ { + rules: [ + { + identifiers: [/^(?!I?Mutable).+/u], + immutability: Immutability.Immutable, + comparator: RuleEnforcementComparator.AtLeast, + }, + ], ignoreInterfaces: false, }, ]; @@ -140,34 +147,6 @@ export type ImmutabilityRule = { comparator: RuleEnforcementComparator; }; -/** - * Get the default immutability rules. - */ -function getDefaultImmutabilityRules(): ImmutabilityRule[] { - return [ - { - identifiers: [/^I?Immutable.+/u], - immutability: Immutability.Immutable, - comparator: RuleEnforcementComparator.AtLeast, - }, - { - identifiers: [/^I?ReadonlyDeep.+/u], - immutability: Immutability.ReadonlyDeep, - comparator: RuleEnforcementComparator.AtLeast, - }, - { - identifiers: [/^I?Readonly.+/u], - immutability: Immutability.ReadonlyShallow, - comparator: RuleEnforcementComparator.AtLeast, - }, - { - identifiers: [/^I?Mutable.+/u], - immutability: Immutability.Mutable, - comparator: RuleEnforcementComparator.AtMost, - }, - ]; -} - /** * Get all the rules that were given and upgrade them. */ @@ -175,14 +154,16 @@ function getRules(options: Options): ImmutabilityRule[] { const [optionsObject] = options; const { rules: rulesOptions } = optionsObject; - if (rulesOptions === undefined) { - return getDefaultImmutabilityRules(); - } - return rulesOptions.map((rule): ImmutabilityRule => { - const identifiers = isReadonlyArray(rule.identifier) - ? rule.identifier.map((id) => new RegExp(id, "u")) - : [new RegExp(rule.identifier, "u")]; + const identifiers = isReadonlyArray(rule.identifiers) + ? rule.identifiers.map((id) => + id instanceof RegExp ? id : new RegExp(id, "u") + ) + : [ + rule.identifiers instanceof RegExp + ? rule.identifiers + : new RegExp(rule.identifiers, "u"), + ]; const immutability = typeof rule.immutability === "string" From 86f354badf6dee2359211e1e67b0caf8c1da5430 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 20:59:17 +1300 Subject: [PATCH 024/100] feat(prefer-property-signatures)!: rename `ignoreIfReadonly` to `ignoreIfReadonlyWrapped` BREAKING CHANGE: rename `ignoreIfReadonly` to `ignoreIfReadonlyWrapped` and set it to `false` by default --- ...ature.md => prefer-property-signatures.md} | 24 +++++++++---------- src/rules/prefer-property-signatures.ts | 12 +++++----- 2 files changed, 17 insertions(+), 19 deletions(-) rename docs/rules/{no-method-signature.md => prefer-property-signatures.md} (71%) diff --git a/docs/rules/no-method-signature.md b/docs/rules/prefer-property-signatures.md similarity index 71% rename from docs/rules/no-method-signature.md rename to docs/rules/prefer-property-signatures.md index abfd170e9..fd35abcdd 100644 --- a/docs/rules/no-method-signature.md +++ b/docs/rules/prefer-property-signatures.md @@ -7,7 +7,7 @@ There are two ways function members can be declared in interfaces and type alias The `MethodSignature` and the `PropertySignature` forms seem equivalent, but only the `PropertySignature` form can have a `readonly` modifier. Because of this any `MethodSignature` will be mutable unless wrapped in the `Readonly` type. -It should be noted however that the `PropertySignature` form for declaring functions does not support overloading. +It should be noted however that the `PropertySignature` do not support overloading. ### ❌ Incorrect @@ -29,12 +29,12 @@ type Foo = { /* eslint functional/prefer-property-signatures: "error" */ type Foo = { - readonly bar: () => string; + bar: () => string; }; -type Foo = Readonly<{ - bar(): string; -}>; +type Foo = { + readonly bar: () => string; +}; ``` ## Options @@ -43,7 +43,7 @@ This rule accepts an options object of the following type: ```ts type Options = { - ignoreIfReadonly: boolean; + ignoreIfReadonlyWrapped: boolean; } ``` @@ -51,20 +51,18 @@ type Options = { ```ts const defaults = { - ignoreIfReadonly: true + ignoreIfReadonlyWrapped: false } ``` -### `ignoreIfReadonly` +### `ignoreIfReadonlyWrapped` -If set to `false`, this option allows for the use of method signatures if they are wrapped in the `Readonly` type. +If set to `true`, method signatures wrapped in the `Readonly` type will not be flagged as violations. -#### ❌ Incorrect - - +#### ✅ Correct ```ts -/* eslint functional/prefer-property-signatures: ["error", { "ignoreIfReadonly": false } ] */ +/* eslint functional/prefer-property-signatures: ["error", { "ignoreIfReadonlyWrapped": true } ] */ type Foo = Readonly<{ bar(): string; diff --git a/src/rules/prefer-property-signatures.ts b/src/rules/prefer-property-signatures.ts index e7335f094..4b7a50a36 100644 --- a/src/rules/prefer-property-signatures.ts +++ b/src/rules/prefer-property-signatures.ts @@ -16,7 +16,7 @@ export const name = "prefer-property-signatures" as const; */ type Options = readonly [ Readonly<{ - ignoreIfReadonly: boolean; + ignoreIfReadonlyWrapped: boolean; }> ]; @@ -27,9 +27,9 @@ const schema: JSONSchema4 = [ { type: "object", properties: { - ignoreIfReadonly: { + ignoreIfReadonlyWrapped: { type: "boolean", - default: true, + default: false, }, }, additionalProperties: false, @@ -41,7 +41,7 @@ const schema: JSONSchema4 = [ */ const defaultOptions: Options = [ { - ignoreIfReadonly: true, + ignoreIfReadonlyWrapped: false, }, ]; @@ -75,9 +75,9 @@ function checkTSMethodSignature( >, options: Options ): RuleResult { - const [{ ignoreIfReadonly }] = options; + const [{ ignoreIfReadonlyWrapped }] = options; - if (ignoreIfReadonly && inReadonly(node)) { + if (ignoreIfReadonlyWrapped && inReadonly(node)) { return { context, descriptors: [] }; } From 46a822835f54c392f443698d0b7eaaee2042b52c Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 21:03:21 +1300 Subject: [PATCH 025/100] docs: tweak common options docs --- docs/rules/options/allow-local-mutation.md | 10 ++++++---- docs/rules/options/ignore-pattern.md | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/rules/options/allow-local-mutation.md b/docs/rules/options/allow-local-mutation.md index 27b9a7028..bc0fa2ca9 100644 --- a/docs/rules/options/allow-local-mutation.md +++ b/docs/rules/options/allow-local-mutation.md @@ -1,5 +1,11 @@ # Using the `allowLocalMutation` option +If this option is set to true, local state is allowed to be mutated. Local state is simply any code inside of a function. + +Note: That using this option can lead to more imperative code in functions so use with care! + +## Details + > If a tree falls in the woods, does it make a sound? > If a pure function mutates some local data in order to produce an immutable return value, is that ok? @@ -8,7 +14,3 @@ In general, it is more important to enforce immutability for state that is passe For example in Redux, the state going in and out of reducers needs to be immutable while the reducer may be allowed to mutate local state in its calculations in order to achieve higher performance. This is what the `allowLocalMutation` option enables. With this option enabled immutability will be enforced everywhere but in local state. Function parameters and return types are not considered local state so they will still be checked. - -If this option is set to true, local state is allowed to be mutated. Local state is simply any code inside of a function. - -Note: That using this option can lead to more imperative code in functions so use with care! diff --git a/docs/rules/options/ignore-pattern.md b/docs/rules/options/ignore-pattern.md index e2ecb8c02..64916a5ca 100644 --- a/docs/rules/options/ignore-pattern.md +++ b/docs/rules/options/ignore-pattern.md @@ -1,6 +1,9 @@ # Using the `ignorePattern` option This option takes a RegExp string or an array of RegExp strings. +It allows for the ability to ignore violations based on the identifier (name) of node in question. + +## Details Some languages are immutable by default but allows you to explicitly declare mutable variables. For example in [reason](https://facebook.github.io/reason/) you can declare mutable record fields like this: From c0a8699260c884924c2b1a31a304cfb138dd0da5 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 21:26:40 +1300 Subject: [PATCH 026/100] fixup: tests --- src/rules/type-declaration-immutability.ts | 4 +- .../prefer-property-signatures/ts/invalid.ts | 8 +-- .../prefer-property-signatures/ts/valid.ts | 12 ++-- .../ts/invalid.ts | 59 ++++++++++----- .../type-declaration-immutability/ts/valid.ts | 71 +++++++++++++------ 5 files changed, 102 insertions(+), 52 deletions(-) diff --git a/src/rules/type-declaration-immutability.ts b/src/rules/type-declaration-immutability.ts index ad1e9bee2..03d826a3e 100644 --- a/src/rules/type-declaration-immutability.ts +++ b/src/rules/type-declaration-immutability.ts @@ -65,9 +65,9 @@ const schema: JSONSchema4 = [ type: "object", properties: { identifiers: { - type: ["string", "array"], + type: ["string", "object", "array"], items: { - type: "string", + type: ["string", "object"], }, }, immutability: { diff --git a/tests/rules/prefer-property-signatures/ts/invalid.ts b/tests/rules/prefer-property-signatures/ts/invalid.ts index 5339a213b..eb2bb4fe2 100644 --- a/tests/rules/prefer-property-signatures/ts/invalid.ts +++ b/tests/rules/prefer-property-signatures/ts/invalid.ts @@ -41,7 +41,7 @@ const tests: ReadonlyArray = [ methodSignature(): void } `, - optionsSet: [[]], + optionsSet: [[{ ignoreIfReadonlyWrapped: true }]], errors: [ { messageId: "generic", @@ -59,7 +59,7 @@ const tests: ReadonlyArray = [ } }> `, - optionsSet: [[]], + optionsSet: [[{ ignoreIfReadonlyWrapped: true }]], errors: [ { messageId: "generic", @@ -79,7 +79,7 @@ const tests: ReadonlyArray = [ }> }>{} `, - optionsSet: [[]], + optionsSet: [[{ ignoreIfReadonlyWrapped: true }]], errors: [ { messageId: "generic", @@ -99,7 +99,7 @@ const tests: ReadonlyArray = [ } }>{} `, - optionsSet: [[{ ignoreIfReadonly: false }]], + optionsSet: [[]], errors: [ { messageId: "generic", diff --git a/tests/rules/prefer-property-signatures/ts/valid.ts b/tests/rules/prefer-property-signatures/ts/valid.ts index 23362945e..7338d38ac 100644 --- a/tests/rules/prefer-property-signatures/ts/valid.ts +++ b/tests/rules/prefer-property-signatures/ts/valid.ts @@ -25,7 +25,7 @@ const tests: ReadonlyArray = [ methodSignature(): void }>{} `, - optionsSet: [[]], + optionsSet: [[{ ignoreIfReadonlyWrapped: true }]], }, { code: dedent` @@ -33,7 +33,7 @@ const tests: ReadonlyArray = [ methodSignature(): void }>{} `, - optionsSet: [[]], + optionsSet: [[{ ignoreIfReadonlyWrapped: true }]], }, { code: dedent` @@ -41,7 +41,7 @@ const tests: ReadonlyArray = [ methodSignature(): void }> `, - optionsSet: [[]], + optionsSet: [[{ ignoreIfReadonlyWrapped: true }]], }, { code: dedent` @@ -49,7 +49,7 @@ const tests: ReadonlyArray = [ methodSignature(): void }> `, - optionsSet: [[]], + optionsSet: [[{ ignoreIfReadonlyWrapped: true }]], }, { code: dedent` @@ -59,7 +59,7 @@ const tests: ReadonlyArray = [ }> }> `, - optionsSet: [[]], + optionsSet: [[{ ignoreIfReadonlyWrapped: true }]], }, { code: dedent` @@ -71,7 +71,7 @@ const tests: ReadonlyArray = [ } }>{} `, - optionsSet: [[]], + optionsSet: [[{ ignoreIfReadonlyWrapped: true }]], }, ]; diff --git a/tests/rules/type-declaration-immutability/ts/invalid.ts b/tests/rules/type-declaration-immutability/ts/invalid.ts index 06592a4e4..d32fab8d0 100644 --- a/tests/rules/type-declaration-immutability/ts/invalid.ts +++ b/tests/rules/type-declaration-immutability/ts/invalid.ts @@ -2,10 +2,35 @@ import { Immutability } from "is-immutable-type"; import type { InvalidTestCase } from "~/tests/helpers/util"; +const recommended = { + rules: [ + { + identifiers: [/^I?Immutable.+/u], + immutability: Immutability.Immutable, + comparator: "AtLeast", + }, + { + identifiers: [/^I?ReadonlyDeep.+/u], + immutability: Immutability.ReadonlyDeep, + comparator: "AtLeast", + }, + { + identifiers: [/^I?Readonly.+/u], + immutability: Immutability.ReadonlyShallow, + comparator: "AtLeast", + }, + { + identifiers: [/^I?Mutable.+/u], + immutability: Immutability.Mutable, + comparator: "AtMost", + }, + ], +}; + const tests: ReadonlyArray = [ { code: "type ReadonlyFoo = { foo: number }", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -21,7 +46,7 @@ const tests: ReadonlyArray = [ }, { code: "type ReadonlyFoo = { readonly foo: number; bar: { baz: string; }; }", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -37,7 +62,7 @@ const tests: ReadonlyArray = [ }, { code: "type ReadonlySet = Set;", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -53,7 +78,7 @@ const tests: ReadonlyArray = [ }, { code: "type ReadonlyMap = Map;", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -69,7 +94,7 @@ const tests: ReadonlyArray = [ }, { code: "type ReadonlyDeepFoo = { readonly foo: number; readonly bar: { baz: string; }; }", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -85,7 +110,7 @@ const tests: ReadonlyArray = [ }, { code: "type ReadonlyDeepSet = ReadonlySet<{ foo: string; }>;", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -101,7 +126,7 @@ const tests: ReadonlyArray = [ }, { code: "type ReadonlyDeepMap = ReadonlyMap;", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -117,7 +142,7 @@ const tests: ReadonlyArray = [ }, { code: "type ImmutableFoo = { readonly foo: number; readonly bar: { baz: string; }; }", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -133,7 +158,7 @@ const tests: ReadonlyArray = [ }, { code: "type ImmutableSet = ReadonlySet<{ readonly foo: string; }>;", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -149,7 +174,7 @@ const tests: ReadonlyArray = [ }, { code: "type ImmutableMap = ReadonlyMap;", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtLeast", @@ -165,7 +190,7 @@ const tests: ReadonlyArray = [ }, { code: "type MutableString = string", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtMost", @@ -181,7 +206,7 @@ const tests: ReadonlyArray = [ }, { code: "type MutableFoo = { readonly foo: number }", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtMost", @@ -197,7 +222,7 @@ const tests: ReadonlyArray = [ }, { code: "type MutableFoo = { readonly foo: number; readonly bar: { baz: string; }; }", - optionsSet: [[]], + optionsSet: [[recommended]], errors: [ { messageId: "AtMost", @@ -218,7 +243,7 @@ const tests: ReadonlyArray = [ { rules: [ { - identifier: "Foo", + identifiers: "Foo", immutability: "ReadonlyDeep", }, ], @@ -245,7 +270,7 @@ const tests: ReadonlyArray = [ { rules: [ { - identifier: "^I?Readonly.+", + identifiers: "^I?Readonly.+", immutability: "ReadonlyDeep", }, ], @@ -267,7 +292,7 @@ const tests: ReadonlyArray = [ }, { code: "type MutableSet = Set;", - optionsSet: [[]], + optionsSet: [[recommended]], settingsSet: [ { immutability: { @@ -292,7 +317,7 @@ const tests: ReadonlyArray = [ }, { code: "type MutableSet = Set;", - optionsSet: [[]], + optionsSet: [[recommended]], settingsSet: [ { immutability: { diff --git a/tests/rules/type-declaration-immutability/ts/valid.ts b/tests/rules/type-declaration-immutability/ts/valid.ts index b6d474eec..b00913129 100644 --- a/tests/rules/type-declaration-immutability/ts/valid.ts +++ b/tests/rules/type-declaration-immutability/ts/valid.ts @@ -3,86 +3,111 @@ import { Immutability } from "is-immutable-type"; import type { ValidTestCase } from "~/tests/helpers/util"; +const recommended = { + rules: [ + { + identifiers: [/^I?Immutable.+/u], + immutability: Immutability.Immutable, + comparator: "AtLeast", + }, + { + identifiers: [/^I?ReadonlyDeep.+/u], + immutability: Immutability.ReadonlyDeep, + comparator: "AtLeast", + }, + { + identifiers: [/^I?Readonly.+/u], + immutability: Immutability.ReadonlyShallow, + comparator: "AtLeast", + }, + { + identifiers: [/^I?Mutable.+/u], + immutability: Immutability.Mutable, + comparator: "AtMost", + }, + ], +}; + const tests: ReadonlyArray = [ { code: "type ReadonlyString = string;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyFoo = { readonly foo: number };", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyFoo = Readonly<{ foo: number }>;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyFoo = { readonly foo: number; readonly bar: { baz: string; }; };", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlySet = ReadonlySet;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyMap = ReadonlyMap;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyDeepString = string;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyDeepFoo = { readonly foo: number; readonly bar: { readonly baz: string; }; };", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyDeepSet = ReadonlySet;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyDeepMap = ReadonlyMap;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyDeepSet = ReadonlySet<{ readonly foo: string; }>;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyDeepMap = ReadonlyMap;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ImmutableString = string;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ImmutableFoo = { readonly foo: number; readonly bar: { readonly baz: string; }; };", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ImmutableSet = Readonly>;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ImmutableMap = Readonly>;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type MutableFoo = { foo: number };", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type MutableFoo = { readonly foo: number; bar: { readonly baz: string; }; };", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type MutableSet = Set<{ readonly foo: string; }>;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type MutableMap = Map;", - optionsSet: [[]], + optionsSet: [[recommended]], }, { code: "type ReadonlyFoo = { foo: number };", @@ -116,7 +141,7 @@ const tests: ReadonlyArray = [ { rules: [ { - identifier: "Foo", + identifiers: "Foo", immutability: "ReadonlyDeep", }, ], @@ -131,7 +156,7 @@ const tests: ReadonlyArray = [ { rules: [ { - identifier: "^I?Readonly.+", + identifiers: "^I?Readonly.+", immutability: "ReadonlyDeep", }, ], @@ -143,7 +168,7 @@ const tests: ReadonlyArray = [ code: dedent` type ReadonlyDeepFoo = ReadonlyDeep<{ foo: { bar: string; }; }>; `, - optionsSet: [[]], + optionsSet: [[recommended]], settingsSet: [ { immutability: { From 044e54ba646a3f6001a16c3e7b19299efaf49eab Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 22:07:42 +1300 Subject: [PATCH 027/100] feat(functional-parameters): add option to ignore lambda function expressions --- docs/rules/functional-parameters.md | 17 +++++++++++++++++ src/configs/recommended.ts | 7 +++++++ src/rules/functional-parameters.ts | 16 +++++++++++++--- src/util/tree.ts | 12 ++++++++++++ .../rules/functional-parameters/es3/invalid.ts | 15 +++++++++++++++ tests/rules/functional-parameters/es3/valid.ts | 7 +++++++ .../rules/functional-parameters/es6/invalid.ts | 15 +++++++++++++++ tests/rules/functional-parameters/es6/valid.ts | 7 +++++++ 8 files changed, 93 insertions(+), 3 deletions(-) diff --git a/docs/rules/functional-parameters.md b/docs/rules/functional-parameters.md index ca35012a9..fb7744011 100644 --- a/docs/rules/functional-parameters.md +++ b/docs/rules/functional-parameters.md @@ -53,6 +53,7 @@ type Options = { allowArgumentsKeyword: boolean; enforceParameterCount: "atLeastOne" | "exactlyOne" | false | { count: "atLeastOne" | "exactlyOne"; + ignoreLambdaExpression: boolean, ignoreIIFE: boolean; }; ignorePattern?: string[] | string; @@ -68,6 +69,7 @@ const defaults = { allowArgumentsKeyword: false, enforceParameterCount: { count: "atLeastOne", + ignoreLambdaExpression: false, ignoreIIFE: true } } @@ -75,6 +77,16 @@ const defaults = { ### Preset Overrides +#### `recommended` + +```ts +const recommendedOptions = { + enforceParameterCount: { + ignoreLambdaExpression: true, + }, +} +``` + #### `lite` ```ts @@ -133,6 +145,11 @@ See [Currying](https://en.wikipedia.org/wiki/Currying) and [Higher-order functio See [enforceParameterCount](#enforceparametercount). +### `enforceParameterCount.ignoreLambdaExpression` + +If true, this option allows for the use of lambda function expressions that do not have any parameters. +Here, a lambda function expression refers to any function being defined in place as passed directly as an argument to another function. + #### `enforceParameterCount.ignoreIIFE` If true, this option allows for the use of [IIFEs](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) that do not have any parameters. diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 7e4f8a705..1fb572a91 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -2,6 +2,7 @@ import { deepmerge } from "deepmerge-ts"; import type { Linter } from "eslint"; import { Immutability } from "is-immutable-type"; +import * as functionalParameters from "~/rules/functional-parameters"; import * as noConditionalStatement from "~/rules/no-conditional-statement"; import * as noLet from "~/rules/no-let"; import * as noThrowStatement from "~/rules/no-throw-statement"; @@ -13,6 +14,12 @@ import strict from "./strict"; const overrides: Linter.Config = { rules: { + [`functional/${functionalParameters.name}`]: [ + "error", + { + enforceParameterCount: { ignoreLambdaExpression: true }, + }, + ], [`functional/${noConditionalStatement.name}`]: [ "error", { diff --git a/src/rules/functional-parameters.ts b/src/rules/functional-parameters.ts index 6a9f5bf3d..3e57de1e5 100644 --- a/src/rules/functional-parameters.ts +++ b/src/rules/functional-parameters.ts @@ -15,7 +15,12 @@ import { import type { ESFunction } from "~/src/util/node-types"; import type { RuleResult } from "~/util/rule"; import { createRuleUsingFunction } from "~/util/rule"; -import { isIIFE, isPropertyAccess, isPropertyName } from "~/util/tree"; +import { + isArgument, + isIIFE, + isPropertyAccess, + isPropertyName, +} from "~/util/tree"; import { isRestElement } from "~/util/typeguard"; /** @@ -42,6 +47,7 @@ type Options = readonly [ | false | Readonly<{ count: ParameterCountOptions; + ignoreLambdaExpression: boolean; ignoreIIFE: boolean; }>; }> @@ -80,6 +86,9 @@ const schema: JSONSchema4 = [ type: "string", enum: ["atLeastOne", "exactlyOne"], }, + ignoreLambdaExpression: { + type: "boolean", + }, ignoreIIFE: { type: "boolean", }, @@ -103,6 +112,7 @@ const defaultOptions: Options = [ allowArgumentsKeyword: false, enforceParameterCount: { count: "atLeastOne", + ignoreLambdaExpression: false, ignoreIIFE: true, }, }, @@ -163,8 +173,8 @@ function getParamCountViolations( enforceParameterCount === false || (node.params.length === 0 && typeof enforceParameterCount === "object" && - enforceParameterCount.ignoreIIFE && - isIIFE(node)) + ((enforceParameterCount.ignoreIIFE && isIIFE(node)) || + (enforceParameterCount.ignoreLambdaExpression && isArgument(node)))) ) { return []; } diff --git a/src/util/tree.ts b/src/util/tree.ts index 957ae18dd..5c211193e 100644 --- a/src/util/tree.ts +++ b/src/util/tree.ts @@ -182,6 +182,18 @@ export function isIIFE(node: ReadonlyDeep): boolean { ); } +/** + * Is the given node being passed as an argument? + */ +export function isArgument(node: ReadonlyDeep): boolean { + return ( + node.parent !== undefined && + isCallExpression(node.parent) && + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + node.parent.arguments.includes(node as any) + ); +} + /** * Get the key the given node is assigned to in its parent ObjectExpression. */ diff --git a/tests/rules/functional-parameters/es3/invalid.ts b/tests/rules/functional-parameters/es3/invalid.ts index 7e6701b1a..0bdb269bc 100644 --- a/tests/rules/functional-parameters/es3/invalid.ts +++ b/tests/rules/functional-parameters/es3/invalid.ts @@ -130,6 +130,21 @@ const tests: ReadonlyArray = [ }, ], }, + { + code: dedent` + function foo(param) {} + foo(function () {}); + `, + optionsSet: [[]], + errors: [ + { + messageId: "paramCountAtLeastOne", + type: "FunctionExpression", + line: 2, + column: 5, + }, + ], + }, ]; export default tests; diff --git a/tests/rules/functional-parameters/es3/valid.ts b/tests/rules/functional-parameters/es3/valid.ts index f89b8fca8..c0d6ca7fb 100644 --- a/tests/rules/functional-parameters/es3/valid.ts +++ b/tests/rules/functional-parameters/es3/valid.ts @@ -105,6 +105,13 @@ const tests: ReadonlyArray = [ ], ], }, + { + code: dedent` + function foo(param) {} + foo(function () {}); + `, + optionsSet: [[{ enforceParameterCount: { ignoreLambdaExpression: true } }]], + }, ]; export default tests; diff --git a/tests/rules/functional-parameters/es6/invalid.ts b/tests/rules/functional-parameters/es6/invalid.ts index fa7066f78..dd42bb654 100644 --- a/tests/rules/functional-parameters/es6/invalid.ts +++ b/tests/rules/functional-parameters/es6/invalid.ts @@ -56,6 +56,21 @@ const tests: ReadonlyArray = [ }, ], }, + { + code: dedent` + function foo(param) {} + foo(() => 1); + `, + optionsSet: [[]], + errors: [ + { + messageId: "paramCountAtLeastOne", + type: "ArrowFunctionExpression", + line: 2, + column: 5, + }, + ], + }, ]; export default tests; diff --git a/tests/rules/functional-parameters/es6/valid.ts b/tests/rules/functional-parameters/es6/valid.ts index a2f192d2f..62651725c 100644 --- a/tests/rules/functional-parameters/es6/valid.ts +++ b/tests/rules/functional-parameters/es6/valid.ts @@ -91,6 +91,13 @@ const tests: ReadonlyArray = [ ], ], }, + { + code: dedent` + function foo(param) {} + foo(() => 1); + `, + optionsSet: [[{ enforceParameterCount: { ignoreLambdaExpression: true } }]], + }, ]; export default tests; From 81178c22bec4d85e7aa14973895d745287abcb5c Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 22:23:32 +1300 Subject: [PATCH 028/100] docs: twaek rule docs --- docs/rules/no-conditional-statement.md | 12 ++---------- docs/rules/no-let.md | 12 ++---------- docs/rules/no-throw-statement.md | 12 ++---------- docs/rules/type-declaration-immutability.md | 2 +- 4 files changed, 7 insertions(+), 31 deletions(-) diff --git a/docs/rules/no-conditional-statement.md b/docs/rules/no-conditional-statement.md index 85e6f2917..4dd5b9708 100644 --- a/docs/rules/no-conditional-statement.md +++ b/docs/rules/no-conditional-statement.md @@ -66,18 +66,10 @@ const defaults = { ### Preset Overrides -#### `recommended` +#### `recommended` and `lite`\*\*\*\* ```ts -const recommendedOptions = { - allowReturningBranches: true, -} -``` - -#### `lite` - -```ts -const liteOptions = { +const recommendedAndLiteOptions = { allowReturningBranches: true, } ``` diff --git a/docs/rules/no-let.md b/docs/rules/no-let.md index 1141b6a44..74b9935f1 100644 --- a/docs/rules/no-let.md +++ b/docs/rules/no-let.md @@ -69,18 +69,10 @@ const defaults = { ### Preset Overrides -#### `recommended` +#### `recommended` and `lite` ```ts -const recommendedOptions = { - allowInForLoopInit: true, -} -``` - -#### `lite` - -```ts -const liteOptions = { +const recommendedAndLiteOptions = { allowInForLoopInit: true, } ``` diff --git a/docs/rules/no-throw-statement.md b/docs/rules/no-throw-statement.md index cdd1ba076..910bf2a40 100644 --- a/docs/rules/no-throw-statement.md +++ b/docs/rules/no-throw-statement.md @@ -59,18 +59,10 @@ const defaults = { ### Preset Overrides -#### `recommended` +#### `recommended` and `lite` ```ts -const recommendedOptions = { - allowInAsyncFunctions: true, -} -``` - -#### `lite` - -```ts -const liteOptions = { +const recommendedAndLiteOptions = { allowInAsyncFunctions: true, } ``` diff --git a/docs/rules/type-declaration-immutability.md b/docs/rules/type-declaration-immutability.md index d4908e0c1..23c995409 100644 --- a/docs/rules/type-declaration-immutability.md +++ b/docs/rules/type-declaration-immutability.md @@ -100,7 +100,7 @@ const defaults = { #### `recommended` and `lite` ```ts -const recommendedOptions = { +const recommendedAndLiteOptions = { rules: [ { identifiers: "I?Immutable.+", From dc99b1ab1929178b7410433e30c4f3c08747b414 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Sun, 25 Sep 2022 22:39:17 +1300 Subject: [PATCH 029/100] fixup: functional-parameters recommended --- docs/rules/functional-parameters.md | 1 + src/configs/recommended.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/rules/functional-parameters.md b/docs/rules/functional-parameters.md index fb7744011..b963b4ab5 100644 --- a/docs/rules/functional-parameters.md +++ b/docs/rules/functional-parameters.md @@ -83,6 +83,7 @@ const defaults = { const recommendedOptions = { enforceParameterCount: { ignoreLambdaExpression: true, + ignoreIIFE: true, }, } ``` diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 1fb572a91..51123416b 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -17,7 +17,10 @@ const overrides: Linter.Config = { [`functional/${functionalParameters.name}`]: [ "error", { - enforceParameterCount: { ignoreLambdaExpression: true }, + enforceParameterCount: { + ignoreLambdaExpression: true, + ignoreIIFE: true, + }, }, ], [`functional/${noConditionalStatement.name}`]: [ From 91a942494331445e478b917090044cc5259253d3 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 29 Sep 2022 16:02:11 +1300 Subject: [PATCH 030/100] fixup: type-declaration-immutability recommended --- src/configs/recommended.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 51123416b..a37a81c40 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -8,6 +8,7 @@ import * as noLet from "~/rules/no-let"; import * as noThrowStatement from "~/rules/no-throw-statement"; import * as noTryStatement from "~/rules/no-try-statement"; import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; +import * as typeDeclarationImmutability from "~/rules/type-declaration-immutability"; import { RuleEnforcementComparator } from "~/rules/type-declaration-immutability"; import strict from "./strict"; @@ -48,7 +49,7 @@ const overrides: Linter.Config = { enforcement: "ReadonlyDeep", }, ], - [`functional/${preferImmutableParameterTypes.name}`]: [ + [`functional/${typeDeclarationImmutability.name}`]: [ "error", { rules: [ From 259511dc62dd6917faa27c390636f69cb6cfe95d Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 29 Sep 2022 16:38:13 +1300 Subject: [PATCH 031/100] refactor: use custom merge function to merge configs --- .../external-typescript-recommended.ts | 4 +-- src/configs/lite.ts | 4 +-- src/configs/recommended.ts | 4 +-- src/configs/strict.ts | 6 ++-- src/util/merge-configs.ts | 35 +++++++++++++++++++ 5 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 src/util/merge-configs.ts diff --git a/src/configs/external-typescript-recommended.ts b/src/configs/external-typescript-recommended.ts index 738f25d72..73132c4a2 100644 --- a/src/configs/external-typescript-recommended.ts +++ b/src/configs/external-typescript-recommended.ts @@ -1,7 +1,7 @@ -import { deepmerge } from "deepmerge-ts"; import type { Linter } from "eslint"; import externalVanillaRecommended from "~/configs/external-vanilla-recommended"; +import { mergeConfigs } from "~/util/merge-configs"; const tsConfig: Linter.Config = { rules: { @@ -10,7 +10,7 @@ const tsConfig: Linter.Config = { }, }; -const fullConfig: Linter.Config = deepmerge( +const fullConfig: Linter.Config = mergeConfigs( externalVanillaRecommended, tsConfig ); diff --git a/src/configs/lite.ts b/src/configs/lite.ts index e34dd69d0..e3f2477fb 100644 --- a/src/configs/lite.ts +++ b/src/configs/lite.ts @@ -1,4 +1,3 @@ -import { deepmerge } from "deepmerge-ts"; import type { Linter } from "eslint"; import * as functionalParameters from "~/rules/functional-parameters"; @@ -6,6 +5,7 @@ import * as immutableData from "~/rules/immutable-data"; import * as noConditionalStatement from "~/rules/no-conditional-statement"; import * as noExpressionStatement from "~/rules/no-expression-statement"; import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; +import { mergeConfigs } from "~/util/merge-configs"; import recommended from "./recommended"; @@ -32,6 +32,6 @@ const overrides: Linter.Config = { }, }; -const config: Linter.Config = deepmerge(recommended, overrides); +const config: Linter.Config = mergeConfigs(recommended, overrides); export default config; diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index a37a81c40..f2d5c7e22 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -1,4 +1,3 @@ -import { deepmerge } from "deepmerge-ts"; import type { Linter } from "eslint"; import { Immutability } from "is-immutable-type"; @@ -10,6 +9,7 @@ import * as noTryStatement from "~/rules/no-try-statement"; import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; import * as typeDeclarationImmutability from "~/rules/type-declaration-immutability"; import { RuleEnforcementComparator } from "~/rules/type-declaration-immutability"; +import { mergeConfigs } from "~/util/merge-configs"; import strict from "./strict"; @@ -79,6 +79,6 @@ const overrides: Linter.Config = { }, }; -const config: Linter.Config = deepmerge(strict, overrides); +const config: Linter.Config = mergeConfigs(strict, overrides); export default config; diff --git a/src/configs/strict.ts b/src/configs/strict.ts index 756047ed2..3a72fb9d2 100644 --- a/src/configs/strict.ts +++ b/src/configs/strict.ts @@ -1,13 +1,13 @@ -import { deepmerge } from "deepmerge-ts"; import type { Linter } from "eslint"; import currying from "~/configs/currying"; import noExceptions from "~/configs/no-exceptions"; import noMutations from "~/configs/no-mutations"; +import noOtherParadigms from "~/configs/no-other-paradigms"; import noStatements from "~/configs/no-statements"; -import noOtherParadigms from "~/src/configs/no-other-paradigms"; +import { mergeConfigs } from "~/util/merge-configs"; -const config: Linter.Config = deepmerge( +const config: Linter.Config = mergeConfigs( currying, noMutations, noExceptions, diff --git a/src/util/merge-configs.ts b/src/util/merge-configs.ts new file mode 100644 index 000000000..66e87239f --- /dev/null +++ b/src/util/merge-configs.ts @@ -0,0 +1,35 @@ +import { deepmergeCustom } from "deepmerge-ts"; + +export const mergeConfigs = deepmergeCustom< + {}, + { + keyPath: ReadonlyArray; + } +>({ + metaDataUpdater: (previousMeta, metaMeta) => { + if (previousMeta === undefined) { + if (metaMeta.key === undefined) { + return { keyPath: [] }; + } + return { keyPath: [metaMeta.key] }; + } + if (metaMeta.key === undefined) { + return previousMeta; + } + return { + ...metaMeta, + keyPath: [...previousMeta.keyPath, metaMeta.key], + }; + }, + mergeArrays(values, utils, meta) { + if ( + meta !== undefined && + meta.keyPath.length >= 2 && + meta.keyPath[0] === "rules" + ) { + return utils.actions.skip; + } + + return utils.actions.defaultMerge; + }, +}); From bbd798b484c17fdb32d45476c88e141402e1377e Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 29 Sep 2022 16:44:31 +1300 Subject: [PATCH 032/100] feat(no-this-expression)!: remove `no-this-expression` from recommended and lite rulesets --- README.md | 2 +- src/configs/recommended.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 86519f68b..23bb34221 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ The [below section](#rules) gives details on which rules are enabled by each rul | ---------------------------------------------------------- | ------------------------------------------------------------------ | :----------------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | | [`no-class`](./docs/rules/no-class.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | | [`no-mixed-type`](./docs/rules/no-mixed-type.md) | Disallow types that contain both callable and non-callable members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`no-this-expression`](./docs/rules/no-this-expression.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`no-this-expression`](./docs/rules/no-this-expression.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | | | | | ### No Statements Rules diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index f2d5c7e22..f86cbe5d5 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -4,6 +4,7 @@ import { Immutability } from "is-immutable-type"; import * as functionalParameters from "~/rules/functional-parameters"; import * as noConditionalStatement from "~/rules/no-conditional-statement"; import * as noLet from "~/rules/no-let"; +import * as noThisExpression from "~/rules/no-this-expression"; import * as noThrowStatement from "~/rules/no-throw-statement"; import * as noTryStatement from "~/rules/no-try-statement"; import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; @@ -36,6 +37,7 @@ const overrides: Linter.Config = { allowInForLoopInit: true, }, ], + [`functional/${noThisExpression.name}`]: "off", [`functional/${noThrowStatement.name}`]: [ "error", { From d5e7b420e59ca8cf5e187d984d728e404b083ae8 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 29 Sep 2022 16:45:29 +1300 Subject: [PATCH 033/100] docs: fix rules gird for functional-parameters --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23bb34221..6e8de2a6f 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ The [below section](#rules) gives details on which rules are enabled by each rul | Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | | ---------------------------------------------------------------- | ----------------------------------------- | :------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :----------: | -| [`functional-parameters`](./docs/rules/functional-parameters.md) | Functions must have functional parameters | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | | | +| [`functional-parameters`](./docs/rules/functional-parameters.md) | Functions must have functional parameters | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | :green_circle: | | | ### Stylistic Rules From 5e2d9c33abf2bc1e0cf758306ee9a62d5e36f373 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 30 Sep 2022 00:28:33 +1300 Subject: [PATCH 034/100] fixup(prefer-immutable-parameter-types): add option ignoreInferredTypes --- .../rules/prefer-immutable-parameter-types.md | 73 ++++++++++++++++++- src/configs/lite.ts | 1 + src/configs/recommended.ts | 1 + src/rules/prefer-immutable-parameter-types.ts | 12 ++- .../ts/valid.ts | 15 ++++ 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/docs/rules/prefer-immutable-parameter-types.md b/docs/rules/prefer-immutable-parameter-types.md index 1733aecb3..96af48a15 100644 --- a/docs/rules/prefer-immutable-parameter-types.md +++ b/docs/rules/prefer-immutable-parameter-types.md @@ -144,6 +144,7 @@ This rule accepts an options object of the following type: ```ts type Options = { enforcement: "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; + ignoreInferredTypes: boolean; } ``` @@ -152,6 +153,7 @@ type Options = { ```ts const defaults = { enforcement: "Immutable", + ignoreInferredTypes: false, } ``` @@ -162,6 +164,7 @@ const defaults = { ```ts const recommendedOptions = { enforcement: "ReadonlyDeep", + ignoreInferredTypes: true, } ``` @@ -170,6 +173,7 @@ const recommendedOptions = { ```ts const liteOptions = { enforcement: "ReadonlyShallow", + ignoreInferredTypes: true, } ``` @@ -177,7 +181,7 @@ const liteOptions = { The level of immutability that should be enforced. -**incorrect**: +#### ❌ Incorrect @@ -189,7 +193,7 @@ function set(arg: ReadonlySet) {} // ReadonlySet is not immutable function map(arg: ReadonlyMap) {} // ReadonlyMap is not immutable ``` -**correct**: +#### ✅ Correct @@ -211,3 +215,68 @@ function set(arg: ReadonlySet<{ foo: string; }>) {} function map(arg: ReadonlyMap<{ foo: string; }>) {} function object(arg: Readonly<{ prop: { foo: string; }; }>) {} ``` + +### `ignoreInferredTypes` + +This option allows you to ignore parameters which don't explicitly specify a +type. This may be desirable in cases where an external dependency specifies a +callback with mutable parameters, and manually annotating the callback's +parameters is undesirable. + + + +#### ❌ Incorrect + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: ["error", { "ignoreInferredTypes": true }] */ + +import { acceptsCallback, type CallbackOptions } from 'external-dependency'; + +acceptsCallback((options: CallbackOptions) => {}); +``` + +
+external-dependency.d.ts + +```ts +export interface CallbackOptions { + prop: string; +} +type Callback = (options: CallbackOptions) => void; +type AcceptsCallback = (callback: Callback) => void; + +export const acceptsCallback: AcceptsCallback; +``` + +
+ +#### ✅ Correct + + + +```ts +/* eslint functional/prefer-immutable-parameter-types: ["error", { "ignoreInferredTypes": true }] */ + +import { acceptsCallback } from 'external-dependency'; + +acceptsCallback(options => {}); +``` + +
+external-dependency.d.ts + +```ts +export interface CallbackOptions { + prop: string; +} +type Callback = (options: CallbackOptions) => void; +type AcceptsCallback = (callback: Callback) => void; + +export const acceptsCallback: AcceptsCallback; +``` + +
+ + diff --git a/src/configs/lite.ts b/src/configs/lite.ts index e3f2477fb..70d82111e 100644 --- a/src/configs/lite.ts +++ b/src/configs/lite.ts @@ -27,6 +27,7 @@ const overrides: Linter.Config = { "error", { enforcement: "ReadonlyShallow", + ignoreInferredTypes: true, }, ], }, diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index f86cbe5d5..0d7c63f78 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -49,6 +49,7 @@ const overrides: Linter.Config = { "error", { enforcement: "ReadonlyDeep", + ignoreInferredTypes: true, }, ], [`functional/${typeDeclarationImmutability.name}`]: [ diff --git a/src/rules/prefer-immutable-parameter-types.ts b/src/rules/prefer-immutable-parameter-types.ts index 046a27c7a..125233e98 100644 --- a/src/rules/prefer-immutable-parameter-types.ts +++ b/src/rules/prefer-immutable-parameter-types.ts @@ -23,6 +23,7 @@ type Options = ReadonlyDeep< Immutability | keyof typeof Immutability, "Unknown" | "Mutable" >; + ignoreInferredTypes: boolean; } ] >; @@ -44,6 +45,9 @@ const schema: JSONSchema4 = [ i !== Immutability[Immutability.Mutable] ), }, + ignoreInferredTypes: { + type: "boolean", + }, }, additionalProperties: false, }, @@ -55,6 +59,7 @@ const schema: JSONSchema4 = [ const defaultOptions: Options = [ { enforcement: Immutability.Immutable, + ignoreInferredTypes: false, }, ]; @@ -91,7 +96,7 @@ function checkFunction( options: Options ): RuleResult { const [optionsObject] = options; - const { enforcement: rawEnforcement } = optionsObject; + const { enforcement: rawEnforcement, ignoreInferredTypes } = optionsObject; const enforcement = typeof rawEnforcement === "string" @@ -108,6 +113,11 @@ function checkFunction( const actualParam = isTSParameterProperty(param) ? param.parameter : param; + + if (ignoreInferredTypes && actualParam.typeAnnotation === undefined) { + return undefined; + } + const immutability = getTypeImmutabilityOfNode(actualParam, context); return immutability >= enforcement diff --git a/tests/rules/prefer-immutable-parameter-types/ts/valid.ts b/tests/rules/prefer-immutable-parameter-types/ts/valid.ts index dfa2c8f1d..0b5cf6ea9 100644 --- a/tests/rules/prefer-immutable-parameter-types/ts/valid.ts +++ b/tests/rules/prefer-immutable-parameter-types/ts/valid.ts @@ -182,6 +182,21 @@ const tests: ReadonlyArray = [ [{ enforcement: "ReadonlyDeep" }], ], }, + { + code: dedent` + type Callback = (options: T) => void; + declare const acceptsCallback: (callback: Callback) => void; + interface CallbackOptions { + prop: string; + } + acceptsCallback(options => {}); + `, + optionsSet: [ + [{ enforcement: "ReadonlyShallow", ignoreInferredTypes: true }], + [{ enforcement: "ReadonlyDeep", ignoreInferredTypes: true }], + [{ enforcement: "Immutable", ignoreInferredTypes: true }], + ], + }, ]; export default tests; From 23a8e46b6ab89eb3d20b9dfcf33f9537a6faeff8 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 30 Sep 2022 01:00:40 +1300 Subject: [PATCH 035/100] docs: remove *s --- docs/rules/no-conditional-statement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-conditional-statement.md b/docs/rules/no-conditional-statement.md index 4dd5b9708..0008fa5f7 100644 --- a/docs/rules/no-conditional-statement.md +++ b/docs/rules/no-conditional-statement.md @@ -66,7 +66,7 @@ const defaults = { ### Preset Overrides -#### `recommended` and `lite`\*\*\*\* +#### `recommended` and `lite` ```ts const recommendedAndLiteOptions = { From d93d5ccf7ab5c769a69e246bb762a4a09907d8f6 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 04:58:59 +1300 Subject: [PATCH 036/100] fixup(prefer-immutable-types): change rule name and add a lot more options --- ...ter-types.md => prefer-immutable-types.md} | 94 ++-- src/configs/all.ts | 4 +- src/configs/lite.ts | 7 +- src/configs/no-mutations.ts | 4 +- src/configs/recommended.ts | 7 +- src/rules/index.ts | 4 +- src/rules/prefer-immutable-parameter-types.ts | 158 ------ src/rules/prefer-immutable-types.ts | 470 ++++++++++++++++++ src/rules/prefer-readonly-type.ts | 2 +- src/util/rule.ts | 82 ++- src/util/typeguard.ts | 7 + .../ts/invalid.ts | 125 ----- .../index.test.ts | 2 +- .../rules/prefer-immutable-types/ts/index.ts | 12 + .../ts/parameters}/index.ts | 0 .../ts/parameters/invalid.ts | 167 +++++++ .../ts/parameters}/valid.ts | 130 ++--- .../ts/return-types/index.ts | 7 + .../ts/return-types/invalid.ts | 135 +++++ .../ts/return-types/valid.ts | 197 ++++++++ .../ts/variables/index.ts | 7 + .../ts/variables/invalid.ts | 179 +++++++ .../ts/variables/valid.ts | 234 +++++++++ tests/rules/work.test.ts | 32 +- 24 files changed, 1652 insertions(+), 414 deletions(-) rename docs/rules/{prefer-immutable-parameter-types.md => prefer-immutable-types.md} (70%) delete mode 100644 src/rules/prefer-immutable-parameter-types.ts create mode 100644 src/rules/prefer-immutable-types.ts delete mode 100644 tests/rules/prefer-immutable-parameter-types/ts/invalid.ts rename tests/rules/{prefer-immutable-parameter-types => prefer-immutable-types}/index.test.ts (63%) create mode 100644 tests/rules/prefer-immutable-types/ts/index.ts rename tests/rules/{prefer-immutable-parameter-types/ts => prefer-immutable-types/ts/parameters}/index.ts (100%) create mode 100644 tests/rules/prefer-immutable-types/ts/parameters/invalid.ts rename tests/rules/{prefer-immutable-parameter-types/ts => prefer-immutable-types/ts/parameters}/valid.ts (50%) create mode 100644 tests/rules/prefer-immutable-types/ts/return-types/index.ts create mode 100644 tests/rules/prefer-immutable-types/ts/return-types/invalid.ts create mode 100644 tests/rules/prefer-immutable-types/ts/return-types/valid.ts create mode 100644 tests/rules/prefer-immutable-types/ts/variables/index.ts create mode 100644 tests/rules/prefer-immutable-types/ts/variables/invalid.ts create mode 100644 tests/rules/prefer-immutable-types/ts/variables/valid.ts diff --git a/docs/rules/prefer-immutable-parameter-types.md b/docs/rules/prefer-immutable-types.md similarity index 70% rename from docs/rules/prefer-immutable-parameter-types.md rename to docs/rules/prefer-immutable-types.md index 96af48a15..d2811efa6 100644 --- a/docs/rules/prefer-immutable-parameter-types.md +++ b/docs/rules/prefer-immutable-types.md @@ -1,30 +1,28 @@ -# Prefer immutable parameter types over mutable ones (prefer-immutable-parameter-types) - -Although other rules can be used to ensure parameter are not mutated, it is -best to explicitly declare that the parameters are immutable. - -It is also worth noting that as immutable types are not assignable to mutable -ones, users will not be able to pass something like a readonly array to a -functional that wants a mutable array; even if the function does not actually -mutate said array. +# Prefer immutable types over mutable ones (prefer-immutable-types) ## Rule Details -This rule differs from the +This rule is deigned to be a replacement for [@typescript-eslint/prefer-readonly-parameter-types](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/prefer-readonly-parameter-types.md) -rule by the fact that it uses the +but also add extra functionality, allowing not just parameters to checked. + +This rule uses the [is-immutable-type](https://www.npmjs.com/package/is-immutable-type) library to calculated immutability. This library allows for more powerful and customizable immutability enforcements to be made. -This rule is designed to replace the aforementioned rule. +With parameters specifically, it is also worth noting that as immutable types +are not assignable to mutable ones, and thus users will not be able to pass +something like a readonly array to a functional that wants a mutable array; even +if the function does not actually mutate said array. +Libraries should therefore always enforce this rule for parameters. ### ❌ Incorrect - + ```ts -/* eslint functional/prefer-immutable-parameter-types: "error" */ +/* eslint functional/prefer-immutable-types: "error" */ function array1(arg: string[]) {} // array is not readonly function array2(arg: ReadonlyArray) {} // array element is not readonly @@ -67,10 +65,10 @@ interface Foo4 { ### ✅ Correct - + ```ts -/* eslint functional/prefer-immutable-parameter-types: "error" */ +/* eslint functional/prefer-immutable-types: "error" */ function array1(arg: ReadonlyArray) {} function array2(arg: ReadonlyArray>) {} @@ -143,8 +141,22 @@ This rule accepts an options object of the following type: ```ts type Options = { - enforcement: "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; + enforcement: "None" | "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; ignoreInferredTypes: boolean; + + parameters?: { + // The same properties as above or just an enforcement value. + }; + returnTypes?: { + // The same properties as above or just an enforcement value. + }; + variables?: { + // The same properties as above or just an enforcement value. + }; + + allowLocalMutation: boolean; + ignoreClass: boolean | "fieldsOnly"; + ignorePattern?: string[] | string; } ``` @@ -153,6 +165,8 @@ type Options = { ```ts const defaults = { enforcement: "Immutable", + allowLocalMutation: false, + ignoreClass: false, ignoreInferredTypes: false, } ``` @@ -163,18 +177,20 @@ const defaults = { ```ts const recommendedOptions = { - enforcement: "ReadonlyDeep", + enforcement: "None", ignoreInferredTypes: true, -} + parameters: "ReadonlyDeep", +}, ``` #### `lite` ```ts const liteOptions = { - enforcement: "ReadonlyShallow", + enforcement: "None", ignoreInferredTypes: true, -} + parameters: "ReadonlyShallow", +}, ``` ### `enforcement` @@ -183,10 +199,10 @@ The level of immutability that should be enforced. #### ❌ Incorrect - + ```ts -/* eslint functional/prefer-immutable-parameter-types: ["error", { "enforcement": "Immutable" }] */ +/* eslint functional/prefer-immutable-types: ["error", { "enforcement": "Immutable" }] */ function array(arg: ReadonlyArray) {} // ReadonlyArray is not immutable function set(arg: ReadonlySet) {} // ReadonlySet is not immutable @@ -195,20 +211,20 @@ function map(arg: ReadonlyMap) {} // ReadonlyMap is not immutable #### ✅ Correct - + ```ts -/* eslint functional/prefer-immutable-parameter-types: ["error", { "enforcement": "Immutable" }] */ +/* eslint functional/prefer-immutable-types: ["error", { "enforcement": "Immutable" }] */ function set(arg: Readonly>) {} function map(arg: Readonly>) {} function object(arg: Readonly<{ prop: string }>) {} ``` - + ```ts -/* eslint functional/prefer-immutable-parameter-types: ["error", { "enforcement": "ReadonlyShallow" }] */ +/* eslint functional/prefer-immutable-types: ["error", { "enforcement": "ReadonlyShallow" }] */ function array(arg: ReadonlyArray<{ foo: string; }>) {} function set(arg: ReadonlySet<{ foo: string; }>) {} @@ -227,10 +243,10 @@ parameters is undesirable. #### ❌ Incorrect - + ```ts -/* eslint functional/prefer-immutable-parameter-types: ["error", { "ignoreInferredTypes": true }] */ +/* eslint functional/prefer-immutable-types: ["error", { "ignoreInferredTypes": true }] */ import { acceptsCallback, type CallbackOptions } from 'external-dependency'; @@ -254,10 +270,10 @@ export const acceptsCallback: AcceptsCallback; #### ✅ Correct - + ```ts -/* eslint functional/prefer-immutable-parameter-types: ["error", { "ignoreInferredTypes": true }] */ +/* eslint functional/prefer-immutable-types: ["error", { "ignoreInferredTypes": true }] */ import { acceptsCallback } from 'external-dependency'; @@ -280,3 +296,19 @@ export const acceptsCallback: AcceptsCallback; + +### `parameters.*`, `returnTypes.*`, `variables.*` + +Override the options specifically for the given type of types. + +### `ignoreClass` + +A boolean to specify if checking classes should be ignored. `false` by default. + +### `allowLocalMutation` + +See the [allowLocalMutation](./options/allow-local-mutation.md) docs. + +### `ignorePattern` + +See the [ignorePattern](./options/ignore-pattern.md) docs. diff --git a/src/configs/all.ts b/src/configs/all.ts index d16dc8172..3a7f991cb 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -13,7 +13,7 @@ import * as noReturnVoid from "~/rules/no-return-void"; import * as noThisExpression from "~/rules/no-this-expression"; import * as noThrowStatement from "~/rules/no-throw-statement"; import * as noTryStatement from "~/rules/no-try-statement"; -import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; +import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import * as preferPropertySignatures from "~/rules/prefer-property-signatures"; import * as preferTacit from "~/rules/prefer-tacit"; import * as typeDeclarationImmutability from "~/rules/type-declaration-immutability"; @@ -33,7 +33,7 @@ const config: Linter.Config = { [`functional/${noThisExpression.name}`]: "error", [`functional/${noThrowStatement.name}`]: "error", [`functional/${noTryStatement.name}`]: "error", - [`functional/${preferImmutableParameterTypes.name}`]: "error", + [`functional/${preferImmutableTypes.name}`]: "error", [`functional/${preferPropertySignatures.name}`]: "error", [`functional/${preferTacit.name}`]: [ "warn", diff --git a/src/configs/lite.ts b/src/configs/lite.ts index 70d82111e..787cb3d39 100644 --- a/src/configs/lite.ts +++ b/src/configs/lite.ts @@ -4,7 +4,7 @@ import * as functionalParameters from "~/rules/functional-parameters"; import * as immutableData from "~/rules/immutable-data"; import * as noConditionalStatement from "~/rules/no-conditional-statement"; import * as noExpressionStatement from "~/rules/no-expression-statement"; -import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; +import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import { mergeConfigs } from "~/util/merge-configs"; import recommended from "./recommended"; @@ -23,11 +23,12 @@ const overrides: Linter.Config = { ], [`functional/${noConditionalStatement.name}`]: "off", [`functional/${noExpressionStatement.name}`]: "off", - [`functional/${preferImmutableParameterTypes.name}`]: [ + [`functional/${preferImmutableTypes.name}`]: [ "error", { - enforcement: "ReadonlyShallow", + enforcement: "None", ignoreInferredTypes: true, + parameters: "ReadonlyShallow", }, ], }, diff --git a/src/configs/no-mutations.ts b/src/configs/no-mutations.ts index 156c974bc..ec0ebf2eb 100644 --- a/src/configs/no-mutations.ts +++ b/src/configs/no-mutations.ts @@ -2,14 +2,14 @@ import type { Linter } from "eslint"; import * as immutableData from "~/rules/immutable-data"; import * as noLet from "~/rules/no-let"; -import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; +import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import * as typeDeclarationImmutability from "~/rules/type-declaration-immutability"; const config: Linter.Config = { rules: { [`functional/${immutableData.name}`]: "error", [`functional/${noLet.name}`]: "error", - [`functional/${preferImmutableParameterTypes.name}`]: "error", + [`functional/${preferImmutableTypes.name}`]: "error", [`functional/${typeDeclarationImmutability.name}`]: "error", }, }; diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 0d7c63f78..8fdba9dab 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -7,7 +7,7 @@ import * as noLet from "~/rules/no-let"; import * as noThisExpression from "~/rules/no-this-expression"; import * as noThrowStatement from "~/rules/no-throw-statement"; import * as noTryStatement from "~/rules/no-try-statement"; -import * as preferImmutableParameterTypes from "~/rules/prefer-immutable-parameter-types"; +import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import * as typeDeclarationImmutability from "~/rules/type-declaration-immutability"; import { RuleEnforcementComparator } from "~/rules/type-declaration-immutability"; import { mergeConfigs } from "~/util/merge-configs"; @@ -45,11 +45,12 @@ const overrides: Linter.Config = { }, ], [`functional/${noTryStatement.name}`]: "off", - [`functional/${preferImmutableParameterTypes.name}`]: [ + [`functional/${preferImmutableTypes.name}`]: [ "error", { - enforcement: "ReadonlyDeep", + enforcement: "None", ignoreInferredTypes: true, + parameters: "ReadonlyDeep", }, ], [`functional/${typeDeclarationImmutability.name}`]: [ diff --git a/src/rules/index.ts b/src/rules/index.ts index f1d2ba501..5cea79fa9 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -11,7 +11,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 preferImmutableParameterTypes from "./prefer-immutable-parameter-types"; +import * as preferImmutableTypes from "./prefer-immutable-types"; import * as preferPropertySignatures from "./prefer-property-signatures"; import * as preferReadonlyTypes from "./prefer-readonly-type"; import * as preferTacit from "./prefer-tacit"; @@ -34,7 +34,7 @@ export const rules = { [noThisExpression.name]: noThisExpression.rule, [noThrowStatement.name]: noThrowStatement.rule, [noTryStatement.name]: noTryStatement.rule, - [preferImmutableParameterTypes.name]: preferImmutableParameterTypes.rule, + [preferImmutableTypes.name]: preferImmutableTypes.rule, [preferPropertySignatures.name]: preferPropertySignatures.rule, [preferReadonlyTypes.name]: preferReadonlyTypes.rule, [preferTacit.name]: preferTacit.rule, diff --git a/src/rules/prefer-immutable-parameter-types.ts b/src/rules/prefer-immutable-parameter-types.ts deleted file mode 100644 index 125233e98..000000000 --- a/src/rules/prefer-immutable-parameter-types.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { ESLintUtils, TSESLint } from "@typescript-eslint/utils"; -import { Immutability } from "is-immutable-type"; -import type { JSONSchema4 } from "json-schema"; -import type { ReadonlyDeep } from "type-fest"; - -import type { ESFunctionType } from "~/util/node-types"; -import type { RuleResult } from "~/util/rule"; -import { getTypeImmutabilityOfNode, createRule } from "~/util/rule"; -import { isDefined, isTSParameterProperty } from "~/util/typeguard"; - -/** - * The name of this rule. - */ -export const name = "prefer-immutable-parameter-types" as const; - -/** - * The options this rule can take. - */ -type Options = ReadonlyDeep< - [ - { - enforcement: Exclude< - Immutability | keyof typeof Immutability, - "Unknown" | "Mutable" - >; - ignoreInferredTypes: boolean; - } - ] ->; - -/** - * The schema for the rule options. - */ -const schema: JSONSchema4 = [ - { - type: "object", - properties: { - enforcement: { - type: ["string", "number"], - enum: Object.values(Immutability).filter( - (i) => - i !== Immutability.Unknown && - i !== Immutability[Immutability.Unknown] && - i !== Immutability.Mutable && - i !== Immutability[Immutability.Mutable] - ), - }, - ignoreInferredTypes: { - type: "boolean", - }, - }, - additionalProperties: false, - }, -]; - -/** - * The default options for the rule. - */ -const defaultOptions: Options = [ - { - enforcement: Immutability.Immutable, - ignoreInferredTypes: false, - }, -]; - -/** - * The possible error messages. - */ -const errorMessages = { - generic: - 'Parameter should have an immutability at least "{{ expected }}" (actual: "{{ actual }}").', -} as const; - -/** - * The meta data for this rule. - */ -const meta: ESLintUtils.NamedCreateRuleMeta = { - type: "suggestion", - docs: { - description: - "Require function parameters to be typed as certain immutability", - recommended: "error", - }, - messages: errorMessages, - schema, -}; - -/** - * Check if the given function node violates this rule. - */ -function checkFunction( - node: ReadonlyDeep, - context: ReadonlyDeep< - TSESLint.RuleContext - >, - options: Options -): RuleResult { - const [optionsObject] = options; - const { enforcement: rawEnforcement, ignoreInferredTypes } = optionsObject; - - const enforcement = - typeof rawEnforcement === "string" - ? Immutability[rawEnforcement] - : rawEnforcement; - - type Descriptor = RuleResult< - keyof typeof errorMessages, - Options - >["descriptors"][number]; - - const descriptors = node.params - .map((param): Descriptor | undefined => { - const actualParam = isTSParameterProperty(param) - ? param.parameter - : param; - - if (ignoreInferredTypes && actualParam.typeAnnotation === undefined) { - return undefined; - } - - const immutability = getTypeImmutabilityOfNode(actualParam, context); - - return immutability >= enforcement - ? undefined - : { - node: actualParam, - messageId: "generic", - data: { - actual: Immutability[immutability], - expected: Immutability[enforcement], - }, - }; - }) - .filter(isDefined); - - return { - context, - descriptors, - }; -} - -// Create the rule. -export const rule = createRule( - name, - meta, - defaultOptions, - { - ArrowFunctionExpression: checkFunction, - FunctionDeclaration: checkFunction, - FunctionExpression: checkFunction, - TSCallSignatureDeclaration: checkFunction, - TSConstructSignatureDeclaration: checkFunction, - TSDeclareFunction: checkFunction, - TSEmptyBodyFunctionExpression: checkFunction, - TSFunctionType: checkFunction, - TSMethodSignature: checkFunction, - } -); diff --git a/src/rules/prefer-immutable-types.ts b/src/rules/prefer-immutable-types.ts new file mode 100644 index 000000000..2366872cc --- /dev/null +++ b/src/rules/prefer-immutable-types.ts @@ -0,0 +1,470 @@ +import type { ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; +import { deepmerge } from "deepmerge-ts"; +import { Immutability } from "is-immutable-type"; +import type { JSONSchema4 } from "json-schema"; +import type { ReadonlyDeep } from "type-fest"; + +import type { + AllowLocalMutationOption, + IgnoreClassOption, + IgnorePatternOption, +} from "~/common/ignore-options"; +import { + allowLocalMutationOptionSchema, + ignoreClassOptionSchema, + ignorePatternOptionSchema, + shouldIgnoreClass, + shouldIgnoreLocalMutation, + shouldIgnorePattern, +} from "~/common/ignore-options"; +import type { ESFunctionType } from "~/util/node-types"; +import type { RuleResult } from "~/util/rule"; +import { + getReturnTypesOfFunction, + getTypeImmutabilityOfNode, + getTypeImmutabilityOfType, + createRule, +} from "~/util/rule"; +import { + hasID, + isDefined, + isPropertyDefinition, + isTSParameterProperty, +} from "~/util/typeguard"; + +/** + * The name of this rule. + */ +export const name = "prefer-immutable-types" as const; + +type RawEnforcement = + | Exclude + | "None" + | false; + +type Option = ReadonlyDeep< + AllowLocalMutationOption & + IgnoreClassOption & + IgnorePatternOption & { + enforcement: RawEnforcement; + ignoreInferredTypes: boolean; + } +>; + +/** + * The options this rule can take. + */ +type Options = ReadonlyDeep< + [ + Option & { + parameters?: Option | RawEnforcement; + returnTypes?: Option | RawEnforcement; + variables?: Option | RawEnforcement; + } + ] +>; + +/** + * The enum options for the level of enforcement. + */ +const enforcementEnumOptions = [ + ...Object.values(Immutability).filter( + (i) => + i !== Immutability.Unknown && + i !== Immutability[Immutability.Unknown] && + i !== Immutability.Mutable && + i !== Immutability[Immutability.Mutable] + ), + "None", + false, +]; + +/** + * The non-shorthand schema for each option. + */ +const optionExpandedSchema: JSONSchema4 = deepmerge( + allowLocalMutationOptionSchema, + ignoreClassOptionSchema, + ignorePatternOptionSchema, + { + enforcement: { + type: ["string", "number", "boolean"], + enum: enforcementEnumOptions, + }, + ignoreInferredTypes: { + type: "boolean", + }, + } +); + +/** + * The schema for each option. + */ +const optionSchema: JSONSchema4 = { + oneOf: [ + { + type: "object", + properties: optionExpandedSchema, + additionalProperties: false, + }, + { + type: ["string", "number", "boolean"], + enum: enforcementEnumOptions, + }, + ], +}; + +/** + * The schema for the rule options. + */ +const schema: JSONSchema4 = [ + { + type: "object", + properties: deepmerge(optionExpandedSchema, { + parameters: optionSchema, + returnTypes: optionSchema, + variables: optionSchema, + }), + additionalProperties: false, + }, +]; + +/** + * The default options for the rule. + */ +const defaultOptions: Options = [ + { + enforcement: Immutability.Immutable, + allowLocalMutation: false, + ignoreInferredTypes: false, + ignoreClass: false, + }, +]; + +/** + * The possible error messages. + */ +const errorMessages = { + parameter: + 'Parameter should have an immutability at least "{{ expected }}" (actual: "{{ actual }}").', + returnType: + 'Return type should have an immutability at least "{{ expected }}" (actual: "{{ actual }}").', + variable: + 'Variable should have an immutability at least "{{ expected }}" (actual: "{{ actual }}").', + propertyImmutability: + 'Property should have an immutability at least "{{ expected }}" (actual: "{{ actual }}").', + propertyModifier: "Property should have a readonly modifier.", +} as const; + +/** + * The meta data for this rule. + */ +const meta: ESLintUtils.NamedCreateRuleMeta = { + type: "suggestion", + docs: { + description: + "Require function parameters to be typed as certain immutability", + recommended: "error", + }, + messages: errorMessages, + schema, +}; + +type Descriptor = RuleResult< + keyof typeof errorMessages, + Options +>["descriptors"][number]; + +/** + * Get the level of enforcement from the raw value given. + */ +function parseEnforcement(rawEnforcement: RawEnforcement) { + return rawEnforcement === "None" + ? false + : typeof rawEnforcement === "string" + ? Immutability[rawEnforcement] + : rawEnforcement; +} + +/** + * Get the parameter type violations. + */ +function getParameterTypeViolations( + node: ReadonlyDeep, + context: ReadonlyDeep< + TSESLint.RuleContext + >, + options: Options +): Descriptor[] { + const [optionsObject] = options; + const { parameters: rawOption } = optionsObject; + const { enforcement: rawEnforcement, ignoreInferredTypes } = + typeof rawOption === "object" + ? rawOption + : { + enforcement: rawOption, + ignoreInferredTypes: defaultOptions[0].ignoreInferredTypes, + }; + + const enforcement = parseEnforcement( + rawEnforcement ?? defaultOptions[0].enforcement + ); + if (enforcement === false) { + return []; + } + + return node.params + .map((param): Descriptor | undefined => { + if (isTSParameterProperty(param) && param.readonly !== true) { + return { + node: param, + messageId: "propertyModifier", + }; + } + + const actualParam = isTSParameterProperty(param) + ? param.parameter + : param; + + if (ignoreInferredTypes && actualParam.typeAnnotation === undefined) { + return undefined; + } + + const immutability = getTypeImmutabilityOfNode(actualParam, context); + + return immutability >= enforcement + ? undefined + : { + node: actualParam, + messageId: "parameter", + data: { + actual: Immutability[immutability], + expected: Immutability[enforcement], + }, + }; + }) + .filter(isDefined); +} + +/** + * Get the return type violations. + */ +function getReturnTypeViolations( + node: ReadonlyDeep, + context: ReadonlyDeep< + TSESLint.RuleContext + >, + options: Options +): Descriptor[] { + const [optionsObject] = options; + const { returnTypes: rawOption } = optionsObject; + const { enforcement: rawEnforcement, ignoreInferredTypes } = + typeof rawOption === "object" + ? rawOption + : { + enforcement: rawOption, + ignoreInferredTypes: defaultOptions[0].ignoreInferredTypes, + }; + + const enforcement = parseEnforcement( + rawEnforcement ?? defaultOptions[0].enforcement + ); + if (enforcement === false) { + return []; + } + + if (ignoreInferredTypes && node.returnType?.typeAnnotation === undefined) { + return []; + } + + if (node.returnType?.typeAnnotation !== undefined) { + const immutability = getTypeImmutabilityOfNode( + node.returnType.typeAnnotation, + context + ); + + return immutability >= enforcement + ? [] + : [ + { + node: node.returnType, + messageId: "returnType", + data: { + actual: Immutability[immutability], + expected: Immutability[enforcement], + }, + }, + ]; + } + + const returnTypes = getReturnTypesOfFunction(node, context); + if (returnTypes === null) { + return []; + } + + const immutabilities = returnTypes.map((returnType) => + getTypeImmutabilityOfType(returnType, context) + ); + + const immutability = Math.min(...immutabilities); + + if (immutability >= enforcement) { + return []; + } + + return [ + { + node: hasID(node) && node.id !== null ? node.id : node, + messageId: "returnType", + data: { + actual: Immutability[immutability], + expected: Immutability[enforcement], + }, + }, + ]; +} + +/** + * Check if the given function node violates this rule. + */ +function checkFunction( + node: ReadonlyDeep, + context: ReadonlyDeep< + TSESLint.RuleContext + >, + options: Options +): RuleResult { + const [optionsObject] = options; + + if ( + shouldIgnoreClass(node, context, optionsObject) || + shouldIgnoreLocalMutation(node, context, optionsObject) || + shouldIgnorePattern(node, context, optionsObject) + ) { + return { + context, + descriptors: [], + }; + } + + const descriptors = [ + ...getParameterTypeViolations(node, context, options), + ...getReturnTypeViolations(node, context, options), + ]; + + return { + context, + descriptors, + }; +} + +/** + * Check if the given function node violates this rule. + */ +function checkVarible( + node: ReadonlyDeep, + context: ReadonlyDeep< + TSESLint.RuleContext + >, + options: Options +): RuleResult { + const [optionsObject] = options; + + if ( + shouldIgnoreClass(node, context, optionsObject) || + shouldIgnoreLocalMutation(node, context, optionsObject) || + shouldIgnorePattern(node, context, optionsObject) + ) { + return { + context, + descriptors: [], + }; + } + + const isProperty = isPropertyDefinition(node); + + if (isProperty && node.readonly !== true) { + return { + context, + descriptors: [ + { + node, + messageId: "propertyModifier", + }, + ], + }; + } + + const { variables: rawOption } = optionsObject; + const { enforcement: rawEnforcement, ignoreInferredTypes } = + typeof rawOption === "object" + ? rawOption + : { + enforcement: rawOption, + ignoreInferredTypes: defaultOptions[0].ignoreInferredTypes, + }; + + const enforcement = parseEnforcement( + rawEnforcement ?? defaultOptions[0].enforcement + ); + if (enforcement === false) { + return { + context, + descriptors: [], + }; + } + + const nodeWithTypeAnnotation = isProperty ? node : node.id; + + if ( + ignoreInferredTypes && + nodeWithTypeAnnotation.typeAnnotation === undefined + ) { + return { + context, + descriptors: [], + }; + } + + const immutability = getTypeImmutabilityOfNode( + nodeWithTypeAnnotation, + context + ); + + return { + context, + descriptors: + immutability >= enforcement + ? [] + : [ + { + node: nodeWithTypeAnnotation, + messageId: isProperty ? "propertyImmutability" : "variable", + data: { + actual: Immutability[immutability], + expected: Immutability[enforcement], + }, + }, + ], + }; +} + +// Create the rule. +export const rule = createRule( + name, + meta, + defaultOptions, + { + ArrowFunctionExpression: checkFunction, + FunctionDeclaration: checkFunction, + FunctionExpression: checkFunction, + TSCallSignatureDeclaration: checkFunction, + TSConstructSignatureDeclaration: checkFunction, + TSDeclareFunction: checkFunction, + TSEmptyBodyFunctionExpression: checkFunction, + TSFunctionType: checkFunction, + TSMethodSignature: checkFunction, + PropertyDefinition: checkVarible, + VariableDeclarator: checkVarible, + } +); diff --git a/src/rules/prefer-readonly-type.ts b/src/rules/prefer-readonly-type.ts index 51dfdd5a7..7d64f793a 100644 --- a/src/rules/prefer-readonly-type.ts +++ b/src/rules/prefer-readonly-type.ts @@ -114,7 +114,7 @@ const errorMessages = { const meta: ESLintUtils.NamedCreateRuleMeta = { deprecated: true, replacedBy: [ - "functional/prefer-immutable-parameter-types", + "functional/prefer-immutable-types", "functional/type-declaration-immutability", ], type: "suggestion", diff --git a/src/util/rule.ts b/src/util/rule.ts index d9a400ca5..5b1f00e15 100644 --- a/src/util/rule.ts +++ b/src/util/rule.ts @@ -9,8 +9,10 @@ import type { ImmutabilityOverrides } from "is-immutable-type"; import { getTypeImmutability, Immutability } from "is-immutable-type"; import type { ReadonlyDeep } from "type-fest"; import type { Node as TSNode, Type } from "typescript"; +import { SignatureKind } from "typescript"; import { getImmutabilityOverrides } from "~/settings"; +import { isNode } from "~/util/typeguard"; // eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle -- This is a special var. const __VERSION__ = "0.0.0-development"; @@ -174,6 +176,7 @@ export function getTypeOfNode< const checker = parserServices.program.getTypeChecker(); const { esTreeNodeToTSNodeMap } = parserServices; + // checker.getReturnTypeOfSignature const nodeType = checker.getTypeAtLocation( esTreeNodeToTSNodeMap.get(node as TSESTree.Node) ); @@ -182,25 +185,79 @@ export function getTypeOfNode< } /** - * Get the type immutability of the the given node. + * Get the return type of the the given function node. */ -export function getTypeImmutabilityOfNode< +export function getReturnTypesOfFunction< Context extends ReadonlyDeep> ->(node: ReadonlyDeep, context: Context): Immutability; +>(node: ReadonlyDeep, context: Context) { + const parserServices = getParserServices(context); + if (parserServices === null) { + return null; + } + + const checker = parserServices.program.getTypeChecker(); + const type = getTypeOfNode(node, parserServices); + if (type === null) { + return null; + } + + const signatures = checker.getSignaturesOfType(type, SignatureKind.Call); + return signatures.map((signature) => + checker.getReturnTypeOfSignature(signature) + ); +} /** * Get the type immutability of the the given node. */ -export function getTypeImmutabilityOfNode( - node: ReadonlyDeep, - parserServices: ParserServices, - overrides?: ImmutabilityOverrides -): Immutability; +export const getTypeImmutabilityOfNode: { + /** + * Get the type immutability of the the given node. + */ + >>( + node: ReadonlyDeep, + context: Context + ): Immutability; + + /** + * Get the type immutability of the the given node. + */ + ( + node: ReadonlyDeep, + parserServices: ParserServices, + overrides?: ImmutabilityOverrides + ): Immutability; +} = getImmutability; + +/** + * Get the type immutability of the the given type. + */ +export const getTypeImmutabilityOfType: { + /** + * Get the type immutability of the the given type. + */ + >>( + type: Type, + context: Context + ): Immutability; + + /** + * Get the type immutability of the the given type. + */ + ( + type: Type, + parserServices: ParserServices, + overrides?: ImmutabilityOverrides + ): Immutability; +} = getImmutability; -export function getTypeImmutabilityOfNode< +/** + * Get the type immutability of the the given node or type. + */ +function getImmutability< Context extends ReadonlyDeep> >( - node: ReadonlyDeep, + nodeOrType: ReadonlyDeep | Type, contextOrServices: Context | ParserServices, explicitOverrides?: ImmutabilityOverrides ): Immutability { @@ -220,7 +277,10 @@ export function getTypeImmutabilityOfNode< const checker = parserServices.program.getTypeChecker(); - const type = getTypeOfNode(node, parserServices); + const type = isNode(nodeOrType) + ? getTypeOfNode(nodeOrType, parserServices) + : nodeOrType; + return getTypeImmutability( checker, type, diff --git a/src/util/typeguard.ts b/src/util/typeguard.ts index 43de077e2..354be563d 100644 --- a/src/util/typeguard.ts +++ b/src/util/typeguard.ts @@ -45,6 +45,13 @@ export function isReadonlyArray( * Node type guards. */ +export function isNode( + node: ReadonlyDeep | Type +): node is ReadonlyDeep { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return (node as any).type !== undefined; +} + export function isArrayExpression( node: ReadonlyDeep ): node is ReadonlyDeep { diff --git a/tests/rules/prefer-immutable-parameter-types/ts/invalid.ts b/tests/rules/prefer-immutable-parameter-types/ts/invalid.ts deleted file mode 100644 index df80ea1fe..000000000 --- a/tests/rules/prefer-immutable-parameter-types/ts/invalid.ts +++ /dev/null @@ -1,125 +0,0 @@ -import dedent from "dedent"; - -import type { InvalidTestCase } from "~/tests/helpers/util"; - -const tests: ReadonlyArray = [ - { - code: "function foo(arg: ReadonlySet) {}", - optionsSet: [[{ enforcement: "Immutable" }]], - errors: [ - { - messageId: "generic", - type: "Identifier", - line: 1, - column: 14, - }, - ], - }, - { - code: "function foo(arg: ReadonlyMap) {}", - optionsSet: [[{ enforcement: "Immutable" }]], - errors: [ - { - messageId: "generic", - type: "Identifier", - line: 1, - column: 14, - }, - ], - }, - { - code: "function foo(arg1: { foo: string }, arg2: { foo: number }) {}", - optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], - ], - errors: [ - { - messageId: "generic", - type: "Identifier", - line: 1, - column: 14, - }, - { - messageId: "generic", - type: "Identifier", - line: 1, - column: 37, - }, - ], - }, - { - code: dedent` - class Foo { - constructor( - private arg1: readonly string[], - public arg2: readonly string[], - protected arg3: readonly string[], - readonly arg4: readonly string[], - ) {} - } - `, - optionsSet: [[{ enforcement: "Immutable" }]], - errors: [ - { - messageId: "generic", - type: "Identifier", - line: 3, - column: 13, - }, - { - messageId: "generic", - type: "Identifier", - line: 4, - column: 12, - }, - { - messageId: "generic", - type: "Identifier", - line: 5, - column: 15, - }, - { - messageId: "generic", - type: "Identifier", - line: 6, - column: 14, - }, - ], - }, - { - code: dedent` - interface Foo { - (arg: readonly string[]): void; - } - `, - optionsSet: [[{ enforcement: "Immutable" }]], - errors: [ - { - messageId: "generic", - type: "Identifier", - line: 2, - column: 4, - }, - ], - }, - { - code: dedent` - interface Foo { - new (arg: readonly string[]): void; - } - `, - optionsSet: [[{ enforcement: "Immutable" }]], - errors: [ - { - messageId: "generic", - type: "Identifier", - line: 2, - column: 8, - }, - ], - }, -]; - -export default tests; diff --git a/tests/rules/prefer-immutable-parameter-types/index.test.ts b/tests/rules/prefer-immutable-types/index.test.ts similarity index 63% rename from tests/rules/prefer-immutable-parameter-types/index.test.ts rename to tests/rules/prefer-immutable-types/index.test.ts index 43c90fa31..c33c04961 100644 --- a/tests/rules/prefer-immutable-parameter-types/index.test.ts +++ b/tests/rules/prefer-immutable-types/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/prefer-immutable-parameter-types"; +import { name, rule } from "~/rules/prefer-immutable-types"; import { testUsing } from "~/tests/helpers/testers"; import tsTests from "./ts"; diff --git a/tests/rules/prefer-immutable-types/ts/index.ts b/tests/rules/prefer-immutable-types/ts/index.ts new file mode 100644 index 000000000..8f2d2eed8 --- /dev/null +++ b/tests/rules/prefer-immutable-types/ts/index.ts @@ -0,0 +1,12 @@ +import parameters from "./parameters"; +import returnTypes from "./return-types"; +import variables from "./variables"; + +export default { + valid: [...parameters.valid, ...returnTypes.valid, ...variables.valid], + invalid: [ + ...parameters.invalid, + ...returnTypes.invalid, + ...variables.invalid, + ], +}; diff --git a/tests/rules/prefer-immutable-parameter-types/ts/index.ts b/tests/rules/prefer-immutable-types/ts/parameters/index.ts similarity index 100% rename from tests/rules/prefer-immutable-parameter-types/ts/index.ts rename to tests/rules/prefer-immutable-types/ts/parameters/index.ts diff --git a/tests/rules/prefer-immutable-types/ts/parameters/invalid.ts b/tests/rules/prefer-immutable-types/ts/parameters/invalid.ts new file mode 100644 index 000000000..3f83b14ee --- /dev/null +++ b/tests/rules/prefer-immutable-types/ts/parameters/invalid.ts @@ -0,0 +1,167 @@ +import dedent from "dedent"; + +import type { InvalidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + { + code: "function foo(arg: ReadonlySet) {}", + optionsSet: [[{ parameters: "Immutable" }]], + errors: [ + { + messageId: "parameter", + type: "Identifier", + line: 1, + column: 14, + }, + ], + }, + { + code: "function foo(arg: ReadonlyMap) {}", + optionsSet: [[{ parameters: "Immutable" }]], + errors: [ + { + messageId: "parameter", + type: "Identifier", + line: 1, + column: 14, + }, + ], + }, + { + code: "function foo(arg1: { foo: string }, arg2: { foo: number }) {}", + optionsSet: [ + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], + ], + errors: [ + { + messageId: "parameter", + type: "Identifier", + line: 1, + column: 14, + }, + { + messageId: "parameter", + type: "Identifier", + line: 1, + column: 37, + }, + ], + }, + { + code: dedent` + class Foo { + constructor( + private readonly arg1: readonly string[], + public readonly arg2: readonly string[], + protected readonly arg3: readonly string[], + readonly arg4: readonly string[], + ) {} + } + `, + optionsSet: [[{ parameters: "Immutable" }]], + errors: [ + { + messageId: "parameter", + type: "Identifier", + line: 3, + column: 22, + }, + { + messageId: "parameter", + type: "Identifier", + line: 4, + column: 21, + }, + { + messageId: "parameter", + type: "Identifier", + line: 5, + column: 24, + }, + { + messageId: "parameter", + type: "Identifier", + line: 6, + column: 14, + }, + ], + }, + { + code: dedent` + interface Foo { + (arg: readonly string[]): void; + } + `, + optionsSet: [[{ parameters: "Immutable" }]], + errors: [ + { + messageId: "parameter", + type: "Identifier", + line: 2, + column: 4, + }, + ], + }, + { + code: dedent` + interface Foo { + new (arg: readonly string[]): void; + } + `, + optionsSet: [[{ parameters: "Immutable" }]], + errors: [ + { + messageId: "parameter", + type: "Identifier", + line: 2, + column: 8, + }, + ], + }, + // Class Parameter Properties. + { + code: dedent` + class Klass { + constructor ( + public publicProp: string, + protected protectedProp: string, + private privateProp: string, + ) { } + } + `, + optionsSet: [[]], + // output: dedent` + // class Klass { + // constructor ( + // public readonly publicProp: string, + // protected readonly protectedProp: string, + // private readonly privateProp: string, + // ) { } + // } + // `, + errors: [ + { + messageId: "propertyModifier", + type: "TSParameterProperty", + line: 3, + column: 5, + }, + { + messageId: "propertyModifier", + type: "TSParameterProperty", + line: 4, + column: 5, + }, + { + messageId: "propertyModifier", + type: "TSParameterProperty", + line: 5, + column: 5, + }, + ], + }, +]; + +export default tests; diff --git a/tests/rules/prefer-immutable-parameter-types/ts/valid.ts b/tests/rules/prefer-immutable-types/ts/parameters/valid.ts similarity index 50% rename from tests/rules/prefer-immutable-parameter-types/ts/valid.ts rename to tests/rules/prefer-immutable-types/ts/parameters/valid.ts index 0b5cf6ea9..1fa30d368 100644 --- a/tests/rules/prefer-immutable-parameter-types/ts/valid.ts +++ b/tests/rules/prefer-immutable-types/ts/parameters/valid.ts @@ -6,49 +6,49 @@ const tests: ReadonlyArray = [ { code: "function foo(arg: boolean) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], }, { code: "function foo(arg: true) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], }, { code: "function foo(arg: string) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], }, { code: "function foo(arg: 'bar') {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], }, { code: "function foo(arg: 'undefined') {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], }, { code: "function foo(arg: readonly string[]) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], settingsSet: [ { @@ -66,98 +66,98 @@ const tests: ReadonlyArray = [ { code: "function foo(arg: ReadonlyArray) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { code: "function foo(arg: readonly [string, number]) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { code: "function foo(arg: Readonly<[string, number]>) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { code: "function foo(arg: { readonly foo: string }) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], }, { code: "function foo(arg: { readonly foo: { readonly bar: number } }) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], }, { code: "function foo(arg: { foo(): void }) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { code: "function foo(arg: { foo: () => void }) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { code: "function foo(arg: ReadonlySet) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { code: "function foo(arg: ReadonlyMap) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { code: "function foo(arg: Readonly>) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], }, { code: "function foo(arg: Readonly>) {}", optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], - [{ enforcement: "Immutable" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], + [{ parameters: "Immutable" }], ], }, { code: dedent` class Foo { constructor( - private arg1: readonly string[], - public arg2: readonly string[], - protected arg3: readonly string[], + private readonly arg1: readonly string[], + public readonly arg2: readonly string[], + protected readonly arg3: readonly string[], readonly arg4: readonly string[], ) {} } `, optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { @@ -167,8 +167,8 @@ const tests: ReadonlyArray = [ } `, optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { @@ -178,8 +178,8 @@ const tests: ReadonlyArray = [ } `, optionsSet: [ - [{ enforcement: "ReadonlyShallow" }], - [{ enforcement: "ReadonlyDeep" }], + [{ parameters: "ReadonlyShallow" }], + [{ parameters: "ReadonlyDeep" }], ], }, { @@ -192,9 +192,27 @@ const tests: ReadonlyArray = [ acceptsCallback(options => {}); `, optionsSet: [ - [{ enforcement: "ReadonlyShallow", ignoreInferredTypes: true }], - [{ enforcement: "ReadonlyDeep", ignoreInferredTypes: true }], - [{ enforcement: "Immutable", ignoreInferredTypes: true }], + [ + { + parameters: { + enforcement: "ReadonlyShallow", + ignoreInferredTypes: true, + }, + }, + ], + [ + { + parameters: { + enforcement: "ReadonlyDeep", + ignoreInferredTypes: true, + }, + }, + ], + [ + { + parameters: { enforcement: "Immutable", ignoreInferredTypes: true }, + }, + ], ], }, ]; diff --git a/tests/rules/prefer-immutable-types/ts/return-types/index.ts b/tests/rules/prefer-immutable-types/ts/return-types/index.ts new file mode 100644 index 000000000..40a005f71 --- /dev/null +++ b/tests/rules/prefer-immutable-types/ts/return-types/index.ts @@ -0,0 +1,7 @@ +import invalid from "./invalid"; +import valid from "./valid"; + +export default { + valid, + invalid, +}; diff --git a/tests/rules/prefer-immutable-types/ts/return-types/invalid.ts b/tests/rules/prefer-immutable-types/ts/return-types/invalid.ts new file mode 100644 index 000000000..4ece68133 --- /dev/null +++ b/tests/rules/prefer-immutable-types/ts/return-types/invalid.ts @@ -0,0 +1,135 @@ +import dedent from "dedent"; + +import type { InvalidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + { + code: "function foo(): ReadonlySet {}", + optionsSet: [[{ returnTypes: "Immutable" }]], + errors: [ + { + messageId: "returnType", + type: "TSTypeAnnotation", + line: 1, + column: 15, + }, + ], + }, + { + code: "function foo(): ReadonlyMap {}", + optionsSet: [[{ returnTypes: "Immutable" }]], + errors: [ + { + messageId: "returnType", + type: "TSTypeAnnotation", + line: 1, + column: 15, + }, + ], + }, + { + code: "function foo() { return { foo: 'bar' }; }", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + errors: [ + { + messageId: "returnType", + type: "Identifier", + line: 1, + column: 10, + }, + ], + }, + { + code: dedent` + function foo(arg: number): { foo: string }; + function foo(arg: string): Readonly<{ foo: number }>; + function foo(arg: unknown): { foo: number }; + function foo(arg: unknown) {} + `, + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + errors: [ + { + messageId: "returnType", + type: "TSTypeAnnotation", + line: 1, + column: 26, + }, + { + messageId: "returnType", + type: "TSTypeAnnotation", + line: 3, + column: 27, + }, + { + messageId: "returnType", + type: "Identifier", + line: 4, + column: 10, + }, + ], + }, + { + code: dedent` + function foo(arg: number): { foo: string }; + function foo(arg: string): Readonly<{ foo: number }>; + function foo(arg: number | string) {} + `, + optionsSet: [ + [ + { + returnTypes: { enforcement: "Immutable", ignoreInferredTypes: true }, + }, + ], + ], + errors: [ + { + messageId: "returnType", + type: "TSTypeAnnotation", + line: 1, + column: 26, + }, + ], + }, + { + code: dedent` + interface Foo { + (arg: string): readonly string[]; + } + `, + optionsSet: [[{ returnTypes: "Immutable" }]], + errors: [ + { + messageId: "returnType", + type: "TSTypeAnnotation", + line: 2, + column: 16, + }, + ], + }, + { + code: dedent` + interface Foo { + new (arg: string): readonly string[]; + } + `, + optionsSet: [[{ returnTypes: "Immutable" }]], + errors: [ + { + messageId: "returnType", + type: "TSTypeAnnotation", + line: 2, + column: 20, + }, + ], + }, +]; + +export default tests; diff --git a/tests/rules/prefer-immutable-types/ts/return-types/valid.ts b/tests/rules/prefer-immutable-types/ts/return-types/valid.ts new file mode 100644 index 000000000..f9899e847 --- /dev/null +++ b/tests/rules/prefer-immutable-types/ts/return-types/valid.ts @@ -0,0 +1,197 @@ +import dedent from "dedent"; + +import type { ValidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + { + code: "function foo(): boolean {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + }, + { + code: "function foo(): true {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + }, + { + code: "function foo(): string {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + }, + { + code: "function foo(): 'bar' {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + }, + { + code: "function foo(): 'undefined' {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + }, + { + code: "function foo(): readonly string[] {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + settingsSet: [ + { + immutability: { + overrides: [ + { + name: "ReadonlyArray", + to: "Immutable", + }, + ], + }, + }, + ], + }, + { + code: "function foo(): ReadonlyArray {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(): readonly [string, number] {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(): Readonly<[string, number]> {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(): { readonly foo: string } {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + }, + { + code: "function foo(): { readonly foo: { readonly bar: number } } {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + }, + { + code: "function foo(): { foo(): void } {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(): { foo: () => void } {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(): ReadonlySet {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(): ReadonlyMap {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + ], + }, + { + code: "function foo(): Readonly> {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + }, + { + code: "function foo(): Readonly> {}", + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + [{ returnTypes: "Immutable" }], + ], + }, + { + code: dedent` + interface Foo { + (): readonly string[]; + } + `, + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + ], + }, + { + code: dedent` + interface Foo { + new (): readonly string[]; + } + `, + optionsSet: [ + [{ returnTypes: "ReadonlyShallow" }], + [{ returnTypes: "ReadonlyDeep" }], + ], + }, + { + code: "function foo() { return { foo: 'bar' }; }", + optionsSet: [ + [ + { + returnTypes: { + enforcement: "ReadonlyShallow", + ignoreInferredTypes: true, + }, + }, + ], + [ + { + returnTypes: { + enforcement: "ReadonlyDeep", + ignoreInferredTypes: true, + }, + }, + ], + [ + { + returnTypes: { enforcement: "Immutable", ignoreInferredTypes: true }, + }, + ], + ], + }, +]; + +export default tests; diff --git a/tests/rules/prefer-immutable-types/ts/variables/index.ts b/tests/rules/prefer-immutable-types/ts/variables/index.ts new file mode 100644 index 000000000..40a005f71 --- /dev/null +++ b/tests/rules/prefer-immutable-types/ts/variables/index.ts @@ -0,0 +1,7 @@ +import invalid from "./invalid"; +import valid from "./valid"; + +export default { + valid, + invalid, +}; diff --git a/tests/rules/prefer-immutable-types/ts/variables/invalid.ts b/tests/rules/prefer-immutable-types/ts/variables/invalid.ts new file mode 100644 index 000000000..bc2cb6b01 --- /dev/null +++ b/tests/rules/prefer-immutable-types/ts/variables/invalid.ts @@ -0,0 +1,179 @@ +import dedent from "dedent"; + +import type { InvalidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + { + code: "const foo: ReadonlySet = {} as any", + optionsSet: [[{ variables: "Immutable" }]], + errors: [ + { + messageId: "variable", + type: "Identifier", + line: 1, + column: 7, + }, + ], + }, + { + code: "const foo: ReadonlyMap = {} as any", + optionsSet: [[{ variables: "Immutable" }]], + errors: [ + { + messageId: "variable", + type: "Identifier", + line: 1, + column: 7, + }, + ], + }, + { + code: "const foo = { foo: 'bar' };", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + errors: [ + { + messageId: "variable", + type: "Identifier", + line: 1, + column: 7, + }, + ], + }, + { + code: dedent` + const foo: Readonly<{ foo: string }> = {} as any, + bar: { foo: number } = {} as any; + `, + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + errors: [ + { + messageId: "variable", + type: "Identifier", + line: 2, + column: 7, + }, + ], + }, + // Local. + { + code: dedent` + function foo() { + let foo: { + a: { foo: number }, + b: string[], + c: () => string[], + d: { [key: string]: string[] }, + [key: string]: any, + } + }; + `, + optionsSet: [[]], + errors: [ + { + messageId: "variable", + type: "Identifier", + line: 2, + column: 7, + }, + { + messageId: "returnType", + type: "TSTypeAnnotation", + line: 5, + column: 11, + }, + ], + }, + // Class Property Signatures. + { + code: dedent` + class Klass { + foo: number; + private bar: number; + static baz: number; + private static qux: number; + } + `, + optionsSet: [[]], + // output: dedent` + // class Klass { + // readonly foo: number; + // private readonly bar: number; + // static readonly baz: number; + // private static readonly qux: number; + // } + // `, + errors: [ + { + messageId: "propertyModifier", + type: "PropertyDefinition", + line: 2, + column: 3, + }, + { + messageId: "propertyModifier", + type: "PropertyDefinition", + line: 3, + column: 3, + }, + { + messageId: "propertyModifier", + type: "PropertyDefinition", + line: 4, + column: 3, + }, + { + messageId: "propertyModifier", + type: "PropertyDefinition", + line: 5, + column: 3, + }, + ], + }, + { + code: dedent` + class Klass { + readonly foo: { foo: number }; + private readonly bar: { foo: number }; + static readonly baz: { foo: number }; + private static readonly qux: { foo: number }; + } + `, + optionsSet: [[]], + errors: [ + { + messageId: "propertyImmutability", + type: "PropertyDefinition", + line: 2, + column: 3, + }, + { + messageId: "propertyImmutability", + type: "PropertyDefinition", + line: 3, + column: 3, + }, + { + messageId: "propertyImmutability", + type: "PropertyDefinition", + line: 4, + column: 3, + }, + { + messageId: "propertyImmutability", + type: "PropertyDefinition", + line: 5, + column: 3, + }, + ], + }, +]; + +export default tests; diff --git a/tests/rules/prefer-immutable-types/ts/variables/valid.ts b/tests/rules/prefer-immutable-types/ts/variables/valid.ts new file mode 100644 index 000000000..8fc2590ac --- /dev/null +++ b/tests/rules/prefer-immutable-types/ts/variables/valid.ts @@ -0,0 +1,234 @@ +import dedent from "dedent"; + +import type { ValidTestCase } from "~/tests/helpers/util"; + +const tests: ReadonlyArray = [ + { + code: "const foo: boolean = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + }, + { + code: "const foo: true = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + }, + { + code: "const foo: string = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + }, + { + code: "const foo: 'bar' = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + }, + { + code: "const foo: 'undefined' = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + }, + { + code: "const foo: readonly string[] = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + settingsSet: [ + { + immutability: { + overrides: [ + { + name: "ReadonlyArray", + to: "Immutable", + }, + ], + }, + }, + ], + }, + { + code: "const foo: ReadonlyArray = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + ], + }, + { + code: "const foo: readonly [string, number] = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + ], + }, + { + code: "const foo: Readonly<[string, number]> = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + ], + }, + { + code: "const foo: { readonly foo: string } = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + }, + { + code: "const foo: { readonly foo: { readonly bar: number } } = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + }, + { + code: "const foo: { foo(): void } = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + ], + }, + { + code: "const foo: { foo: () => void } = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + ], + }, + { + code: "const foo: ReadonlySet = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + ], + }, + { + code: "const foo: ReadonlyMap = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + ], + }, + { + code: "const foo: Readonly> = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + }, + { + code: "const foo: Readonly> = {} as any", + optionsSet: [ + [{ variables: "ReadonlyShallow" }], + [{ variables: "ReadonlyDeep" }], + [{ variables: "Immutable" }], + ], + }, + { + code: "const foo = { foo: 'bar' };", + optionsSet: [ + [ + { + variables: { + enforcement: "ReadonlyShallow", + ignoreInferredTypes: true, + }, + }, + ], + [ + { + variables: { + enforcement: "ReadonlyDeep", + ignoreInferredTypes: true, + }, + }, + ], + [ + { + variables: { enforcement: "Immutable", ignoreInferredTypes: true }, + }, + ], + ], + }, + // Ignore Classes. + { + code: dedent` + class Klass { + foo: number; + private bar: number; + static baz: number; + private static qux: number; + } + `, + optionsSet: [[{ ignoreClass: true }]], + }, + // Allow Local. + { + code: dedent` + function foo() { + let foo: { + a: { foo: number }, + b: string[], + c: () => string[], + d: { [key: string]: string[] }, + [key: string]: any, + } + }; + `, + optionsSet: [[{ allowLocalMutation: true }]], + }, + // Ignore Prefix. + { + code: dedent` + let mutableFoo: string[] = []; + `, + optionsSet: [[{ ignorePattern: "^mutable" }]], + }, + { + code: dedent` + class Klass { + mutableA: number; + private mutableB: number; + } + `, + optionsSet: [[{ ignorePattern: "^mutable" }]], + }, + // Ignore Suffix. + { + code: dedent` + let fooMutable: string[] = []; + `, + optionsSet: [[{ ignorePattern: "Mutable$" }]], + }, + { + code: dedent` + class Klass { + AMutable: number; + private BMutable: number; + } + `, + optionsSet: [[{ ignorePattern: "Mutable$" }]], + }, +]; + +export default tests; diff --git a/tests/rules/work.test.ts b/tests/rules/work.test.ts index 65f7feb77..5a7fbf30d 100644 --- a/tests/rules/work.test.ts +++ b/tests/rules/work.test.ts @@ -11,7 +11,7 @@ import { testUsing } from "~/tests/helpers/testers"; * Step 1. * Import the rule to test. */ -import { name, rule } from "~/rules/type-declaration-immutability"; +import { name, rule } from "~/rules/prefer-immutable-types"; /* * Step 2a. @@ -32,24 +32,18 @@ const valid: ReadonlyArray = [ * Or provide an invalid test case. */ const invalid: ReadonlyArray = [ - // { - // code: dedent` - // // Invalid Code. - // `, - // optionsSet: [[]], - // settingsSet: [{}] - // output: dedent` - // // Fixed Code - Remove if rule doesn't have a fixer. - // `, - // errors: [ - // { - // messageId: "generic", - // type: "ClassDeclaration", - // line: 2, - // column: 8 - // } - // ] - // } + { + code: "function foo(): ReadonlySet {}", + optionsSet: [[{ returnTypes: "Immutable" }]], + errors: [ + { + messageId: "returnType", + type: "TSTypeAnnotation", + line: 1, + column: 15, + }, + ], + }, ]; /* From 76a8e2d28e32cfcd5a6477f8552b5fb43442a79d Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 18:04:13 +1300 Subject: [PATCH 037/100] feat(no-classes)!: rename rule from `no-class` --- README.md | 2 +- docs/rules/no-class.md | 6 +++--- docs/rules/no-this-expression.md | 2 +- docs/user-guide/migrating-from-tslint.md | 2 +- src/configs/all.ts | 4 ++-- src/configs/no-other-paradigms.ts | 4 ++-- src/rules/index.ts | 4 ++-- src/rules/{no-class.ts => no-classes.ts} | 2 +- tests/rules/no-class.test/index.test.ts | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) rename src/rules/{no-class.ts => no-classes.ts} (97%) diff --git a/README.md b/README.md index 6e8de2a6f..d6bd378e9 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ The [below section](#rules) gives details on which rules are enabled by each rul | Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | | ---------------------------------------------------------- | ------------------------------------------------------------------ | :----------------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | -| [`no-class`](./docs/rules/no-class.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`no-classes`](./docs/rules/no-classes.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | | [`no-mixed-type`](./docs/rules/no-mixed-type.md) | Disallow types that contain both callable and non-callable members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | | [`no-this-expression`](./docs/rules/no-this-expression.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | | | | | diff --git a/docs/rules/no-class.md b/docs/rules/no-class.md index ac425721a..b0e44bbd2 100644 --- a/docs/rules/no-class.md +++ b/docs/rules/no-class.md @@ -1,4 +1,4 @@ -# Disallow classes (no-class) +# Disallow classes (no-classes) Disallow use of the `class` keyword. @@ -9,7 +9,7 @@ Disallow use of the `class` keyword. ```js -/* eslint functional/no-class: "error" */ +/* eslint functional/no-classes: "error" */ class Dog { constructor(name, age) { @@ -30,7 +30,7 @@ console.log(`${dogA.name} is ${dogA.ageInDogYears} in dog years.`); ### ✅ Correct ```js -/* eslint functional/no-class: "error" */ +/* eslint functional/no-classes: "error" */ function getAgeInDogYears(age) { return 7 * age; diff --git a/docs/rules/no-this-expression.md b/docs/rules/no-this-expression.md index a04aecd4b..fa434408f 100644 --- a/docs/rules/no-this-expression.md +++ b/docs/rules/no-this-expression.md @@ -2,7 +2,7 @@ ## Rule Details -This rule is companion rule to the [no-class](./no-class.md) rule. +This rule is companion rule to the [no-classes](./no-classes.md) rule. See the its docs for more info. ### ❌ Incorrect diff --git a/docs/user-guide/migrating-from-tslint.md b/docs/user-guide/migrating-from-tslint.md index 60af88289..6312d5c57 100644 --- a/docs/user-guide/migrating-from-tslint.md +++ b/docs/user-guide/migrating-from-tslint.md @@ -58,7 +58,7 @@ Below is a table mapping the `eslint-plugin-functional` rules to their `tslint-i | [`functional/immutable-data`](../rules/immutable-data.md) | `no-object-mutation`, `no-array-mutation` & `no-delete` | | [`functional/no-method-signature`](../rules/no-method-signature.md) | `no-method-signature` | | [`functional/no-this-expression`](../rules/no-this-expression.md) | `no-this` | -| [`functional/no-class`](../rules/no-class.md) | `no-class` | +| [`functional/no-classes`](../rules/no-classes.md) | `no-classes` | | [`functional/no-mixed-type`](../rules/no-mixed-type.md) | `no-mixed-interface` | | [`functional/no-expression-statement`](../rules/no-expression-statement.md) | `no-expression-statement` | | [`functional/no-conditional-statement`](../rules/no-conditional-statement.md) | `no-if-statement` | diff --git a/src/configs/all.ts b/src/configs/all.ts index 3a7f991cb..16e4308ac 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -2,7 +2,7 @@ import type { Linter } from "eslint"; import * as functionalParameters from "~/rules/functional-parameters"; import * as immutableData from "~/rules/immutable-data"; -import * as noClass from "~/rules/no-class"; +import * as noClasses from "~/rules/no-classes"; import * as noConditionalStatement from "~/rules/no-conditional-statement"; import * as noExpressionStatement from "~/rules/no-expression-statement"; import * as noLet from "~/rules/no-let"; @@ -22,7 +22,7 @@ const config: Linter.Config = { rules: { [`functional/${functionalParameters.name}`]: "error", [`functional/${immutableData.name}`]: "error", - [`functional/${noClass.name}`]: "error", + [`functional/${noClasses.name}`]: "error", [`functional/${noConditionalStatement.name}`]: "error", [`functional/${noExpressionStatement.name}`]: "error", [`functional/${noLet.name}`]: "error", diff --git a/src/configs/no-other-paradigms.ts b/src/configs/no-other-paradigms.ts index 53e53d632..47c2060d8 100644 --- a/src/configs/no-other-paradigms.ts +++ b/src/configs/no-other-paradigms.ts @@ -1,12 +1,12 @@ import type { Linter } from "eslint"; -import * as noClass from "~/rules/no-class"; +import * as noClasses from "~/rules/no-classes"; import * as noMixedType from "~/rules/no-mixed-type"; import * as noThisExpression from "~/rules/no-this-expression"; const config: Linter.Config = { rules: { - [`functional/${noClass.name}`]: "error", + [`functional/${noClasses.name}`]: "error", [`functional/${noMixedType.name}`]: "error", [`functional/${noThisExpression.name}`]: "error", }, diff --git a/src/rules/index.ts b/src/rules/index.ts index 5cea79fa9..b1358e296 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -1,6 +1,6 @@ import * as functionalParameters from "./functional-parameters"; import * as immutableData from "./immutable-data"; -import * as noClass from "./no-class"; +import * as noClasses from "./no-classes"; import * as noConditionalStatement from "./no-conditional-statement"; import * as noExpressionStatement from "./no-expression-statement"; import * as noLet from "./no-let"; @@ -23,7 +23,7 @@ import * as typeDeclarationImmutability from "./type-declaration-immutability"; export const rules = { [functionalParameters.name]: functionalParameters.rule, [immutableData.name]: immutableData.rule, - [noClass.name]: noClass.rule, + [noClasses.name]: noClasses.rule, [noConditionalStatement.name]: noConditionalStatement.rule, [noExpressionStatement.name]: noExpressionStatement.rule, [noLet.name]: noLet.rule, diff --git a/src/rules/no-class.ts b/src/rules/no-classes.ts similarity index 97% rename from src/rules/no-class.ts rename to src/rules/no-classes.ts index f5b955f4e..279aa033f 100644 --- a/src/rules/no-class.ts +++ b/src/rules/no-classes.ts @@ -9,7 +9,7 @@ import { createRule } from "~/util/rule"; /** * The name of this rule. */ -export const name = "no-class" as const; +export const name = "no-classes" as const; /** * The options this rule can take. diff --git a/tests/rules/no-class.test/index.test.ts b/tests/rules/no-class.test/index.test.ts index c4caef99a..997ad3e55 100644 --- a/tests/rules/no-class.test/index.test.ts +++ b/tests/rules/no-class.test/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/no-class"; +import { name, rule } from "~/rules/no-classes"; import { testUsing } from "~/tests/helpers/testers"; import es6Tests from "./es6"; From 82b21fa954ec8ead473bbb9bc10273d3cc927e40 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 18:05:41 +1300 Subject: [PATCH 038/100] feat(no-conditional-statements)!: rename rule from `no-conditional-statement` --- CHANGELOG.md | 10 +++--- README.md | 14 ++++---- docs/rules/no-conditional-statement.md | 8 ++--- docs/user-guide/migrating-from-tslint.md | 34 +++++++++---------- src/configs/all.ts | 4 +-- src/configs/lite.ts | 4 +-- src/configs/no-statements.ts | 4 +-- src/configs/recommended.ts | 4 +-- src/rules/index.ts | 4 +-- ...tement.ts => no-conditional-statements.ts} | 2 +- tests/.eslintrc.json | 2 +- .../no-conditional-statement/index.test.ts | 2 +- 12 files changed, 46 insertions(+), 46 deletions(-) rename src/rules/{no-conditional-statement.ts => no-conditional-statements.ts} (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f6b384e..3847137e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,13 +126,13 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). ### Fixed -- fix(no-conditional-statement): break/continue are no longer treated as returning inside of a switch [`#272`](https://github.com/eslint-functional/eslint-plugin-functional/issues/272) +- fix(no-conditional-statements): break/continue are no longer treated as returning inside of a switch [`#272`](https://github.com/eslint-functional/eslint-plugin-functional/issues/272) ## [v3.7.1](https://github.com/eslint-functional/eslint-plugin-functional/compare/v3.7.0...v3.7.1) - 2021-09-20 ### Fixed -- fix(no-conditional-statement): branch with break/continue statements now treated as returning branch [`#269`](https://github.com/eslint-functional/eslint-plugin-functional/issues/269) +- fix(no-conditional-statements): branch with break/continue statements now treated as returning branch [`#269`](https://github.com/eslint-functional/eslint-plugin-functional/issues/269) ## [v3.7.0](https://github.com/eslint-functional/eslint-plugin-functional/compare/v3.6.0...v3.7.0) - 2021-08-28 @@ -165,7 +165,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). ### Commits -- feat(no-conditional-statement): allow switches that exhaust all types [`35a72f1`](https://github.com/eslint-functional/eslint-plugin-functional/commit/35a72f1f9243aa5207851df1b5e5c25f0918e3bc) +- feat(no-conditional-statements): allow switches that exhaust all types [`35a72f1`](https://github.com/eslint-functional/eslint-plugin-functional/commit/35a72f1f9243aa5207851df1b5e5c25f0918e3bc) ## [v3.4.0](https://github.com/eslint-functional/eslint-plugin-functional/compare/v3.3.0...v3.4.0) - 2021-07-31 @@ -183,7 +183,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). ### Fixed -- feat(no-conditional-statement): support never-returning functions for option allowReturningBranches [`#99`](https://github.com/eslint-functional/eslint-plugin-functional/issues/99) +- feat(no-conditional-statements): support never-returning functions for option allowReturningBranches [`#99`](https://github.com/eslint-functional/eslint-plugin-functional/issues/99) ## [v3.2.2](https://github.com/eslint-functional/eslint-plugin-functional/compare/v3.2.1...v3.2.2) - 2021-07-23 @@ -414,7 +414,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - no-return-void [`#28`](https://github.com/eslint-functional/eslint-plugin-functional/pull/28) - functional-parameters [`#27`](https://github.com/eslint-functional/eslint-plugin-functional/pull/27) - feat(readonly-keyword) Add support for parameter properties [`#26`](https://github.com/eslint-functional/eslint-plugin-functional/pull/26) -- no-conditional-statement [`#23`](https://github.com/eslint-functional/eslint-plugin-functional/pull/23) +- no-conditional-statements [`#23`](https://github.com/eslint-functional/eslint-plugin-functional/pull/23) - chore: Remove no-delete rule. [`#21`](https://github.com/eslint-functional/eslint-plugin-functional/pull/21) - new rule: immutable-data [`#22`](https://github.com/eslint-functional/eslint-plugin-functional/pull/22) diff --git a/README.md b/README.md index d6bd378e9..2fca35342 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,12 @@ The [below section](#rules) gives details on which rules are enabled by each rul ### No Statements Rules -| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | -| ---------------------------------------------------------------------- | -------------------------------------------------------------- | :-----------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | -| [`no-conditional-statement`](./docs/rules/no-conditional-statement.md) | Disallow conditional statements (`if` and `switch` statements) | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | | | :thought_balloon: | -| [`no-expression-statement`](./docs/rules/no-expression-statement.md) | Disallow expressions to cause side-effects | :heavy_check_mark: | :heavy_check_mark: | | | | :thought_balloon: | -| [`no-loop-statement`](./docs/rules/no-loop-statement.md) | Disallow imperative loops | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| [`no-return-void`](./docs/rules/no-return-void.md) | Disallow functions that return nothing | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | +| ------------------------------------------------------------------------ | -------------------------------------------------------------- | :-----------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | +| [`no-conditional-statements`](./docs/rules/no-conditional-statements.md) | Disallow conditional statements (`if` and `switch` statements) | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | | | :thought_balloon: | +| [`no-expression-statement`](./docs/rules/no-expression-statement.md) | Disallow expressions to cause side-effects | :heavy_check_mark: | :heavy_check_mark: | | | | :thought_balloon: | +| [`no-loop-statement`](./docs/rules/no-loop-statement.md) | Disallow imperative loops | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`no-return-void`](./docs/rules/no-return-void.md) | Disallow functions that return nothing | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | ### No Exceptions Rules @@ -158,7 +158,7 @@ These rules are what are included in the _external recommended_ rulesets. This rule is helpful when working with classes. - [@typescript-eslint/switch-exhaustiveness-check](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md)\ - Although our [no-conditional-statement](./docs/rules/no-conditional-statement.md) rule also performs this check, this rule has a fixer that will implement the unimplemented cases which can be useful. + Although our [no-conditional-statements](./docs/rules/no-conditional-statements.md) rule also performs this check, this rule has a fixer that will implement the unimplemented cases which can be useful. ## Prior work diff --git a/docs/rules/no-conditional-statement.md b/docs/rules/no-conditional-statement.md index 0008fa5f7..6b3918030 100644 --- a/docs/rules/no-conditional-statement.md +++ b/docs/rules/no-conditional-statement.md @@ -1,4 +1,4 @@ -# Disallow conditional statements (no-conditional-statement) +# Disallow conditional statements (no-conditional-statements) This rule disallows conditional statements such as `if` and `switch`. @@ -14,7 +14,7 @@ For more background see this [blog post](https://hackernoon.com/rethinking-javas ```js -/* eslint functional/no-conditional-statement: "error" */ +/* eslint functional/no-conditional-statements: "error" */ let x; if (i === 1) { @@ -27,13 +27,13 @@ if (i === 1) { ### ✅ Correct ```js -/* eslint functional/no-conditional-statement: "error" */ +/* eslint functional/no-conditional-statements: "error" */ const x = i === 1 ? 2 : 3; ``` ```js -/* eslint functional/no-conditional-statement: "error" */ +/* eslint functional/no-conditional-statements: "error" */ function foo(x, y) { return ( diff --git a/docs/user-guide/migrating-from-tslint.md b/docs/user-guide/migrating-from-tslint.md index 6312d5c57..5adf77a92 100644 --- a/docs/user-guide/migrating-from-tslint.md +++ b/docs/user-guide/migrating-from-tslint.md @@ -51,20 +51,20 @@ In order for the parser to have access to type information, it needs access to y Below is a table mapping the `eslint-plugin-functional` rules to their `tslint-immutable` equivalents. -| `eslint-plugin-functional` Rule | Equivalent `tslint-immutable` Rules | -| ----------------------------------------------------------------------------- | ------------------------------------------------------- | -| [`functional/prefer-readonly-type`](../rules/prefer-readonly-type.md) | `readonly-keyword` & `readonly-array` | -| [`functional/no-let`](../rules/no-let.md) | `no-let` | -| [`functional/immutable-data`](../rules/immutable-data.md) | `no-object-mutation`, `no-array-mutation` & `no-delete` | -| [`functional/no-method-signature`](../rules/no-method-signature.md) | `no-method-signature` | -| [`functional/no-this-expression`](../rules/no-this-expression.md) | `no-this` | -| [`functional/no-classes`](../rules/no-classes.md) | `no-classes` | -| [`functional/no-mixed-type`](../rules/no-mixed-type.md) | `no-mixed-interface` | -| [`functional/no-expression-statement`](../rules/no-expression-statement.md) | `no-expression-statement` | -| [`functional/no-conditional-statement`](../rules/no-conditional-statement.md) | `no-if-statement` | -| [`functional/no-loop-statement`](../rules/no-loop-statement.md) | `no-loop-statement` | -| [`functional/no-return-void`](../rules/no-return-void.md) | - | -| [`functional/no-throw-statement`](../rules/no-throw-statement.md) | `no-throw` | -| [`functional/no-try-statement`](../rules/no-try-statement.md) | `no-try` | -| [`functional/no-promise-reject`](../rules/no-promise-reject.md) | `no-reject` | -| [`functional/functional-parameters`](../rules/functional-parameters.md) | - | +| `eslint-plugin-functional` Rule | Equivalent `tslint-immutable` Rules | +| ------------------------------------------------------------------------------- | ------------------------------------------------------- | +| [`functional/prefer-readonly-type`](../rules/prefer-readonly-type.md) | `readonly-keyword` & `readonly-array` | +| [`functional/no-let`](../rules/no-let.md) | `no-let` | +| [`functional/immutable-data`](../rules/immutable-data.md) | `no-object-mutation`, `no-array-mutation` & `no-delete` | +| [`functional/no-method-signature`](../rules/no-method-signature.md) | `no-method-signature` | +| [`functional/no-this-expression`](../rules/no-this-expression.md) | `no-this` | +| [`functional/no-classes`](../rules/no-classes.md) | `no-classes` | +| [`functional/no-mixed-type`](../rules/no-mixed-type.md) | `no-mixed-interface` | +| [`functional/no-expression-statement`](../rules/no-expression-statement.md) | `no-expression-statement` | +| [`functional/no-conditional-statements`](../rules/no-conditional-statements.md) | `no-if-statement` | +| [`functional/no-loop-statement`](../rules/no-loop-statement.md) | `no-loop-statement` | +| [`functional/no-return-void`](../rules/no-return-void.md) | - | +| [`functional/no-throw-statement`](../rules/no-throw-statement.md) | `no-throw` | +| [`functional/no-try-statement`](../rules/no-try-statement.md) | `no-try` | +| [`functional/no-promise-reject`](../rules/no-promise-reject.md) | `no-reject` | +| [`functional/functional-parameters`](../rules/functional-parameters.md) | - | diff --git a/src/configs/all.ts b/src/configs/all.ts index 16e4308ac..023cd135a 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -3,7 +3,7 @@ import type { Linter } from "eslint"; import * as functionalParameters from "~/rules/functional-parameters"; import * as immutableData from "~/rules/immutable-data"; import * as noClasses from "~/rules/no-classes"; -import * as noConditionalStatement from "~/rules/no-conditional-statement"; +import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noExpressionStatement from "~/rules/no-expression-statement"; import * as noLet from "~/rules/no-let"; import * as noLoop from "~/rules/no-loop-statement"; @@ -23,7 +23,7 @@ const config: Linter.Config = { [`functional/${functionalParameters.name}`]: "error", [`functional/${immutableData.name}`]: "error", [`functional/${noClasses.name}`]: "error", - [`functional/${noConditionalStatement.name}`]: "error", + [`functional/${noConditionalStatements.name}`]: "error", [`functional/${noExpressionStatement.name}`]: "error", [`functional/${noLet.name}`]: "error", [`functional/${noLoop.name}`]: "error", diff --git a/src/configs/lite.ts b/src/configs/lite.ts index 787cb3d39..d5d0cdb2a 100644 --- a/src/configs/lite.ts +++ b/src/configs/lite.ts @@ -2,7 +2,7 @@ import type { Linter } from "eslint"; import * as functionalParameters from "~/rules/functional-parameters"; import * as immutableData from "~/rules/immutable-data"; -import * as noConditionalStatement from "~/rules/no-conditional-statement"; +import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noExpressionStatement from "~/rules/no-expression-statement"; import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import { mergeConfigs } from "~/util/merge-configs"; @@ -21,7 +21,7 @@ const overrides: Linter.Config = { "error", { ignoreClass: "fieldsOnly" }, ], - [`functional/${noConditionalStatement.name}`]: "off", + [`functional/${noConditionalStatements.name}`]: "off", [`functional/${noExpressionStatement.name}`]: "off", [`functional/${preferImmutableTypes.name}`]: [ "error", diff --git a/src/configs/no-statements.ts b/src/configs/no-statements.ts index 6e9cd3d58..82d66b592 100644 --- a/src/configs/no-statements.ts +++ b/src/configs/no-statements.ts @@ -1,13 +1,13 @@ import type { Linter } from "eslint"; -import * as noConditionalStatement from "~/rules/no-conditional-statement"; +import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noExpressionStatement from "~/rules/no-expression-statement"; import * as noLoop from "~/rules/no-loop-statement"; import * as noReturnVoid from "~/rules/no-return-void"; const config: Linter.Config = { rules: { - [`functional/${noConditionalStatement.name}`]: "error", + [`functional/${noConditionalStatements.name}`]: "error", [`functional/${noExpressionStatement.name}`]: "error", [`functional/${noLoop.name}`]: "error", [`functional/${noReturnVoid.name}`]: "error", diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 8fdba9dab..7bd8fa27a 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -2,7 +2,7 @@ import type { Linter } from "eslint"; import { Immutability } from "is-immutable-type"; import * as functionalParameters from "~/rules/functional-parameters"; -import * as noConditionalStatement from "~/rules/no-conditional-statement"; +import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noLet from "~/rules/no-let"; import * as noThisExpression from "~/rules/no-this-expression"; import * as noThrowStatement from "~/rules/no-throw-statement"; @@ -25,7 +25,7 @@ const overrides: Linter.Config = { }, }, ], - [`functional/${noConditionalStatement.name}`]: [ + [`functional/${noConditionalStatements.name}`]: [ "error", { allowReturningBranches: true, diff --git a/src/rules/index.ts b/src/rules/index.ts index b1358e296..2ded115a6 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -1,7 +1,7 @@ import * as functionalParameters from "./functional-parameters"; import * as immutableData from "./immutable-data"; import * as noClasses from "./no-classes"; -import * as noConditionalStatement from "./no-conditional-statement"; +import * as noConditionalStatements from "./no-conditional-statements"; import * as noExpressionStatement from "./no-expression-statement"; import * as noLet from "./no-let"; import * as noLoop from "./no-loop-statement"; @@ -24,7 +24,7 @@ export const rules = { [functionalParameters.name]: functionalParameters.rule, [immutableData.name]: immutableData.rule, [noClasses.name]: noClasses.rule, - [noConditionalStatement.name]: noConditionalStatement.rule, + [noConditionalStatements.name]: noConditionalStatements.rule, [noExpressionStatement.name]: noExpressionStatement.rule, [noLet.name]: noLet.rule, [noLoop.name]: noLoop.rule, diff --git a/src/rules/no-conditional-statement.ts b/src/rules/no-conditional-statements.ts similarity index 99% rename from src/rules/no-conditional-statement.ts rename to src/rules/no-conditional-statements.ts index edcc32c44..cf6e684a8 100644 --- a/src/rules/no-conditional-statement.ts +++ b/src/rules/no-conditional-statements.ts @@ -21,7 +21,7 @@ import { /** * The name of this rule. */ -export const name = "no-conditional-statement" as const; +export const name = "no-conditional-statements" as const; /** * The options this rule can take. diff --git a/tests/.eslintrc.json b/tests/.eslintrc.json index 0967526bc..6edc67da3 100644 --- a/tests/.eslintrc.json +++ b/tests/.eslintrc.json @@ -8,7 +8,7 @@ "eslint-comments/disable-enable-pair": "off", "eslint-comments/no-unlimited-disable": "off", "functional/functional-parameters": "off", - "functional/no-conditional-statement": "off", + "functional/no-conditional-statements": "off", "functional/no-expression-statement": "off", "functional/no-loop-statement": "off", "functional/no-return-void": "off", diff --git a/tests/rules/no-conditional-statement/index.test.ts b/tests/rules/no-conditional-statement/index.test.ts index a129abbda..d7ab033fc 100644 --- a/tests/rules/no-conditional-statement/index.test.ts +++ b/tests/rules/no-conditional-statement/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/no-conditional-statement"; +import { name, rule } from "~/rules/no-conditional-statements"; import { testUsing } from "~/tests/helpers/testers"; import es3Tests from "./es3"; From d0f9e98f451b881a1df69e92ce78abf18f2c76f6 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 18:07:34 +1300 Subject: [PATCH 039/100] feat(no-expression-statements)!: rename rule from `no-expression-statement` --- .eslintrc.json | 2 +- CHANGELOG.md | 6 +++--- README.md | 2 +- docs/rules/no-expression-statement.md | 12 ++++++------ docs/user-guide/migrating-from-tslint.md | 2 +- src/configs/all.ts | 4 ++-- src/configs/lite.ts | 4 ++-- src/configs/no-statements.ts | 4 ++-- src/rules/index.ts | 4 ++-- ...sion-statement.ts => no-expression-statements.ts} | 2 +- src/settings/immutability.ts | 2 +- src/util/rule.ts | 4 ++-- tests/.eslintrc.json | 2 +- tests/rules/no-expression-statement/index.test.ts | 2 +- 14 files changed, 26 insertions(+), 26 deletions(-) rename src/rules/{no-expression-statement.ts => no-expression-statements.ts} (97%) diff --git a/.eslintrc.json b/.eslintrc.json index 82d053783..105582954 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,7 +45,7 @@ "files": ["./src/**/*"], "extends": ["plugin:eslint-plugin/recommended"], "rules": { - "functional/no-expression-statement": "error" + "functional/no-expression-statements": "error" } }, { diff --git a/CHANGELOG.md b/CHANGELOG.md index 3847137e0..fe1e027e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -150,7 +150,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). ### Fixed -- feat(no-expression-statement): add option ignoreVoid [`#71`](https://github.com/eslint-functional/eslint-plugin-functional/issues/71) +- feat(no-expression-statements): add option ignoreVoid [`#71`](https://github.com/eslint-functional/eslint-plugin-functional/issues/71) - docs: update tslint migration guide [`#214`](https://github.com/eslint-functional/eslint-plugin-functional/issues/214) ## [v3.5.0](https://github.com/eslint-functional/eslint-plugin-functional/compare/v3.4.1...v3.5.0) - 2021-08-01 @@ -312,11 +312,11 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). ### Merged -- feat(no-expression-statement): allow specifying directive prologues [`#74`](https://github.com/eslint-functional/eslint-plugin-functional/pull/74) +- feat(no-expression-statements): allow specifying directive prologues [`#74`](https://github.com/eslint-functional/eslint-plugin-functional/pull/74) ### Fixed -- fix(no-expression-statement): allow specifying directive prologues [`#68`](https://github.com/eslint-functional/eslint-plugin-functional/issues/68) +- fix(no-expression-statements): allow specifying directive prologues [`#68`](https://github.com/eslint-functional/eslint-plugin-functional/issues/68) ## [v1.0.1](https://github.com/eslint-functional/eslint-plugin-functional/compare/v1.0.0...v1.0.1) - 2019-12-11 diff --git a/README.md b/README.md index 2fca35342..dea5e29ed 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ The [below section](#rules) gives details on which rules are enabled by each rul | Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | | ------------------------------------------------------------------------ | -------------------------------------------------------------- | :-----------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | | [`no-conditional-statements`](./docs/rules/no-conditional-statements.md) | Disallow conditional statements (`if` and `switch` statements) | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | | | :thought_balloon: | -| [`no-expression-statement`](./docs/rules/no-expression-statement.md) | Disallow expressions to cause side-effects | :heavy_check_mark: | :heavy_check_mark: | | | | :thought_balloon: | +| [`no-expression-statements`](./docs/rules/no-expression-statements.md) | Disallow expressions to cause side-effects | :heavy_check_mark: | :heavy_check_mark: | | | | :thought_balloon: | | [`no-loop-statement`](./docs/rules/no-loop-statement.md) | Disallow imperative loops | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | | [`no-return-void`](./docs/rules/no-return-void.md) | Disallow functions that return nothing | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | diff --git a/docs/rules/no-expression-statement.md b/docs/rules/no-expression-statement.md index e6b1a1682..e5318823c 100644 --- a/docs/rules/no-expression-statement.md +++ b/docs/rules/no-expression-statement.md @@ -1,4 +1,4 @@ -# Using expressions to cause side-effects not allowed (no-expression-statement) +# Using expressions to cause side-effects not allowed (no-expression-statements) This rule checks that the value of an expression is assigned to a variable and thus helps promote side-effect free (pure) functions. @@ -11,7 +11,7 @@ When you call a function and don’t use it’s return value, chances are high t ```js -/* eslint functional/no-expression-statement: "error" */ +/* eslint functional/no-expression-statements: "error" */ console.log("Hello world!"); ``` @@ -19,7 +19,7 @@ console.log("Hello world!"); ```js -/* eslint functional/no-expression-statement: "error" */ +/* eslint functional/no-expression-statements: "error" */ array.push(3); ``` @@ -27,7 +27,7 @@ array.push(3); ```js -/* eslint functional/no-expression-statement: "error" */ +/* eslint functional/no-expression-statements: "error" */ foo(bar); ``` @@ -35,7 +35,7 @@ foo(bar); ### ✅ Correct ```js -/* eslint functional/no-expression-statement: "error" */ +/* eslint functional/no-expression-statements: "error" */ const baz = foo(bar); ``` @@ -43,7 +43,7 @@ const baz = foo(bar); ```js -/* eslint functional/no-expression-statement: ["error", { "ignoreVoid": true }] */ +/* eslint functional/no-expression-statements: ["error", { "ignoreVoid": true }] */ console.log("hello world"); ``` diff --git a/docs/user-guide/migrating-from-tslint.md b/docs/user-guide/migrating-from-tslint.md index 5adf77a92..54bf4b6d4 100644 --- a/docs/user-guide/migrating-from-tslint.md +++ b/docs/user-guide/migrating-from-tslint.md @@ -60,7 +60,7 @@ Below is a table mapping the `eslint-plugin-functional` rules to their `tslint-i | [`functional/no-this-expression`](../rules/no-this-expression.md) | `no-this` | | [`functional/no-classes`](../rules/no-classes.md) | `no-classes` | | [`functional/no-mixed-type`](../rules/no-mixed-type.md) | `no-mixed-interface` | -| [`functional/no-expression-statement`](../rules/no-expression-statement.md) | `no-expression-statement` | +| [`functional/no-expression-statements`](../rules/no-expression-statements.md) | `no-expression-statements` | | [`functional/no-conditional-statements`](../rules/no-conditional-statements.md) | `no-if-statement` | | [`functional/no-loop-statement`](../rules/no-loop-statement.md) | `no-loop-statement` | | [`functional/no-return-void`](../rules/no-return-void.md) | - | diff --git a/src/configs/all.ts b/src/configs/all.ts index 023cd135a..ed1dd279b 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -4,7 +4,7 @@ import * as functionalParameters from "~/rules/functional-parameters"; import * as immutableData from "~/rules/immutable-data"; import * as noClasses from "~/rules/no-classes"; import * as noConditionalStatements from "~/rules/no-conditional-statements"; -import * as noExpressionStatement from "~/rules/no-expression-statement"; +import * as noExpressionStatements from "~/rules/no-expression-statements"; import * as noLet from "~/rules/no-let"; import * as noLoop from "~/rules/no-loop-statement"; import * as noMixedType from "~/rules/no-mixed-type"; @@ -24,7 +24,7 @@ const config: Linter.Config = { [`functional/${immutableData.name}`]: "error", [`functional/${noClasses.name}`]: "error", [`functional/${noConditionalStatements.name}`]: "error", - [`functional/${noExpressionStatement.name}`]: "error", + [`functional/${noExpressionStatements.name}`]: "error", [`functional/${noLet.name}`]: "error", [`functional/${noLoop.name}`]: "error", [`functional/${noMixedType.name}`]: "error", diff --git a/src/configs/lite.ts b/src/configs/lite.ts index d5d0cdb2a..e1c441f80 100644 --- a/src/configs/lite.ts +++ b/src/configs/lite.ts @@ -3,7 +3,7 @@ import type { Linter } from "eslint"; import * as functionalParameters from "~/rules/functional-parameters"; import * as immutableData from "~/rules/immutable-data"; import * as noConditionalStatements from "~/rules/no-conditional-statements"; -import * as noExpressionStatement from "~/rules/no-expression-statement"; +import * as noExpressionStatements from "~/rules/no-expression-statements"; import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import { mergeConfigs } from "~/util/merge-configs"; @@ -22,7 +22,7 @@ const overrides: Linter.Config = { { ignoreClass: "fieldsOnly" }, ], [`functional/${noConditionalStatements.name}`]: "off", - [`functional/${noExpressionStatement.name}`]: "off", + [`functional/${noExpressionStatements.name}`]: "off", [`functional/${preferImmutableTypes.name}`]: [ "error", { diff --git a/src/configs/no-statements.ts b/src/configs/no-statements.ts index 82d66b592..4eb3ae775 100644 --- a/src/configs/no-statements.ts +++ b/src/configs/no-statements.ts @@ -1,14 +1,14 @@ import type { Linter } from "eslint"; import * as noConditionalStatements from "~/rules/no-conditional-statements"; -import * as noExpressionStatement from "~/rules/no-expression-statement"; +import * as noExpressionStatements from "~/rules/no-expression-statements"; import * as noLoop from "~/rules/no-loop-statement"; import * as noReturnVoid from "~/rules/no-return-void"; const config: Linter.Config = { rules: { [`functional/${noConditionalStatements.name}`]: "error", - [`functional/${noExpressionStatement.name}`]: "error", + [`functional/${noExpressionStatements.name}`]: "error", [`functional/${noLoop.name}`]: "error", [`functional/${noReturnVoid.name}`]: "error", }, diff --git a/src/rules/index.ts b/src/rules/index.ts index 2ded115a6..dd73d0698 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -2,7 +2,7 @@ import * as functionalParameters from "./functional-parameters"; import * as immutableData from "./immutable-data"; import * as noClasses from "./no-classes"; import * as noConditionalStatements from "./no-conditional-statements"; -import * as noExpressionStatement from "./no-expression-statement"; +import * as noExpressionStatements from "./no-expression-statements"; import * as noLet from "./no-let"; import * as noLoop from "./no-loop-statement"; import * as noMixedType from "./no-mixed-type"; @@ -25,7 +25,7 @@ export const rules = { [immutableData.name]: immutableData.rule, [noClasses.name]: noClasses.rule, [noConditionalStatements.name]: noConditionalStatements.rule, - [noExpressionStatement.name]: noExpressionStatement.rule, + [noExpressionStatements.name]: noExpressionStatements.rule, [noLet.name]: noLet.rule, [noLoop.name]: noLoop.rule, [noMixedType.name]: noMixedType.rule, diff --git a/src/rules/no-expression-statement.ts b/src/rules/no-expression-statements.ts similarity index 97% rename from src/rules/no-expression-statement.ts rename to src/rules/no-expression-statements.ts index 3c9ea32b5..f7e005165 100644 --- a/src/rules/no-expression-statement.ts +++ b/src/rules/no-expression-statements.ts @@ -16,7 +16,7 @@ import { isVoidType } from "~/util/typeguard"; /** * The name of this rule. */ -export const name = "no-expression-statement" as const; +export const name = "no-expression-statements" as const; /** * The options this rule can take. diff --git a/src/settings/immutability.ts b/src/settings/immutability.ts index f8d6f0cda..91a0f7c3d 100644 --- a/src/settings/immutability.ts +++ b/src/settings/immutability.ts @@ -47,7 +47,7 @@ export function getImmutabilityOverrides( if (!cachedSettings.has(settings)) { const overrides = loadImmutabilityOverrides(settings); - // eslint-disable-next-line functional/no-expression-statement + // eslint-disable-next-line functional/no-expression-statements cachedSettings.set(settings, overrides); return overrides; } diff --git a/src/util/rule.ts b/src/util/rule.ts index 5b1f00e15..e5006758e 100644 --- a/src/util/rule.ts +++ b/src/util/rule.ts @@ -52,7 +52,7 @@ type RuleFunctionsMap< // This function can't be functional as it needs to interact with 3rd-party // libraries that aren't functional. -/* eslint-disable functional/no-return-void, functional/no-expression-statement */ +/* eslint-disable functional/no-return-void, functional/no-expression-statements */ /** * Create a function that processes common options and then runs the given * check. @@ -82,7 +82,7 @@ function checkNode< } }; } -/* eslint-enable functional/no-return-void, functional/no-expression-statement */ +/* eslint-enable functional/no-return-void, functional/no-expression-statements */ /** * Create a rule. diff --git a/tests/.eslintrc.json b/tests/.eslintrc.json index 6edc67da3..965c3ea8b 100644 --- a/tests/.eslintrc.json +++ b/tests/.eslintrc.json @@ -9,7 +9,7 @@ "eslint-comments/no-unlimited-disable": "off", "functional/functional-parameters": "off", "functional/no-conditional-statements": "off", - "functional/no-expression-statement": "off", + "functional/no-expression-statements": "off", "functional/no-loop-statement": "off", "functional/no-return-void": "off", "import/no-named-as-default-member": "off", diff --git a/tests/rules/no-expression-statement/index.test.ts b/tests/rules/no-expression-statement/index.test.ts index 0851c55be..36bfb87f1 100644 --- a/tests/rules/no-expression-statement/index.test.ts +++ b/tests/rules/no-expression-statement/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/no-expression-statement"; +import { name, rule } from "~/rules/no-expression-statements"; import { testUsing } from "~/tests/helpers/testers"; import es3Tests from "./es3"; From 683209dd326b6405ecdcbe552aa271788c1a78af Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 18:11:09 +1300 Subject: [PATCH 040/100] feat(no-loop-statements)!: rename rule from `no-loop-statement` --- README.md | 2 +- cz-adapter/engine.ts | 2 +- docs/rules/no-loop-statement.md | 10 +++++----- docs/user-guide/migrating-from-tslint.md | 2 +- src/configs/all.ts | 4 ++-- src/configs/no-statements.ts | 4 ++-- src/rules/index.ts | 4 ++-- .../{no-loop-statement.ts => no-loop-statements.ts} | 2 +- src/util/rule.ts | 2 +- tests/.eslintrc.json | 2 +- tests/rules/no-loop-statement/index.test.ts | 2 +- 11 files changed, 18 insertions(+), 18 deletions(-) rename src/rules/{no-loop-statement.ts => no-loop-statements.ts} (96%) diff --git a/README.md b/README.md index dea5e29ed..0fd51a21d 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ The [below section](#rules) gives details on which rules are enabled by each rul | ------------------------------------------------------------------------ | -------------------------------------------------------------- | :-----------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | | [`no-conditional-statements`](./docs/rules/no-conditional-statements.md) | Disallow conditional statements (`if` and `switch` statements) | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | | | :thought_balloon: | | [`no-expression-statements`](./docs/rules/no-expression-statements.md) | Disallow expressions to cause side-effects | :heavy_check_mark: | :heavy_check_mark: | | | | :thought_balloon: | -| [`no-loop-statement`](./docs/rules/no-loop-statement.md) | Disallow imperative loops | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`no-loop-statements`](./docs/rules/no-loop-statements.md) | Disallow imperative loops | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | | [`no-return-void`](./docs/rules/no-return-void.md) | Disallow functions that return nothing | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | ### No Exceptions Rules diff --git a/cz-adapter/engine.ts b/cz-adapter/engine.ts index df23c631e..3aaca55f4 100644 --- a/cz-adapter/engine.ts +++ b/cz-adapter/engine.ts @@ -287,7 +287,7 @@ function filterSubject(options: Options) { m_subject.charAt(0).toLowerCase() + m_subject.slice(1, m_subject.length); } - // eslint-disable-next-line functional/no-loop-statement + // eslint-disable-next-line functional/no-loop-statements while (m_subject.endsWith(".")) { m_subject = m_subject.slice(0, -1); } diff --git a/docs/rules/no-loop-statement.md b/docs/rules/no-loop-statement.md index 0ea908528..d54f59856 100644 --- a/docs/rules/no-loop-statement.md +++ b/docs/rules/no-loop-statement.md @@ -1,4 +1,4 @@ -# Disallow imperative loops (no-loop-statement) +# Disallow imperative loops (no-loop-statements) This rule disallows for loop statements, including `for`, `for...of`, `for...in`, `while`, and `do...while`. @@ -14,7 +14,7 @@ For more background see this [blog post](https://hackernoon.com/rethinking-javas ```js -/* eslint functional/no-loop-statement: "error" */ +/* eslint functional/no-loop-statements: "error" */ const numbers = [1, 2, 3]; const double = []; @@ -26,7 +26,7 @@ for (let i = 0; i < numbers.length; i++) { ```js -/* eslint functional/no-loop-statement: "error" */ +/* eslint functional/no-loop-statements: "error" */ const numbers = [1, 2, 3]; let sum = 0; @@ -38,13 +38,13 @@ for (const number of numbers) { ### ✅ Correct ```js -/* eslint functional/no-loop-statement: "error" */ +/* eslint functional/no-loop-statements: "error" */ const numbers = [1, 2, 3]; const double = numbers.map((n) => n * 2); ``` ```js -/* eslint functional/no-loop-statement: "error" */ +/* eslint functional/no-loop-statements: "error" */ const numbers = [1, 2, 3]; const sum = numbers.reduce((carry, number) => carry + number, 0); diff --git a/docs/user-guide/migrating-from-tslint.md b/docs/user-guide/migrating-from-tslint.md index 54bf4b6d4..5410b75e1 100644 --- a/docs/user-guide/migrating-from-tslint.md +++ b/docs/user-guide/migrating-from-tslint.md @@ -62,7 +62,7 @@ Below is a table mapping the `eslint-plugin-functional` rules to their `tslint-i | [`functional/no-mixed-type`](../rules/no-mixed-type.md) | `no-mixed-interface` | | [`functional/no-expression-statements`](../rules/no-expression-statements.md) | `no-expression-statements` | | [`functional/no-conditional-statements`](../rules/no-conditional-statements.md) | `no-if-statement` | -| [`functional/no-loop-statement`](../rules/no-loop-statement.md) | `no-loop-statement` | +| [`functional/no-loop-statements`](../rules/no-loop-statements.md) | `no-loop-statements` | | [`functional/no-return-void`](../rules/no-return-void.md) | - | | [`functional/no-throw-statement`](../rules/no-throw-statement.md) | `no-throw` | | [`functional/no-try-statement`](../rules/no-try-statement.md) | `no-try` | diff --git a/src/configs/all.ts b/src/configs/all.ts index ed1dd279b..5e3787911 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -6,7 +6,7 @@ import * as noClasses from "~/rules/no-classes"; import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noExpressionStatements from "~/rules/no-expression-statements"; import * as noLet from "~/rules/no-let"; -import * as noLoop from "~/rules/no-loop-statement"; +import * as noLoopStatements from "~/rules/no-loop-statements"; import * as noMixedType from "~/rules/no-mixed-type"; import * as noPromiseReject from "~/rules/no-promise-reject"; import * as noReturnVoid from "~/rules/no-return-void"; @@ -26,7 +26,7 @@ const config: Linter.Config = { [`functional/${noConditionalStatements.name}`]: "error", [`functional/${noExpressionStatements.name}`]: "error", [`functional/${noLet.name}`]: "error", - [`functional/${noLoop.name}`]: "error", + [`functional/${noLoopStatements.name}`]: "error", [`functional/${noMixedType.name}`]: "error", [`functional/${noPromiseReject.name}`]: "error", [`functional/${noReturnVoid.name}`]: "error", diff --git a/src/configs/no-statements.ts b/src/configs/no-statements.ts index 4eb3ae775..d394c99b3 100644 --- a/src/configs/no-statements.ts +++ b/src/configs/no-statements.ts @@ -2,14 +2,14 @@ import type { Linter } from "eslint"; import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noExpressionStatements from "~/rules/no-expression-statements"; -import * as noLoop from "~/rules/no-loop-statement"; +import * as noLoopStatements from "~/rules/no-loop-statements"; import * as noReturnVoid from "~/rules/no-return-void"; const config: Linter.Config = { rules: { [`functional/${noConditionalStatements.name}`]: "error", [`functional/${noExpressionStatements.name}`]: "error", - [`functional/${noLoop.name}`]: "error", + [`functional/${noLoopStatements.name}`]: "error", [`functional/${noReturnVoid.name}`]: "error", }, }; diff --git a/src/rules/index.ts b/src/rules/index.ts index dd73d0698..9ab7b9c47 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -4,7 +4,7 @@ import * as noClasses from "./no-classes"; import * as noConditionalStatements from "./no-conditional-statements"; import * as noExpressionStatements from "./no-expression-statements"; import * as noLet from "./no-let"; -import * as noLoop from "./no-loop-statement"; +import * as noLoopStatements from "./no-loop-statements"; import * as noMixedType from "./no-mixed-type"; import * as noPromiseReject from "./no-promise-reject"; import * as noReturnVoid from "./no-return-void"; @@ -27,7 +27,7 @@ export const rules = { [noConditionalStatements.name]: noConditionalStatements.rule, [noExpressionStatements.name]: noExpressionStatements.rule, [noLet.name]: noLet.rule, - [noLoop.name]: noLoop.rule, + [noLoopStatements.name]: noLoopStatements.rule, [noMixedType.name]: noMixedType.rule, [noPromiseReject.name]: noPromiseReject.rule, [noReturnVoid.name]: noReturnVoid.rule, diff --git a/src/rules/no-loop-statement.ts b/src/rules/no-loop-statements.ts similarity index 96% rename from src/rules/no-loop-statement.ts rename to src/rules/no-loop-statements.ts index f9c012c99..02c1b94af 100644 --- a/src/rules/no-loop-statement.ts +++ b/src/rules/no-loop-statements.ts @@ -9,7 +9,7 @@ import { createRule } from "~/util/rule"; /** * The name of this rule. */ -export const name = "no-loop-statement" as const; +export const name = "no-loop-statements" as const; /** * The options this rule can take. diff --git a/src/util/rule.ts b/src/util/rule.ts index e5006758e..db3e075e6 100644 --- a/src/util/rule.ts +++ b/src/util/rule.ts @@ -74,7 +74,7 @@ function checkNode< return (node: Node) => { const result = check(node, context, options); - // eslint-disable-next-line functional/no-loop-statement -- can't really be avoided. + // eslint-disable-next-line functional/no-loop-statements -- can't really be avoided. for (const descriptor of result.descriptors) { result.context.report( descriptor as TSESLint.ReportDescriptor diff --git a/tests/.eslintrc.json b/tests/.eslintrc.json index 965c3ea8b..ac3d4d7a1 100644 --- a/tests/.eslintrc.json +++ b/tests/.eslintrc.json @@ -10,7 +10,7 @@ "functional/functional-parameters": "off", "functional/no-conditional-statements": "off", "functional/no-expression-statements": "off", - "functional/no-loop-statement": "off", + "functional/no-loop-statements": "off", "functional/no-return-void": "off", "import/no-named-as-default-member": "off", "sonarjs/no-duplicate-string": "off", diff --git a/tests/rules/no-loop-statement/index.test.ts b/tests/rules/no-loop-statement/index.test.ts index 1572ed562..ec3003995 100644 --- a/tests/rules/no-loop-statement/index.test.ts +++ b/tests/rules/no-loop-statement/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/no-loop-statement"; +import { name, rule } from "~/rules/no-loop-statements"; import { testUsing } from "~/tests/helpers/testers"; import es3Tests from "./es3"; From 392f9e8cf10ec2d09222e65f4136cb3480fcadfd Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 18:12:29 +1300 Subject: [PATCH 041/100] feat(no-mixed-types)!: rename rule from `no-mixed-type` --- CHANGELOG.md | 4 ++-- README.md | 2 +- docs/rules/no-mixed-type.md | 8 ++++---- docs/user-guide/migrating-from-tslint.md | 2 +- src/configs/all.ts | 4 ++-- src/configs/no-other-paradigms.ts | 4 ++-- src/rules/index.ts | 4 ++-- src/rules/{no-mixed-type.ts => no-mixed-types.ts} | 2 +- tests/rules/no-mixed-type/index.test.ts | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) rename src/rules/{no-mixed-type.ts => no-mixed-types.ts} (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1e027e3..f68f2d902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -248,7 +248,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(prefer-readonly-type): add support for mapped types [`#107`](https://github.com/eslint-functional/eslint-plugin-functional/pull/107) - fix(immutable-data): add support for ignoring exceptions for update expressions [`#108`](https://github.com/eslint-functional/eslint-plugin-functional/pull/108) - fix(type guard): cast with `as` as the type guard doesn't seem to be working correctly anymore [`#111`](https://github.com/eslint-functional/eslint-plugin-functional/pull/111) -- improvement(no-mixed-type): rule violations now mark the type as wrong, not members of the type [`#93`](https://github.com/eslint-functional/eslint-plugin-functional/pull/93) +- improvement(no-mixed-types): rule violations now mark the type as wrong, not members of the type [`#93`](https://github.com/eslint-functional/eslint-plugin-functional/pull/93) - Reduce use of .reduce() [`#92`](https://github.com/eslint-functional/eslint-plugin-functional/pull/92) ### Fixed @@ -386,7 +386,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - Refactor out the checkNode function for createRule. [`#48`](https://github.com/eslint-functional/eslint-plugin-functional/pull/48) - Text matching of MemberExpression nodes now includes the property name [`#47`](https://github.com/eslint-functional/eslint-plugin-functional/pull/47) - Test configs [`#43`](https://github.com/eslint-functional/eslint-plugin-functional/pull/43) -- feat(no-mixed-type): no-mixed-interface -> no-mixed-type [`#42`](https://github.com/eslint-functional/eslint-plugin-functional/pull/42) +- feat(no-mixed-types): no-mixed-interface -> no-mixed-types [`#42`](https://github.com/eslint-functional/eslint-plugin-functional/pull/42) - feat(configs): Create additional configs for each category of rules. [`#40`](https://github.com/eslint-functional/eslint-plugin-functional/pull/40) - feat(functional-parameters): Add option to allow iifes [`#39`](https://github.com/eslint-functional/eslint-plugin-functional/pull/39) - new rule: prefer-type [`#38`](https://github.com/eslint-functional/eslint-plugin-functional/pull/38) diff --git a/README.md b/README.md index 0fd51a21d..91f1a8b3d 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ The [below section](#rules) gives details on which rules are enabled by each rul | Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | | ---------------------------------------------------------- | ------------------------------------------------------------------ | :----------------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | | [`no-classes`](./docs/rules/no-classes.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| [`no-mixed-type`](./docs/rules/no-mixed-type.md) | Disallow types that contain both callable and non-callable members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| [`no-mixed-types`](./docs/rules/no-mixed-types.md) | Disallow types that contain both callable and non-callable members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | | [`no-this-expression`](./docs/rules/no-this-expression.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | | | | | ### No Statements Rules diff --git a/docs/rules/no-mixed-type.md b/docs/rules/no-mixed-type.md index f3cec6c8c..6ffecb7f7 100644 --- a/docs/rules/no-mixed-type.md +++ b/docs/rules/no-mixed-type.md @@ -1,4 +1,4 @@ -# Restrict types so that only members of the same kind of are allowed in them (no-mixed-type) +# Restrict types so that only members of the same kind of are allowed in them (no-mixed-types) This rule enforces that an aliased type literal or an interface only has one type of members, eg. only data properties or only functions. @@ -11,7 +11,7 @@ Mixing functions and data properties in the same type is a sign of object-orient ```ts -/* eslint functional/no-mixed-type: "error" */ +/* eslint functional/no-mixed-types: "error" */ type Foo = { prop1: string; @@ -22,7 +22,7 @@ type Foo = { ### ✅ Correct ```ts -/* eslint functional/no-mixed-type: "error" */ +/* eslint functional/no-mixed-types: "error" */ type Foo = { prop1: string; @@ -31,7 +31,7 @@ type Foo = { ``` ```ts -/* eslint functional/no-mixed-type: "error" */ +/* eslint functional/no-mixed-types: "error" */ type Foo = { prop1: () => string; diff --git a/docs/user-guide/migrating-from-tslint.md b/docs/user-guide/migrating-from-tslint.md index 5410b75e1..f61190739 100644 --- a/docs/user-guide/migrating-from-tslint.md +++ b/docs/user-guide/migrating-from-tslint.md @@ -59,7 +59,7 @@ Below is a table mapping the `eslint-plugin-functional` rules to their `tslint-i | [`functional/no-method-signature`](../rules/no-method-signature.md) | `no-method-signature` | | [`functional/no-this-expression`](../rules/no-this-expression.md) | `no-this` | | [`functional/no-classes`](../rules/no-classes.md) | `no-classes` | -| [`functional/no-mixed-type`](../rules/no-mixed-type.md) | `no-mixed-interface` | +| [`functional/no-mixed-types`](../rules/no-mixed-types.md) | `no-mixed-interface` | | [`functional/no-expression-statements`](../rules/no-expression-statements.md) | `no-expression-statements` | | [`functional/no-conditional-statements`](../rules/no-conditional-statements.md) | `no-if-statement` | | [`functional/no-loop-statements`](../rules/no-loop-statements.md) | `no-loop-statements` | diff --git a/src/configs/all.ts b/src/configs/all.ts index 5e3787911..5146d7e3a 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -7,7 +7,7 @@ import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noExpressionStatements from "~/rules/no-expression-statements"; import * as noLet from "~/rules/no-let"; import * as noLoopStatements from "~/rules/no-loop-statements"; -import * as noMixedType from "~/rules/no-mixed-type"; +import * as noMixedTypes from "~/rules/no-mixed-types"; import * as noPromiseReject from "~/rules/no-promise-reject"; import * as noReturnVoid from "~/rules/no-return-void"; import * as noThisExpression from "~/rules/no-this-expression"; @@ -27,7 +27,7 @@ const config: Linter.Config = { [`functional/${noExpressionStatements.name}`]: "error", [`functional/${noLet.name}`]: "error", [`functional/${noLoopStatements.name}`]: "error", - [`functional/${noMixedType.name}`]: "error", + [`functional/${noMixedTypes.name}`]: "error", [`functional/${noPromiseReject.name}`]: "error", [`functional/${noReturnVoid.name}`]: "error", [`functional/${noThisExpression.name}`]: "error", diff --git a/src/configs/no-other-paradigms.ts b/src/configs/no-other-paradigms.ts index 47c2060d8..25b04bc9b 100644 --- a/src/configs/no-other-paradigms.ts +++ b/src/configs/no-other-paradigms.ts @@ -1,13 +1,13 @@ import type { Linter } from "eslint"; import * as noClasses from "~/rules/no-classes"; -import * as noMixedType from "~/rules/no-mixed-type"; +import * as noMixedTypes from "~/rules/no-mixed-types"; import * as noThisExpression from "~/rules/no-this-expression"; const config: Linter.Config = { rules: { [`functional/${noClasses.name}`]: "error", - [`functional/${noMixedType.name}`]: "error", + [`functional/${noMixedTypes.name}`]: "error", [`functional/${noThisExpression.name}`]: "error", }, }; diff --git a/src/rules/index.ts b/src/rules/index.ts index 9ab7b9c47..bba64698e 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -5,7 +5,7 @@ import * as noConditionalStatements from "./no-conditional-statements"; import * as noExpressionStatements from "./no-expression-statements"; import * as noLet from "./no-let"; import * as noLoopStatements from "./no-loop-statements"; -import * as noMixedType from "./no-mixed-type"; +import * as noMixedTypes from "./no-mixed-types"; import * as noPromiseReject from "./no-promise-reject"; import * as noReturnVoid from "./no-return-void"; import * as noThisExpression from "./no-this-expression"; @@ -28,7 +28,7 @@ export const rules = { [noExpressionStatements.name]: noExpressionStatements.rule, [noLet.name]: noLet.rule, [noLoopStatements.name]: noLoopStatements.rule, - [noMixedType.name]: noMixedType.rule, + [noMixedTypes.name]: noMixedTypes.rule, [noPromiseReject.name]: noPromiseReject.rule, [noReturnVoid.name]: noReturnVoid.rule, [noThisExpression.name]: noThisExpression.rule, diff --git a/src/rules/no-mixed-type.ts b/src/rules/no-mixed-types.ts similarity index 98% rename from src/rules/no-mixed-type.ts rename to src/rules/no-mixed-types.ts index 0b38ff9ac..debee0ba1 100644 --- a/src/rules/no-mixed-type.ts +++ b/src/rules/no-mixed-types.ts @@ -10,7 +10,7 @@ import { isTSPropertySignature, isTSTypeLiteral } from "~/util/typeguard"; /** * The name of this rule. */ -export const name = "no-mixed-type" as const; +export const name = "no-mixed-types" as const; /** * The options this rule can take. diff --git a/tests/rules/no-mixed-type/index.test.ts b/tests/rules/no-mixed-type/index.test.ts index 007df2bf0..facaa82b5 100644 --- a/tests/rules/no-mixed-type/index.test.ts +++ b/tests/rules/no-mixed-type/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/no-mixed-type"; +import { name, rule } from "~/rules/no-mixed-types"; import { testUsing } from "~/tests/helpers/testers"; import tsTests from "./ts"; From 10c3bb6addc9e741296db43f1d464bf6483142c7 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 18:13:44 +1300 Subject: [PATCH 042/100] feat(no-this-expressions)!: rename rule from `no-this-expression` --- README.md | 10 +++++----- docs/rules/no-class.md | 2 +- docs/rules/no-this-expression.md | 6 +++--- docs/user-guide/migrating-from-tslint.md | 2 +- src/configs/all.ts | 4 ++-- src/configs/no-other-paradigms.ts | 4 ++-- src/configs/recommended.ts | 4 ++-- src/rules/index.ts | 4 ++-- .../{no-this-expression.ts => no-this-expressions.ts} | 2 +- tests/rules/no-this-expression/index.test.ts | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) rename src/rules/{no-this-expression.ts => no-this-expressions.ts} (96%) diff --git a/README.md b/README.md index 91f1a8b3d..1eaa3dea8 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,11 @@ The [below section](#rules) gives details on which rules are enabled by each rul ### No Other Paradigms Rules -| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | -| ---------------------------------------------------------- | ------------------------------------------------------------------ | :----------------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | -| [`no-classes`](./docs/rules/no-classes.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | -| [`no-mixed-types`](./docs/rules/no-mixed-types.md) | Disallow types that contain both callable and non-callable members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | -| [`no-this-expression`](./docs/rules/no-this-expression.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | | | | | +| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | +| ------------------------------------------------------------ | ------------------------------------------------------------------ | :----------------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :---------------: | +| [`no-classes`](./docs/rules/no-classes.md) | Disallow classes | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | | +| [`no-mixed-types`](./docs/rules/no-mixed-types.md) | Disallow types that contain both callable and non-callable members | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | :thought_balloon: | +| [`no-this-expressions`](./docs/rules/no-this-expressions.md) | Disallow `this` access | :heavy_check_mark: | :heavy_check_mark: | | | | | ### No Statements Rules diff --git a/docs/rules/no-class.md b/docs/rules/no-class.md index b0e44bbd2..d4da22351 100644 --- a/docs/rules/no-class.md +++ b/docs/rules/no-class.md @@ -46,7 +46,7 @@ console.log(`${dogA.name} is ${getAgeInDogYears(dogA.age)} in dog years.`); ### React Examples -Thanks to libraries like [recompose](https://github.com/acdlite/recompose) and Redux's [React Container components](http://redux.js.org/docs/basics/UsageWithReact.html), there's not much reason to build Components using `React.createClass` or ES6 classes anymore. The `no-this-expression` rule makes this explicit. +Thanks to libraries like [recompose](https://github.com/acdlite/recompose) and Redux's [React Container components](http://redux.js.org/docs/basics/UsageWithReact.html), there's not much reason to build Components using `React.createClass` or ES6 classes anymore. The `no-this-expressions` rule makes this explicit. ```js const Message = React.createClass({ diff --git a/docs/rules/no-this-expression.md b/docs/rules/no-this-expression.md index fa434408f..177d10797 100644 --- a/docs/rules/no-this-expression.md +++ b/docs/rules/no-this-expression.md @@ -1,4 +1,4 @@ -# Disallow this access (no-this-expression) +# Disallow this access (no-this-expressions) ## Rule Details @@ -10,7 +10,7 @@ See the its docs for more info. ```js -/* eslint functional/no-this-expression: "error" */ +/* eslint functional/no-this-expressions: "error" */ const foo = this.value + 17; ``` @@ -18,7 +18,7 @@ const foo = this.value + 17; ### ✅ Correct ```js -/* eslint functional/no-this-expression: "error" */ +/* eslint functional/no-this-expressions: "error" */ const foo = object.value + 17; ``` diff --git a/docs/user-guide/migrating-from-tslint.md b/docs/user-guide/migrating-from-tslint.md index f61190739..8f131ecf6 100644 --- a/docs/user-guide/migrating-from-tslint.md +++ b/docs/user-guide/migrating-from-tslint.md @@ -57,7 +57,7 @@ Below is a table mapping the `eslint-plugin-functional` rules to their `tslint-i | [`functional/no-let`](../rules/no-let.md) | `no-let` | | [`functional/immutable-data`](../rules/immutable-data.md) | `no-object-mutation`, `no-array-mutation` & `no-delete` | | [`functional/no-method-signature`](../rules/no-method-signature.md) | `no-method-signature` | -| [`functional/no-this-expression`](../rules/no-this-expression.md) | `no-this` | +| [`functional/no-this-expressions`](../rules/no-this-expressions.md) | `no-this` | | [`functional/no-classes`](../rules/no-classes.md) | `no-classes` | | [`functional/no-mixed-types`](../rules/no-mixed-types.md) | `no-mixed-interface` | | [`functional/no-expression-statements`](../rules/no-expression-statements.md) | `no-expression-statements` | diff --git a/src/configs/all.ts b/src/configs/all.ts index 5146d7e3a..754c99822 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -10,7 +10,7 @@ import * as noLoopStatements from "~/rules/no-loop-statements"; import * as noMixedTypes from "~/rules/no-mixed-types"; import * as noPromiseReject from "~/rules/no-promise-reject"; import * as noReturnVoid from "~/rules/no-return-void"; -import * as noThisExpression from "~/rules/no-this-expression"; +import * as noThisExpressions from "~/rules/no-this-expressions"; import * as noThrowStatement from "~/rules/no-throw-statement"; import * as noTryStatement from "~/rules/no-try-statement"; import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; @@ -30,7 +30,7 @@ const config: Linter.Config = { [`functional/${noMixedTypes.name}`]: "error", [`functional/${noPromiseReject.name}`]: "error", [`functional/${noReturnVoid.name}`]: "error", - [`functional/${noThisExpression.name}`]: "error", + [`functional/${noThisExpressions.name}`]: "error", [`functional/${noThrowStatement.name}`]: "error", [`functional/${noTryStatement.name}`]: "error", [`functional/${preferImmutableTypes.name}`]: "error", diff --git a/src/configs/no-other-paradigms.ts b/src/configs/no-other-paradigms.ts index 25b04bc9b..5d7f114a6 100644 --- a/src/configs/no-other-paradigms.ts +++ b/src/configs/no-other-paradigms.ts @@ -2,13 +2,13 @@ import type { Linter } from "eslint"; import * as noClasses from "~/rules/no-classes"; import * as noMixedTypes from "~/rules/no-mixed-types"; -import * as noThisExpression from "~/rules/no-this-expression"; +import * as noThisExpressions from "~/rules/no-this-expressions"; const config: Linter.Config = { rules: { [`functional/${noClasses.name}`]: "error", [`functional/${noMixedTypes.name}`]: "error", - [`functional/${noThisExpression.name}`]: "error", + [`functional/${noThisExpressions.name}`]: "error", }, }; diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 7bd8fa27a..565144dc1 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -4,7 +4,7 @@ import { Immutability } from "is-immutable-type"; import * as functionalParameters from "~/rules/functional-parameters"; import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noLet from "~/rules/no-let"; -import * as noThisExpression from "~/rules/no-this-expression"; +import * as noThisExpressions from "~/rules/no-this-expressions"; import * as noThrowStatement from "~/rules/no-throw-statement"; import * as noTryStatement from "~/rules/no-try-statement"; import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; @@ -37,7 +37,7 @@ const overrides: Linter.Config = { allowInForLoopInit: true, }, ], - [`functional/${noThisExpression.name}`]: "off", + [`functional/${noThisExpressions.name}`]: "off", [`functional/${noThrowStatement.name}`]: [ "error", { diff --git a/src/rules/index.ts b/src/rules/index.ts index bba64698e..820fa91d5 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -8,7 +8,7 @@ import * as noLoopStatements from "./no-loop-statements"; import * as noMixedTypes from "./no-mixed-types"; import * as noPromiseReject from "./no-promise-reject"; import * as noReturnVoid from "./no-return-void"; -import * as noThisExpression from "./no-this-expression"; +import * as noThisExpressions from "./no-this-expressions"; import * as noThrowStatement from "./no-throw-statement"; import * as noTryStatement from "./no-try-statement"; import * as preferImmutableTypes from "./prefer-immutable-types"; @@ -31,7 +31,7 @@ export const rules = { [noMixedTypes.name]: noMixedTypes.rule, [noPromiseReject.name]: noPromiseReject.rule, [noReturnVoid.name]: noReturnVoid.rule, - [noThisExpression.name]: noThisExpression.rule, + [noThisExpressions.name]: noThisExpressions.rule, [noThrowStatement.name]: noThrowStatement.rule, [noTryStatement.name]: noTryStatement.rule, [preferImmutableTypes.name]: preferImmutableTypes.rule, diff --git a/src/rules/no-this-expression.ts b/src/rules/no-this-expressions.ts similarity index 96% rename from src/rules/no-this-expression.ts rename to src/rules/no-this-expressions.ts index 96f012856..7b5bdfcd0 100644 --- a/src/rules/no-this-expression.ts +++ b/src/rules/no-this-expressions.ts @@ -8,7 +8,7 @@ import { createRule } from "~/util/rule"; /** * The name of this rule. */ -export const name = "no-this-expression" as const; +export const name = "no-this-expressions" as const; /** * The options this rule can take. diff --git a/tests/rules/no-this-expression/index.test.ts b/tests/rules/no-this-expression/index.test.ts index 6589a9b72..8666d942f 100644 --- a/tests/rules/no-this-expression/index.test.ts +++ b/tests/rules/no-this-expression/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/no-this-expression"; +import { name, rule } from "~/rules/no-this-expressions"; import { testUsing } from "~/tests/helpers/testers"; import es3Tests from "./es3"; From 4be92c8c2a485d870704379219f46e7fdb572ca8 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 18:15:05 +1300 Subject: [PATCH 043/100] feat(no-throw-statements)!: rename rule from `no-throw-statement` --- CHANGELOG.md | 2 +- README.md | 10 +++++----- docs/rules/no-throw-statement.md | 10 +++++----- docs/rules/no-try-statement.md | 2 +- docs/user-guide/migrating-from-tslint.md | 2 +- src/configs/all.ts | 4 ++-- src/configs/no-exceptions.ts | 4 ++-- src/configs/recommended.ts | 4 ++-- src/rules/index.ts | 4 ++-- .../{no-throw-statement.ts => no-throw-statements.ts} | 2 +- tests/rules/no-throw-statement/index.test.ts | 2 +- 11 files changed, 23 insertions(+), 23 deletions(-) rename src/rules/{no-throw-statement.ts => no-throw-statements.ts} (97%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f68f2d902..9540372aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ All notable changes to this project will be documented in this file. Dates are d ### Features -* **no-throw-statement:** add an option to allow throw statements within async functions ([#330](https://github.com/eslint-functional/eslint-plugin-functional/issues/330)) ([7cee76b](https://github.com/eslint-functional/eslint-plugin-functional/commit/7cee76b0baeeea20dc32546c133b35f2dc12e01d)) +* **no-throw-statements:** add an option to allow throw statements within async functions ([#330](https://github.com/eslint-functional/eslint-plugin-functional/issues/330)) ([7cee76b](https://github.com/eslint-functional/eslint-plugin-functional/commit/7cee76b0baeeea20dc32546c133b35f2dc12e01d)) ## [4.1.1](https://github.com/eslint-functional/eslint-plugin-functional/compare/v4.1.0...v4.1.1) (2022-01-08) diff --git a/README.md b/README.md index 1eaa3dea8..c00738d40 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,11 @@ The [below section](#rules) gives details on which rules are enabled by each rul ### No Exceptions Rules -| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | -| ---------------------------------------------------------- | ----------------------------------------------------- | :-----------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :----------: | -| [`no-promise-reject`](./docs/rules/no-promise-reject.md) | Disallow rejecting Promises | | | | | | | -| [`no-throw-statement`](./docs/rules/no-throw-statement.md) | Disallow throwing exceptions | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | :green_circle: | | | -| [`no-try-statement`](./docs/rules/no-try-statement.md) | Disallow try-catch[-finally] and try-finally patterns | :heavy_check_mark: | :heavy_check_mark: | | | | | +| Name | Description | :fleur_de_lis: | :speak_no_evil: | :see_no_evil: | :hear_no_evil: | :wrench: | :blue_heart: | +| ------------------------------------------------------------ | ----------------------------------------------------- | :-----------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :----------: | +| [`no-promise-reject`](./docs/rules/no-promise-reject.md) | Disallow rejecting Promises | | | | | | | +| [`no-throw-statements`](./docs/rules/no-throw-statements.md) | Disallow throwing exceptions | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | :green_circle: | | | +| [`no-try-statement`](./docs/rules/no-try-statement.md) | Disallow try-catch[-finally] and try-finally patterns | :heavy_check_mark: | :heavy_check_mark: | | | | | ### Currying Rules diff --git a/docs/rules/no-throw-statement.md b/docs/rules/no-throw-statement.md index 910bf2a40..bcce19f77 100644 --- a/docs/rules/no-throw-statement.md +++ b/docs/rules/no-throw-statement.md @@ -1,4 +1,4 @@ -# Disallow throwing exceptions (no-throw-statement) +# Disallow throwing exceptions (no-throw-statements) This rule disallows the `throw` keyword. @@ -12,7 +12,7 @@ As an alternative a function should return an error or in the case of an async f ```js -/* eslint functional/no-throw-statement: "error" */ +/* eslint functional/no-throw-statements: "error" */ throw new Error("Something went wrong."); ``` @@ -20,7 +20,7 @@ throw new Error("Something went wrong."); ### ✅ Correct ```js -/* eslint functional/no-throw-statement: "error" */ +/* eslint functional/no-throw-statements: "error" */ function divide(x, y) { return y === 0 ? new Error("Cannot divide by zero.") : x / y; @@ -28,7 +28,7 @@ function divide(x, y) { ``` ```js -/* eslint functional/no-throw-statement: "error" */ +/* eslint functional/no-throw-statements: "error" */ async function divide(x, y) { const [xv, yv] = await Promise.all([x, y]); @@ -75,7 +75,7 @@ This essentially allows throw statements to be used as return statements for err #### ✅ Correct ```js -/* eslint functional/no-throw-statement: ["error", { "allowInAsyncFunctions": true }] */ +/* eslint functional/no-throw-statements: ["error", { "allowInAsyncFunctions": true }] */ async function divide(x, y) { const [xv, yv] = await Promise.all([x, y]); diff --git a/docs/rules/no-try-statement.md b/docs/rules/no-try-statement.md index 4fbd815d6..876b2f63f 100644 --- a/docs/rules/no-try-statement.md +++ b/docs/rules/no-try-statement.md @@ -4,7 +4,7 @@ This rule disallows the `try` keyword. ## Rule Details -Try statements are not part of functional programming. See [no-throw-statement](./no-throw-statement.md) for more information. +Try statements are not part of functional programming. See [no-throw-statements](./no-throw-statements.md) for more information. ### ❌ Incorrect diff --git a/docs/user-guide/migrating-from-tslint.md b/docs/user-guide/migrating-from-tslint.md index 8f131ecf6..8076d8745 100644 --- a/docs/user-guide/migrating-from-tslint.md +++ b/docs/user-guide/migrating-from-tslint.md @@ -64,7 +64,7 @@ Below is a table mapping the `eslint-plugin-functional` rules to their `tslint-i | [`functional/no-conditional-statements`](../rules/no-conditional-statements.md) | `no-if-statement` | | [`functional/no-loop-statements`](../rules/no-loop-statements.md) | `no-loop-statements` | | [`functional/no-return-void`](../rules/no-return-void.md) | - | -| [`functional/no-throw-statement`](../rules/no-throw-statement.md) | `no-throw` | +| [`functional/no-throw-statements`](../rules/no-throw-statements.md) | `no-throw` | | [`functional/no-try-statement`](../rules/no-try-statement.md) | `no-try` | | [`functional/no-promise-reject`](../rules/no-promise-reject.md) | `no-reject` | | [`functional/functional-parameters`](../rules/functional-parameters.md) | - | diff --git a/src/configs/all.ts b/src/configs/all.ts index 754c99822..255d13004 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -11,7 +11,7 @@ import * as noMixedTypes from "~/rules/no-mixed-types"; import * as noPromiseReject from "~/rules/no-promise-reject"; import * as noReturnVoid from "~/rules/no-return-void"; import * as noThisExpressions from "~/rules/no-this-expressions"; -import * as noThrowStatement from "~/rules/no-throw-statement"; +import * as noThrowStatements from "~/rules/no-throw-statements"; import * as noTryStatement from "~/rules/no-try-statement"; import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import * as preferPropertySignatures from "~/rules/prefer-property-signatures"; @@ -31,7 +31,7 @@ const config: Linter.Config = { [`functional/${noPromiseReject.name}`]: "error", [`functional/${noReturnVoid.name}`]: "error", [`functional/${noThisExpressions.name}`]: "error", - [`functional/${noThrowStatement.name}`]: "error", + [`functional/${noThrowStatements.name}`]: "error", [`functional/${noTryStatement.name}`]: "error", [`functional/${preferImmutableTypes.name}`]: "error", [`functional/${preferPropertySignatures.name}`]: "error", diff --git a/src/configs/no-exceptions.ts b/src/configs/no-exceptions.ts index 56fd903a6..2484a3f35 100644 --- a/src/configs/no-exceptions.ts +++ b/src/configs/no-exceptions.ts @@ -1,11 +1,11 @@ import type { Linter } from "eslint"; -import * as noThrowStatement from "~/rules/no-throw-statement"; +import * as noThrowStatements from "~/rules/no-throw-statements"; import * as noTryStatement from "~/rules/no-try-statement"; const config: Linter.Config = { rules: { - [`functional/${noThrowStatement.name}`]: "error", + [`functional/${noThrowStatements.name}`]: "error", [`functional/${noTryStatement.name}`]: "error", }, }; diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 565144dc1..420b37f9e 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -5,7 +5,7 @@ import * as functionalParameters from "~/rules/functional-parameters"; import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noLet from "~/rules/no-let"; import * as noThisExpressions from "~/rules/no-this-expressions"; -import * as noThrowStatement from "~/rules/no-throw-statement"; +import * as noThrowStatements from "~/rules/no-throw-statements"; import * as noTryStatement from "~/rules/no-try-statement"; import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import * as typeDeclarationImmutability from "~/rules/type-declaration-immutability"; @@ -38,7 +38,7 @@ const overrides: Linter.Config = { }, ], [`functional/${noThisExpressions.name}`]: "off", - [`functional/${noThrowStatement.name}`]: [ + [`functional/${noThrowStatements.name}`]: [ "error", { allowInAsyncFunctions: true, diff --git a/src/rules/index.ts b/src/rules/index.ts index 820fa91d5..f1f91c699 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -9,7 +9,7 @@ import * as noMixedTypes from "./no-mixed-types"; import * as noPromiseReject from "./no-promise-reject"; import * as noReturnVoid from "./no-return-void"; import * as noThisExpressions from "./no-this-expressions"; -import * as noThrowStatement from "./no-throw-statement"; +import * as noThrowStatements from "./no-throw-statements"; import * as noTryStatement from "./no-try-statement"; import * as preferImmutableTypes from "./prefer-immutable-types"; import * as preferPropertySignatures from "./prefer-property-signatures"; @@ -32,7 +32,7 @@ export const rules = { [noPromiseReject.name]: noPromiseReject.rule, [noReturnVoid.name]: noReturnVoid.rule, [noThisExpressions.name]: noThisExpressions.rule, - [noThrowStatement.name]: noThrowStatement.rule, + [noThrowStatements.name]: noThrowStatements.rule, [noTryStatement.name]: noTryStatement.rule, [preferImmutableTypes.name]: preferImmutableTypes.rule, [preferPropertySignatures.name]: preferPropertySignatures.rule, diff --git a/src/rules/no-throw-statement.ts b/src/rules/no-throw-statements.ts similarity index 97% rename from src/rules/no-throw-statement.ts rename to src/rules/no-throw-statements.ts index 9cd011aee..a2949b1d2 100644 --- a/src/rules/no-throw-statement.ts +++ b/src/rules/no-throw-statements.ts @@ -9,7 +9,7 @@ import { createRule } from "~/util/rule"; /** * The name of this rule. */ -export const name = "no-throw-statement" as const; +export const name = "no-throw-statements" as const; /** * The options this rule can take. diff --git a/tests/rules/no-throw-statement/index.test.ts b/tests/rules/no-throw-statement/index.test.ts index 5d7eb7c6f..3107a795f 100644 --- a/tests/rules/no-throw-statement/index.test.ts +++ b/tests/rules/no-throw-statement/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/no-throw-statement"; +import { name, rule } from "~/rules/no-throw-statements"; import { testUsing } from "~/tests/helpers/testers"; import es3Tests from "./es3"; From e88828a9755018d7ab1c79a7b33d6cce1912f1c4 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 18:17:53 +1300 Subject: [PATCH 044/100] feat(no-try-statements)!: rename rule from `no-try-statement` --- README.md | 2 +- docs/rules/no-try-statement.md | 6 +++--- docs/user-guide/migrating-from-tslint.md | 2 +- src/configs/all.ts | 4 ++-- src/configs/no-exceptions.ts | 4 ++-- src/configs/recommended.ts | 4 ++-- src/rules/index.ts | 4 ++-- src/rules/{no-try-statement.ts => no-try-statements.ts} | 2 +- src/util/conditional-imports/.eslintrc.json | 2 +- tests/rules/no-try-statement/index.test.ts | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) rename src/rules/{no-try-statement.ts => no-try-statements.ts} (97%) diff --git a/README.md b/README.md index c00738d40..f98179b45 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ The [below section](#rules) gives details on which rules are enabled by each rul | ------------------------------------------------------------ | ----------------------------------------------------- | :-----------------------------------------------: | :-----------------------------------------: | :--------------------------------------------: | :--------------------------------------: | :------: | :----------: | | [`no-promise-reject`](./docs/rules/no-promise-reject.md) | Disallow rejecting Promises | | | | | | | | [`no-throw-statements`](./docs/rules/no-throw-statements.md) | Disallow throwing exceptions | :heavy_check_mark: | :heavy_check_mark: | :green_circle: | :green_circle: | | | -| [`no-try-statement`](./docs/rules/no-try-statement.md) | Disallow try-catch[-finally] and try-finally patterns | :heavy_check_mark: | :heavy_check_mark: | | | | | +| [`no-try-statements`](./docs/rules/no-try-statements.md) | Disallow try-catch[-finally] and try-finally patterns | :heavy_check_mark: | :heavy_check_mark: | | | | | ### Currying Rules diff --git a/docs/rules/no-try-statement.md b/docs/rules/no-try-statement.md index 876b2f63f..84b03019c 100644 --- a/docs/rules/no-try-statement.md +++ b/docs/rules/no-try-statement.md @@ -1,4 +1,4 @@ -# Disallow try-catch[-finally] and try-finally patterns (no-try-statement) +# Disallow try-catch[-finally] and try-finally patterns (no-try-statements) This rule disallows the `try` keyword. @@ -11,7 +11,7 @@ Try statements are not part of functional programming. See [no-throw-statements] ```js -/* eslint functional/no-try-statement: "error" */ +/* eslint functional/no-try-statements: "error" */ try { doSomethingThatMightGoWrong(); // <-- Might throw an exception. @@ -23,7 +23,7 @@ try { ### ✅ Correct ```js -/* eslint functional/no-try-statement: "error" */ +/* eslint functional/no-try-statements: "error" */ doSomethingThatMightGoWrong() // <-- Returns a Promise .catch((error) => { diff --git a/docs/user-guide/migrating-from-tslint.md b/docs/user-guide/migrating-from-tslint.md index 8076d8745..f36977966 100644 --- a/docs/user-guide/migrating-from-tslint.md +++ b/docs/user-guide/migrating-from-tslint.md @@ -65,6 +65,6 @@ Below is a table mapping the `eslint-plugin-functional` rules to their `tslint-i | [`functional/no-loop-statements`](../rules/no-loop-statements.md) | `no-loop-statements` | | [`functional/no-return-void`](../rules/no-return-void.md) | - | | [`functional/no-throw-statements`](../rules/no-throw-statements.md) | `no-throw` | -| [`functional/no-try-statement`](../rules/no-try-statement.md) | `no-try` | +| [`functional/no-try-statements`](../rules/no-try-statements.md) | `no-try` | | [`functional/no-promise-reject`](../rules/no-promise-reject.md) | `no-reject` | | [`functional/functional-parameters`](../rules/functional-parameters.md) | - | diff --git a/src/configs/all.ts b/src/configs/all.ts index 255d13004..05e23985b 100644 --- a/src/configs/all.ts +++ b/src/configs/all.ts @@ -12,7 +12,7 @@ import * as noPromiseReject from "~/rules/no-promise-reject"; import * as noReturnVoid from "~/rules/no-return-void"; import * as noThisExpressions from "~/rules/no-this-expressions"; import * as noThrowStatements from "~/rules/no-throw-statements"; -import * as noTryStatement from "~/rules/no-try-statement"; +import * as noTryStatements from "~/rules/no-try-statements"; import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import * as preferPropertySignatures from "~/rules/prefer-property-signatures"; import * as preferTacit from "~/rules/prefer-tacit"; @@ -32,7 +32,7 @@ const config: Linter.Config = { [`functional/${noReturnVoid.name}`]: "error", [`functional/${noThisExpressions.name}`]: "error", [`functional/${noThrowStatements.name}`]: "error", - [`functional/${noTryStatement.name}`]: "error", + [`functional/${noTryStatements.name}`]: "error", [`functional/${preferImmutableTypes.name}`]: "error", [`functional/${preferPropertySignatures.name}`]: "error", [`functional/${preferTacit.name}`]: [ diff --git a/src/configs/no-exceptions.ts b/src/configs/no-exceptions.ts index 2484a3f35..8fec821b0 100644 --- a/src/configs/no-exceptions.ts +++ b/src/configs/no-exceptions.ts @@ -1,12 +1,12 @@ import type { Linter } from "eslint"; import * as noThrowStatements from "~/rules/no-throw-statements"; -import * as noTryStatement from "~/rules/no-try-statement"; +import * as noTryStatements from "~/rules/no-try-statements"; const config: Linter.Config = { rules: { [`functional/${noThrowStatements.name}`]: "error", - [`functional/${noTryStatement.name}`]: "error", + [`functional/${noTryStatements.name}`]: "error", }, }; diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 420b37f9e..e18eda385 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -6,7 +6,7 @@ import * as noConditionalStatements from "~/rules/no-conditional-statements"; import * as noLet from "~/rules/no-let"; import * as noThisExpressions from "~/rules/no-this-expressions"; import * as noThrowStatements from "~/rules/no-throw-statements"; -import * as noTryStatement from "~/rules/no-try-statement"; +import * as noTryStatements from "~/rules/no-try-statements"; import * as preferImmutableTypes from "~/rules/prefer-immutable-types"; import * as typeDeclarationImmutability from "~/rules/type-declaration-immutability"; import { RuleEnforcementComparator } from "~/rules/type-declaration-immutability"; @@ -44,7 +44,7 @@ const overrides: Linter.Config = { allowInAsyncFunctions: true, }, ], - [`functional/${noTryStatement.name}`]: "off", + [`functional/${noTryStatements.name}`]: "off", [`functional/${preferImmutableTypes.name}`]: [ "error", { diff --git a/src/rules/index.ts b/src/rules/index.ts index f1f91c699..a2c1cb361 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -10,7 +10,7 @@ import * as noPromiseReject from "./no-promise-reject"; import * as noReturnVoid from "./no-return-void"; import * as noThisExpressions from "./no-this-expressions"; import * as noThrowStatements from "./no-throw-statements"; -import * as noTryStatement from "./no-try-statement"; +import * as noTryStatements from "./no-try-statements"; import * as preferImmutableTypes from "./prefer-immutable-types"; import * as preferPropertySignatures from "./prefer-property-signatures"; import * as preferReadonlyTypes from "./prefer-readonly-type"; @@ -33,7 +33,7 @@ export const rules = { [noReturnVoid.name]: noReturnVoid.rule, [noThisExpressions.name]: noThisExpressions.rule, [noThrowStatements.name]: noThrowStatements.rule, - [noTryStatement.name]: noTryStatement.rule, + [noTryStatements.name]: noTryStatements.rule, [preferImmutableTypes.name]: preferImmutableTypes.rule, [preferPropertySignatures.name]: preferPropertySignatures.rule, [preferReadonlyTypes.name]: preferReadonlyTypes.rule, diff --git a/src/rules/no-try-statement.ts b/src/rules/no-try-statements.ts similarity index 97% rename from src/rules/no-try-statement.ts rename to src/rules/no-try-statements.ts index cf88e7a73..b52873047 100644 --- a/src/rules/no-try-statement.ts +++ b/src/rules/no-try-statements.ts @@ -8,7 +8,7 @@ import { createRule } from "~/util/rule"; /** * The name of this rule. */ -export const name = "no-try-statement" as const; +export const name = "no-try-statements" as const; /** * The options this rule can take. diff --git a/src/util/conditional-imports/.eslintrc.json b/src/util/conditional-imports/.eslintrc.json index 493563629..813283012 100644 --- a/src/util/conditional-imports/.eslintrc.json +++ b/src/util/conditional-imports/.eslintrc.json @@ -2,7 +2,7 @@ "rules": { "@typescript-eslint/no-var-requires": "off", "functional/functional-parameters": "off", - "functional/no-try-statement": "off", + "functional/no-try-statements": "off", "import/no-extraneous-dependencies": [ "error", { diff --git a/tests/rules/no-try-statement/index.test.ts b/tests/rules/no-try-statement/index.test.ts index 5e78de444..bc3e7b397 100644 --- a/tests/rules/no-try-statement/index.test.ts +++ b/tests/rules/no-try-statement/index.test.ts @@ -1,4 +1,4 @@ -import { name, rule } from "~/rules/no-try-statement"; +import { name, rule } from "~/rules/no-try-statements"; import { testUsing } from "~/tests/helpers/testers"; import es3Tests from "./es3"; From b47e983b822842d1f46824a1e8c98893d45e8cb8 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 22:38:12 +1300 Subject: [PATCH 045/100] feat!: rename many of the options --- docs/rules/immutable-data.md | 8 +- docs/rules/no-let.md | 10 +- docs/rules/no-return-void.md | 10 +- docs/rules/prefer-immutable-types.md | 45 +++-- src/common/ignore-options.ts | 86 ++++----- src/configs/lite.ts | 6 +- src/configs/recommended.ts | 4 +- src/rules/functional-parameters.ts | 6 +- src/rules/immutable-data.ts | 51 ++++-- src/rules/no-expression-statements.ts | 3 +- src/rules/no-let.ts | 37 ++-- src/rules/no-mixed-types.ts | 45 +++-- src/rules/no-return-void.ts | 10 +- src/rules/prefer-immutable-types.ts | 164 ++++++++++-------- src/rules/prefer-readonly-type.ts | 161 ++++++++++------- src/rules/prefer-tacit.ts | 2 +- src/rules/type-declaration-immutability.ts | 4 +- tests/common/ignore-options.test.ts | 46 ++--- .../immutable-data/es6/object/invalid.ts | 2 +- .../rules/immutable-data/es6/object/valid.ts | 2 +- tests/rules/no-let/es6/invalid.ts | 12 +- tests/rules/no-let/es6/valid.ts | 6 +- tests/rules/no-return-void/ts/invalid.ts | 8 +- tests/rules/no-return-void/ts/valid.ts | 6 +- .../ts/variables/valid.ts | 7 +- tests/rules/work.test.ts | 29 ++-- 26 files changed, 420 insertions(+), 350 deletions(-) diff --git a/docs/rules/immutable-data.md b/docs/rules/immutable-data.md index 28cf3fb25..0c8769120 100644 --- a/docs/rules/immutable-data.md +++ b/docs/rules/immutable-data.md @@ -63,7 +63,7 @@ type Options = { forArrays: boolean; forObjects: boolean; } - ignoreClass: boolean | "fieldsOnly"; + ignoreClasses: boolean | "fieldsOnly"; ignoreImmediateMutation: boolean; ignorePattern?: string[] | string; ignoreAccessorPattern?: string[] | string; @@ -75,7 +75,7 @@ type Options = { ```ts type Options = { assumeTypes: true; - ignoreClass: false; + ignoreClasses: false; ignoreImmediateMutation: true; }; ``` @@ -86,7 +86,7 @@ type Options = { ```ts const liteOptions = { - ignoreClass: "fieldsOnly", + ignoreClasses: "fieldsOnly", } ``` @@ -121,7 +121,7 @@ const original = ["foo", "bar", "baz"]; const sorted = [...original].sort((a, b) => a.localeCompare(b)); // This is OK with ignoreImmediateMutation. ``` -### `ignoreClass` +### `ignoreClasses` Ignore mutations inside classes. diff --git a/docs/rules/no-let.md b/docs/rules/no-let.md index 74b9935f1..9b40b77e7 100644 --- a/docs/rules/no-let.md +++ b/docs/rules/no-let.md @@ -53,7 +53,7 @@ This rule accepts an options object of the following type: ```ts type Options = { - allowLocalMutation: boolean; + allowInFunctions: boolean; ignorePattern?: string[] | string; } ``` @@ -63,7 +63,7 @@ type Options = { ```ts const defaults = { allowInForLoopInit: false, - allowLocalMutation: false + allowInFunctions: false } ``` @@ -112,9 +112,11 @@ for (let [index, element] of array.entries()) { } ``` -### `allowLocalMutation` +### `allowInFunctions` -See the [allowLocalMutation](./options/allow-local-mutation.md) docs. +If true, the rule will not flag any statements that are inside of function bodies. + +See the [allowLocalMutation](./options/allow-local-mutation.md) docs for more information. ### `ignorePattern` diff --git a/docs/rules/no-return-void.md b/docs/rules/no-return-void.md index 5d5619f5e..0b0cf1303 100644 --- a/docs/rules/no-return-void.md +++ b/docs/rules/no-return-void.md @@ -41,7 +41,7 @@ This rule accepts an options object of the following type: type Options = { allowNull: boolean; allowUndefined: boolean; - ignoreImplicit: boolean; + ignoreInferredTypes: boolean; } ``` @@ -51,18 +51,18 @@ type Options = { const defaults = { allowNull: true, allowUndefined: true, - ignoreImplicit: false, + ignoreInferredTypes: false, } ``` -### allowNull +### `allowNull` If true allow returning null. -### allowUndefined +### `allowUndefined` If true allow returning undefined. -### ignoreImplicit +### `ignoreInferredTypes` If true ignore functions that don't explicitly specify a return type. diff --git a/docs/rules/prefer-immutable-types.md b/docs/rules/prefer-immutable-types.md index d2811efa6..f7046f23f 100644 --- a/docs/rules/prefer-immutable-types.md +++ b/docs/rules/prefer-immutable-types.md @@ -143,20 +143,30 @@ This rule accepts an options object of the following type: type Options = { enforcement: "None" | "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; ignoreInferredTypes: boolean; + ignoreClasses: boolean | "fieldsOnly"; + ignorePattern?: string[] | string; parameters?: { - // The same properties as above or just an enforcement value. + enforcement: "None" | "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; + ignoreInferredTypes: boolean; + ignoreClasses: boolean | "fieldsOnly"; + ignorePattern?: string[] | string; }; + returnTypes?: { - // The same properties as above or just an enforcement value. + enforcement: "None" | "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; + ignoreInferredTypes: boolean; + ignoreClasses: boolean | "fieldsOnly"; + ignorePattern?: string[] | string; }; + variables?: { - // The same properties as above or just an enforcement value. + enforcement: "None" | "ReadonlyShallow" | "ReadonlyDeep" | "Immutable"; + allowInFunctions: boolean; + ignoreInferredTypes: boolean; + ignoreClasses: boolean | "fieldsOnly"; + ignorePattern?: string[] | string; }; - - allowLocalMutation: boolean; - ignoreClass: boolean | "fieldsOnly"; - ignorePattern?: string[] | string; } ``` @@ -166,7 +176,7 @@ type Options = { const defaults = { enforcement: "Immutable", allowLocalMutation: false, - ignoreClass: false, + ignoreClasses: false, ignoreInferredTypes: false, } ``` @@ -179,7 +189,9 @@ const defaults = { const recommendedOptions = { enforcement: "None", ignoreInferredTypes: true, - parameters: "ReadonlyDeep", + parameters: { + enforcement: "ReadonlyDeep", + }, }, ``` @@ -189,7 +201,9 @@ const recommendedOptions = { const liteOptions = { enforcement: "None", ignoreInferredTypes: true, - parameters: "ReadonlyShallow", + parameters: { + enforcement: "ReadonlyShallow", + }, }, ``` @@ -239,6 +253,10 @@ type. This may be desirable in cases where an external dependency specifies a callback with mutable parameters, and manually annotating the callback's parameters is undesirable. +### `ignoreClasses` + +A boolean to specify if checking classes should be ignored. `false` by default. + #### ❌ Incorrect @@ -301,13 +319,12 @@ export const acceptsCallback: AcceptsCallback; Override the options specifically for the given type of types. -### `ignoreClass` -A boolean to specify if checking classes should be ignored. `false` by default. +#### `variables.ignoreInFunctions` -### `allowLocalMutation` +If true, the rule will not flag any variables that are inside of function bodies. -See the [allowLocalMutation](./options/allow-local-mutation.md) docs. +See the [allowLocalMutation](./options/allow-local-mutation.md) docs for more information. ### `ignorePattern` diff --git a/src/common/ignore-options.ts b/src/common/ignore-options.ts index 887d8ea47..a07c232d8 100644 --- a/src/common/ignore-options.ts +++ b/src/common/ignore-options.ts @@ -14,28 +14,12 @@ import { isThisExpression, } from "~/util/typeguard"; -/** - * The option to allow local mutations. - */ -export type AllowLocalMutationOption = { - readonly allowLocalMutation: boolean; -}; - -/** - * The schema for the option to allow local mutations. - */ -export const allowLocalMutationOptionSchema: JSONSchema4["properties"] = { - allowLocalMutation: { - type: "boolean", - }, -}; - /** * The option to ignore patterns. */ -export type IgnorePatternOption = { - readonly ignorePattern?: ReadonlyArray | string; -}; +export type IgnorePatternOption = Readonly<{ + ignorePattern?: ReadonlyArray | string; +}>; /** * The schema for the option to ignore patterns. @@ -52,9 +36,9 @@ export const ignorePatternOptionSchema: JSONSchema4["properties"] = { /** * The option to ignore accessor patterns. */ -export type IgnoreAccessorPatternOption = { - readonly ignoreAccessorPattern?: ReadonlyArray | string; -}; +export type IgnoreAccessorPatternOption = Readonly<{ + ignoreAccessorPattern?: ReadonlyArray | string; +}>; /** * The schema for the option to ignore accessor patterns. @@ -71,15 +55,15 @@ export const ignoreAccessorPatternOptionSchema: JSONSchema4["properties"] = { /** * The option to ignore classes. */ -export type IgnoreClassOption = { - readonly ignoreClass: boolean | "fieldsOnly"; -}; +export type IgnoreClassesOption = Readonly<{ + ignoreClasses: boolean | "fieldsOnly"; +}>; /** * The schema for the option to ignore classes. */ -export const ignoreClassOptionSchema: JSONSchema4["properties"] = { - ignoreClass: { +export const ignoreClassesOptionSchema: JSONSchema4["properties"] = { + ignoreClasses: { oneOf: [ { type: "boolean", @@ -95,15 +79,15 @@ export const ignoreClassOptionSchema: JSONSchema4["properties"] = { /** * The option to ignore interfaces. */ -export type IgnoreInterfaceOption = { - readonly ignoreInterface: boolean; -}; +export type IgnoreInterfacesOption = Readonly<{ + ignoreInterfaces: boolean; +}>; /** * The schema for the option to ignore interfaces. */ -export const ignoreInterfaceOptionSchema: JSONSchema4["properties"] = { - ignoreInterface: { +export const ignoreInterfacesOptionSchema: JSONSchema4["properties"] = { + ignoreInterfaces: { type: "boolean", }, }; @@ -111,9 +95,9 @@ export const ignoreInterfaceOptionSchema: JSONSchema4["properties"] = { /** * The option to ignore prefix selector. */ -export type IgnorePrefixSelectorOption = { - readonly ignorePrefixSelector?: ReadonlyArray | string; -}; +export type IgnorePrefixSelectorOption = Readonly<{ + ignorePrefixSelector?: ReadonlyArray | string; +}>; /** * The schema for the option to ignore prefix selector. @@ -213,29 +197,29 @@ function shouldIgnoreViaAccessorPattern( /** * Should the given node be allowed base off the following rule options? * - * - AllowLocalMutationOption. + * - AllowInFunctionOption. */ -export function shouldIgnoreLocalMutation( +export function shouldIgnoreInFunction( node: ReadonlyDeep, context: ReadonlyDeep>, - { allowLocalMutation }: Partial + allowInFunction: boolean | undefined ): boolean { - return allowLocalMutation === true && inFunctionBody(node); + return allowInFunction === true && inFunctionBody(node); } /** * Should the given node be allowed base off the following rule options? * - * - IgnoreClassOption. + * - IgnoreClassesOption. */ -export function shouldIgnoreClass( +export function shouldIgnoreClasses( node: ReadonlyDeep, context: ReadonlyDeep>, - { ignoreClass }: Partial + ignoreClasses: Partial["ignoreClasses"] ): boolean { return ( - (ignoreClass === true && inClass(node)) || - (ignoreClass === "fieldsOnly" && + (ignoreClasses === true && inClass(node)) || + (ignoreClasses === "fieldsOnly" && (isPropertyDefinition(node) || (isAssignmentExpression(node) && inClass(node) && @@ -247,14 +231,14 @@ export function shouldIgnoreClass( /** * Should the given node be allowed base off the following rule options? * - * - IgnoreInterfaceOption. + * - IgnoreInterfacesOption. */ -export function shouldIgnoreInterface( +export function shouldIgnoreInterfaces( node: ReadonlyDeep, context: ReadonlyDeep>, - { ignoreInterface }: Partial + ignoreInterfaces: Partial["ignoreInterfaces"] ): boolean { - return ignoreInterface === true && inInterface(node); + return ignoreInterfaces === true && inInterface(node); } /** @@ -266,10 +250,8 @@ export function shouldIgnoreInterface( export function shouldIgnorePattern( node: ReadonlyDeep, context: ReadonlyDeep>, - { - ignorePattern, - ignoreAccessorPattern, - }: Partial + ignorePattern: Partial["ignorePattern"], + ignoreAccessorPattern?: Partial["ignoreAccessorPattern"] ): boolean { const texts = getNodeIdentifierTexts(node, context); diff --git a/src/configs/lite.ts b/src/configs/lite.ts index e1c441f80..788b33e53 100644 --- a/src/configs/lite.ts +++ b/src/configs/lite.ts @@ -19,7 +19,7 @@ const overrides: Linter.Config = { ], [`functional/${immutableData.name}`]: [ "error", - { ignoreClass: "fieldsOnly" }, + { ignoreClasses: "fieldsOnly" }, ], [`functional/${noConditionalStatements.name}`]: "off", [`functional/${noExpressionStatements.name}`]: "off", @@ -28,7 +28,9 @@ const overrides: Linter.Config = { { enforcement: "None", ignoreInferredTypes: true, - parameters: "ReadonlyShallow", + parameters: { + enforcement: "ReadonlyShallow", + }, }, ], }, diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index e18eda385..8924bd54e 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -50,7 +50,9 @@ const overrides: Linter.Config = { { enforcement: "None", ignoreInferredTypes: true, - parameters: "ReadonlyDeep", + parameters: { + enforcement: "ReadonlyDeep", + }, }, ], [`functional/${typeDeclarationImmutability.name}`]: [ diff --git a/src/rules/functional-parameters.ts b/src/rules/functional-parameters.ts index 3e57de1e5..a4beb90d0 100644 --- a/src/rules/functional-parameters.ts +++ b/src/rules/functional-parameters.ts @@ -218,8 +218,9 @@ function checkFunction( options: Options ): RuleResult { const [optionsObject] = options; + const { ignorePattern } = optionsObject; - if (shouldIgnorePattern(node, context, optionsObject)) { + if (shouldIgnorePattern(node, context, ignorePattern)) { return { context, descriptors: [], @@ -246,8 +247,9 @@ function checkIdentifier( options: Options ): RuleResult { const [optionsObject] = options; + const { ignorePattern } = optionsObject; - if (shouldIgnorePattern(node, context, optionsObject)) { + if (shouldIgnorePattern(node, context, ignorePattern)) { return { context, descriptors: [], diff --git a/src/rules/immutable-data.ts b/src/rules/immutable-data.ts index afe5a2ecf..ac3a27208 100644 --- a/src/rules/immutable-data.ts +++ b/src/rules/immutable-data.ts @@ -6,13 +6,13 @@ import type { ReadonlyDeep } from "type-fest"; import type { IgnoreAccessorPatternOption, IgnorePatternOption, - IgnoreClassOption, + IgnoreClassesOption, } from "~/common/ignore-options"; import { shouldIgnorePattern, - shouldIgnoreClass, + shouldIgnoreClasses, ignoreAccessorPatternOptionSchema, - ignoreClassOptionSchema, + ignoreClassesOptionSchema, ignorePatternOptionSchema, } from "~/common/ignore-options"; import { isExpected } from "~/util/misc"; @@ -40,7 +40,7 @@ export const name = "immutable-data" as const; */ type Options = readonly [ IgnoreAccessorPatternOption & - IgnoreClassOption & + IgnoreClassesOption & IgnorePatternOption & Readonly<{ ignoreImmediateMutation: boolean; @@ -62,7 +62,7 @@ const schema: JSONSchema4 = [ properties: deepmerge( ignorePatternOptionSchema, ignoreAccessorPatternOptionSchema, - ignoreClassOptionSchema, + ignoreClassesOptionSchema, { ignoreImmediateMutation: { type: "boolean", @@ -97,7 +97,7 @@ const schema: JSONSchema4 = [ */ const defaultOptions: Options = [ { - ignoreClass: false, + ignoreClasses: false, ignoreImmediateMutation: true, assumeTypes: { forArrays: true, @@ -190,11 +190,12 @@ function checkAssignmentExpression( options: Options ): RuleResult { const [optionsObject] = options; + const { ignorePattern, ignoreAccessorPattern, ignoreClasses } = optionsObject; if ( !isMemberExpression(node.left) || - shouldIgnoreClass(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) + shouldIgnoreClasses(node, context, ignoreClasses) || + shouldIgnorePattern(node, context, ignorePattern, ignoreAccessorPattern) ) { return { context, @@ -221,11 +222,12 @@ function checkUnaryExpression( options: Options ): RuleResult { const [optionsObject] = options; + const { ignorePattern, ignoreAccessorPattern, ignoreClasses } = optionsObject; if ( !isMemberExpression(node.argument) || - shouldIgnoreClass(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) + shouldIgnoreClasses(node, context, ignoreClasses) || + shouldIgnorePattern(node, context, ignorePattern, ignoreAccessorPattern) ) { return { context, @@ -251,11 +253,17 @@ function checkUpdateExpression( options: Options ): RuleResult { const [optionsObject] = options; + const { ignorePattern, ignoreAccessorPattern, ignoreClasses } = optionsObject; if ( !isMemberExpression(node.argument) || - shouldIgnoreClass(node.argument, context, optionsObject) || - shouldIgnorePattern(node.argument, context, optionsObject) + shouldIgnoreClasses(node.argument, context, ignoreClasses) || + shouldIgnorePattern( + node.argument, + context, + ignorePattern, + ignoreAccessorPattern + ) ) { return { context, @@ -324,13 +332,19 @@ function checkCallExpression( options: Options ): RuleResult { const [optionsObject] = options; + const { ignorePattern, ignoreAccessorPattern, ignoreClasses } = optionsObject; // Not potential object mutation? if ( !isMemberExpression(node.callee) || !isIdentifier(node.callee.property) || - shouldIgnoreClass(node.callee.object, context, optionsObject) || - shouldIgnorePattern(node.callee.object, context, optionsObject) + shouldIgnoreClasses(node.callee.object, context, ignoreClasses) || + shouldIgnorePattern( + node.callee.object, + context, + ignorePattern, + ignoreAccessorPattern + ) ) { return { context, @@ -375,8 +389,13 @@ function checkCallExpression( node.arguments.length >= 2 && (isIdentifier(node.arguments[0]) || isMemberExpression(node.arguments[0])) && - !shouldIgnoreClass(node.arguments[0], context, optionsObject) && - !shouldIgnorePattern(node.arguments[0], context, optionsObject) && + !shouldIgnoreClasses(node.arguments[0], context, ignoreClasses) && + !shouldIgnorePattern( + node.arguments[0], + context, + ignorePattern, + ignoreAccessorPattern + ) && isObjectConstructorType( getTypeOfNode(node.callee.object, context), assumeTypesForObjects, diff --git a/src/rules/no-expression-statements.ts b/src/rules/no-expression-statements.ts index f7e005165..ef435721d 100644 --- a/src/rules/no-expression-statements.ts +++ b/src/rules/no-expression-statements.ts @@ -83,8 +83,9 @@ function checkExpressionStatement( options: Options ): RuleResult { const [optionsObject] = options; + const { ignorePattern } = optionsObject; - if (shouldIgnorePattern(node, context, optionsObject)) { + if (shouldIgnorePattern(node, context, ignorePattern)) { return { context, descriptors: [], diff --git a/src/rules/no-let.ts b/src/rules/no-let.ts index fe4c9c832..5eca7a984 100644 --- a/src/rules/no-let.ts +++ b/src/rules/no-let.ts @@ -3,14 +3,10 @@ import { deepmerge } from "deepmerge-ts"; import type { JSONSchema4 } from "json-schema"; import type { ReadonlyDeep } from "type-fest"; -import type { - AllowLocalMutationOption, - IgnorePatternOption, -} from "~/common/ignore-options"; +import type { IgnorePatternOption } from "~/common/ignore-options"; import { shouldIgnorePattern, - shouldIgnoreLocalMutation, - allowLocalMutationOptionSchema, + shouldIgnoreInFunction, ignorePatternOptionSchema, } from "~/common/ignore-options"; import type { RuleResult } from "~/util/rule"; @@ -26,10 +22,10 @@ export const name = "no-let" as const; * The options this rule can take. */ type Options = readonly [ - AllowLocalMutationOption & - IgnorePatternOption & + IgnorePatternOption & Readonly<{ allowInForLoopInit: boolean; + allowInFunctions: boolean; }> ]; @@ -39,15 +35,14 @@ type Options = readonly [ const schema: JSONSchema4 = [ { type: "object", - properties: deepmerge( - allowLocalMutationOptionSchema, - ignorePatternOptionSchema, - { - allowInForLoopInit: { - type: "boolean", - }, - } - ), + properties: deepmerge(ignorePatternOptionSchema, { + allowInForLoopInit: { + type: "boolean", + }, + allowInFunctions: { + type: "boolean", + }, + }), additionalProperties: false, }, ]; @@ -58,7 +53,7 @@ const schema: JSONSchema4 = [ const defaultOptions: Options = [ { allowInForLoopInit: false, - allowLocalMutation: false, + allowInFunctions: false, }, ]; @@ -94,12 +89,12 @@ function checkVariableDeclaration( options: Options ): RuleResult { const [optionsObject] = options; - const { allowInForLoopInit } = optionsObject; + const { allowInForLoopInit, ignorePattern, allowInFunctions } = optionsObject; if ( node.kind !== "let" || - shouldIgnoreLocalMutation(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) || + shouldIgnoreInFunction(node, context, allowInFunctions) || + shouldIgnorePattern(node, context, ignorePattern) || (allowInForLoopInit && inForLoopInitializer(node)) ) { return { diff --git a/src/rules/no-mixed-types.ts b/src/rules/no-mixed-types.ts index debee0ba1..c9ec62751 100644 --- a/src/rules/no-mixed-types.ts +++ b/src/rules/no-mixed-types.ts @@ -4,7 +4,7 @@ import type { JSONSchema4 } from "json-schema"; import type { ReadonlyDeep } from "type-fest"; import type { RuleResult } from "~/util/rule"; -import { createRule } from "~/util/rule"; +import { createRuleUsingFunction } from "~/util/rule"; import { isTSPropertySignature, isTSTypeLiteral } from "~/util/typeguard"; /** @@ -124,14 +124,11 @@ function checkTSInterfaceDeclaration( >, options: Options ): RuleResult { - const [{ checkInterfaces }] = options; - return { context, - descriptors: - checkInterfaces && hasTypeElementViolations(node.body.body) - ? [{ node, messageId: "generic" }] - : [], + descriptors: hasTypeElementViolations(node.body.body) + ? [{ node, messageId: "generic" }] + : [], }; } @@ -145,12 +142,9 @@ function checkTSTypeAliasDeclaration( >, options: Options ): RuleResult { - const [{ checkTypeLiterals }] = options; - return { context, descriptors: - checkTypeLiterals && isTSTypeLiteral(node.typeAnnotation) && hasTypeElementViolations(node.typeAnnotation.members) ? [{ node, messageId: "generic" }] @@ -159,12 +153,25 @@ function checkTSTypeAliasDeclaration( } // Create the rule. -export const rule = createRule( - name, - meta, - defaultOptions, - { - TSInterfaceDeclaration: checkTSInterfaceDeclaration, - TSTypeAliasDeclaration: checkTSTypeAliasDeclaration, - } -); +export const rule = createRuleUsingFunction< + keyof typeof errorMessages, + Options +>(name, meta, defaultOptions, (context, options) => { + const [{ checkInterfaces, checkTypeLiterals }] = options; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return Object.fromEntries( + ( + [ + [ + "TSInterfaceDeclaration", + checkInterfaces ? checkTSInterfaceDeclaration : undefined, + ], + [ + "TSTypeAliasDeclaration", + checkTypeLiterals ? checkTSTypeAliasDeclaration : undefined, + ], + ] as const + ).filter(([sel, fn]) => fn !== undefined) + ); +}); diff --git a/src/rules/no-return-void.ts b/src/rules/no-return-void.ts index 9259c9871..4a32823ca 100644 --- a/src/rules/no-return-void.ts +++ b/src/rules/no-return-void.ts @@ -27,7 +27,7 @@ type Options = readonly [ Readonly<{ allowNull: boolean; allowUndefined: boolean; - ignoreImplicit: boolean; + ignoreInferredTypes: boolean; }> ]; @@ -44,7 +44,7 @@ const schema: JSONSchema4 = [ allowUndefined: { type: "boolean", }, - ignoreImplicit: { + ignoreInferredTypes: { type: "boolean", }, }, @@ -59,7 +59,7 @@ const defaultOptions: Options = [ { allowNull: true, allowUndefined: true, - ignoreImplicit: false, + ignoreInferredTypes: false, }, ]; @@ -93,10 +93,10 @@ function checkFunction( >, options: Options ): RuleResult { - const [{ ignoreImplicit, allowNull, allowUndefined }] = options; + const [{ ignoreInferredTypes, allowNull, allowUndefined }] = options; if (node.returnType === undefined) { - if (!ignoreImplicit && isFunctionLike(node)) { + if (!ignoreInferredTypes && isFunctionLike(node)) { const functionType = getTypeOfNode(node, context); const returnType = functionType ?.getCallSignatures()?.[0] diff --git a/src/rules/prefer-immutable-types.ts b/src/rules/prefer-immutable-types.ts index 2366872cc..072a5e20b 100644 --- a/src/rules/prefer-immutable-types.ts +++ b/src/rules/prefer-immutable-types.ts @@ -5,16 +5,14 @@ import type { JSONSchema4 } from "json-schema"; import type { ReadonlyDeep } from "type-fest"; import type { - AllowLocalMutationOption, - IgnoreClassOption, + IgnoreClassesOption, IgnorePatternOption, } from "~/common/ignore-options"; import { - allowLocalMutationOptionSchema, - ignoreClassOptionSchema, + ignoreClassesOptionSchema, ignorePatternOptionSchema, - shouldIgnoreClass, - shouldIgnoreLocalMutation, + shouldIgnoreClasses, + shouldIgnoreInFunction, shouldIgnorePattern, } from "~/common/ignore-options"; import type { ESFunctionType } from "~/util/node-types"; @@ -43,8 +41,7 @@ type RawEnforcement = | false; type Option = ReadonlyDeep< - AllowLocalMutationOption & - IgnoreClassOption & + IgnoreClassesOption & IgnorePatternOption & { enforcement: RawEnforcement; ignoreInferredTypes: boolean; @@ -59,7 +56,11 @@ type Options = ReadonlyDeep< Option & { parameters?: Option | RawEnforcement; returnTypes?: Option | RawEnforcement; - variables?: Option | RawEnforcement; + variables?: + | (Option & { + ignoreInFunctions?: boolean; + }) + | RawEnforcement; } ] >; @@ -83,8 +84,7 @@ const enforcementEnumOptions = [ * The non-shorthand schema for each option. */ const optionExpandedSchema: JSONSchema4 = deepmerge( - allowLocalMutationOptionSchema, - ignoreClassOptionSchema, + ignoreClassesOptionSchema, ignorePatternOptionSchema, { enforcement: { @@ -123,7 +123,23 @@ const schema: JSONSchema4 = [ properties: deepmerge(optionExpandedSchema, { parameters: optionSchema, returnTypes: optionSchema, - variables: optionSchema, + variables: { + oneOf: [ + { + type: "object", + properties: deepmerge(optionExpandedSchema, { + ignoreInFunctions: { + type: "boolean", + }, + }), + additionalProperties: false, + }, + { + type: ["string", "number", "boolean"], + enum: enforcementEnumOptions, + }, + ], + }, }), additionalProperties: false, }, @@ -135,9 +151,8 @@ const schema: JSONSchema4 = [ const defaultOptions: Options = [ { enforcement: Immutability.Immutable, - allowLocalMutation: false, ignoreInferredTypes: false, - ignoreClass: false, + ignoreClasses: false, }, ]; @@ -198,18 +213,28 @@ function getParameterTypeViolations( ): Descriptor[] { const [optionsObject] = options; const { parameters: rawOption } = optionsObject; - const { enforcement: rawEnforcement, ignoreInferredTypes } = - typeof rawOption === "object" - ? rawOption - : { - enforcement: rawOption, - ignoreInferredTypes: defaultOptions[0].ignoreInferredTypes, - }; + const { + enforcement: rawEnforcement, + ignoreInferredTypes, + ignoreClasses, + ignorePattern, + } = typeof rawOption === "object" + ? rawOption + : { + enforcement: rawOption, + ignoreInferredTypes: optionsObject.ignoreInferredTypes, + ignoreClasses: optionsObject.ignoreClasses, + ignorePattern: optionsObject.ignorePattern, + }; const enforcement = parseEnforcement( - rawEnforcement ?? defaultOptions[0].enforcement + rawEnforcement ?? optionsObject.enforcement ); - if (enforcement === false) { + if ( + enforcement === false || + shouldIgnoreClasses(node, context, ignoreClasses) || + shouldIgnorePattern(node, context, ignorePattern) + ) { return []; } @@ -258,22 +283,30 @@ function getReturnTypeViolations( ): Descriptor[] { const [optionsObject] = options; const { returnTypes: rawOption } = optionsObject; - const { enforcement: rawEnforcement, ignoreInferredTypes } = - typeof rawOption === "object" - ? rawOption - : { - enforcement: rawOption, - ignoreInferredTypes: defaultOptions[0].ignoreInferredTypes, - }; + const { + enforcement: rawEnforcement, + ignoreInferredTypes, + ignoreClasses, + ignorePattern, + } = typeof rawOption === "object" + ? rawOption + : { + enforcement: rawOption, + ignoreInferredTypes: optionsObject.ignoreInferredTypes, + ignoreClasses: optionsObject.ignoreClasses, + ignorePattern: optionsObject.ignorePattern, + }; const enforcement = parseEnforcement( - rawEnforcement ?? defaultOptions[0].enforcement + rawEnforcement ?? optionsObject.enforcement ); - if (enforcement === false) { - return []; - } - if (ignoreInferredTypes && node.returnType?.typeAnnotation === undefined) { + if ( + enforcement === false || + (ignoreInferredTypes && node.returnType?.typeAnnotation === undefined) || + shouldIgnoreClasses(node, context, ignoreClasses) || + shouldIgnorePattern(node, context, ignorePattern) + ) { return []; } @@ -334,19 +367,6 @@ function checkFunction( >, options: Options ): RuleResult { - const [optionsObject] = options; - - if ( - shouldIgnoreClass(node, context, optionsObject) || - shouldIgnoreLocalMutation(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) - ) { - return { - context, - descriptors: [], - }; - } - const descriptors = [ ...getParameterTypeViolations(node, context, options), ...getReturnTypeViolations(node, context, options), @@ -370,10 +390,33 @@ function checkVarible( ): RuleResult { const [optionsObject] = options; + const { variables: rawOption } = optionsObject; + const { + enforcement: rawEnforcement, + ignoreInferredTypes, + ignoreClasses, + ignorePattern, + ignoreInFunctions: rawIgnoreInFunctions, + } = typeof rawOption === "object" + ? rawOption + : { + enforcement: rawOption, + ignoreInferredTypes: optionsObject.ignoreInferredTypes, + ignoreClasses: optionsObject.ignoreClasses, + ignorePattern: optionsObject.ignorePattern, + ignoreInFunctions: false, + }; + + const enforcement = parseEnforcement( + rawEnforcement ?? optionsObject.enforcement + ); + const ignoreInFunctions = rawIgnoreInFunctions ?? false; + if ( - shouldIgnoreClass(node, context, optionsObject) || - shouldIgnoreLocalMutation(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) + enforcement === false || + shouldIgnoreClasses(node, context, ignoreClasses) || + shouldIgnoreInFunction(node, context, ignoreInFunctions) || + shouldIgnorePattern(node, context, ignorePattern) ) { return { context, @@ -395,25 +438,6 @@ function checkVarible( }; } - const { variables: rawOption } = optionsObject; - const { enforcement: rawEnforcement, ignoreInferredTypes } = - typeof rawOption === "object" - ? rawOption - : { - enforcement: rawOption, - ignoreInferredTypes: defaultOptions[0].ignoreInferredTypes, - }; - - const enforcement = parseEnforcement( - rawEnforcement ?? defaultOptions[0].enforcement - ); - if (enforcement === false) { - return { - context, - descriptors: [], - }; - } - const nodeWithTypeAnnotation = isProperty ? node : node.id; if ( diff --git a/src/rules/prefer-readonly-type.ts b/src/rules/prefer-readonly-type.ts index 7d64f793a..0750e8799 100644 --- a/src/rules/prefer-readonly-type.ts +++ b/src/rules/prefer-readonly-type.ts @@ -1,23 +1,12 @@ import type { ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; -import { deepmerge } from "deepmerge-ts"; import type { JSONSchema4 } from "json-schema"; import type { ReadonlyDeep } from "type-fest"; -import type { - AllowLocalMutationOption, - IgnoreClassOption, - IgnoreInterfaceOption, - IgnorePatternOption, -} from "~/common/ignore-options"; import { - shouldIgnoreLocalMutation, - shouldIgnoreClass, - shouldIgnoreInterface, + shouldIgnoreInFunction, + shouldIgnoreClasses, + shouldIgnoreInterfaces, shouldIgnorePattern, - allowLocalMutationOptionSchema, - ignoreClassOptionSchema, - ignoreInterfaceOptionSchema, - ignorePatternOptionSchema, } from "~/common/ignore-options"; import type { ESArrayTupleType } from "~/src/util/node-types"; import type { RuleResult } from "~/util/rule"; @@ -45,15 +34,15 @@ export const name = "prefer-readonly-type" as const; * The options this rule can take. */ type Options = readonly [ - AllowLocalMutationOption & - IgnoreClassOption & - IgnoreInterfaceOption & - IgnorePatternOption & - Readonly<{ - allowMutableReturnType: boolean; - checkImplicit: boolean; - ignoreCollections: boolean; - }> + Readonly<{ + allowLocalMutation: boolean; + allowMutableReturnType: boolean; + checkImplicit: boolean; + ignoreCollections: boolean; + ignoreClass: boolean | "fieldsOnly"; + ignoreInterface: boolean; + ignorePattern?: ReadonlyArray | string; + }> ]; /** @@ -62,23 +51,40 @@ type Options = readonly [ const schema: JSONSchema4 = [ { type: "object", - properties: deepmerge( - allowLocalMutationOptionSchema, - ignorePatternOptionSchema, - ignoreClassOptionSchema, - ignoreInterfaceOptionSchema, - { - allowMutableReturnType: { - type: "boolean", - }, - checkImplicit: { - type: "boolean", + properties: { + allowLocalMutation: { + type: "boolean", + }, + ignorePattern: { + type: ["string", "array"], + items: { + type: "string", }, - ignoreCollections: { - type: "boolean", - }, - } - ), + }, + ignoreClass: { + oneOf: [ + { + type: "boolean", + }, + { + type: "string", + enum: ["fieldsOnly"], + }, + ], + }, + ignoreInterface: { + type: "boolean", + }, + allowMutableReturnType: { + type: "boolean", + }, + checkImplicit: { + type: "boolean", + }, + ignoreCollections: { + type: "boolean", + }, + }, additionalProperties: false, }, ]; @@ -151,13 +157,20 @@ function checkArrayOrTupleType( options: Options ): RuleResult { const [optionsObject] = options; - const { allowMutableReturnType, ignoreCollections } = optionsObject; + const { + allowLocalMutation, + allowMutableReturnType, + ignoreClass, + ignoreCollections, + ignoreInterface, + ignorePattern, + } = optionsObject; if ( - shouldIgnoreClass(node, context, optionsObject) || - shouldIgnoreInterface(node, context, optionsObject) || - shouldIgnoreLocalMutation(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) || + shouldIgnoreClasses(node, context, ignoreClass) || + shouldIgnoreInterfaces(node, context, ignoreInterface) || + shouldIgnoreInFunction(node, context, allowLocalMutation) || + shouldIgnorePattern(node, context, ignorePattern) || ignoreCollections ) { return { @@ -208,12 +221,14 @@ function checkMappedType( options: Options ): RuleResult { const [optionsObject] = options; + const { allowLocalMutation, ignoreClass, ignoreInterface, ignorePattern } = + optionsObject; if ( - shouldIgnoreClass(node, context, optionsObject) || - shouldIgnoreInterface(node, context, optionsObject) || - shouldIgnoreLocalMutation(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) + shouldIgnoreClasses(node, context, ignoreClass) || + shouldIgnoreInterfaces(node, context, ignoreInterface) || + shouldIgnoreInFunction(node, context, allowLocalMutation) || + shouldIgnorePattern(node, context, ignorePattern) ) { return { context, @@ -251,13 +266,20 @@ function checkTypeReference( options: Options ): RuleResult { const [optionsObject] = options; - const { allowMutableReturnType, ignoreCollections } = optionsObject; + const { + allowLocalMutation, + ignoreClass, + ignoreInterface, + ignorePattern, + allowMutableReturnType, + ignoreCollections, + } = optionsObject; if ( - shouldIgnoreClass(node, context, optionsObject) || - shouldIgnoreInterface(node, context, optionsObject) || - shouldIgnoreLocalMutation(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) + shouldIgnoreClasses(node, context, ignoreClass) || + shouldIgnoreInterfaces(node, context, ignoreInterface) || + shouldIgnoreInFunction(node, context, allowLocalMutation) || + shouldIgnorePattern(node, context, ignorePattern) ) { return { context, @@ -314,13 +336,19 @@ function checkProperty( options: Options ): RuleResult { const [optionsObject] = options; - const { allowMutableReturnType } = optionsObject; + const { + allowLocalMutation, + ignoreClass, + ignoreInterface, + ignorePattern, + allowMutableReturnType, + } = optionsObject; if ( - shouldIgnoreClass(node, context, optionsObject) || - shouldIgnoreInterface(node, context, optionsObject) || - shouldIgnoreLocalMutation(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) + shouldIgnoreClasses(node, context, ignoreClass) || + shouldIgnoreInterfaces(node, context, ignoreInterface) || + shouldIgnoreInFunction(node, context, allowLocalMutation) || + shouldIgnorePattern(node, context, ignorePattern) ) { return { context, @@ -373,14 +401,21 @@ function checkImplicitType( options: Options ): RuleResult { const [optionsObject] = options; - const { checkImplicit, ignoreCollections } = optionsObject; + const { + allowLocalMutation, + ignoreClass, + ignoreInterface, + ignorePattern, + checkImplicit, + ignoreCollections, + } = optionsObject; if ( !checkImplicit || - shouldIgnoreClass(node, context, optionsObject) || - shouldIgnoreInterface(node, context, optionsObject) || - shouldIgnoreLocalMutation(node, context, optionsObject) || - shouldIgnorePattern(node, context, optionsObject) + shouldIgnoreClasses(node, context, ignoreClass) || + shouldIgnoreInterfaces(node, context, ignoreInterface) || + shouldIgnoreInFunction(node, context, allowLocalMutation) || + shouldIgnorePattern(node, context, ignorePattern) ) { return { context, diff --git a/src/rules/prefer-tacit.ts b/src/rules/prefer-tacit.ts index a35a32320..0dab381ff 100644 --- a/src/rules/prefer-tacit.ts +++ b/src/rules/prefer-tacit.ts @@ -3,7 +3,7 @@ import { deepmerge } from "deepmerge-ts"; import type { JSONSchema4 } from "json-schema"; import * as semver from "semver"; import type { ReadonlyDeep } from "type-fest"; -import type { FunctionLikeDeclaration, Type } from "typescript"; +import type { Type } from "typescript"; import type { IgnorePatternOption } from "~/common/ignore-options"; import { ignorePatternOptionSchema } from "~/common/ignore-options"; diff --git a/src/rules/type-declaration-immutability.ts b/src/rules/type-declaration-immutability.ts index 03d826a3e..17680161c 100644 --- a/src/rules/type-declaration-immutability.ts +++ b/src/rules/type-declaration-immutability.ts @@ -279,9 +279,9 @@ function checkTypeDeclaration( options: Options ): RuleResult { const [optionsObject] = options; - const { ignoreInterfaces } = optionsObject; + const { ignoreInterfaces, ignorePattern } = optionsObject; if ( - shouldIgnorePattern(node, context, optionsObject) || + shouldIgnorePattern(node, context, ignorePattern) || (ignoreInterfaces && isTSInterfaceDeclaration(node)) ) { return { diff --git a/tests/common/ignore-options.test.ts b/tests/common/ignore-options.test.ts index 524382a12..07a881da5 100644 --- a/tests/common/ignore-options.test.ts +++ b/tests/common/ignore-options.test.ts @@ -1,47 +1,18 @@ import assert from "node:assert"; -import type { TSESLint, TSESTree } from "@typescript-eslint/utils"; +import type { TSESLint } from "@typescript-eslint/utils"; import test from "ava"; import dedent from "dedent"; import RuleTester from "eslint-ava-rule-tester"; -import type { ReadonlyDeep } from "type-fest"; import type { - AllowLocalMutationOption, IgnoreAccessorPatternOption, - IgnoreClassOption, - IgnoreInterfaceOption, IgnorePatternOption, } from "~/common/ignore-options"; -import { - shouldIgnoreClass, - shouldIgnoreInterface, - shouldIgnoreLocalMutation, - shouldIgnorePattern, -} from "~/common/ignore-options"; +import { shouldIgnorePattern } from "~/common/ignore-options"; import { filename, configs } from "~/tests/helpers/configs"; import { testWrapper } from "~/tests/helpers/testers"; import { addFilename, createDummyRule } from "~/tests/helpers/util"; -import type { BaseOptions } from "~/util/rule"; - -function shouldIgnore( - node: ReadonlyDeep, - context: ReadonlyDeep>, - options: Partial< - AllowLocalMutationOption & - IgnoreAccessorPatternOption & - IgnoreClassOption & - IgnoreInterfaceOption & - IgnorePatternOption - > -): boolean { - return [ - shouldIgnorePattern, - shouldIgnoreClass, - shouldIgnoreInterface, - shouldIgnoreLocalMutation, - ].some((testShouldIgnore) => testShouldIgnore(node, context, options)); -} /** * Create a dummy rule that operates on AssignmentExpression nodes. @@ -51,7 +22,14 @@ function createDummyAssignmentExpressionRule() { const [allowed, options] = context.options; return { AssignmentExpression: (node) => { - assert(shouldIgnore(node, context, options) === allowed); + assert( + shouldIgnorePattern( + node, + context, + options.ignorePattern, + options.ignoreAccessorPattern + ) === allowed + ); }, }; }); @@ -273,7 +251,9 @@ new RuleTester(testWrapper(test), configs.es10).run( const [allowed, options] = context.options; return { ExpressionStatement: (node) => { - assert(shouldIgnore(node, context, options) === allowed); + assert( + shouldIgnorePattern(node, context, options.ignorePattern) === allowed + ); }, }; }), diff --git a/tests/rules/immutable-data/es6/object/invalid.ts b/tests/rules/immutable-data/es6/object/invalid.ts index a0133ee57..c2ed40b01 100644 --- a/tests/rules/immutable-data/es6/object/invalid.ts +++ b/tests/rules/immutable-data/es6/object/invalid.ts @@ -61,7 +61,7 @@ const tests: ReadonlyArray = [ } } `, - optionsSet: [[{ ignoreClass: "fieldsOnly" }]], + optionsSet: [[{ ignoreClasses: "fieldsOnly" }]], errors: [ { messageId: "generic", diff --git a/tests/rules/immutable-data/es6/object/valid.ts b/tests/rules/immutable-data/es6/object/valid.ts index 98a599bc2..a45c89594 100644 --- a/tests/rules/immutable-data/es6/object/valid.ts +++ b/tests/rules/immutable-data/es6/object/valid.ts @@ -41,7 +41,7 @@ const tests: ReadonlyArray = [ } } `, - optionsSet: [[{ ignoreClass: true }], [{ ignoreClass: "fieldsOnly" }]], + optionsSet: [[{ ignoreClasses: true }], [{ ignoreClasses: "fieldsOnly" }]], }, ]; diff --git a/tests/rules/no-let/es6/invalid.ts b/tests/rules/no-let/es6/invalid.ts index 0bd82f0e7..4857c636a 100644 --- a/tests/rules/no-let/es6/invalid.ts +++ b/tests/rules/no-let/es6/invalid.ts @@ -7,7 +7,7 @@ const tests: ReadonlyArray = [ code: `let x;`, optionsSet: [ [], - [{ allowLocalMutation: true }], + [{ allowInFunctions: true }], [{ ignorePattern: "^mutable" }], ], errors: [ @@ -23,7 +23,7 @@ const tests: ReadonlyArray = [ code: `let x = 0;`, optionsSet: [ [], - [{ allowLocalMutation: true }], + [{ allowInFunctions: true }], [{ ignorePattern: "^mutable" }], ], errors: [ @@ -39,7 +39,7 @@ const tests: ReadonlyArray = [ code: `for (let x = 0; x < 1; x++);`, optionsSet: [ [], - [{ allowLocalMutation: true }], + [{ allowInFunctions: true }], [{ ignorePattern: "^mutable" }], ], errors: [ @@ -55,7 +55,7 @@ const tests: ReadonlyArray = [ code: `for (let x = 0, y = 0; x < 1; x++);`, optionsSet: [ [], - [{ allowLocalMutation: true }], + [{ allowInFunctions: true }], [{ ignorePattern: "^mutable" }], ], errors: [ @@ -71,7 +71,7 @@ const tests: ReadonlyArray = [ code: `for (let x in {});`, optionsSet: [ [], - [{ allowLocalMutation: true }], + [{ allowInFunctions: true }], [{ ignorePattern: "^mutable" }], ], errors: [ @@ -87,7 +87,7 @@ const tests: ReadonlyArray = [ code: `for (let x of []);`, optionsSet: [ [], - [{ allowLocalMutation: true }], + [{ allowInFunctions: true }], [{ ignorePattern: "^mutable" }], ], errors: [ diff --git a/tests/rules/no-let/es6/valid.ts b/tests/rules/no-let/es6/valid.ts index 005c5d5e4..2064581e3 100644 --- a/tests/rules/no-let/es6/valid.ts +++ b/tests/rules/no-let/es6/valid.ts @@ -10,7 +10,7 @@ const tests: ReadonlyArray = [ let y = 0; } `, - optionsSet: [[{ allowLocalMutation: true }]], + optionsSet: [[{ allowInFunctions: true }]], }, { code: dedent` @@ -19,7 +19,7 @@ const tests: ReadonlyArray = [ let y = 0; } `, - optionsSet: [[{ allowLocalMutation: true }]], + optionsSet: [[{ allowInFunctions: true }]], }, { code: dedent` @@ -30,7 +30,7 @@ const tests: ReadonlyArray = [ } } `, - optionsSet: [[{ allowLocalMutation: true }]], + optionsSet: [[{ allowInFunctions: true }]], }, { code: dedent` diff --git a/tests/rules/no-return-void/ts/invalid.ts b/tests/rules/no-return-void/ts/invalid.ts index 3e89651dc..2b73a0afb 100644 --- a/tests/rules/no-return-void/ts/invalid.ts +++ b/tests/rules/no-return-void/ts/invalid.ts @@ -63,7 +63,7 @@ const tests: ReadonlyArray = [ return baz => { console.log(bar, baz); } } `, - optionsSet: [[{ ignoreImplicit: true }]], + optionsSet: [[{ ignoreInferredTypes: true }]], errors: [ { messageId: "generic", @@ -81,9 +81,9 @@ const tests: ReadonlyArray = [ } `, optionsSet: [ - [{ ignoreImplicit: false }], - [{ ignoreImplicit: false, allowNull: false }], - [{ ignoreImplicit: false, allowUndefined: false }], + [{ ignoreInferredTypes: false }], + [{ ignoreInferredTypes: false, allowNull: false }], + [{ ignoreInferredTypes: false, allowUndefined: false }], ], errors: [ { diff --git a/tests/rules/no-return-void/ts/valid.ts b/tests/rules/no-return-void/ts/valid.ts index 761a5fa5a..952cf495d 100644 --- a/tests/rules/no-return-void/ts/valid.ts +++ b/tests/rules/no-return-void/ts/valid.ts @@ -27,9 +27,9 @@ const tests: ReadonlyArray = [ } `, optionsSet: [ - [{ ignoreImplicit: true }], - [{ ignoreImplicit: true, allowNull: false }], - [{ ignoreImplicit: true, allowUndefined: false }], + [{ ignoreInferredTypes: true }], + [{ ignoreInferredTypes: true, allowNull: false }], + [{ ignoreInferredTypes: true, allowUndefined: false }], ], }, // Allow null. diff --git a/tests/rules/prefer-immutable-types/ts/variables/valid.ts b/tests/rules/prefer-immutable-types/ts/variables/valid.ts index 8fc2590ac..c98742b9c 100644 --- a/tests/rules/prefer-immutable-types/ts/variables/valid.ts +++ b/tests/rules/prefer-immutable-types/ts/variables/valid.ts @@ -180,7 +180,7 @@ const tests: ReadonlyArray = [ private static qux: number; } `, - optionsSet: [[{ ignoreClass: true }]], + optionsSet: [[{ ignoreClasses: true }]], }, // Allow Local. { @@ -189,13 +189,12 @@ const tests: ReadonlyArray = [ let foo: { a: { foo: number }, b: string[], - c: () => string[], - d: { [key: string]: string[] }, + c: { [key: string]: string[] }, [key: string]: any, } }; `, - optionsSet: [[{ allowLocalMutation: true }]], + optionsSet: [[{ variables: { ignoreInFunctions: true } }]], }, // Ignore Prefix. { diff --git a/tests/rules/work.test.ts b/tests/rules/work.test.ts index 5a7fbf30d..fd014ee6a 100644 --- a/tests/rules/work.test.ts +++ b/tests/rules/work.test.ts @@ -23,7 +23,7 @@ const valid: ReadonlyArray = [ // // Valid Code. // `, // optionsSet: [[]], - // settingsSet: [{}] + // settingsSet: [{}], // } ]; @@ -32,18 +32,21 @@ const valid: ReadonlyArray = [ * Or provide an invalid test case. */ const invalid: ReadonlyArray = [ - { - code: "function foo(): ReadonlySet {}", - optionsSet: [[{ returnTypes: "Immutable" }]], - errors: [ - { - messageId: "returnType", - type: "TSTypeAnnotation", - line: 1, - column: 15, - }, - ], - }, + // { + // code: dedent` + // // Invalid Code. + // `, + // optionsSet: [[]], + // settingsSet: [{}], + // errors: [ + // { + // messageId: "returnType", + // type: "TSTypeAnnotation", + // line: 1, + // column: 1, + // }, + // ], + // }, ]; /* From d67ecb89097c37d31bec2b9ffab1f8795addbba1 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 4 Oct 2022 23:12:48 +1300 Subject: [PATCH 046/100] chore: put breaking marker in the right place --- cz-adapter/engine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cz-adapter/engine.ts b/cz-adapter/engine.ts index 3aaca55f4..bd56c4ce6 100644 --- a/cz-adapter/engine.ts +++ b/cz-adapter/engine.ts @@ -191,7 +191,7 @@ function doCommit( const scopeValue = answers.scope ?? answers.scopeRules ?? ""; const scope = scopeValue.length > 0 ? `(${scopeValue})` : ""; // Hard limit is applied by the validate. - const head = `${answers.type + breakingMarker + scope}: ${answers.subject}`; + const head = `${answers.type + scope + breakingMarker}: ${answers.subject}`; const bodyValue = (answers.body ?? "").trim(); const bodyValueWithBreaking = From 1f307bcae1ce8db0491a65e427c675386c544f89 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Wed, 5 Oct 2022 05:01:51 +1300 Subject: [PATCH 047/100] fixup(prefer-immutable-types): fix bugs --- src/rules/prefer-immutable-types.ts | 26 ++++++++++++++++++-------- src/settings/immutability.ts | 26 ++++++++++++++++---------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/rules/prefer-immutable-types.ts b/src/rules/prefer-immutable-types.ts index 072a5e20b..4da0017f6 100644 --- a/src/rules/prefer-immutable-types.ts +++ b/src/rules/prefer-immutable-types.ts @@ -54,12 +54,14 @@ type Option = ReadonlyDeep< type Options = ReadonlyDeep< [ Option & { - parameters?: Option | RawEnforcement; - returnTypes?: Option | RawEnforcement; + parameters?: Partial