diff --git a/packages/eslint-plugin/docs/rules/unbound-method.md b/packages/eslint-plugin/docs/rules/unbound-method.md index a9a4fe396a2..15063dbb4f3 100644 --- a/packages/eslint-plugin/docs/rules/unbound-method.md +++ b/packages/eslint-plugin/docs/rules/unbound-method.md @@ -4,6 +4,8 @@ Warns when a method is used outside of a method call. Class functions don't preserve the class scope when passed as standalone variables. +If your function does not access `this`, [you can annotate it with `this: void`](https://www.typescriptlang.org/docs/handbook/2/functions.html#declaring-this-in-a-function), or consider using an arrow function instead. + ## Rule Details Examples of **incorrect** code for this rule diff --git a/packages/eslint-plugin/src/rules/member-delimiter-style.ts b/packages/eslint-plugin/src/rules/member-delimiter-style.ts index 64147b37cf2..de0a60f013c 100644 --- a/packages/eslint-plugin/src/rules/member-delimiter-style.ts +++ b/packages/eslint-plugin/src/rules/member-delimiter-style.ts @@ -1,4 +1,5 @@ import { + TSESLint, TSESTree, AST_NODE_TYPES, } from '@typescript-eslint/experimental-utils'; @@ -11,6 +12,9 @@ type TypeOptions = { delimiter?: Delimiter; requireLast?: boolean; }; +type TypeOptionsWithType = TypeOptions & { + type: string; +}; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions type BaseOptions = { multiline?: TypeOptions; @@ -29,6 +33,20 @@ type MessageIds = | 'unexpectedSemi' | 'expectedComma' | 'expectedSemi'; +type LastTokenType = TSESTree.Token; + +interface MakeFixFunctionParams { + optsNone: boolean; + optsSemi: boolean; + lastToken: LastTokenType; + missingDelimiter: boolean; + lastTokenLine: string; + isSingleLine: boolean; +} + +type MakeFixFunctionReturnType = + | ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix) + | null; const definition = { type: 'object', @@ -54,6 +72,47 @@ const definition = { additionalProperties: false, }; +const isLastTokenEndOfLine = (token: string, line: string): boolean => { + const positionInLine = line.indexOf(token); + + return positionInLine === line.length - 1; +}; + +const makeFixFunction = ({ + optsNone, + optsSemi, + lastToken, + missingDelimiter, + lastTokenLine, + isSingleLine, +}: MakeFixFunctionParams): MakeFixFunctionReturnType => { + // if removing is the action but last token is not the end of the line + if ( + optsNone && + !isLastTokenEndOfLine(lastToken.value, lastTokenLine) && + !isSingleLine + ) { + return null; + } + + return (fixer: TSESLint.RuleFixer): TSESLint.RuleFix => { + if (optsNone) { + // remove the unneeded token + return fixer.remove(lastToken); + } + + const token = optsSemi ? ';' : ','; + + if (missingDelimiter) { + // add the missing delimiter + return fixer.insertTextAfter(lastToken, token); + } + + // correct the current delimiter + return fixer.replaceText(lastToken, token); + }; +}; + export default util.createRule({ name: 'member-delimiter-style', meta: { @@ -127,7 +186,7 @@ export default util.createRule({ */ function checkLastToken( member: TSESTree.TypeElement, - opts: TypeOptions, + opts: TypeOptionsWithType, isLast: boolean, ): void { /** @@ -147,10 +206,14 @@ export default util.createRule({ const lastToken = sourceCode.getLastToken(member, { includeComments: false, }); + if (!lastToken) { return; } + const sourceCodeLines = sourceCode.getLines(); + const lastTokenLine = sourceCodeLines[lastToken?.loc.start.line - 1]; + const optsSemi = getOption('semi'); const optsComma = getOption('comma'); const optsNone = getOption('none'); @@ -193,22 +256,14 @@ export default util.createRule({ }, }, messageId, - fix(fixer) { - if (optsNone) { - // remove the unneeded token - return fixer.remove(lastToken); - } - - const token = optsSemi ? ';' : ','; - - if (missingDelimiter) { - // add the missing delimiter - return fixer.insertTextAfter(lastToken, token); - } - - // correct the current delimiter - return fixer.replaceText(lastToken, token); - }, + fix: makeFixFunction({ + optsNone, + optsSemi, + lastToken, + missingDelimiter, + lastTokenLine, + isSingleLine: opts.type === 'single-line', + }), }); } } @@ -239,7 +294,9 @@ export default util.createRule({ node.type === AST_NODE_TYPES.TSInterfaceBody ? interfaceOptions : typeLiteralOptions; - const opts = isSingleLine ? typeOpts.singleline : typeOpts.multiline; + const opts = isSingleLine + ? { ...typeOpts.singleline, type: 'single-line' } + : { ...typeOpts.multiline, type: 'multi-line' }; members.forEach((member, index) => { checkLastToken(member, opts ?? {}, index === members.length - 1); diff --git a/packages/eslint-plugin/src/rules/object-curly-spacing.ts b/packages/eslint-plugin/src/rules/object-curly-spacing.ts index 581e7ac1555..74161246a31 100644 --- a/packages/eslint-plugin/src/rules/object-curly-spacing.ts +++ b/packages/eslint-plugin/src/rules/object-curly-spacing.ts @@ -63,7 +63,7 @@ export default createRule({ * @param token The token to use for the report. */ function reportNoBeginningSpace( - node: TSESTree.TSTypeLiteral, + node: TSESTree.TSMappedType | TSESTree.TSTypeLiteral, token: TSESTree.Token, ): void { const nextToken = context @@ -89,7 +89,7 @@ export default createRule({ * @param token The token to use for the report. */ function reportNoEndingSpace( - node: TSESTree.TSTypeLiteral, + node: TSESTree.TSMappedType | TSESTree.TSTypeLiteral, token: TSESTree.Token, ): void { const previousToken = context @@ -115,7 +115,7 @@ export default createRule({ * @param token The token to use for the report. */ function reportRequiredBeginningSpace( - node: TSESTree.TSTypeLiteral, + node: TSESTree.TSMappedType | TSESTree.TSTypeLiteral, token: TSESTree.Token, ): void { context.report({ @@ -137,7 +137,7 @@ export default createRule({ * @param token The token to use for the report. */ function reportRequiredEndingSpace( - node: TSESTree.TSTypeLiteral, + node: TSESTree.TSMappedType | TSESTree.TSTypeLiteral, token: TSESTree.Token, ): void { context.report({ @@ -162,7 +162,7 @@ export default createRule({ * @param last The last token to check (should be closing brace) */ function validateBraceSpacing( - node: TSESTree.TSTypeLiteral, + node: TSESTree.TSMappedType | TSESTree.TSTypeLiteral, first: TSESTree.Token, second: TSESTree.Token | TSESTree.Comment, penultimate: TSESTree.Token | TSESTree.Comment, @@ -175,7 +175,10 @@ export default createRule({ const openingCurlyBraceMustBeSpaced = options.arraysInObjectsException && - secondType === AST_NODE_TYPES.TSIndexSignature + [ + AST_NODE_TYPES.TSMappedType, + AST_NODE_TYPES.TSIndexSignature, + ].includes(secondType) ? !options.spaced : options.spaced; @@ -197,15 +200,19 @@ export default createRule({ isClosingBracketToken(penultimate)) || (options.objectsInObjectsException && isClosingBraceToken(penultimate)); - const penultimateType = - shouldCheckPenultimate && - sourceCode.getNodeByRangeIndex(penultimate.range[0])!.type; + const penultimateType = shouldCheckPenultimate + ? sourceCode.getNodeByRangeIndex(penultimate.range[0])!.type + : undefined; const closingCurlyBraceMustBeSpaced = (options.arraysInObjectsException && penultimateType === AST_NODE_TYPES.TSTupleType) || (options.objectsInObjectsException && - penultimateType === AST_NODE_TYPES.TSTypeLiteral) + penultimateType !== undefined && + [ + AST_NODE_TYPES.TSMappedType, + AST_NODE_TYPES.TSTypeLiteral, + ].includes(penultimateType)) ? !options.spaced : options.spaced; @@ -246,6 +253,18 @@ export default createRule({ const rules = baseRule.create(context); return { ...rules, + TSMappedType(node: TSESTree.TSMappedType): void { + const first = sourceCode.getFirstToken(node)!; + const last = sourceCode.getLastToken(node)!; + const second = sourceCode.getTokenAfter(first, { + includeComments: true, + })!; + const penultimate = sourceCode.getTokenBefore(last, { + includeComments: true, + })!; + + validateBraceSpacing(node, first, second, penultimate, last); + }, TSTypeLiteral(node: TSESTree.TSTypeLiteral): void { if (node.members.length === 0) { return; diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 2ec66bc2aa3..ecf9e652819 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -16,7 +16,7 @@ interface Config { export type Options = [Config]; -export type MessageIds = 'unbound'; +export type MessageIds = 'unbound' | 'unboundWithoutThisAnnotation'; /** * The following is a list of exceptions to the rule @@ -121,6 +121,9 @@ const getNodeName = (node: TSESTree.Node): string | null => const getMemberFullName = (node: TSESTree.MemberExpression): string => `${getNodeName(node.object)}.${getNodeName(node.property)}`; +const BASE_MESSAGE = + 'Avoid referencing unbound methods which may cause unintentional scoping of `this`.'; + export default util.createRule({ name: 'unbound-method', meta: { @@ -132,8 +135,11 @@ export default util.createRule({ requiresTypeChecking: true, }, messages: { - unbound: - 'Avoid referencing unbound methods which may cause unintentional scoping of `this`.', + unbound: BASE_MESSAGE, + unboundWithoutThisAnnotation: + BASE_MESSAGE + + '\n' + + 'If your function does not access `this`, you can annotate it with `this: void`, or consider using an arrow function instead.', }, schema: [ { @@ -160,6 +166,26 @@ export default util.createRule({ context.getFilename(), ); + function checkMethodAndReport( + node: TSESTree.Node, + symbol: ts.Symbol | undefined, + ): void { + if (!symbol) { + return; + } + + const { dangerous, firstParamIsThis } = checkMethod(symbol, ignoreStatic); + if (dangerous) { + context.report({ + messageId: + firstParamIsThis === false + ? 'unboundWithoutThisAnnotation' + : 'unbound', + node, + }); + } + } + return { MemberExpression(node: TSESTree.MemberExpression): void { if (isSafeUse(node)) { @@ -179,14 +205,8 @@ export default util.createRule({ } const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); - const symbol = checker.getSymbolAtLocation(originalNode); - if (symbol && isDangerousMethod(symbol, ignoreStatic)) { - context.report({ - messageId: 'unbound', - node, - }); - } + checkMethodAndReport(node, checker.getSymbolAtLocation(originalNode)); }, 'VariableDeclarator, AssignmentExpression'( node: TSESTree.VariableDeclarator | TSESTree.AssignmentExpression, @@ -219,13 +239,10 @@ export default util.createRule({ return; } - const symbol = initTypes.getProperty(property.key.name); - if (symbol && isDangerousMethod(symbol, ignoreStatic)) { - context.report({ - messageId: 'unbound', - node, - }); - } + checkMethodAndReport( + node, + initTypes.getProperty(property.key.name), + ); } }); } @@ -234,44 +251,52 @@ export default util.createRule({ }, }); -function isDangerousMethod(symbol: ts.Symbol, ignoreStatic: boolean): boolean { +function checkMethod( + symbol: ts.Symbol, + ignoreStatic: boolean, +): { dangerous: boolean; firstParamIsThis?: boolean } { const { valueDeclaration } = symbol; if (!valueDeclaration) { // working around https://github.com/microsoft/TypeScript/issues/31294 - return false; + return { dangerous: false }; } switch (valueDeclaration.kind) { case ts.SyntaxKind.PropertyDeclaration: - return ( - (valueDeclaration as ts.PropertyDeclaration).initializer?.kind === - ts.SyntaxKind.FunctionExpression - ); + return { + dangerous: + (valueDeclaration as ts.PropertyDeclaration).initializer?.kind === + ts.SyntaxKind.FunctionExpression, + }; case ts.SyntaxKind.MethodDeclaration: case ts.SyntaxKind.MethodSignature: { const decl = valueDeclaration as | ts.MethodDeclaration | ts.MethodSignature; const firstParam = decl.parameters[0]; - const thisArgIsVoid = + const firstParamIsThis = firstParam?.name.kind === ts.SyntaxKind.Identifier && - firstParam?.name.escapedText === 'this' && + firstParam?.name.escapedText === 'this'; + const thisArgIsVoid = + firstParamIsThis && firstParam?.type?.kind === ts.SyntaxKind.VoidKeyword; - return ( - !thisArgIsVoid && - !( - ignoreStatic && - tsutils.hasModifier( - valueDeclaration.modifiers, - ts.SyntaxKind.StaticKeyword, - ) - ) - ); + return { + dangerous: + !thisArgIsVoid && + !( + ignoreStatic && + tsutils.hasModifier( + valueDeclaration.modifiers, + ts.SyntaxKind.StaticKeyword, + ) + ), + firstParamIsThis, + }; } } - return false; + return { dangerous: false }; } function isSafeUse(node: TSESTree.Node): boolean { diff --git a/packages/eslint-plugin/tests/rules/member-delimiter-style.test.ts b/packages/eslint-plugin/tests/rules/member-delimiter-style.test.ts index c69e54e2af0..fa75383df8e 100644 --- a/packages/eslint-plugin/tests/rules/member-delimiter-style.test.ts +++ b/packages/eslint-plugin/tests/rules/member-delimiter-style.test.ts @@ -851,6 +851,24 @@ interface Foo { }, { code: ` +type Test = { + a: { + one: 1 + }; b: 2 +}; + `, + output: null, + options: [{ multiline: { delimiter: 'none' } }], + errors: [ + { + messageId: 'unexpectedSemi', + line: 5, + column: 5, + }, + ], + }, + { + code: ` interface Foo { name: string age: number diff --git a/packages/eslint-plugin/tests/rules/object-curly-spacing.test.ts b/packages/eslint-plugin/tests/rules/object-curly-spacing.test.ts index 5c28cbef7ed..ecde35b775f 100644 --- a/packages/eslint-plugin/tests/rules/object-curly-spacing.test.ts +++ b/packages/eslint-plugin/tests/rules/object-curly-spacing.test.ts @@ -570,6 +570,71 @@ ruleTester.run('object-curly-spacing', rule, { code: 'const x:{[key: string]: [number]}', }, + // default - mapped types + { + code: "const x:{[k in 'union']: number}", + }, + { + code: "const x:{ // line-comment\n[k in 'union']: number\n}", + }, + { + code: "const x:{// line-comment\n[k in 'union']: number\n}", + }, + { + code: + "const x:{/* inline-comment */[k in 'union']: number/* inline-comment */}", + }, + { + code: "const x:{\n[k in 'union']: number\n}", + }, + { + code: "const x:{[k in 'union']: {[k in 'union']: number}}", + }, + { + code: "const x:{[k in 'union']: [number]}", + }, + { + code: "const x:{[k in 'union']: value}", + }, + + // never - mapped types + { + code: "const x:{[k in 'union']: {[k in 'union']: number} }", + options: ['never', { objectsInObjects: true }], + }, + { + code: "const x:{[k in 'union']: {[k in 'union']: number}}", + options: ['never', { objectsInObjects: false }], + }, + { + code: "const x:{[k in 'union']: () => {[k in 'union']: number} }", + options: ['never', { objectsInObjects: true }], + }, + { + code: "const x:{[k in 'union']: () => {[k in 'union']: number}}", + options: ['never', { objectsInObjects: false }], + }, + { + code: "const x:{[k in 'union']: [ number ]}", + options: ['never', { arraysInObjects: false }], + }, + { + code: "const x:{ [k in 'union']: value}", + options: ['never', { arraysInObjects: true }], + }, + { + code: "const x:{[k in 'union']: value}", + options: ['never', { arraysInObjects: false }], + }, + { + code: "const x:{ [k in 'union']: [number] }", + options: ['never', { arraysInObjects: true }], + }, + { + code: "const x:{[k in 'union']: [number]}", + options: ['never', { arraysInObjects: false }], + }, + // never - object literal types { code: 'const x:{f: {g: number} }', @@ -612,6 +677,69 @@ ruleTester.run('object-curly-spacing', rule, { options: ['never', { arraysInObjects: false }], }, + // always - mapped types + { + code: "const x:{ [k in 'union']: number }", + options: ['always'], + }, + { + code: "const x:{ // line-comment\n[k in 'union']: number\n}", + options: ['always'], + }, + { + code: + "const x:{ /* inline-comment */ [k in 'union']: number /* inline-comment */ }", + options: ['always'], + }, + { + code: "const x:{\n[k in 'union']: number\n}", + options: ['always'], + }, + { + code: "const x:{ [k in 'union']: [number] }", + options: ['always'], + }, + + // always - mapped types - objectsInObjects + { + code: "const x:{ [k in 'union']: { [k in 'union']: number } }", + options: ['always', { objectsInObjects: true }], + }, + { + code: "const x:{ [k in 'union']: { [k in 'union']: number }}", + options: ['always', { objectsInObjects: false }], + }, + { + code: "const x:{ [k in 'union']: () => { [k in 'union']: number } }", + options: ['always', { objectsInObjects: true }], + }, + { + code: "const x:{ [k in 'union']: () => { [k in 'union']: number }}", + options: ['always', { objectsInObjects: false }], + }, + + // always - mapped types - arraysInObjects + { + code: "type x = { [k in 'union']: number }", + options: ['always'], + }, + { + code: "const x:{ [k in 'union']: [number] }", + options: ['always', { arraysInObjects: true }], + }, + { + code: "const x:{ [k in 'union']: value }", + options: ['always', { arraysInObjects: true }], + }, + { + code: "const x:{[k in 'union']: value }", + options: ['always', { arraysInObjects: false }], + }, + { + code: "const x:{[k in 'union']: [number]}", + options: ['always', { arraysInObjects: false }], + }, + // always - object literal types { code: 'const x:{}', @@ -642,7 +770,7 @@ ruleTester.run('object-curly-spacing', rule, { options: ['always'], }, - // always - objectsInObjects + // always - literal types - objectsInObjects { code: 'const x:{ f: { g: number } }', options: ['always', { objectsInObjects: true }], @@ -660,7 +788,7 @@ ruleTester.run('object-curly-spacing', rule, { options: ['always', { objectsInObjects: false }], }, - // always - arraysInObjects + // always - literal types - arraysInObjects { code: 'const x:{ f: [number] }', options: ['always', { arraysInObjects: true }], @@ -1912,6 +2040,7 @@ ruleTester.run('object-curly-spacing', rule, { }, // object literal types + // never - literal types { code: 'type x = { f: number }', output: 'type x = {f: number}', @@ -1930,6 +2059,7 @@ ruleTester.run('object-curly-spacing', rule, { output: 'type x = {f: number}', errors: [{ messageId: 'unexpectedSpaceBefore' }], }, + // always - literal types { code: 'type x = {f: number}', output: 'type x = { f: number }', @@ -1951,5 +2081,58 @@ ruleTester.run('object-curly-spacing', rule, { options: ['always'], errors: [{ messageId: 'requireSpaceBefore' }], }, + + // never - mapped types + { + code: "type x = { [k in 'union']: number }", + output: "type x = {[k in 'union']: number}", + errors: [ + { messageId: 'unexpectedSpaceAfter' }, + { messageId: 'unexpectedSpaceBefore' }, + ], + }, + { + code: "type x = { [k in 'union']: number}", + output: "type x = {[k in 'union']: number}", + errors: [{ messageId: 'unexpectedSpaceAfter' }], + }, + { + code: "type x = {[k in 'union']: number }", + output: "type x = {[k in 'union']: number}", + errors: [{ messageId: 'unexpectedSpaceBefore' }], + }, + // always - mapped types + { + code: "type x = {[k in 'union']: number}", + output: "type x = { [k in 'union']: number }", + options: ['always'], + errors: [ + { messageId: 'requireSpaceAfter' }, + { messageId: 'requireSpaceBefore' }, + ], + }, + { + code: "type x = {[k in 'union']: number }", + output: "type x = { [k in 'union']: number }", + options: ['always'], + errors: [{ messageId: 'requireSpaceAfter' }], + }, + { + code: "type x = { [k in 'union']: number}", + output: "type x = { [k in 'union']: number }", + options: ['always'], + errors: [{ messageId: 'requireSpaceBefore' }], + }, + // Mapped and literal types mix + { + code: "type x = { [k in 'union']: { [k: string]: number } }", + output: "type x = {[k in 'union']: {[k: string]: number}}", + errors: [ + { messageId: 'unexpectedSpaceAfter' }, + { messageId: 'unexpectedSpaceAfter' }, + { messageId: 'unexpectedSpaceBefore' }, + { messageId: 'unexpectedSpaceBefore' }, + ], + }, ], }); diff --git a/packages/eslint-plugin/tests/rules/unbound-method.test.ts b/packages/eslint-plugin/tests/rules/unbound-method.test.ts index 911206b1f21..3075e93fe67 100644 --- a/packages/eslint-plugin/tests/rules/unbound-method.test.ts +++ b/packages/eslint-plugin/tests/rules/unbound-method.test.ts @@ -42,7 +42,7 @@ function addContainsMethodsClassInvalid( errors: [ { line: 18, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], })); @@ -298,7 +298,7 @@ Promise.resolve().then(console.log); errors: [ { line: 10, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -310,7 +310,7 @@ const x = console.log; errors: [ { line: 3, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -325,15 +325,15 @@ function foo(arg: ContainsMethods | null) { errors: [ { line: 20, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, { line: 21, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, { line: 22, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -375,7 +375,7 @@ ContainsMethods.unboundStatic; errors: [ { line: 8, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -390,7 +390,7 @@ const x = CommunicationError.prototype.foo; errors: [ { line: 5, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -400,7 +400,7 @@ const x = CommunicationError.prototype.foo; errors: [ { line: 1, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -419,7 +419,7 @@ instance.unbound = x; // THIS SHOULD NOT errors: [ { line: 9, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -447,7 +447,7 @@ const { unbound } = new Foo(); errors: [ { line: 5, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -476,7 +476,7 @@ let unbound; errors: [ { line: 6, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -505,7 +505,7 @@ const { foo } = CommunicationError.prototype; errors: [ { line: 5, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -520,7 +520,7 @@ let foo; errors: [ { line: 6, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -532,7 +532,7 @@ const { log } = console; errors: [ { line: 3, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -541,7 +541,7 @@ const { log } = console; errors: [ { line: 1, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -562,7 +562,7 @@ class OtherClass extends BaseClass { { line: 8, column: 15, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], }, @@ -584,7 +584,7 @@ class OtherClass extends BaseClass { { line: 9, column: 9, - messageId: 'unbound', + messageId: 'unboundWithoutThisAnnotation', }, ], },