diff --git a/src/compiler/inspectValue.ts b/src/compiler/inspectValue.ts index 86b45b65031b7..8433ee6e4890d 100644 --- a/src/compiler/inspectValue.ts +++ b/src/compiler/inspectValue.ts @@ -26,6 +26,7 @@ namespace ts { } export interface ValueInfoObject extends ValueInfoBase { readonly kind: ValueKind.Object; + readonly hasNontrivialPrototype: boolean; readonly members: ReadonlyArray; } @@ -63,7 +64,9 @@ namespace ts { const builtin = getBuiltinType(name, value as object, recurser); if (builtin !== undefined) return builtin; const entries = getEntriesOfObject(value as object); - return { kind: ValueKind.Object, name, members: flatMap(entries, ({ key, value }) => getValueInfo(key, value, recurser)) }; + const hasNontrivialPrototype = Object.getPrototypeOf(value) !== Object.prototype; + const members = flatMap(entries, ({ key, value }) => getValueInfo(key, value, recurser)); + return { kind: ValueKind.Object, name, hasNontrivialPrototype, members }; } return { kind: ValueKind.Const, name, typeName: isNullOrUndefined(value) ? "any" : typeof value }; }, diff --git a/src/services/codefixes/generateTypes.ts b/src/services/codefixes/generateTypes.ts index 14807ff45b1d6..040d58c8e1187 100644 --- a/src/services/codefixes/generateTypes.ts +++ b/src/services/codefixes/generateTypes.ts @@ -36,13 +36,15 @@ namespace ts { case ValueKind.FunctionOrClass: return [...exportEquals(), ...functionOrClassToStatements(modifiers, name, info)]; case ValueKind.Object: - const { members } = info; - if (kind === OutputKind.ExportEquals) { - return flatMap(members, v => toStatements(v, OutputKind.NamedExport)); - } - if (members.some(m => m.kind === ValueKind.FunctionOrClass)) { - // If some member is a function, use a namespace so it gets a FunctionDeclaration or ClassDeclaration. - return [...exportDefault(), createNamespace(modifiers, name, flatMap(members, toNamespaceMemberStatements))]; + const { members, hasNontrivialPrototype } = info; + if (!hasNontrivialPrototype) { + if (kind === OutputKind.ExportEquals) { + return flatMap(members, v => toStatements(v, OutputKind.NamedExport)); + } + if (members.some(m => m.kind === ValueKind.FunctionOrClass)) { + // If some member is a function, use a namespace so it gets a FunctionDeclaration or ClassDeclaration. + return [...exportDefault(), createNamespace(modifiers, name, flatMap(members, toNamespaceMemberStatements))]; + } } // falls through case ValueKind.Const: @@ -62,10 +64,20 @@ namespace ts { function functionOrClassToStatements(modifiers: Modifiers, name: string, { source, prototypeMembers, namespaceMembers }: ValueInfoFunctionOrClass): ReadonlyArray { const fnAst = parseClassOrFunctionBody(source); const { parameters, returnType } = fnAst === undefined ? { parameters: emptyArray, returnType: anyType() } : getParametersAndReturnType(fnAst); - const instanceProperties = typeof fnAst === "object" ? getConstructorFunctionInstanceProperties(fnAst) : emptyArray; + const protoOrInstanceMembers = createMap(); + if (typeof fnAst === "object") getConstructorFunctionInstanceProperties(fnAst, protoOrInstanceMembers); + for (const p of prototypeMembers) { + // ignore non-functions on the prototype + if (p.kind === ValueKind.FunctionOrClass) { + const m = tryGetMethod(p); + if (m) { + protoOrInstanceMembers.set(p.name, m); + } + } + } const classStaticMembers: ClassElement[] | undefined = - instanceProperties.length !== 0 || prototypeMembers.length !== 0 || fnAst === undefined || typeof fnAst !== "number" && fnAst.kind === SyntaxKind.Constructor ? [] : undefined; + protoOrInstanceMembers.size !== 0 || fnAst === undefined || typeof fnAst !== "number" && fnAst.kind === SyntaxKind.Constructor ? [] : undefined; const namespaceStatements = flatMap(namespaceMembers, info => { if (!isValidIdentifier(info.name)) return undefined; @@ -91,6 +103,9 @@ namespace ts { return undefined; } } + break; + default: + Debug.assertNever(info); } } return toStatements(info, OutputKind.NamespaceMember); @@ -106,9 +121,7 @@ namespace ts { [ ...classStaticMembers, ...(parameters.length ? [createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, parameters, /*body*/ undefined)] : emptyArray), - ...instanceProperties, - // ignore non-functions on the prototype - ...mapDefined(prototypeMembers, info => info.kind === ValueKind.FunctionOrClass ? tryGetMethod(info) : undefined), + ...arrayFrom(protoOrInstanceMembers.values()), ]) : createFunctionDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, name, /*typeParameters*/ undefined, parameters, returnType, /*body*/ undefined); return [decl, ...(namespaceStatements.length === 0 ? emptyArray : [createNamespace(modifiers && modifiers.map(m => getSynthesizedDeepClone(m)), name, namespaceStatements)])]; @@ -150,16 +163,16 @@ namespace ts { } // Parses assignments to "this.x" in the constructor into class property declarations - function getConstructorFunctionInstanceProperties(fnAst: FunctionOrConstructorNode): ReadonlyArray { - const members: PropertyDeclaration[] = []; + function getConstructorFunctionInstanceProperties(fnAst: FunctionOrConstructorNode, members: Map): void { forEachOwnNodeOfFunction(fnAst, node => { if (isAssignmentExpression(node, /*excludeCompoundAssignment*/ true) && isPropertyAccessExpression(node.left) && node.left.expression.kind === SyntaxKind.ThisKeyword) { const name = node.left.name.text; - if (!isJsPrivate(name)) members.push(createProperty(/*decorators*/ undefined, /*modifiers*/ undefined, name, /*questionOrExclamationToken*/ undefined, anyType(), /*initializer*/ undefined)); + if (!isJsPrivate(name)) { + getOrUpdate(members, name, () => createProperty(/*decorators*/ undefined, /*modifiers*/ undefined, name, /*questionOrExclamationToken*/ undefined, anyType(), /*initializer*/ undefined)); + } } }); - return members; } interface ParametersAndReturnType { readonly parameters: ReadonlyArray; readonly returnType: TypeNode; } diff --git a/tests/cases/fourslash/generateTypes_classes.ts b/tests/cases/fourslash/generateTypes_classes.ts index e1d6479f926e8..9168346874377 100644 --- a/tests/cases/fourslash/generateTypes_classes.ts +++ b/tests/cases/fourslash/generateTypes_classes.ts @@ -111,4 +111,39 @@ declare class example { `, }, +{ + // No duplicate instance members + value: (() => { + class C { + constructor() { + (this as any).x = 0; + (this as any).x = 1; + (this as any).m = 0; + } + m() {} + } + return C; + })(), + output: +`export = example; +declare class example { + x: any; + m(): void; +} +`, +}, + +{ + // nontrivial prototype marks something as an instance + value: (() => { + const obj = Object.create({}); + obj.m = function() { this.x = 0; } + return { obj }; + })(), + output: +`export const obj: { + m: Function; +}; +`, +}, );