From 35cc99b72e9dda2729517218ebf4f6fae6ecfd9c Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 24 Jul 2019 19:24:50 -0400 Subject: [PATCH] feat(eslint-plugin): added new rule typedef (#581) --- packages/eslint-plugin/README.md | 1 + packages/eslint-plugin/ROADMAP.md | 3 +- packages/eslint-plugin/docs/rules/typedef.md | 264 ++++++++++ packages/eslint-plugin/src/configs/all.json | 1 + packages/eslint-plugin/src/rules/index.ts | 4 +- packages/eslint-plugin/src/rules/typedef.ts | 136 +++++ .../eslint-plugin/tests/rules/typedef.test.ts | 476 ++++++++++++++++++ 7 files changed, 883 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/typedef.md create mode 100644 packages/eslint-plugin/src/rules/typedef.ts create mode 100644 packages/eslint-plugin/tests/rules/typedef.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index ce069a2aff6..167bb2b4933 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -181,6 +181,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | | [`@typescript-eslint/triple-slash-reference`](./docs/rules/triple-slash-reference.md) | Sets preference level for triple slash directives versus ES6-style import declarations | | | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/typedef`](./docs/rules/typedef.md) | Requires type annotations to exist | | | | | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope | | | :thought_balloon: | | [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one by using a union or an optional/rest parameter | | | | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index a30bd468d17..ab59ea66ab0 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -32,8 +32,8 @@ | [`only-arrow-functions`] | 🔌 | [`prefer-arrow/prefer-arrow-functions`] | | [`prefer-for-of`] | ✅ | [`@typescript-eslint/prefer-for-of`] | | [`promise-function-async`] | ✅ | [`@typescript-eslint/promise-function-async`] | -| [`typedef`] | 🛑 | N/A | | [`typedef-whitespace`] | ✅ | [`@typescript-eslint/type-annotation-spacing`] | +| [`typedef`] | ✅ | [`@typescript-eslint/typedef`] | | [`unified-signatures`] | ✅ | [`@typescript-eslint/unified-signatures`] | [1] The ESLint rule only supports exact string matching, rather than regular expressions
@@ -592,6 +592,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/no-unnecessary-type-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.md [`@typescript-eslint/no-var-requires`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-var-requires.md [`@typescript-eslint/type-annotation-spacing`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/type-annotation-spacing.md +[`@typescript-eslint/typedef`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/typedef.md [`@typescript-eslint/unified-signatures`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/unified-signatures.md [`@typescript-eslint/no-misused-new`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-misused-new.md [`@typescript-eslint/no-object-literal-type-assertion`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-object-literal-type-assertion.md diff --git a/packages/eslint-plugin/docs/rules/typedef.md b/packages/eslint-plugin/docs/rules/typedef.md new file mode 100644 index 00000000000..4498ab95610 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/typedef.md @@ -0,0 +1,264 @@ +# Require type annotations to exist (typedef) + +TypeScript cannot always infer types for all places in code. +Some locations require type annotations for their types to be inferred. + +```ts +class ContainsText { + // There must be a type annotation here to infer the type + delayedText: string; + + // `typedef` requires a type annotation here to maintain consistency + immediateTextExplicit: string = 'text'; + + // This is still a string type because of its initial value + immediateTextImplicit = 'text'; +} +``` + +> Note: requiring type annotations unnecessarily can be cumbersome to maintain and generally reduces code readability. +> TypeScript is often better at inferring types than easily written type annotations would allow. +> Instead of enabling `typedef`, it is generally recommended to use the `--noImplicitAny` and/or `--strictPropertyInitialization` compiler options to enforce type annotations only when useful. + +## Rule Details + +This rule can enforce type annotations in locations regardless of whether they're required. +This is typically used to maintain consistency for element types that sometimes require them. + +> To enforce type definitions existing on call signatures as per TSLint's `arrow-call-signature` and `call-signature` options, use `explicit-function-return-type`. + +## Options + +This rule has an object option that may receive any of the following as booleans: + +- `"arrayDestructuring"` +- `"arrowParameter"`: `true` by default +- `"memberVariableDeclaration"`: `true` by default +- `"objectDestructuring"` +- `"parameter"`: `true` by default +- `"propertyDeclaration"`: `true` by default +- `"variableDeclaration"` + +For example, with the following configuration: + +```json +{ + "rules": { + "typedef": [ + "error", + { + "arrowParameter": false, + "variableDeclaration": true + } + ] + } +} +``` + +- Type annotations on arrow function parameters are not required +- Type annotations on variables are required +- Options otherwise adhere to the defaults + +### arrayDestructuring + +Whether to enforce type annotations on variables declared using array destructuring. + +Examples of **incorrect** code with `{ "arrayDestructuring": true }`: + +```ts +const [a] = [1]; +const [b, c] = [1, 2]; +``` + +Examples of **correct** code with `{ "arrayDestructuring": true }`: + +```ts +const [a]: number[] = [1]; +const [b]: [number] = [2]; +const [c, d]: [boolean, string] = [true, 'text']; +``` + +### arrowParameter + +Whether to enforce type annotations for parameters of arrow functions. + +Examples of **incorrect** code with `{ "arrowParameter": true }`: + +```ts +const logsSize = size => console.log(size); + +['hello', 'world'].map(text => text.length); + +const mapper = { + map: text => text + '...', +}; +``` + +Examples of **correct** code with `{ "arrowParameter": true }`: + +```ts +const logsSize = (size: number) => console.log(text); + +['hello', 'world'].map((text: string) => text.length); + +const mapper = { + map: (text: string) => text + '...', +}; +``` + +### memberVariableDeclaration + +Whether to enforce type annotations on member variables of classes. + +Examples of **incorrect** code with `{ "memberVariableDeclaration": true }`: + +```ts +class ContainsText { + delayedText; + immediateTextImplicit = 'text'; +} +``` + +Examples of **correct** code with `{ "memberVariableDeclaration": true }`: + +```ts +class ContainsText { + delayedText: string; + immediateTextImplicit: string = 'text'; +} +``` + +### objectDestructuring + +Whether to enforce type annotations on variables declared using object destructuring. + +Examples of **incorrect** code with `{ "objectDestructuring": true }`: + +```ts +const { length } = 'text'; +const [b, c] = Math.random() ? [1, 2] : [3, 4]; +``` + +Examples of **correct** code with `{ "objectDestructuring": true }`: + +```ts +const { length }: { length: number } = 'text'; +const [b, c]: [number, number] = Math.random() ? [1, 2] : [3, 4]; +``` + +### parameter + +Whether to enforce type annotations for parameters of functions and methods. + +Examples of **incorrect** code with `{ "parameter": true }`: + +```ts +function logsSize(size): void { + console.log(size); +} + +const doublesSize = function(size): numeber { + return size * 2; +}; + +const divider = { + curriesSize(size): number { + return size; + }, + dividesSize: function(size): number { + return size / 2; + }, +}; + +class Logger { + log(text): boolean { + console.log('>', text); + return true; + } +} +``` + +Examples of **correct** code with `{ "parameter": true }`: + +```ts +function logsSize(size: number): void { + console.log(size); +} + +const doublesSize = function(size: number): numeber { + return size * 2; +}; + +const divider = { + curriesSize(size: number): number { + return size; + }, + dividesSize: function(size: number): number { + return size / 2; + }, +}; + +class Logger { + log(text: boolean): boolean { + console.log('>', text); + return true; + } +} +``` + +### propertyDeclaration + +Whether to enforce type annotations for properties of interfaces and types. + +Examples of **incorrect** code with `{ "propertyDeclaration": true }`: + +```ts +type Members = { + member; + otherMember; +}; +``` + +Examples of **correct** code with `{ "propertyDeclaration": true }`: + +```ts +type Members = { + member: boolean; + otherMember: string; +}; +``` + +### variableDeclaration + +Whether to enforce type annotations for variable declarations, excluding array and object destructuring. + +Examples of **incorrect** code with `{ "variableDeclaration": true }`: + +```ts +const text = 'text'; +let initialText = 'text'; +let delayedText; +``` + +Examples of **correct** code with `{ "variableDeclaration": true }`: + +```ts +const text: string = 'text'; +let initialText: string = 'text'; +let delayedText: string; +``` + +## When Not To Use It + +If you are using stricter TypeScript compiler options, particularly `--noImplicitAny` and/or `--strictPropertyInitialization`, you likely don't need this rule. + +In general, if you do not consider the cost of writing unnecessary type annotations reasonable, then do not use this rule. + +## Further Reading + +- [TypeScript Type System](https://basarat.gitbooks.io/typescript/docs/types/type-system.html) +- [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html) + +## Compatibility + +- TSLint: [typedef](https://palantir.github.io/tslint/rules/typedef) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index cf3f61c69cd..dac2e6b3f6b 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -69,6 +69,7 @@ "@typescript-eslint/strict-boolean-expressions": "error", "@typescript-eslint/triple-slash-reference": "error", "@typescript-eslint/type-annotation-spacing": "error", + "@typescript-eslint/typedef": "error", "@typescript-eslint/unbound-method": "error", "@typescript-eslint/unified-signatures": "error" } diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 4ae1d9b8ea8..1d1bc59a846 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -56,6 +56,7 @@ import semi from './semi'; import strictBooleanExpressions from './strict-boolean-expressions'; import tripleSlashReference from './triple-slash-reference'; import typeAnnotationSpacing from './type-annotation-spacing'; +import typedef from './typedef'; import unboundMethod from './unbound-method'; import unifiedSignatures from './unified-signatures'; @@ -114,10 +115,11 @@ export default { 'require-array-sort-compare': requireArraySortCompare, 'require-await': requireAwait, 'restrict-plus-operands': restrictPlusOperands, - semi: semi, 'strict-boolean-expressions': strictBooleanExpressions, 'triple-slash-reference': tripleSlashReference, 'type-annotation-spacing': typeAnnotationSpacing, 'unbound-method': unboundMethod, 'unified-signatures': unifiedSignatures, + semi: semi, + typedef: typedef, }; diff --git a/packages/eslint-plugin/src/rules/typedef.ts b/packages/eslint-plugin/src/rules/typedef.ts new file mode 100644 index 00000000000..a53f173303c --- /dev/null +++ b/packages/eslint-plugin/src/rules/typedef.ts @@ -0,0 +1,136 @@ +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; +import * as util from '../util'; + +const enum OptionKeys { + ArrayDestructuring = 'arrayDestructuring', + ArrowParameter = 'arrowParameter', + MemberVariableDeclaration = 'memberVariableDeclaration', + ObjectDestructuring = 'objectDestructuring', + Parameter = 'parameter', + PropertyDeclaration = 'propertyDeclaration', + VariableDeclaration = 'variableDeclaration', +} + +type Options = { [k in OptionKeys]?: boolean }; + +type MessageIds = 'expectedTypedef' | 'expectedTypedefNamed'; + +export default util.createRule<[Options], MessageIds>({ + name: 'typedef', + meta: { + docs: { + description: 'Requires type annotations to exist', + category: 'Stylistic Issues', + recommended: false, + }, + messages: { + expectedTypedef: 'expected a type annotation', + expectedTypedefNamed: 'expected {{name}} to have a type annotation', + }, + schema: [ + { + type: 'object', + properties: { + [OptionKeys.ArrayDestructuring]: { type: 'boolean' }, + [OptionKeys.ArrowParameter]: { type: 'boolean' }, + [OptionKeys.MemberVariableDeclaration]: { type: 'boolean' }, + [OptionKeys.ObjectDestructuring]: { type: 'boolean' }, + [OptionKeys.Parameter]: { type: 'boolean' }, + [OptionKeys.PropertyDeclaration]: { type: 'boolean' }, + [OptionKeys.VariableDeclaration]: { type: 'boolean' }, + }, + }, + ], + type: 'suggestion', + }, + defaultOptions: [ + { + [OptionKeys.ArrowParameter]: true, + [OptionKeys.MemberVariableDeclaration]: true, + [OptionKeys.Parameter]: true, + [OptionKeys.PropertyDeclaration]: true, + }, + ], + create(context, [options]) { + function report(location: TSESTree.Node, name?: string) { + context.report({ + node: location, + messageId: name ? 'expectedTypedefNamed' : 'expectedTypedef', + data: { name }, + }); + } + + function getNodeName(node: TSESTree.Parameter | TSESTree.PropertyName) { + return node.type === AST_NODE_TYPES.Identifier ? node.name : undefined; + } + + function checkParameters(params: TSESTree.Parameter[]) { + for (const param of params) { + if ( + param.type !== AST_NODE_TYPES.TSParameterProperty && + !param.typeAnnotation + ) { + report(param, getNodeName(param)); + } + } + } + + return { + ArrayPattern(node) { + if (options[OptionKeys.ArrayDestructuring] && !node.typeAnnotation) { + report(node); + } + }, + ArrowFunctionExpression(node) { + if (options[OptionKeys.ArrowParameter]) { + checkParameters(node.params); + } + }, + ClassProperty(node) { + if ( + options[OptionKeys.MemberVariableDeclaration] && + !node.typeAnnotation + ) { + report( + node, + node.key.type === AST_NODE_TYPES.Identifier + ? node.key.name + : undefined, + ); + } + }, + 'FunctionDeclaration, FunctionExpression'( + node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, + ) { + if (options[OptionKeys.Parameter]) { + checkParameters(node.params); + } + }, + ObjectPattern(node) { + if (options[OptionKeys.ObjectDestructuring] && !node.typeAnnotation) { + report(node); + } + }, + 'TSIndexSignature, TSPropertySignature'( + node: TSESTree.TSIndexSignature | TSESTree.TSPropertySignature, + ) { + if (options[OptionKeys.PropertyDeclaration] && !node.typeAnnotation) { + report( + node, + node.type === AST_NODE_TYPES.TSPropertySignature + ? getNodeName(node.key) + : undefined, + ); + } + }, + VariableDeclarator(node) { + if ( + options[OptionKeys.VariableDeclaration] && + !node.id.typeAnnotation + ) { + report(node, getNodeName(node.id)); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/typedef.test.ts b/packages/eslint-plugin/tests/rules/typedef.test.ts new file mode 100644 index 00000000000..8c281fe5e39 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/typedef.test.ts @@ -0,0 +1,476 @@ +import rule from '../../src/rules/typedef'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, +}); + +ruleTester.run('typedef', rule, { + valid: [ + // Array destructuring + { + code: `const [a]: [number] = [1]`, + options: [ + { + arrayDestructuring: true, + }, + ], + }, + { + code: `const [a, b]: [number, number] = [1, 2]`, + options: [ + { + arrayDestructuring: true, + }, + ], + }, + { + code: `const [a] = 1;`, + options: [ + { + arrayDestructuring: false, + }, + ], + }, + `let a: number; + [a] = [1];`, + // Arrow parameters + `((a: number): void => {})()`, + `((a: string, b: string): void => {})()`, + { + code: `((a: number): void => { })()`, + options: [ + { + arrowParameter: false, + }, + ], + }, + { + code: `((a: string, b: string): void => { })()`, + options: [ + { + arrowParameter: false, + }, + ], + }, + // Member variable declarations + `class Test { + state: number; + }`, + `class Test { + state: number = 1; + }`, + { + code: `class Test { + state = 1; + }`, + options: [ + { + memberVariableDeclaration: false, + }, + ], + }, + // Object destructuring + { + code: `const { a }: { a: number } = { a: 1 }`, + options: [ + { + objectDestructuring: true, + }, + ], + }, + { + code: `const { a, b }: { [i: string]: number } = { a: 1, b: 2 }`, + options: [ + { + objectDestructuring: true, + }, + ], + }, + { + code: `const { a } = { a: 1 };`, + options: [ + { + objectDestructuring: false, + }, + ], + }, + // Parameters + `function receivesNumber(a: number): void { }`, + `function receivesStrings(a: string, b: string): void { }`, + `function receivesNumber([a]: [number]): void { }`, + `function receivesNumbers([a, b]: number[]): void { }`, + `function receivesString({ a }: { a: string }): void { }`, + `function receivesStrings({ a, b }: { [i: string ]: string }): void { }`, + // Property declarations + `type Test = { + member: number; + };`, + `type Test = { + [i: string]: number; + };`, + `interface Test { + member: string; + };`, + `interface Test { + [i: number]: string; + };`, + { + code: `type Test = { + member; + };`, + options: [ + { + propertyDeclaration: false, + }, + ], + }, + { + code: `type Test = { + [i: string]; + };`, + options: [ + { + propertyDeclaration: false, + }, + ], + }, + // Variable declarations + { + code: `const x: string = "";`, + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let x: string = "";`, + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let x: string;`, + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `const a = 1;`, + options: [ + { + variableDeclaration: false, + }, + ], + }, + { + code: `let a;`, + options: [ + { + variableDeclaration: false, + }, + ], + }, + { + code: `let a = 1;`, + options: [ + { + variableDeclaration: false, + }, + ], + }, + ], + invalid: [ + // Array destructuring + { + code: `const [a] = [1]`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + options: [ + { + arrayDestructuring: true, + }, + ], + }, + { + code: `const [a, b] = [1, 2]`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + options: [ + { + arrayDestructuring: true, + }, + ], + }, + // Object destructuring + { + code: `const { a } = { a: 1 }`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + options: [ + { + objectDestructuring: true, + }, + ], + }, + { + code: `const { a, b } = { a: 1, b: 2 }`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + options: [ + { + objectDestructuring: true, + }, + ], + }, + // Arrow parameters + { + code: `const receivesNumber = (a): void => { }`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `const receivesStrings = (a, b): void => { }`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + { + data: { name: 'b' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + // Member variable declarations + { + code: `class Test { + state = 1; + }`, + errors: [ + { + data: { name: 'state' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `class Test { + ["state"] = 1; + }`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + }, + // Parameters + { + code: `function receivesNumber(a): void { }`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `function receivesStrings(a, b): void { }`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + { + data: { name: 'b' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `function receivesNumber([a]): void { }`, + errors: [ + { + column: 25, + messageId: 'expectedTypedef', + }, + ], + }, + { + code: `function receivesNumbers([a, b]): void { }`, + errors: [ + { + column: 26, + messageId: 'expectedTypedef', + }, + ], + }, + { + code: `function receivesString({ a }): void { }`, + errors: [ + { + column: 25, + messageId: 'expectedTypedef', + }, + ], + }, + { + code: `function receivesStrings({ a, b }): void { }`, + errors: [ + { + column: 26, + messageId: 'expectedTypedef', + }, + ], + }, + // Property declarations + { + code: `type Test = { + member; + };`, + errors: [ + { + data: { name: 'member' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `type Test = { + [i: string]; + };`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + }, + { + code: `interface Test { + member; + };`, + errors: [ + { + data: { name: 'member' }, + messageId: 'expectedTypedefNamed', + }, + ], + }, + { + code: `interface Test { + [i: string]; + };`, + errors: [ + { + messageId: 'expectedTypedef', + }, + ], + }, + // Variable declarations + { + code: `const a = 1;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `const a = 1, b: number = 2, c = 3;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + { + data: { name: 'c' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let a;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let a = 1;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + { + code: `let a = 1, b: number, c = 2;`, + errors: [ + { + data: { name: 'a' }, + messageId: 'expectedTypedefNamed', + }, + { + data: { name: 'c' }, + messageId: 'expectedTypedefNamed', + }, + ], + options: [ + { + variableDeclaration: true, + }, + ], + }, + ], +});