Skip to content

Commit fe1ba9b

Browse files
author
Andy
authored
Improvements to generateTypes (#28458)
* Improvements to generateTypes * createProperty only if necessary
1 parent b8968fa commit fe1ba9b

File tree

3 files changed

+68
-17
lines changed

3 files changed

+68
-17
lines changed

src/compiler/inspectValue.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ namespace ts {
2626
}
2727
export interface ValueInfoObject extends ValueInfoBase {
2828
readonly kind: ValueKind.Object;
29+
readonly hasNontrivialPrototype: boolean;
2930
readonly members: ReadonlyArray<ValueInfo>;
3031
}
3132

@@ -63,7 +64,9 @@ namespace ts {
6364
const builtin = getBuiltinType(name, value as object, recurser);
6465
if (builtin !== undefined) return builtin;
6566
const entries = getEntriesOfObject(value as object);
66-
return { kind: ValueKind.Object, name, members: flatMap(entries, ({ key, value }) => getValueInfo(key, value, recurser)) };
67+
const hasNontrivialPrototype = Object.getPrototypeOf(value) !== Object.prototype;
68+
const members = flatMap(entries, ({ key, value }) => getValueInfo(key, value, recurser));
69+
return { kind: ValueKind.Object, name, hasNontrivialPrototype, members };
6770
}
6871
return { kind: ValueKind.Const, name, typeName: isNullOrUndefined(value) ? "any" : typeof value };
6972
},

src/services/codefixes/generateTypes.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ namespace ts {
3636
case ValueKind.FunctionOrClass:
3737
return [...exportEquals(), ...functionOrClassToStatements(modifiers, name, info)];
3838
case ValueKind.Object:
39-
const { members } = info;
40-
if (kind === OutputKind.ExportEquals) {
41-
return flatMap(members, v => toStatements(v, OutputKind.NamedExport));
42-
}
43-
if (members.some(m => m.kind === ValueKind.FunctionOrClass)) {
44-
// If some member is a function, use a namespace so it gets a FunctionDeclaration or ClassDeclaration.
45-
return [...exportDefault(), createNamespace(modifiers, name, flatMap(members, toNamespaceMemberStatements))];
39+
const { members, hasNontrivialPrototype } = info;
40+
if (!hasNontrivialPrototype) {
41+
if (kind === OutputKind.ExportEquals) {
42+
return flatMap(members, v => toStatements(v, OutputKind.NamedExport));
43+
}
44+
if (members.some(m => m.kind === ValueKind.FunctionOrClass)) {
45+
// If some member is a function, use a namespace so it gets a FunctionDeclaration or ClassDeclaration.
46+
return [...exportDefault(), createNamespace(modifiers, name, flatMap(members, toNamespaceMemberStatements))];
47+
}
4648
}
4749
// falls through
4850
case ValueKind.Const:
@@ -62,10 +64,20 @@ namespace ts {
6264
function functionOrClassToStatements(modifiers: Modifiers, name: string, { source, prototypeMembers, namespaceMembers }: ValueInfoFunctionOrClass): ReadonlyArray<Statement> {
6365
const fnAst = parseClassOrFunctionBody(source);
6466
const { parameters, returnType } = fnAst === undefined ? { parameters: emptyArray, returnType: anyType() } : getParametersAndReturnType(fnAst);
65-
const instanceProperties = typeof fnAst === "object" ? getConstructorFunctionInstanceProperties(fnAst) : emptyArray;
67+
const protoOrInstanceMembers = createMap<MethodDeclaration | PropertyDeclaration>();
68+
if (typeof fnAst === "object") getConstructorFunctionInstanceProperties(fnAst, protoOrInstanceMembers);
69+
for (const p of prototypeMembers) {
70+
// ignore non-functions on the prototype
71+
if (p.kind === ValueKind.FunctionOrClass) {
72+
const m = tryGetMethod(p);
73+
if (m) {
74+
protoOrInstanceMembers.set(p.name, m);
75+
}
76+
}
77+
}
6678

6779
const classStaticMembers: ClassElement[] | undefined =
68-
instanceProperties.length !== 0 || prototypeMembers.length !== 0 || fnAst === undefined || typeof fnAst !== "number" && fnAst.kind === SyntaxKind.Constructor ? [] : undefined;
80+
protoOrInstanceMembers.size !== 0 || fnAst === undefined || typeof fnAst !== "number" && fnAst.kind === SyntaxKind.Constructor ? [] : undefined;
6981

7082
const namespaceStatements = flatMap(namespaceMembers, info => {
7183
if (!isValidIdentifier(info.name)) return undefined;
@@ -91,6 +103,9 @@ namespace ts {
91103
return undefined;
92104
}
93105
}
106+
break;
107+
default:
108+
Debug.assertNever(info);
94109
}
95110
}
96111
return toStatements(info, OutputKind.NamespaceMember);
@@ -106,9 +121,7 @@ namespace ts {
106121
[
107122
...classStaticMembers,
108123
...(parameters.length ? [createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, parameters, /*body*/ undefined)] : emptyArray),
109-
...instanceProperties,
110-
// ignore non-functions on the prototype
111-
...mapDefined(prototypeMembers, info => info.kind === ValueKind.FunctionOrClass ? tryGetMethod(info) : undefined),
124+
...arrayFrom(protoOrInstanceMembers.values()),
112125
])
113126
: createFunctionDeclaration(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, name, /*typeParameters*/ undefined, parameters, returnType, /*body*/ undefined);
114127
return [decl, ...(namespaceStatements.length === 0 ? emptyArray : [createNamespace(modifiers && modifiers.map(m => getSynthesizedDeepClone(m)), name, namespaceStatements)])];
@@ -150,16 +163,16 @@ namespace ts {
150163
}
151164

152165
// Parses assignments to "this.x" in the constructor into class property declarations
153-
function getConstructorFunctionInstanceProperties(fnAst: FunctionOrConstructorNode): ReadonlyArray<PropertyDeclaration> {
154-
const members: PropertyDeclaration[] = [];
166+
function getConstructorFunctionInstanceProperties(fnAst: FunctionOrConstructorNode, members: Map<MethodDeclaration | PropertyDeclaration>): void {
155167
forEachOwnNodeOfFunction(fnAst, node => {
156168
if (isAssignmentExpression(node, /*excludeCompoundAssignment*/ true) &&
157169
isPropertyAccessExpression(node.left) && node.left.expression.kind === SyntaxKind.ThisKeyword) {
158170
const name = node.left.name.text;
159-
if (!isJsPrivate(name)) members.push(createProperty(/*decorators*/ undefined, /*modifiers*/ undefined, name, /*questionOrExclamationToken*/ undefined, anyType(), /*initializer*/ undefined));
171+
if (!isJsPrivate(name)) {
172+
getOrUpdate(members, name, () => createProperty(/*decorators*/ undefined, /*modifiers*/ undefined, name, /*questionOrExclamationToken*/ undefined, anyType(), /*initializer*/ undefined));
173+
}
160174
}
161175
});
162-
return members;
163176
}
164177

165178
interface ParametersAndReturnType { readonly parameters: ReadonlyArray<ParameterDeclaration>; readonly returnType: TypeNode; }

tests/cases/fourslash/generateTypes_classes.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,39 @@ declare class example {
111111
`,
112112
},
113113

114+
{
115+
// No duplicate instance members
116+
value: (() => {
117+
class C {
118+
constructor() {
119+
(this as any).x = 0;
120+
(this as any).x = 1;
121+
(this as any).m = 0;
122+
}
123+
m() {}
124+
}
125+
return C;
126+
})(),
127+
output:
128+
`export = example;
129+
declare class example {
130+
x: any;
131+
m(): void;
132+
}
133+
`,
134+
},
135+
136+
{
137+
// nontrivial prototype marks something as an instance
138+
value: (() => {
139+
const obj = Object.create({});
140+
obj.m = function() { this.x = 0; }
141+
return { obj };
142+
})(),
143+
output:
144+
`export const obj: {
145+
m: Function;
146+
};
147+
`,
148+
},
114149
);

0 commit comments

Comments
 (0)