From 10334dcf20a5c96747b4275cf1cb8eb61d887bd4 Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Mon, 3 Oct 2022 10:51:23 +0300 Subject: [PATCH 1/3] feat(37440): add QF to handle missing exports --- src/compiler/checker.ts | 31 ---- src/compiler/diagnosticMessages.json | 8 + src/compiler/utilities.ts | 26 +++ src/services/codefixes/fixAddMissingMember.ts | 4 - .../codefixes/fixImportNonExportedMember.ts | 161 ++++++++++++++++++ src/services/tsconfig.json | 1 + src/services/utilities.ts | 4 + .../codeFixImportNonExportedMember1.ts | 22 +++ .../codeFixImportNonExportedMember10.ts | 26 +++ .../codeFixImportNonExportedMember2.ts | 20 +++ .../codeFixImportNonExportedMember3.ts | 23 +++ .../codeFixImportNonExportedMember4.ts | 12 ++ .../codeFixImportNonExportedMember5.ts | 12 ++ .../codeFixImportNonExportedMember6.ts | 25 +++ .../codeFixImportNonExportedMember7.ts | 30 ++++ .../codeFixImportNonExportedMember8.ts | 22 +++ .../codeFixImportNonExportedMember9.ts | 26 +++ .../codeFixImportNonExportedMember_all1.ts | 22 +++ .../codeFixImportNonExportedMember_all2.ts | 24 +++ .../codeFixImportNonExportedMember_all3.ts | 25 +++ .../codeFixImportNonExportedMember_all4.ts | 28 +++ .../codeFixImportNonExportedMember_all5.ts | 79 +++++++++ 22 files changed, 596 insertions(+), 35 deletions(-) create mode 100644 src/services/codefixes/fixImportNonExportedMember.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember1.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember10.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember2.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember3.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember4.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember5.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember6.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember7.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember8.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember9.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember_all1.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember_all2.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember_all3.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember_all4.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0e4f940238cd3..c882c2d4ded02 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7237,16 +7237,6 @@ namespace ts { return statements; } - function canHaveExportModifier(node: Statement): node is Extract { - return isEnumDeclaration(node) || - isVariableStatement(node) || - isFunctionDeclaration(node) || - isClassDeclaration(node) || - (isModuleDeclaration(node) && !isExternalModuleAugmentation(node) && !isGlobalScopeAugmentation(node)) || - isInterfaceDeclaration(node) || - isTypeDeclaration(node); - } - function addExportModifier(node: Extract) { const flags = (getEffectiveModifierFlags(node) | ModifierFlags.Export) & ~ModifierFlags.Ambient; return factory.updateModifiers(node, flags); @@ -42665,27 +42655,6 @@ namespace ts { getNameOfDeclaration(name.parent) === name; } - function isTypeDeclaration(node: Node): node is TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | TypeAliasDeclaration | JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag | EnumDeclaration | ImportClause | ImportSpecifier | ExportSpecifier { - switch (node.kind) { - case SyntaxKind.TypeParameter: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.JSDocTypedefTag: - case SyntaxKind.JSDocCallbackTag: - case SyntaxKind.JSDocEnumTag: - return true; - case SyntaxKind.ImportClause: - return (node as ImportClause).isTypeOnly; - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ExportSpecifier: - return (node as ImportSpecifier | ExportSpecifier).parent.parent.isTypeOnly; - default: - return false; - } - } - // True if the given identifier is part of a type reference function isTypeReferenceIdentifier(node: EntityName): boolean { while (node.parent.kind === SyntaxKind.QualifiedName) { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 7a8f3192d917c..cb15433fe3fe2 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -6687,6 +6687,14 @@ "category": "Message", "code": 90058 }, + "Export '{0}' from module '{1}'": { + "category": "Message", + "code": 90059 + }, + "Export all missing members": { + "category": "Message", + "code": 90060 + }, "Convert function to an ES2015 class": { "category": "Message", diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index cdc0db9407b34..0571a9c92df0d 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7752,4 +7752,30 @@ namespace ts { export function getParameterTypeNode(parameter: ParameterDeclaration | JSDocParameterTag) { return parameter.kind === SyntaxKind.JSDocParameterTag ? parameter.typeExpression?.type : parameter.type; } + + export function isTypeDeclaration(node: Node): node is TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | TypeAliasDeclaration | JSDocTypedefTag | JSDocCallbackTag | JSDocEnumTag | EnumDeclaration | ImportClause | ImportSpecifier | ExportSpecifier { + switch (node.kind) { + case SyntaxKind.TypeParameter: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.JSDocTypedefTag: + case SyntaxKind.JSDocCallbackTag: + case SyntaxKind.JSDocEnumTag: + return true; + case SyntaxKind.ImportClause: + return (node as ImportClause).isTypeOnly; + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ExportSpecifier: + return (node as ImportSpecifier | ExportSpecifier).parent.parent.isTypeOnly; + default: + return false; + } + } + + export function canHaveExportModifier(node: Node): node is Extract { + return isEnumDeclaration(node) || isVariableStatement(node) || isFunctionDeclaration(node) || isClassDeclaration(node) + || isInterfaceDeclaration(node) || isTypeDeclaration(node) || (isModuleDeclaration(node) && !isExternalModuleAugmentation(node) && !isGlobalScopeAugmentation(node)); + } } diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 2542a0732342b..af36c3936b000 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -264,10 +264,6 @@ namespace ts.codefix { return undefined; } - function isSourceFileFromLibrary(program: Program, node: SourceFile) { - return program.isSourceFileFromExternalLibrary(node) || program.isSourceFileDefaultLibrary(node); - } - function getActionsForMissingMemberDeclaration(context: CodeFixContext, info: TypeLikeDeclarationInfo): CodeFixAction[] | undefined { return info.isJSFile ? singleElementArray(createActionForAddMissingMemberInJavascriptFile(context, info)) : createActionsForAddMissingMemberInTypeScriptFile(context, info); diff --git a/src/services/codefixes/fixImportNonExportedMember.ts b/src/services/codefixes/fixImportNonExportedMember.ts new file mode 100644 index 0000000000000..8fbc60fb17da1 --- /dev/null +++ b/src/services/codefixes/fixImportNonExportedMember.ts @@ -0,0 +1,161 @@ +/* @internal */ +namespace ts.codefix { + const fixId = "fixImportNonExportedMember"; + const errorCodes = [ + Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported.code, + ]; + + registerCodeFix({ + errorCodes, + fixIds: [fixId], + getCodeActions(context) { + const { sourceFile, span, program } = context; + const info = getInfo(sourceFile, span.start, program); + if (info === undefined) return undefined; + + const changes = textChanges.ChangeTracker.with(context, t => doChange(t, info)); + return [createCodeFixAction(fixId, changes, [Diagnostics.Export_0_from_module_1, info.exportName.node.text, info.moduleSpecifier], fixId, Diagnostics.Export_all_missing_members)]; + }, + getAllCodeActions(context) { + const { program } = context; + return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { + const exports = new Map(); + + eachDiagnostic(context, errorCodes, diag => { + const info = getInfo(diag.file, diag.start, program); + if (info === undefined) return undefined; + + const { exportName, node, moduleSourceFile } = info; + if (tryGetExportDeclaration(moduleSourceFile, exportName.isTypeOnly) === undefined && canHaveExportModifier(node)) { + changes.insertExportModifier(moduleSourceFile, node); + } + else { + const moduleExports = exports.get(moduleSourceFile) || { typeOnlyExports: [], exports: [] }; + if (exportName.isTypeOnly) { + moduleExports.typeOnlyExports.push(exportName); + } + else { + moduleExports.exports.push(exportName); + } + exports.set(moduleSourceFile, moduleExports); + } + }); + + exports.forEach((moduleExports, moduleSourceFile) => { + const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ true); + if (exportDeclaration && exportDeclaration.isTypeOnly) { + doChanges(changes, moduleSourceFile, moduleExports.typeOnlyExports, exportDeclaration); + doChanges(changes, moduleSourceFile, moduleExports.exports, tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ false)); + } + else { + doChanges(changes, moduleSourceFile, [...moduleExports.exports, ...moduleExports.typeOnlyExports], exportDeclaration); + } + }); + })); + } + }); + + interface ModuleExports { + typeOnlyExports: ExportName[]; + exports: ExportName[]; + } + + interface ExportName { + node: Identifier; + isTypeOnly: boolean; + } + + interface Info { + exportName: ExportName; + node: Declaration | VariableStatement; + moduleSourceFile: SourceFile; + moduleSpecifier: string; + } + + function getInfo(sourceFile: SourceFile, pos: number, program: Program): Info | undefined { + const token = getTokenAtPosition(sourceFile, pos); + if (isIdentifier(token)) { + const importDeclaration = findAncestor(token, isImportDeclaration); + if (importDeclaration === undefined) return undefined; + + const moduleSpecifier = isStringLiteral(importDeclaration.moduleSpecifier) ? importDeclaration.moduleSpecifier.text : undefined; + if (moduleSpecifier === undefined) return undefined; + + const resolvedModule = getResolvedModule(sourceFile, moduleSpecifier, /*mode*/ undefined); + if (resolvedModule === undefined) return undefined; + + const moduleSourceFile = program.getSourceFile(resolvedModule.resolvedFileName); + if (moduleSourceFile === undefined || isSourceFileFromLibrary(program, moduleSourceFile)) return undefined; + + const moduleSymbol = moduleSourceFile.symbol; + const locals = moduleSymbol.valueDeclaration?.locals; + if (locals === undefined) return undefined; + + const localSymbol = locals.get(token.escapedText); + if (localSymbol === undefined) return undefined; + + const node = getNodeOfSymbol(localSymbol); + if (node === undefined) return undefined; + + const exportName = { node: token, isTypeOnly: isTypeDeclaration(node) }; + return { exportName, node, moduleSourceFile, moduleSpecifier }; + } + return undefined; + } + + function doChange(changes: textChanges.ChangeTracker, { exportName, node, moduleSourceFile }: Info) { + const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, exportName.isTypeOnly); + if (exportDeclaration) { + updateExport(changes, moduleSourceFile, exportDeclaration, [exportName]); + } + else if (canHaveExportModifier(node)) { + changes.insertExportModifier(moduleSourceFile, node); + } + else { + createExport(changes, moduleSourceFile, [exportName]); + } + } + + function doChanges(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleExports: ExportName[], node: ExportDeclaration | undefined) { + if (node) { + updateExport(changes, sourceFile, node, moduleExports); + } + else { + createExport(changes, sourceFile, moduleExports); + } + } + + function tryGetExportDeclaration(sourceFile: SourceFile, isTypeOnly: boolean) { + const predicate = (node: Node): node is ExportDeclaration => + isExportDeclaration(node) && (isTypeOnly && node.isTypeOnly || !node.isTypeOnly); + return findLast(sourceFile.statements, predicate); + } + + function updateExport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: ExportDeclaration, names: ExportName[]) { + const namedExports = node.exportClause && isNamedExports(node.exportClause) ? node.exportClause.elements : factory.createNodeArray([]); + const exportNames = map(names, n => ({ ...n, isTypeOnly: node.isTypeOnly ? false : n.isTypeOnly })); + changes.replaceNode(sourceFile, node, + factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, + factory.createNamedExports( + factory.createNodeArray([...namedExports, ...createExportSpecifiers(exportNames)], /*hasTrailingComma*/ namedExports.hasTrailingComma)), node.moduleSpecifier, node.assertClause)); + } + + function createExport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, names: ExportName[]) { + changes.insertNodeAtEndOfScope(sourceFile, sourceFile, + factory.createExportDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, + factory.createNamedExports(createExportSpecifiers(names)), /*moduleSpecifier*/ undefined, /*assertClause*/ undefined)); + } + + function createExportSpecifiers(names: ExportName[]) { + return factory.createNodeArray(map(names, n => factory.createExportSpecifier(n.isTypeOnly, /*propertyName*/ undefined, n.node))); + } + + function getNodeOfSymbol(symbol: Symbol) { + if (symbol.valueDeclaration === undefined) { + return firstOrUndefined(symbol.declarations); + } + const declaration = symbol.valueDeclaration; + const variableStatement = isVariableDeclaration(declaration) ? tryCast(declaration.parent.parent, isVariableStatement) : undefined; + return variableStatement && length(variableStatement.declarationList.declarations) === 1 ? variableStatement : declaration; + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 2237163ebb20d..2099fe308c911 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -71,6 +71,7 @@ "codefixes/fixOverrideModifier.ts", "codefixes/fixNoPropertyAccessFromIndexSignature.ts", "codefixes/fixImplicitThis.ts", + "codefixes/fixImportNonExportedMember.ts", "codefixes/fixIncorrectNamedTupleSyntax.ts", "codefixes/fixSpelling.ts", "codefixes/returnValueCorrect.ts", diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 4d24e57d051ee..5fe65b8fb75f0 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3421,5 +3421,9 @@ namespace ts { return jsx === JsxEmit.React || jsx === JsxEmit.ReactNative; } + export function isSourceFileFromLibrary(program: Program, node: SourceFile) { + return program.isSourceFileFromExternalLibrary(node) || program.isSourceFileDefaultLibrary(node); + } + // #endregion } diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember1.ts b/tests/cases/fourslash/codeFixImportNonExportedMember1.ts new file mode 100644 index 0000000000000..0cdc08cdc0428 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember1.ts @@ -0,0 +1,22 @@ +/// + +// @module: esnext +// @filename: /a.ts +////declare function foo(): any +////declare function bar(): any; +////export { foo }; + +// @filename: /b.ts +////import { bar } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "bar", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`declare function foo(): any +declare function bar(): any; +export { foo, bar };`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember10.ts b/tests/cases/fourslash/codeFixImportNonExportedMember10.ts new file mode 100644 index 0000000000000..a9d74e1874de0 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember10.ts @@ -0,0 +1,26 @@ +/// + +// @module: esnext +// @filename: /a.ts +/////** +//// * foo +//// */ +////function foo() {} +////export const bar = 1; + +// @filename: /b.ts +////import { foo } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "foo", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`/** + * foo + */ +export function foo() {} +export const bar = 1;`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember2.ts b/tests/cases/fourslash/codeFixImportNonExportedMember2.ts new file mode 100644 index 0000000000000..09df5a64b2238 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember2.ts @@ -0,0 +1,20 @@ +/// + +// @module: esnext +// @filename: /a.ts +////export declare function foo(): any; +////declare function bar(): any; + +// @filename: /b.ts +////import { bar } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "bar", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`export declare function foo(): any; +export declare function bar(): any;`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember3.ts b/tests/cases/fourslash/codeFixImportNonExportedMember3.ts new file mode 100644 index 0000000000000..1bd5dc6a9b0e3 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember3.ts @@ -0,0 +1,23 @@ +/// + +// @module: esnext +// @filename: /a.ts +////let foo = 1, bar = 1; +////export const baz = 1; + +// @filename: /b.ts +////import { bar } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "bar", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`let foo = 1, bar = 1; +export const baz = 1; + +export { bar }; +`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember4.ts b/tests/cases/fourslash/codeFixImportNonExportedMember4.ts new file mode 100644 index 0000000000000..58f73f536e5ed --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember4.ts @@ -0,0 +1,12 @@ +/// + +// @module: esnext +// @filename: /a.d.ts +////declare function foo(): any; +////declare function bar(): any; + +// @filename: /b.ts +////import { bar } from "./a"; + +goTo.file("/b.ts"); +verify.not.codeFixAvailable("fixImportNonExportedMember"); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember5.ts b/tests/cases/fourslash/codeFixImportNonExportedMember5.ts new file mode 100644 index 0000000000000..b2a6162e871a2 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember5.ts @@ -0,0 +1,12 @@ +/// + +// @moduleResolution: node +// @module: esnext +// @filename: /node_modules/foo/index.js +////function bar() {} + +// @filename: /b.ts +////import { bar } from "./foo"; + +goTo.file("/b.ts"); +verify.not.codeFixAvailable("fixImportNonExportedMember"); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember6.ts b/tests/cases/fourslash/codeFixImportNonExportedMember6.ts new file mode 100644 index 0000000000000..9594271bb34d0 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember6.ts @@ -0,0 +1,25 @@ +/// + +// @module: esnext +// @filename: /a.ts +////let a = 1, b = 1; +////type T = number; +////export type { T }; + +// @filename: /b.ts +////import { b } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "b", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`let a = 1, b = 1; +type T = number; +export type { T }; + +export { b }; +`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember7.ts b/tests/cases/fourslash/codeFixImportNonExportedMember7.ts new file mode 100644 index 0000000000000..4c30d26f732c8 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember7.ts @@ -0,0 +1,30 @@ +/// + +// @module: esnext +// @filename: /a.ts +////const a = 1 +////const b = 1; +////export { a, b }; +//// +////type T2 = number; +////type T1 = number; +////export type { T1 }; + +// @filename: /b.ts +////import { T2 } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "T2", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`const a = 1 +const b = 1; +export { a, b }; + +type T2 = number; +type T1 = number; +export type { T1, T2 };`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember8.ts b/tests/cases/fourslash/codeFixImportNonExportedMember8.ts new file mode 100644 index 0000000000000..4ebc52012f7fd --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember8.ts @@ -0,0 +1,22 @@ +/// + +// @module: esnext +// @filename: /a.ts +////const a = 1; +////type T = number; +////export { a }; + +// @filename: /b.ts +////import { T } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "T", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`const a = 1; +type T = number; +export { a, type T };`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember9.ts b/tests/cases/fourslash/codeFixImportNonExportedMember9.ts new file mode 100644 index 0000000000000..127718cb3ddf8 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember9.ts @@ -0,0 +1,26 @@ +/// + +// @module: esnext +// @filename: /a.ts +/////** +//// * foo +//// */ +////function foo() {} +////export {}; + +// @filename: /b.ts +////import { foo } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "foo", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`/** + * foo + */ +function foo() {} +export { foo };`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all1.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all1.ts new file mode 100644 index 0000000000000..3cbd27ced0be3 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all1.ts @@ -0,0 +1,22 @@ +/// + +// @module: esnext +// @filename: /a.ts +////declare function foo(): any; +////declare function bar(): any; +////export declare function baz(): any; + +// @filename: /b.ts +////import { foo, bar } from "./a"; + +goTo.file("/b.ts"); +verify.codeFixAll({ + fixId: "fixImportNonExportedMember", + fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + newFileContent: { + "/a.ts": +`export declare function foo(): any; +export declare function bar(): any; +export declare function baz(): any;` + }, +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all2.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all2.ts new file mode 100644 index 0000000000000..566bf5a1d9ee1 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all2.ts @@ -0,0 +1,24 @@ +/// + +// @module: esnext +// @filename: /a.ts +////declare function foo(): any; +////declare function bar(): any; +////declare function baz(): any; +////export { baz }; + +// @filename: /b.ts +////import { foo, bar } from "./a"; + +goTo.file("/b.ts"); +verify.codeFixAll({ + fixId: "fixImportNonExportedMember", + fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + newFileContent: { + "/a.ts": +`declare function foo(): any; +declare function bar(): any; +declare function baz(): any; +export { baz, foo, bar };` + }, +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all3.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all3.ts new file mode 100644 index 0000000000000..84e5c28762bd8 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all3.ts @@ -0,0 +1,25 @@ +/// + +// @module: esnext +// @filename: /a.ts +////let a = 1, b = 1; +////let c = 1, d = 1; +////export const e = 1; + +// @filename: /b.ts +////import { b, d } from "./a"; + +goTo.file("/b.ts"); +verify.codeFixAll({ + fixId: "fixImportNonExportedMember", + fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + newFileContent: { + "/a.ts": +`let a = 1, b = 1; +let c = 1, d = 1; +export const e = 1; + +export { b, d }; +` + }, +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all4.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all4.ts new file mode 100644 index 0000000000000..1c73742fbbd32 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all4.ts @@ -0,0 +1,28 @@ +/// + +// @module: esnext +// @filename: /a.ts +////const a = 1; +////export const foo = 1; + +// @filename: /b.ts +////const b = 1; +////export const bar = 1; + +// @filename: /c.ts +////import { a } from "./a"; +////import { b } from "./b"; + +goTo.file("/c.ts"); +verify.codeFixAll({ + fixId: "fixImportNonExportedMember", + fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + newFileContent: { + "/a.ts": +`export const a = 1; +export const foo = 1;`, + "/b.ts": +`export const b = 1; +export const bar = 1;` + }, +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts new file mode 100644 index 0000000000000..2d8f9bbf47a5c --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts @@ -0,0 +1,79 @@ +/// + +// @module: esnext +// @filename: /a.ts +////let a = 1, b = 1, c = 1; +////export { b }; +//// +////type T2 = number; +////type T1 = number; +////export type { T2 }; + +// @filename: /b.ts +////let a = 1, b = 1, c = 1; +//// +////type T3 = number; +////type T4 = number; +////export type { T4 }; + +// @filename: /c.ts +////let a = 1, b = 1, c = 1; +//// +////type T5 = number; +////type T6 = number; +////export { a }; + +// @filename: /d.ts +////export const a = 1; +////let b = 1, c = 1, d = 1; +//// +////type T7 = number; +////type T8 = number; + +// @filename: /e.ts +////import { T1, a } from "./a"; +////import { T3, b } from "./b"; +////import { T5, c } from "./c"; +////import { T7, d } from "./d"; + +goTo.file("/e.ts"); +verify.codeFixAll({ + fixId: "fixImportNonExportedMember", + fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + newFileContent: { + "/a.ts": +`let a = 1, b = 1, c = 1; +export { b, a }; + +type T2 = number; +type T1 = number; +export type { T2, T1 };`, + + "/b.ts": +`let a = 1, b = 1, c = 1; + +type T3 = number; +type T4 = number; +export type { T4, T3 }; + +export { b }; +`, + + "/c.ts": +`let a = 1, b = 1, c = 1; + +type T5 = number; +type T6 = number; +export { a, c, type T5 };`, + + "/d.ts": +`export const a = 1; +let b = 1, c = 1, d = 1; + +export type T7 = number; +type T8 = number; + +export { d }; +`, + }, +}); From f49694fefc0c18619a0036b4bd06b4ab80da18be Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Tue, 4 Oct 2022 22:40:24 +0300 Subject: [PATCH 2/3] change diagnostic message --- src/compiler/diagnosticMessages.json | 2 +- src/services/codefixes/fixImportNonExportedMember.ts | 2 +- tests/cases/fourslash/codeFixImportNonExportedMember_all1.ts | 2 +- tests/cases/fourslash/codeFixImportNonExportedMember_all2.ts | 2 +- tests/cases/fourslash/codeFixImportNonExportedMember_all3.ts | 2 +- tests/cases/fourslash/codeFixImportNonExportedMember_all4.ts | 2 +- tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index cb15433fe3fe2..62f26cf014cc9 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -6691,7 +6691,7 @@ "category": "Message", "code": 90059 }, - "Export all missing members": { + "Export all referenced locals": { "category": "Message", "code": 90060 }, diff --git a/src/services/codefixes/fixImportNonExportedMember.ts b/src/services/codefixes/fixImportNonExportedMember.ts index 8fbc60fb17da1..0f9b1ba7ae6df 100644 --- a/src/services/codefixes/fixImportNonExportedMember.ts +++ b/src/services/codefixes/fixImportNonExportedMember.ts @@ -14,7 +14,7 @@ namespace ts.codefix { if (info === undefined) return undefined; const changes = textChanges.ChangeTracker.with(context, t => doChange(t, info)); - return [createCodeFixAction(fixId, changes, [Diagnostics.Export_0_from_module_1, info.exportName.node.text, info.moduleSpecifier], fixId, Diagnostics.Export_all_missing_members)]; + return [createCodeFixAction(fixId, changes, [Diagnostics.Export_0_from_module_1, info.exportName.node.text, info.moduleSpecifier], fixId, Diagnostics.Export_all_referenced_locals)]; }, getAllCodeActions(context) { const { program } = context; diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all1.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all1.ts index 3cbd27ced0be3..e9936a8d7105a 100644 --- a/tests/cases/fourslash/codeFixImportNonExportedMember_all1.ts +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all1.ts @@ -12,7 +12,7 @@ goTo.file("/b.ts"); verify.codeFixAll({ fixId: "fixImportNonExportedMember", - fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + fixAllDescription: ts.Diagnostics.Export_all_referenced_locals.message, newFileContent: { "/a.ts": `export declare function foo(): any; diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all2.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all2.ts index 566bf5a1d9ee1..5912d3dd131fd 100644 --- a/tests/cases/fourslash/codeFixImportNonExportedMember_all2.ts +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all2.ts @@ -13,7 +13,7 @@ goTo.file("/b.ts"); verify.codeFixAll({ fixId: "fixImportNonExportedMember", - fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + fixAllDescription: ts.Diagnostics.Export_all_referenced_locals.message, newFileContent: { "/a.ts": `declare function foo(): any; diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all3.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all3.ts index 84e5c28762bd8..56f41d93d5ed5 100644 --- a/tests/cases/fourslash/codeFixImportNonExportedMember_all3.ts +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all3.ts @@ -12,7 +12,7 @@ goTo.file("/b.ts"); verify.codeFixAll({ fixId: "fixImportNonExportedMember", - fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + fixAllDescription: ts.Diagnostics.Export_all_referenced_locals.message, newFileContent: { "/a.ts": `let a = 1, b = 1; diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all4.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all4.ts index 1c73742fbbd32..7f805c68e73ec 100644 --- a/tests/cases/fourslash/codeFixImportNonExportedMember_all4.ts +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all4.ts @@ -16,7 +16,7 @@ goTo.file("/c.ts"); verify.codeFixAll({ fixId: "fixImportNonExportedMember", - fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + fixAllDescription: ts.Diagnostics.Export_all_referenced_locals.message, newFileContent: { "/a.ts": `export const a = 1; diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts index 2d8f9bbf47a5c..403b294176462 100644 --- a/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts @@ -39,7 +39,7 @@ goTo.file("/e.ts"); verify.codeFixAll({ fixId: "fixImportNonExportedMember", - fixAllDescription: ts.Diagnostics.Export_all_missing_members.message, + fixAllDescription: ts.Diagnostics.Export_all_referenced_locals.message, newFileContent: { "/a.ts": `let a = 1, b = 1, c = 1; From 78b5170ae7867485a967e7383cde43f3b3fb4c04 Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Thu, 13 Oct 2022 23:59:53 +0300 Subject: [PATCH 3/3] add type modifier only if isolatedModules is set or if the export declaration already uses type modifiers --- .../codefixes/fixImportNonExportedMember.ts | 42 ++++++++++--------- .../codeFixImportNonExportedMember11.ts | 21 ++++++++++ .../codeFixImportNonExportedMember12.ts | 22 ++++++++++ .../codeFixImportNonExportedMember13.ts | 23 ++++++++++ .../codeFixImportNonExportedMember8.ts | 2 +- .../codeFixImportNonExportedMember_all5.ts | 2 +- .../codeFixImportNonExportedMember_all6.ts | 35 ++++++++++++++++ .../codeFixImportNonExportedMember_all7.ts | 26 ++++++++++++ 8 files changed, 151 insertions(+), 22 deletions(-) create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember11.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember12.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember13.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember_all6.ts create mode 100644 tests/cases/fourslash/codeFixImportNonExportedMember_all7.ts diff --git a/src/services/codefixes/fixImportNonExportedMember.ts b/src/services/codefixes/fixImportNonExportedMember.ts index 0f9b1ba7ae6df..5eb3a0e81d301 100644 --- a/src/services/codefixes/fixImportNonExportedMember.ts +++ b/src/services/codefixes/fixImportNonExportedMember.ts @@ -13,7 +13,7 @@ namespace ts.codefix { const info = getInfo(sourceFile, span.start, program); if (info === undefined) return undefined; - const changes = textChanges.ChangeTracker.with(context, t => doChange(t, info)); + const changes = textChanges.ChangeTracker.with(context, t => doChange(t, program, info)); return [createCodeFixAction(fixId, changes, [Diagnostics.Export_0_from_module_1, info.exportName.node.text, info.moduleSpecifier], fixId, Diagnostics.Export_all_referenced_locals)]; }, getAllCodeActions(context) { @@ -44,11 +44,11 @@ namespace ts.codefix { exports.forEach((moduleExports, moduleSourceFile) => { const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ true); if (exportDeclaration && exportDeclaration.isTypeOnly) { - doChanges(changes, moduleSourceFile, moduleExports.typeOnlyExports, exportDeclaration); - doChanges(changes, moduleSourceFile, moduleExports.exports, tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ false)); + doChanges(changes, program, moduleSourceFile, moduleExports.typeOnlyExports, exportDeclaration); + doChanges(changes, program, moduleSourceFile, moduleExports.exports, tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ false)); } else { - doChanges(changes, moduleSourceFile, [...moduleExports.exports, ...moduleExports.typeOnlyExports], exportDeclaration); + doChanges(changes, program, moduleSourceFile, [...moduleExports.exports, ...moduleExports.typeOnlyExports], exportDeclaration); } }); })); @@ -103,25 +103,27 @@ namespace ts.codefix { return undefined; } - function doChange(changes: textChanges.ChangeTracker, { exportName, node, moduleSourceFile }: Info) { + function doChange(changes: textChanges.ChangeTracker, program: Program, { exportName, node, moduleSourceFile }: Info) { const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, exportName.isTypeOnly); if (exportDeclaration) { - updateExport(changes, moduleSourceFile, exportDeclaration, [exportName]); + updateExport(changes, program, moduleSourceFile, exportDeclaration, [exportName]); } else if (canHaveExportModifier(node)) { changes.insertExportModifier(moduleSourceFile, node); } else { - createExport(changes, moduleSourceFile, [exportName]); + createExport(changes, program, moduleSourceFile, [exportName]); } } - function doChanges(changes: textChanges.ChangeTracker, sourceFile: SourceFile, moduleExports: ExportName[], node: ExportDeclaration | undefined) { - if (node) { - updateExport(changes, sourceFile, node, moduleExports); - } - else { - createExport(changes, sourceFile, moduleExports); + function doChanges(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, moduleExports: ExportName[], node: ExportDeclaration | undefined) { + if (length(moduleExports)) { + if (node) { + updateExport(changes, program, sourceFile, node, moduleExports); + } + else { + createExport(changes, program, sourceFile, moduleExports); + } } } @@ -131,23 +133,23 @@ namespace ts.codefix { return findLast(sourceFile.statements, predicate); } - function updateExport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, node: ExportDeclaration, names: ExportName[]) { + function updateExport(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, node: ExportDeclaration, names: ExportName[]) { const namedExports = node.exportClause && isNamedExports(node.exportClause) ? node.exportClause.elements : factory.createNodeArray([]); - const exportNames = map(names, n => ({ ...n, isTypeOnly: node.isTypeOnly ? false : n.isTypeOnly })); + const allowTypeModifier = !node.isTypeOnly && !!(program.getCompilerOptions().isolatedModules || find(namedExports, e => e.isTypeOnly)); changes.replaceNode(sourceFile, node, factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, factory.createNamedExports( - factory.createNodeArray([...namedExports, ...createExportSpecifiers(exportNames)], /*hasTrailingComma*/ namedExports.hasTrailingComma)), node.moduleSpecifier, node.assertClause)); + factory.createNodeArray([...namedExports, ...createExportSpecifiers(names, allowTypeModifier)], /*hasTrailingComma*/ namedExports.hasTrailingComma)), node.moduleSpecifier, node.assertClause)); } - function createExport(changes: textChanges.ChangeTracker, sourceFile: SourceFile, names: ExportName[]) { + function createExport(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, names: ExportName[]) { changes.insertNodeAtEndOfScope(sourceFile, sourceFile, factory.createExportDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, - factory.createNamedExports(createExportSpecifiers(names)), /*moduleSpecifier*/ undefined, /*assertClause*/ undefined)); + factory.createNamedExports(createExportSpecifiers(names, /*allowTypeModifier*/ !!program.getCompilerOptions().isolatedModules)), /*moduleSpecifier*/ undefined, /*assertClause*/ undefined)); } - function createExportSpecifiers(names: ExportName[]) { - return factory.createNodeArray(map(names, n => factory.createExportSpecifier(n.isTypeOnly, /*propertyName*/ undefined, n.node))); + function createExportSpecifiers(names: ExportName[], allowTypeModifier: boolean) { + return factory.createNodeArray(map(names, n => factory.createExportSpecifier(allowTypeModifier && n.isTypeOnly, /*propertyName*/ undefined, n.node))); } function getNodeOfSymbol(symbol: Symbol) { diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember11.ts b/tests/cases/fourslash/codeFixImportNonExportedMember11.ts new file mode 100644 index 0000000000000..c386f3e5f8270 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember11.ts @@ -0,0 +1,21 @@ +/// + +// @module: esnext +// @isolatedModules: true +// @filename: /a.ts +////type T = {}; +////export {}; + +// @filename: /b.ts +////import { T } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "T", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`type T = {}; +export { type T };`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember12.ts b/tests/cases/fourslash/codeFixImportNonExportedMember12.ts new file mode 100644 index 0000000000000..25d030ba2585e --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember12.ts @@ -0,0 +1,22 @@ +/// + +// @module: esnext +// @filename: /a.ts +////type T1 = {}; +////type T2 = {}; +////export { type T1 }; + +// @filename: /b.ts +////import { T2 } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "T2", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`type T1 = {}; +type T2 = {}; +export { type T1, type T2 };`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember13.ts b/tests/cases/fourslash/codeFixImportNonExportedMember13.ts new file mode 100644 index 0000000000000..9db9a90e7f171 --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember13.ts @@ -0,0 +1,23 @@ +/// + +// @module: esnext +// @isolatedModules: true +// @filename: /a.ts +////type T1 = {}; +////type T2 = {}; +////export type { T1 }; + +// @filename: /b.ts +////import { T2 } from "./a"; + +goTo.file("/b.ts"); +verify.codeFix({ + description: [ts.Diagnostics.Export_0_from_module_1.message, "T2", "./a"], + index: 0, + newFileContent: { + "/a.ts": +`type T1 = {}; +type T2 = {}; +export type { T1, T2 };`, + } +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember8.ts b/tests/cases/fourslash/codeFixImportNonExportedMember8.ts index 4ebc52012f7fd..2dc1c10015a02 100644 --- a/tests/cases/fourslash/codeFixImportNonExportedMember8.ts +++ b/tests/cases/fourslash/codeFixImportNonExportedMember8.ts @@ -17,6 +17,6 @@ verify.codeFix({ "/a.ts": `const a = 1; type T = number; -export { a, type T };`, +export { a, T };`, } }); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts index 403b294176462..6597d39952d35 100644 --- a/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all5.ts @@ -64,7 +64,7 @@ export { b }; type T5 = number; type T6 = number; -export { a, c, type T5 };`, +export { a, c, T5 };`, "/d.ts": `export const a = 1; diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all6.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all6.ts new file mode 100644 index 0000000000000..cd10a7732fe2b --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all6.ts @@ -0,0 +1,35 @@ +/// + +// @module: esnext +// @isolatedModules: true +// @filename: /a.ts +////type T1 = {}; +////const a = 1; +////const b = 1; +////export { a }; + +// @filename: /b.ts +////type T2 = {}; +////type T3 = {}; +////export type { T2 }; + +// @filename: /c.ts +////import { b, T1 } from "./a"; +////import { T3 } from "./b"; + +goTo.file("/c.ts"); +verify.codeFixAll({ + fixId: "fixImportNonExportedMember", + fixAllDescription: ts.Diagnostics.Export_all_referenced_locals.message, + newFileContent: { + "/a.ts": +`type T1 = {}; +const a = 1; +const b = 1; +export { a, b, type T1 };`, + "/b.ts": +`type T2 = {}; +type T3 = {}; +export type { T2, T3 };`, + }, +}); diff --git a/tests/cases/fourslash/codeFixImportNonExportedMember_all7.ts b/tests/cases/fourslash/codeFixImportNonExportedMember_all7.ts new file mode 100644 index 0000000000000..2bc48ab7b29ad --- /dev/null +++ b/tests/cases/fourslash/codeFixImportNonExportedMember_all7.ts @@ -0,0 +1,26 @@ +/// + +// @module: esnext +// @filename: /a.ts +////type T1 = {}; +////type T2 = {}; +////type T3 = {}; +////const a = 1; +////export { a, type T1 }; + +// @filename: /b.ts +////import { T2, T3 } from "./a"; + +goTo.file("/b.ts"); +verify.codeFixAll({ + fixId: "fixImportNonExportedMember", + fixAllDescription: ts.Diagnostics.Export_all_referenced_locals.message, + newFileContent: { + "/a.ts": +`type T1 = {}; +type T2 = {}; +type T3 = {}; +const a = 1; +export { a, type T1, type T2, type T3 };`, + }, +});