diff --git a/README.md b/README.md index 0fe4f3c4f..144791ba9 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,9 @@ The [below section](#rules) gives details on which rules are enabled by each rul ### Currying -| Name | Description | πŸ’Ό | ⚠️ | 🚫 | πŸ”§ | πŸ’‘ | πŸ’­ | ❌ | -| :----------------------------------------------------------- | :----------------------------- | :--------------------------- | :-- | :-- | :-- | :-- | :-- | :-- | -| [functional-parameters](docs/rules/functional-parameters.md) | Enforce functional parameters. | β˜‘οΈ βœ… πŸ”’ ![badge-currying][] | | | | | | | +| Name | Description | πŸ’Ό | ⚠️ | 🚫 | πŸ”§ | πŸ’‘ | πŸ’­ | ❌ | +| :----------------------------------------------------------- | :----------------------------- | :--------------------------- | :-- | :---------------------------- | :-- | :-- | :-- | :-- | +| [functional-parameters](docs/rules/functional-parameters.md) | Enforce functional parameters. | β˜‘οΈ βœ… πŸ”’ ![badge-currying][] | | ![badge-disableTypeChecked][] | | | πŸ’­ | | ### No Exceptions diff --git a/docs/rules/functional-parameters.md b/docs/rules/functional-parameters.md index 007c779f4..f993f007e 100644 --- a/docs/rules/functional-parameters.md +++ b/docs/rules/functional-parameters.md @@ -3,7 +3,9 @@ # Enforce functional parameters (`functional/functional-parameters`) -πŸ’Ό This rule is enabled in the following configs: `currying`, β˜‘οΈ `lite`, βœ… `recommended`, πŸ”’ `strict`. +πŸ’ΌπŸš« This rule is enabled in the following configs: `currying`, β˜‘οΈ `lite`, βœ… `recommended`, πŸ”’ `strict`. This rule is _disabled_ in the `disableTypeChecked` config. + +πŸ’­ This rule requires [type information](https://typescript-eslint.io/linting/typed-linting). @@ -11,6 +13,8 @@ Disallow use of rest parameters, the `arguments` keyword and enforces that functions take at least 1 parameter. +Note: type information is only required when using the [overrides](#overrides) option. + ## Rule Details In functions, `arguments` is a special variable that is implicitly available. @@ -74,6 +78,36 @@ type Options = { }; ignoreIdentifierPattern?: string[] | string; ignorePrefixSelector?: string[] | string; + overrides?: Array<{ + match: Array< + | { + from: "file"; + path?: string; + name?: string | string[]; + pattern?: RegExp | RegExp[]; + ignoreName?: string | string[]; + ignorePattern?: RegExp | RegExp[]; + } + | { + from: "lib"; + name?: string | string[]; + pattern?: RegExp | RegExp[]; + ignoreName?: string | string[]; + ignorePattern?: RegExp | RegExp[]; + } + | { + from: "package"; + package?: string; + name?: string | string[]; + pattern?: RegExp | RegExp[]; + ignoreName?: string | string[]; + ignorePattern?: RegExp | RegExp[]; + } + >; + options: Omit; + inherit?: boolean; + disable: boolean; + }>; }; ``` @@ -208,3 +242,31 @@ const sum = [1, 2, 3].reduce((carry, current) => current, 0); This option takes a RegExp string or an array of RegExp strings. It allows for the ability to ignore violations based on a function's name. + +### `overrides` + +_Using this option requires type infomation._ + +Allows for applying overrides to the options based on the function's type. +This can be used to override the settings for types coming from 3rd party libraries. + +Note: Only the first matching override will be used. + +#### `overrides[n].specifiers` + +A specifier, or an array of specifiers to match the function type against. + +In the case of reference types, both the type and its generics will be recursively checked. +If any of them match, the specifier will be considered a match. + +#### `overrides[n].options` + +The options to use when a specifiers matches. + +#### `overrides[n].inherit` + +Inherit the root options? Default is `true`. + +#### `overrides[n].disable` + +If true, when a specifier matches, this rule will not be applied to the matching node. diff --git a/knip.jsonc b/knip.jsonc index 1b26dc178..92335fa5a 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -2,7 +2,7 @@ "$schema": "node_modules/knip/schema-jsonc.json", "entry": ["src/index.ts!", "tests/**/*.test.ts"], "project": ["src/**/*.ts!", "tests/**/*.{js,ts}"], - "ignore": ["tests/fixture/file.ts", "src/utils/schemas.ts"], + "ignore": ["tests/fixture/file.ts"], "ignoreDependencies": [ // Unknown reason for issue. "@vitest/coverage-v8", diff --git a/src/rules/functional-parameters.ts b/src/rules/functional-parameters.ts index de1803f61..4d65a75b0 100644 --- a/src/rules/functional-parameters.ts +++ b/src/rules/functional-parameters.ts @@ -9,9 +9,13 @@ import { deepmerge } from "deepmerge-ts"; import { type IgnoreIdentifierPatternOption, type IgnorePrefixSelectorOption, + type OverridableOptions, + type RawOverridableOptions, + getCoreOptions, ignoreIdentifierPatternOptionSchema, ignorePrefixSelectorOptionSchema, shouldIgnorePattern, + upgradeRawOverridableOptions, } from "#/options"; import { ruleNameScope } from "#/utils/misc"; import type { ESFunction } from "#/utils/node-types"; @@ -21,7 +25,9 @@ import { type RuleResult, createRuleUsingFunction, } from "#/utils/rule"; +import { overridableOptionsSchema } from "#/utils/schemas"; import { + getEnclosingFunction, isArgument, isGetter, isIIFE, @@ -46,83 +52,82 @@ export const fullName: `${typeof ruleNameScope}/${typeof name}` = `${ruleNameSco */ type ParameterCountOptions = "atLeastOne" | "exactlyOne"; +type CoreOptions = IgnoreIdentifierPatternOption & + IgnorePrefixSelectorOption & { + allowRestParameter: boolean; + allowArgumentsKeyword: boolean; + enforceParameterCount: + | ParameterCountOptions + | false + | { + count: ParameterCountOptions; + ignoreLambdaExpression: boolean; + ignoreIIFE: boolean; + ignoreGettersAndSetters: boolean; + }; + }; + /** * The options this rule can take. */ -type Options = [ - IgnoreIdentifierPatternOption & - IgnorePrefixSelectorOption & { - allowRestParameter: boolean; - allowArgumentsKeyword: boolean; - enforceParameterCount: - | ParameterCountOptions - | false - | { - count: ParameterCountOptions; - ignoreLambdaExpression: boolean; - ignoreIIFE: boolean; - ignoreGettersAndSetters: boolean; - }; - }, -]; +type RawOptions = [RawOverridableOptions]; +type Options = OverridableOptions; -/** - * The schema for the rule options. - */ -const schema: JSONSchema4[] = [ +const coreOptionsPropertiesSchema = deepmerge( + ignoreIdentifierPatternOptionSchema, + ignorePrefixSelectorOptionSchema, { - type: "object", - properties: deepmerge( - ignoreIdentifierPatternOptionSchema, - ignorePrefixSelectorOptionSchema, - { - allowRestParameter: { + allowRestParameter: { + type: "boolean", + }, + allowArgumentsKeyword: { + type: "boolean", + }, + enforceParameterCount: { + oneOf: [ + { type: "boolean", + enum: [false], }, - allowArgumentsKeyword: { - type: "boolean", + { + type: "string", + enum: ["atLeastOne", "exactlyOne"], }, - enforceParameterCount: { - oneOf: [ - { - type: "boolean", - enum: [false], - }, - { + { + type: "object", + properties: { + count: { type: "string", enum: ["atLeastOne", "exactlyOne"], }, - { - type: "object", - properties: { - count: { - type: "string", - enum: ["atLeastOne", "exactlyOne"], - }, - ignoreGettersAndSetters: { - type: "boolean", - }, - ignoreLambdaExpression: { - type: "boolean", - }, - ignoreIIFE: { - type: "boolean", - }, - }, - additionalProperties: false, + ignoreGettersAndSetters: { + type: "boolean", + }, + ignoreLambdaExpression: { + type: "boolean", + }, + ignoreIIFE: { + type: "boolean", }, - ], + }, + additionalProperties: false, }, - } satisfies JSONSchema4ObjectSchema["properties"], - ), - additionalProperties: false, + ], + }, }, +) as NonNullable; + +/** + * The schema for the rule options. + */ +const schema: JSONSchema4[] = [ + overridableOptionsSchema(coreOptionsPropertiesSchema), ]; /** * The default options for the rule. */ -const defaultOptions: Options = [ +const defaultOptions: RawOptions = [ { allowRestParameter: false, allowArgumentsKeyword: false, @@ -157,7 +162,7 @@ const meta: NamedCreateRuleCustomMeta = { description: "Enforce functional parameters.", recommended: "recommended", recommendedSeverity: "error", - requiresTypeChecking: false, + requiresTypeChecking: true, }, messages: errorMessages, schema, @@ -167,9 +172,9 @@ const meta: NamedCreateRuleCustomMeta = { * Get the rest parameter violations. */ function getRestParamViolations( - [{ allowRestParameter }]: Readonly, + { allowRestParameter }: Readonly, node: ESFunction, -): RuleResult["descriptors"] { +): RuleResult["descriptors"] { return !allowRestParameter && node.params.length > 0 && isRestElement(node.params.at(-1)) @@ -186,9 +191,9 @@ function getRestParamViolations( * Get the parameter count violations. */ function getParamCountViolations( - [{ enforceParameterCount }]: Readonly, + { enforceParameterCount }: Readonly, node: ESFunction, -): RuleResult["descriptors"] { +): RuleResult["descriptors"] { if ( enforceParameterCount === false || (node.params.length === 0 && @@ -234,11 +239,24 @@ function getParamCountViolations( */ function checkFunction( node: ESFunction, - context: Readonly>, - options: Readonly, -): RuleResult { - const [optionsObject] = options; - const { ignoreIdentifierPattern } = optionsObject; + context: Readonly>, + rawOptions: Readonly, +): RuleResult { + const options = upgradeRawOverridableOptions(rawOptions[0]); + const optionsToUse = getCoreOptions( + node, + context, + options, + ); + + if (optionsToUse === null) { + return { + context, + descriptors: [], + }; + } + + const { ignoreIdentifierPattern } = optionsToUse; if (shouldIgnorePattern(node, context, ignoreIdentifierPattern)) { return { @@ -250,8 +268,8 @@ function checkFunction( return { context, descriptors: [ - ...getRestParamViolations(options, node), - ...getParamCountViolations(options, node), + ...getRestParamViolations(optionsToUse, node), + ...getParamCountViolations(optionsToUse, node), ], }; } @@ -261,11 +279,31 @@ function checkFunction( */ function checkIdentifier( node: TSESTree.Identifier, - context: Readonly>, - options: Readonly, -): RuleResult { - const [optionsObject] = options; - const { ignoreIdentifierPattern } = optionsObject; + context: Readonly>, + rawOptions: Readonly, +): RuleResult { + if (node.name !== "arguments") { + return { + context, + descriptors: [], + }; + } + + const functionNode = getEnclosingFunction(node); + const options = upgradeRawOverridableOptions(rawOptions[0]); + const optionsToUse = + functionNode === null + ? options + : getCoreOptions(functionNode, context, options); + + if (optionsToUse === null) { + return { + context, + descriptors: [], + }; + } + + const { ignoreIdentifierPattern } = optionsToUse; if (shouldIgnorePattern(node, context, ignoreIdentifierPattern)) { return { @@ -274,15 +312,12 @@ function checkIdentifier( }; } - const { allowArgumentsKeyword } = optionsObject; + const { allowArgumentsKeyword } = optionsToUse; return { context, descriptors: - !allowArgumentsKeyword && - node.name === "arguments" && - !isPropertyName(node) && - !isPropertyAccess(node) + !allowArgumentsKeyword && !isPropertyName(node) && !isPropertyAccess(node) ? [ { node, @@ -294,8 +329,8 @@ function checkIdentifier( } // Create the rule. -export const rule: Rule = - createRuleUsingFunction( +export const rule: Rule = + createRuleUsingFunction( name, meta, defaultOptions, diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index dc29305d0..0b2dc7609 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -2,6 +2,7 @@ import type { JSONSchema4, JSONSchema4ObjectSchema, } from "@typescript-eslint/utils/json-schema"; +import { deepmerge } from "deepmerge-ts"; const typeSpecifierPatternSchemaProperties: JSONSchema4ObjectSchema["properties"] = { @@ -63,9 +64,6 @@ const typeSpecifierSchema: JSONSchema4 = { ], }; -export const typeSpecifiersSchema: JSONSchema4 = - schemaInstanceOrInstanceArray(typeSpecifierSchema); - export function schemaInstanceOrInstanceArray( items: JSONSchema4, ): NonNullable[string] { @@ -79,3 +77,37 @@ export function schemaInstanceOrInstanceArray( ], }; } + +export function overridableOptionsSchema( + coreOptionsPropertiesSchema: NonNullable< + JSONSchema4ObjectSchema["properties"] + >, +): JSONSchema4 { + return { + type: "object", + properties: deepmerge(coreOptionsPropertiesSchema, { + overrides: { + type: "array", + items: { + type: "object", + properties: { + specifiers: schemaInstanceOrInstanceArray(typeSpecifierSchema), + options: { + type: "object", + properties: coreOptionsPropertiesSchema, + additionalProperties: false, + }, + inherit: { + type: "boolean", + }, + disable: { + type: "boolean", + }, + }, + additionalProperties: false, + }, + }, + } satisfies JSONSchema4ObjectSchema["properties"]), + additionalProperties: false, + }; +} diff --git a/tests/rules/functional-parameters.test.ts b/tests/rules/functional-parameters.test.ts index 533d87789..ddffad913 100644 --- a/tests/rules/functional-parameters.test.ts +++ b/tests/rules/functional-parameters.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { name, rule } from "#/rules/functional-parameters"; -import { esLatestConfig } from "../utils/configs"; +import { esLatestConfig, typescriptConfig } from "../utils/configs"; describe(name, () => { describe("javascript - es latest", () => { @@ -435,4 +435,110 @@ describe(name, () => { }); }); }); + + describe("typescript", () => { + const { valid, invalid } = createRuleTester({ + name, + rule, + configs: typescriptConfig, + }); + + describe("overrides", () => { + it('override value works - "allowRestParameter"', () => { + const code = dedent` + function foo(...bar: string[]) { + console.log(bar); + } + `; + + valid({ + code, + options: { + allowRestParameter: false, + overrides: [ + { + specifiers: { + from: "file", + }, + options: { + allowRestParameter: true, + }, + }, + ], + }, + }); + }); + + it('override value works - "allowArgumentsKeyword"', () => { + const code = dedent` + function foo(bar: string[]) { + console.log(arguments); + } + `; + + valid({ + code, + options: { + allowArgumentsKeyword: false, + overrides: [ + { + specifiers: { + from: "file", + }, + options: { + allowArgumentsKeyword: true, + }, + }, + ], + }, + }); + }); + + it('disbale override works - "allowRestParameter"', () => { + const code = dedent` + function foo(...bar: string[]) { + console.log(bar); + } + `; + + valid({ + code, + options: { + allowRestParameter: false, + overrides: [ + { + specifiers: { + from: "file", + }, + disable: true, + }, + ], + }, + }); + }); + + it('disbale override works - "allowArgumentsKeyword"', () => { + const code = dedent` + function foo(bar: string[]) { + console.log(arguments); + } + `; + + valid({ + code, + options: { + allowArgumentsKeyword: false, + overrides: [ + { + specifiers: { + from: "file", + }, + disable: true, + }, + ], + }, + }); + }); + }); + }); });