diff --git a/.cspell.json b/.cspell.json index 6b30bf254bdf..0516d1196d70 100644 --- a/.cspell.json +++ b/.cspell.json @@ -103,6 +103,7 @@ "typedef", "typedefs", "unfixable", + "unoptimized", "unprefixed", "Zacher" ], diff --git a/README.md b/README.md index c507cd8460a7..df0c05c590ef 100644 --- a/README.md +++ b/README.md @@ -20,21 +20,16 @@ 👆

---- - ## Packages included in this project -- [`@typescript-eslint/eslint-plugin`](./packages/eslint-plugin/) - An ESLint-plugin with many lint rules for TypeScript features, as well as type-aware lint rules. - -- [`@typescript-eslint/parser`](./packages/parser/) - A ESLint-parser which enables ESLint to be able to understand TypeScript syntax. It's powered by our `typescript-estree` package. - -- [`@typescript-eslint/eslint-plugin-tslint`](./packages/eslint-plugin-tslint) - An ESLint-plugin that allows you to bridge ESLint and TSLint to help you migrate from TSLint to ESLint. - -- [`@typescript-eslint/experimental-utils`](./packages/experimental-utils) - Public utilities for working ESLint and for writing ESLint-plugins in TypeScript. - -- [`@typescript-eslint/typescript-estree`](./packages/typescript-estree/) - A parser which takes TypeScript source code and produces an [ESTree](https://github.com/estree/estree)-compatible AST. +See https://typescript-eslint.io/docs/development/architecture/packages for more details. -- [`@typescript-eslint/scope-manager`](./packages/scope-manager) - An [`eslint-scope`](https://github.com/eslint/eslint-scope)-compatible scope analyser that can understand TypeScript language features such as types, enums and namespaces. +- [`@typescript-eslint/eslint-plugin`](./packages/eslint-plugin) +- [`@typescript-eslint/parser`](./packages/parser) +- [`@typescript-eslint/eslint-plugin-tslint`](./packages/eslint-plugin-tslint) +- [`@typescript-eslint/experimental-utils`](./packages/experimental-utils) +- [`@typescript-eslint/typescript-estree`](./packages/typescript-estree) +- [`@typescript-eslint/scope-manager`](./packages/scope-manager) ## Versioning diff --git a/docs/getting-started/README.md b/docs/README.md similarity index 78% rename from docs/getting-started/README.md rename to docs/README.md index 55d6c068521c..aa14517ebd62 100644 --- a/docs/getting-started/README.md +++ b/docs/README.md @@ -10,4 +10,4 @@ The docs are broken down into the following categories: - [I want to lint my TypeScript codebase.](./linting/README.md) -- [(TODO) I want to write an ESLint plugin in TypeScript.](./plugin-development/README.md) +- [I want to develop an ESLint plugin in TypeScript.](./development/CUSTOM_RULES.md) diff --git a/docs/development/CUSTOM_RULES.md b/docs/development/CUSTOM_RULES.md new file mode 100644 index 000000000000..2c235725f5c7 --- /dev/null +++ b/docs/development/CUSTOM_RULES.md @@ -0,0 +1,268 @@ +--- +id: custom-rules +sidebar_label: Custom Rules +title: Custom Rules +--- + +:::important +You should be familiar with [ESLint's developer guide](https://eslint.org/docs/developer-guide) and [Development > Architecture](./architecture/asts) before writing custom rules. +::: + +As long as you are using `@typescript-eslint/parser` as the `parser` in your ESLint configuration, custom ESLint rules generally work the same way for JavaScript and TypeScript code. +The main two changes to custom rules writing are: + +- [AST Extensions](#ast-extensions): targeting TypeScript-specific syntax in your rule selectors +- [Typed Rules](#typed-rules): using the TypeScript type checker to inform rule logic + +## AST Extensions + +`@typescript-eslint/estree` creates AST nodes for TypeScript syntax with names that begin with `TS`, such as `TSInterfaceDeclaration` and `TSTypeAnnotation`. +These nodes are treated just like any other AST node. +You can query for them in your rule selectors. + +This rule written in JavaScript bans interfaces that start with a lower-case letter: + +```js +export const rule = { + create(context) { + return { + TSInterfaceDeclaration(node) { + if (/[a-z]/.test(node.id.name[0])) { + context.report({ + messageId: 'uppercase', + node: node.id, + }); + } + }, + }; + }, + meta: { + docs: { + category: 'Best Practices', + description: 'Interface names should start with an upper-case letter.', + }, + messages: { + uppercase: 'Start this name with an upper-case letter.', + }, + type: 'suggestion', + schema: [], + }, +}; +``` + +### Writing Rules in TypeScript + +The `@typescript-eslint/experimental-utils` package acts as a replacement package for `eslint` that exports all the same objects and types, but with typescript-eslint support. + +:::caution +`@types/eslint` types are based on `@types/estree` and do not recognize typescript-eslint nodes and properties. +You should generally not need to import from `eslint` when writing custom typescript-eslint rules in TypeScript. +::: + +#### Rule Types + +`@typescript-eslint/experimental-utils` exports a `RuleModule` interface that allows specifying generics for: + +- `MessageIds`: a union of string literal message IDs that may be reported +- `Options`: what options users may configure for the rule + +```ts +import { TSESLint } from '@typescript-eslint/experimental-utils'; + +export const rule: TSESLint.RuleModule<'uppercase', []> = { + create(context /* : Readonly> */) { + // ... + }, +}; +``` + +For groups of rules that share a common documentation URL, a `RuleCreator` function is exported. +It takes in a function that transforms a rule name into its documentation URL, then returns a function that takes in a rule module object. +The returned function is able to infer message IDs from `meta.messages`. + +```ts +import { ESLintUtils } from '@typescript-eslint/experimental-utils'; + +const createRule = ESLintUtils.RuleCreator( + name => `https://example.com/rule/${name}`, +); + +// Type: const rule: RuleModule<"uppercase", ...> +export const rule = createRule({ + create(context) { + // ... + }, + meta: { + messages: { + uppercase: 'Start this name with an upper-case letter.', + }, + // ... + }, +}); +``` + +#### Node Types + +TypeScript types for nodes exist in a `TSESTree` namespace exported by `@typescript-eslint/experimental-utils`. +The above rule body could be better written in TypeScript with a type annotation on the `node`: + +```ts +import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + +// ... + +export const rule = createRule({ + create(context) { + return { + TSInterfaceDeclaration(node: TSESTree.TSInterfaceDeclaration) { + // ... + }, + }; + }, + // ... +}); +``` + +An `AST_NODE_TYPES` enum is exported as well to hold the values for AST node `type` properties. +`TSESTree.Node` is available as union type that uses its `type` member as a discriminant. + +For example, checking `node.type` can narrow down the type of the `node`: + +```ts +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; + +export function describeNode(node: TSESTree.Node): string { + switch (node.type) { + case AST_NODE_TYPES.ArrayExpression: + return `Array containing ${node.elements.map(describeNode).join(', ')}`; + + case AST_NODE_TYPES.Literal: + return `Literal value ${node.raw}`; + + default: + return '🤷'; + } +} +``` + +## Type Checking + +:::tip +Read TypeScript's [Compiler APIs > Using the Type Checker](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#using-the-type-checker) section for how to use a program's type checker. +::: + +The biggest addition typescript-eslint brings to ESLint rules is the ability to use TypeScript's type checker APIs. + +`@typescript-eslint/experimental-utils` exports an `ESLintUtils` namespace containing a `getParserServices` function that takes in an ESLint context and returns a `parserServices` object. + +That `parserServices` object contains: + +- `program`: A full TypeScript `ts.Program` object +- `esTreeNodeToTSNodeMap`: Map of `@typescript-eslint/estree` `TSESTree.Node` nodes to their TypeScript `ts.Node` equivalents +- `tsNodeToESTreeNodeMap`: Map of TypeScript `ts.Node` nodes to their `@typescript-eslint/estree` `TSESTree.Node` equivalents + +By mapping from ESTree nodes to TypeScript nodes and retrieving the TypeScript program from the parser services, rules are able to ask TypeScript for full type information on those nodes. + +This rule bans for-of looping over an enum by using the type-checker via typescript-eslint and TypeScript APIs: + +```ts +import { ESLintUtils } from '@typescript-eslint/experimental-utils'; +import * as ts from 'typescript'; +import * as tsutils from 'tsutils'; + +export const rule: eslint.Rule.RuleModule = { + create(context) { + return { + ForOfStatement(node) { + // 1. Grab the TypeScript program from parser services + const parserServices = ESLintUtils.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + + // 2. Find the backing TS node for the ES node, then that TS type + const originalNode = parserServices.esTreeNodeToTSNodeMap.get( + node.right, + ); + const nodeType = checker.getTypeAtLocation(node); + + // 3. Check the TS node type using the TypeScript APIs + if (tsutils.isTypeFlagSet(nodeType, ts.TypeFlags.EnumLike)) { + context.report({ + messageId: 'loopOverEnum', + node: node.right, + }); + } + }, + }; + }, + meta: { + docs: { + category: 'Best Practices', + description: 'Avoid looping over enums.', + }, + messages: { + loopOverEnum: 'Do not loop over enums.', + }, + type: 'suggestion', + schema: [], + }, +}; +``` + +## Testing + +`@typescript-eslint/experimental-utils` exports a `RuleTester` with a similar API to the built-in [ESLint `RuleTester`](https://eslint.org/docs/developer-guide/nodejs-api#ruletester). +It should be provided with the same `parser` and `parserOptions` you would use in your ESLint configuration. + +### Testing Untyped Rules + +For rules that don't need type information, passing just the `parser` will do: + +```ts +import { ESLintUtils } from '@typescript-eslint/experimental-utils'; +import rule from './my-rule'; + +const ruleTester = new ESLintUtils.RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('my-rule', rule { + valid: [/* ... */], + invalid: [/* ... */], +}); +``` + +### Testing Typed Rules + +For rules that do need type information, `parserOptions` must be passed in as well. +Tests must have at least an absolute `tsconfigRootDir` path provided as well as a relative `project` path from that directory: + +```ts +import { ESLintUtils } from '@typescript-eslint/experimental-utils'; +import rule from './my-typed-rule'; + +const ruleTester = new ESLintUtils.RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + } +}); + +ruleTester.run('my-typed-rule', rule { + valid: [/* ... */], + invalid: [/* ... */], +}); +``` + +:::note +For now, `ESLintUtils.RuleTester` requires the following physical files be present on disk for typed rules: + +- `tsconfig.json`: tsconfig used as the test "project" +- One of the following two files: + - `file.ts`: blank test file used for normal TS tests + - `file.tsx`: blank test file used for tests with `parserOptions: { ecmaFeatures: { jsx: true } }` + +::: diff --git a/docs/development/architecture/ASTS.md b/docs/development/architecture/ASTS.md new file mode 100644 index 000000000000..055ffa713d8b --- /dev/null +++ b/docs/development/architecture/ASTS.md @@ -0,0 +1,51 @@ +--- +id: asts +title: ASTs +sidebar_label: ASTs +--- + +## Abstract Syntax Trees (AST)s + +Parsers such as those in ESLint and TypeScript read in the text of source code and parse it into a standard format they can reason about known as an **Abstract Syntax Tree** (AST). +ASTs are called such because although they might contain information on the location of constructs within source code, they are an abstract representation that cares more about the semantic structure. + +For example, given this line of code: + +```js +1 + 2; +``` + +ESLint would natively understand it as an object like: + +```json +{ + "type": "ExpressionStatement", + "expression": { + "type": "BinaryExpression", + "left": { + "type": "Literal", + "value": 1, + "raw": "1" + }, + "operator": "+", + "right": { + "type": "Literal", + "value": 2, + "raw": "2" + } + } +} +``` + +ESLint uses an AST format known as **[`estree`]**. + +ESTree is more broadly used than just for ESLint -- it is the de facto community standard. +ESLint's built-in parser that outputs an `estree`-shaped AST is also a separate package, called **[`espree`]**. + +:::note +You can play more with various ASTs such as ESTree on [astexplorer.net] and read more details on their [Wikipedia article](https://en.wikipedia.org/wiki/Abstract_syntax_tree). +::: + +[astexplorer.net]: https://astexplorer.net +[`espree`]: https://github.com/eslint/espree +[`estree`]: https://github.com/estree/estree diff --git a/docs/development/architecture/PACKAGES.md b/docs/development/architecture/PACKAGES.md new file mode 100644 index 000000000000..7e169187423e --- /dev/null +++ b/docs/development/architecture/PACKAGES.md @@ -0,0 +1,78 @@ +--- +id: packages +title: Packages +sidebar_label: Packages +--- + +This page describes the top-level packages exported by the [typescript-eslint monorepo](https://github.com/typescript-eslint/typescript-eslint). +Each of these are published as npm packages under the `@typescript-eslint` organization. + +## `@typescript-eslint/eslint-plugin` + +[`@typescript-eslint/eslint-plugin`] is the core [ESLint plugin](https://eslint.org/docs/user-guide/configuring/plugins) used by consumers to load in custom rules and rule configurations lists from typescript-eslint. +Those rules rely on ESLint using the `@typescript-eslint/parser` package described below, and are generally built using the other packages on this page. + +## `@typescript-eslint/parser` + +[`@typescript-eslint/parser`] takes in ESLint configuration settings, reads in TypeScript source text, and produces an ESTree AST. +This is necessary because TypeScript produces a different, incompatible AST format to the one that ESLint requires to work. + +For example, this is not valid JavaScript code because it contains the `: number` type annotation: + +```ts +let x: number = 1; +``` + +ESLint's native Espree parser would raise an error attempting to parse it. + +Additionally, because TypeScript is developed separately and with different goals from ESLint, ESTree, and Espree, its AST also represents nodes differently in many cases. +TS's AST is optimized for its use case of parsing incomplete code and typechecking. +ESTree is unoptimized and intended for "general purpose" use-cases of traversing the AST. + +See more on configuring custom parsers with ESLint on [ESLint's User Guide > Configuring > Plugins](https://eslint.org/docs/user-guide/configuring/plugins#specifying-parser). + +:::tip +You can select `@typescript-eslint/parser` on [astexplorer.net](https://astexplorer.net)'s top-middle ⚙ dropdown that defaults to Acorn. +::: + +## `@typescript-eslint/typescript-estree` + +[`@typescript-eslint/typescript-estree`] is used by `@typescript-eslint/parser` to take TypeScript source code and produce the equivalent ESTree AST. +It works by: + +1. Invoking the TypeScript compiler on the given source code in order to + produce a TypeScript AST +2. Converting that TypeScript AST into an ESTree AST + +> Because [`@typescript-eslint/typescript-estree`] has a very specific purpose, it is reusable for tools with similar +> requirements to ESLint. +> It is therefore also used to power the amazing opinionated code formatter [Prettier](https://prettier.io)'s TypeScript support. + +## `@typescript-eslint/scope-manager` + +[`@typescript-eslint/scope-manager`] is a fork of [`eslint-scope`](https://github.com/eslint/eslint-scope), enhanced to support TypeScript functionality. + +A "scope analyser" traverses an AST and builds a model of how variables (and in our case, types) are defined and consumed by the source code. +This form of static analysis allows you to understand and trace variables throughout the program, allowing you to access powerful information about a program without needing to drop into the much, much heavier type information. + +## `@typescript-eslint/experimental-utils` + +[`@typescript-eslint/experimental-utils`] contains public utilities for writing custom rules and plugins in TypeScript. +Rules declared in `@typescript-eslint/eslint-plugin` are created using its utility functions. +Any custom rules you write generally will be as well. + +## `@typescript-eslint/eslint-plugin-tslint` + +[`@typescript-eslint/eslint-plugin-tslint`] is a separate ESLint plugin that allows running TSLint rules within ESLint to help you migrate from TSLint to ESLint. + +:::caution +**TSLint is deprecated.** It is in your best interest to migrate off it entirely. See [Linting > TSLint](../../linting/TSLINT.md). +::: + +[`@typescript-eslint/eslint-plugin-tslint`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin-tslint +[`@typescript-eslint/eslint-plugin`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin +[`@typescript-eslint/experimental-utils`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/experimental-utils +[`@typescript-eslint/parser`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser +[`@typescript-eslint/scope-manager`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/scope-manager +[`@typescript-eslint/typescript-estree`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/typescript-estree +[`@typescript-eslint/typescript-estree`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/typescript-estree diff --git a/docs/getting-started/linting/ARCHITECTURE.md b/docs/getting-started/linting/ARCHITECTURE.md deleted file mode 100644 index fb21504303c4..000000000000 --- a/docs/getting-started/linting/ARCHITECTURE.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -id: architecture -title: Architecture -sidebar_label: Architecture ---- - -## Abstract Syntax Trees (AST)s - -Parsers such as those in ESLint and TypeScript read in the text of source code and parse it into a standard format they can reason about. -ASTs are called such because although they might contain information on the location of constructs within source code, they are an abstract representation that cares more about the semantic structure. - -For example, given this line of code: - -```js -1 + 2; -``` - -ESLint would natively understand it as an object like: - -```json -{ - "type": "ExpressionStatement", - "expression": { - "type": "BinaryExpression", - "left": { - "type": "Literal", - "value": 1, - "raw": "1" - }, - "operator": "+", - "right": { - "type": "Literal", - "value": 2, - "raw": "2" - } - } -} -``` - -ESLint uses an AST format known as **[`estree`]**. - -ESTree is more broadly used than just for ESLint -- it is the de facto community standard. -ESLint' built-in parser that outputs an `estree`-shaped AST is also a separate package, called **[`espree`]**. - -:::note -You can play more with various ASTs such as ESTree on [astexplorer.net] and read more details on their [Wikipedia article](https://en.wikipedia.org/wiki/Abstract_syntax_tree). -::: - -## `@typescript-eslint/parser` - -TypeScript produces a different AST format to the one that ESLint requires to work. -This means that by default, the TypeScript AST is not compatible with ESLint. - -For example: - -```ts -let x: number = 1; -``` - -That is not valid JavaScript code because it contains the `: number` type annotation. -ESLint's native Espree parser would raise an error attempting to parse it. - -Additionally, because TypeScript is developed separately from ESLint, ESTree, and Espree, its AST also represents nodes differently in many cases. -Many nodes have different names or different member structures. - -[`@typescript-eslint/parser`] is a parser that takes in ESLint configuration settings, reads in TypeScript source text, and produces an ESTree AST. - -ESLint allows specifying custom parsers such as `@typescript-eslint/parser`. -See more on https://eslint.org/docs/user-guide/configuring/plugins#specifying-parser. - -:::note -You can select the `@typescript-eslint/parser` on the top-middle ⚙ dropdown in [astexplorer.net] that defaults to Acorn. -::: - -### `@typescript-eslint/typescript-estree` - -[`@typescript-eslint/typescript-estree`] is the utility package used by `@typescript-eslint/parser` to take TypeScript source code and produce the equivalent ESTree AST. -It works by: - -1. Invoking the TypeScript compiler on the given source code in order to - produce a TypeScript AST -2. Converting that TypeScript AST into an ESTree AST - -> Because [`@typescript-eslint/typescript-estree`] has a very specific purpose, it is reusable for tools with similar -> requirements to ESLint. -> It is therefore also used to power the amazing opinionated code formatter [Prettier](https://prettier.io)'s TypeScript support. - -### `@typescript-eslint/scope-manager` - -`@typescript-eslint/scope-manager` is a fork of [`eslint-scope`](https://github.com/eslint/eslint-scope), enhanced to support TypeScript functionality. - -A "scope analyser" traverses an AST and builds a model of how variables (and in our case, types) are defined and consumed by the source code. -This form of static analysis allows you to understand and trace variables throughout the program, allowing you to access powerful information about a program without needing to drop into the much, much heavier type information. - -[`@typescript-eslint/parser`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser -[`@typescript-eslint/scope-manager`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/scope-manager -[`@typescript-eslint/typescript-estree`]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/typescript-estree -[astexplorer.net]: https://astexplorer.net -[`espree`]: https://github.com/eslint/espree -[`estree`]: https://github.com/estree/estree diff --git a/docs/getting-started/plugin-development/README.md b/docs/getting-started/plugin-development/README.md deleted file mode 100644 index 7484b2f26a71..000000000000 --- a/docs/getting-started/plugin-development/README.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -id: plugins -title: Writing an ESLint Plugin in TypeScript -sidebar_label: Writing an ESLint Plugin in TypeScript ---- - -TODO: - -- talk about how to setup the folder structure -- talk about how to consume `experimental-utils` to create an empty rule -- talk about https://eslint.org/docs/developer-guide/selectors and how to use the strict types -- talk about how to write tests - -TODO: (advanced) - -- talk about how to use type information diff --git a/docs/getting-started/linting/MONOREPO.md b/docs/linting/MONOREPO.md similarity index 100% rename from docs/getting-started/linting/MONOREPO.md rename to docs/linting/MONOREPO.md diff --git a/docs/getting-started/linting/README.md b/docs/linting/README.md similarity index 100% rename from docs/getting-started/linting/README.md rename to docs/linting/README.md diff --git a/docs/getting-started/linting/TROUBLESHOOTING.md b/docs/linting/TROUBLESHOOTING.md similarity index 100% rename from docs/getting-started/linting/TROUBLESHOOTING.md rename to docs/linting/TROUBLESHOOTING.md diff --git a/docs/getting-started/linting/TSLINT.md b/docs/linting/TSLINT.md similarity index 100% rename from docs/getting-started/linting/TSLINT.md rename to docs/linting/TSLINT.md diff --git a/docs/getting-started/linting/TYPED_LINTING.md b/docs/linting/TYPED_LINTING.md similarity index 100% rename from docs/getting-started/linting/TYPED_LINTING.md rename to docs/linting/TYPED_LINTING.md diff --git a/packages/website/docusaurus.config.js b/packages/website/docusaurus.config.js index b8d59f357d29..9388a4b85bcb 100644 --- a/packages/website/docusaurus.config.js +++ b/packages/website/docusaurus.config.js @@ -105,6 +105,10 @@ const config = { theme: lightCodeTheme, darkTheme: darkCodeTheme, }, + tableOfContents: { + maxHeadingLevel: 4, + minHeadingLevel: 2, + }, }), }; diff --git a/packages/website/sidebars/sidebar.base.js b/packages/website/sidebars/sidebar.base.js index c68cc0193835..5d50b266eef5 100644 --- a/packages/website/sidebars/sidebar.base.js +++ b/packages/website/sidebars/sidebar.base.js @@ -1,26 +1,33 @@ module.exports = { - docs: { - Guides: [ - 'getting-started/README', - { - type: 'category', - label: 'Linting', - collapsed: false, - items: [ - 'getting-started/linting/linting', - 'getting-started/linting/type-linting', - 'getting-started/linting/monorepo', - 'getting-started/linting/troubleshooting', - 'getting-started/linting/architecture', - 'getting-started/linting/tslint', - ], - }, - { - type: 'category', - label: 'Plugins', - collapsed: false, - items: ['getting-started/plugin-development/plugins'], - }, - ], - }, + docs: [ + 'README', + { + type: 'category', + label: 'Linting', + collapsed: false, + items: [ + 'linting/linting', + 'linting/type-linting', + 'linting/monorepo', + 'linting/troubleshooting', + 'linting/tslint', + ], + }, + { + type: 'category', + label: 'Development', + collapsed: false, + items: [ + { + label: 'Architecture', + type: 'category', + items: [ + 'development/architecture/asts', + 'development/architecture/packages', + ], + }, + 'development/custom-rules', + ], + }, + ], };