Skip to content

Commit 839e72e

Browse files
committed
feat(28491): add QF to declare missing properties
1 parent 69cc9ba commit 839e72e

17 files changed

+488
-26
lines changed

src/compiler/checker.ts

+1
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ namespace ts {
393393
getDiagnostics,
394394
getGlobalDiagnostics,
395395
getRecursionIdentity,
396+
getUnmatchedProperties,
396397
getTypeOfSymbolAtLocation: (symbol, locationIn) => {
397398
const location = getParseTreeNode(locationIn);
398399
return location ? getTypeOfSymbolAtLocation(symbol, location) : errorType;

src/compiler/diagnosticMessages.json

+8
Original file line numberDiff line numberDiff line change
@@ -6496,6 +6496,14 @@
64966496
"category": "Message",
64976497
"code": 95164
64986498
},
6499+
"Add missing properties": {
6500+
"category": "Message",
6501+
"code": 95165
6502+
},
6503+
"Add all missing properties": {
6504+
"category": "Message",
6505+
"code": 95166
6506+
},
64996507

65006508
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
65016509
"category": "Error",

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4272,6 +4272,7 @@ namespace ts {
42724272
/* @internal */ getInstantiationCount(): number;
42734273
/* @internal */ getRelationCacheSizes(): { assignable: number, identity: number, subtype: number, strictSubtype: number };
42744274
/* @internal */ getRecursionIdentity(type: Type): object | undefined;
4275+
/* @internal */ getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator<Symbol>;
42754276

42764277
/* @internal */ isArrayType(type: Type): boolean;
42774278
/* @internal */ isTupleType(type: Type): boolean;

src/services/codefixes/fixAddMissingMember.ts

+122-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/* @internal */
22
namespace ts.codefix {
33
const fixMissingMember = "fixMissingMember";
4+
const fixMissingProperties = "fixMissingProperties";
45
const fixMissingFunctionDeclaration = "fixMissingFunctionDeclaration";
6+
57
const errorCodes = [
68
Diagnostics.Property_0_does_not_exist_on_type_1.code,
79
Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2.code,
@@ -19,6 +21,10 @@ namespace ts.codefix {
1921
if (!info) {
2022
return undefined;
2123
}
24+
if (info.kind === InfoKind.ObjectLiteral) {
25+
const changes = textChanges.ChangeTracker.with(context, t => addObjectLiteralProperties(t, context, info));
26+
return [createCodeFixAction(fixMissingProperties, changes, Diagnostics.Add_missing_properties, fixMissingProperties, Diagnostics.Add_all_missing_properties)];
27+
}
2228
if (info.kind === InfoKind.Function) {
2329
const changes = textChanges.ChangeTracker.with(context, t => addFunctionDeclaration(t, context, info));
2430
return [createCodeFixAction(fixMissingFunctionDeclaration, changes, [Diagnostics.Add_missing_function_declaration_0, info.token.text], fixMissingFunctionDeclaration, Diagnostics.Add_all_missing_function_declarations)];
@@ -29,7 +35,7 @@ namespace ts.codefix {
2935
}
3036
return concatenate(getActionsForMissingMethodDeclaration(context, info), getActionsForMissingMemberDeclaration(context, info));
3137
},
32-
fixIds: [fixMissingMember, fixMissingFunctionDeclaration],
38+
fixIds: [fixMissingMember, fixMissingFunctionDeclaration, fixMissingProperties],
3339
getAllCodeActions: context => {
3440
const { program, fixId } = context;
3541
const checker = program.getTypeChecker();
@@ -48,11 +54,15 @@ namespace ts.codefix {
4854
addFunctionDeclaration(changes, context, info);
4955
}
5056
}
57+
else if (fixId === fixMissingProperties) {
58+
if (info.kind === InfoKind.ObjectLiteral) {
59+
addObjectLiteralProperties(changes, context, info);
60+
}
61+
}
5162
else {
5263
if (info.kind === InfoKind.Enum) {
5364
addEnumMemberDeclaration(changes, checker, info);
5465
}
55-
5666
if (info.kind === InfoKind.ClassOrInterface) {
5767
const { parentDeclaration, token } = info;
5868
const infos = getOrUpdate(typeDeclToMembers, parentDeclaration, () => []);
@@ -92,8 +102,8 @@ namespace ts.codefix {
92102
},
93103
});
94104

95-
const enum InfoKind { Enum, ClassOrInterface, Function }
96-
type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo;
105+
const enum InfoKind { Enum, ClassOrInterface, Function, ObjectLiteral }
106+
type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo | ObjectLiteralInfo;
97107

98108
interface EnumInfo {
99109
readonly kind: InfoKind.Enum;
@@ -120,6 +130,13 @@ namespace ts.codefix {
120130
readonly parentDeclaration: SourceFile | ModuleDeclaration;
121131
}
122132

133+
interface ObjectLiteralInfo {
134+
readonly kind: InfoKind.ObjectLiteral;
135+
readonly token: Identifier;
136+
readonly properties: Symbol[];
137+
readonly parentDeclaration: ObjectLiteralExpression;
138+
}
139+
123140
function getInfo(sourceFile: SourceFile, tokenPos: number, checker: TypeChecker, program: Program): Info | undefined {
124141
// The identifier of the missing property. eg:
125142
// this.missing = 1;
@@ -130,6 +147,13 @@ namespace ts.codefix {
130147
}
131148

132149
const { parent } = token;
150+
if (isIdentifier(token) && hasInitializer(parent) && parent.initializer && isObjectLiteralExpression(parent.initializer)) {
151+
const properties = arrayFrom(checker.getUnmatchedProperties(checker.getTypeAtLocation(parent.initializer), checker.getTypeAtLocation(token), /* requireOptionalProperties */ false, /* matchDiscriminantProperties */ false));
152+
if (length(properties)) {
153+
return { kind: InfoKind.ObjectLiteral, token, properties, parentDeclaration: parent.initializer };
154+
}
155+
}
156+
133157
if (isIdentifier(token) && isCallExpression(parent)) {
134158
return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile };
135159
}
@@ -248,7 +272,7 @@ namespace ts.codefix {
248272
}
249273

250274
function initializePropertyToUndefined(obj: Expression, propertyName: string) {
251-
return factory.createExpressionStatement(factory.createAssignment(factory.createPropertyAccessExpression(obj, propertyName), factory.createIdentifier("undefined")));
275+
return factory.createExpressionStatement(factory.createAssignment(factory.createPropertyAccessExpression(obj, propertyName), createUndefined()));
252276
}
253277

254278
function createActionsForAddMissingMemberInTypeScriptFile(context: CodeFixContext, { parentDeclaration, declSourceFile, modifierFlags, token }: ClassOrInterfaceInfo): CodeFixAction[] | undefined {
@@ -405,4 +429,97 @@ namespace ts.codefix {
405429
const functionDeclaration = createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) as FunctionDeclaration;
406430
changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration);
407431
}
432+
433+
function addObjectLiteralProperties(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: ObjectLiteralInfo) {
434+
const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host);
435+
const quotePreference = getQuotePreference(context.sourceFile, context.preferences);
436+
const checker = context.program.getTypeChecker();
437+
const props = map(info.properties, prop => {
438+
const initializer = prop.valueDeclaration ? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
439+
return factory.createPropertyAssignment(prop.name, initializer);
440+
});
441+
changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true));
442+
}
443+
444+
function tryGetInitializerValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type): Expression {
445+
if (type.flags & TypeFlags.AnyOrUnknown) {
446+
return createUndefined();
447+
}
448+
if (type.flags & (TypeFlags.String | TypeFlags.TemplateLiteral)) {
449+
return factory.createStringLiteral("", /* isSingleQuote */ quotePreference === QuotePreference.Single);
450+
}
451+
if (type.flags & TypeFlags.Number) {
452+
return factory.createNumericLiteral(0);
453+
}
454+
if (type.flags & TypeFlags.BigInt) {
455+
return factory.createBigIntLiteral("0n");
456+
}
457+
if (type.flags & TypeFlags.Boolean) {
458+
return factory.createFalse();
459+
}
460+
if (type.flags & TypeFlags.EnumLike) {
461+
const enumMember = type.symbol.exports ? firstOrUndefined(arrayFrom(type.symbol.exports.values())) : type.symbol;
462+
const name = checker.symbolToExpression(type.symbol.parent ? type.symbol.parent : type.symbol, SymbolFlags.Value, /*enclosingDeclaration*/ undefined, /*flags*/ undefined);
463+
return enumMember === undefined || name === undefined ? factory.createNumericLiteral(0) : factory.createPropertyAccessExpression(name, checker.symbolToString(enumMember));
464+
}
465+
if (type.flags & TypeFlags.NumberLiteral) {
466+
return factory.createNumericLiteral((type as NumberLiteralType).value);
467+
}
468+
if (type.flags & TypeFlags.BigIntLiteral) {
469+
return factory.createBigIntLiteral((type as BigIntLiteralType).value);
470+
}
471+
if (type.flags & TypeFlags.StringLiteral) {
472+
return factory.createStringLiteral((type as StringLiteralType).value, /* isSingleQuote */ quotePreference === QuotePreference.Single);
473+
}
474+
if (type.flags & TypeFlags.BooleanLiteral) {
475+
return (type === checker.getFalseType() || type === checker.getFalseType(/*fresh*/ true)) ? factory.createFalse() : factory.createTrue();
476+
}
477+
if (type.flags & TypeFlags.Null) {
478+
return factory.createNull();
479+
}
480+
if (type.flags & TypeFlags.Union) {
481+
const expression = firstDefined((type as UnionType).types, t => tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, t));
482+
return expression ?? createUndefined();
483+
}
484+
if (checker.isArrayLikeType(type)) {
485+
return factory.createArrayLiteralExpression();
486+
}
487+
if (isObjectLiteralType(type)) {
488+
const props = map(checker.getPropertiesOfType(type), prop => {
489+
const initializer = prop.valueDeclaration ? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
490+
return factory.createPropertyAssignment(prop.name, initializer);
491+
});
492+
return factory.createObjectLiteralExpression(props, /*multiLine*/ true);
493+
}
494+
if (getObjectFlags(type) & ObjectFlags.Anonymous) {
495+
const decl = find(type.symbol.declarations || emptyArray, or(isFunctionTypeNode, isMethodSignature, isMethodDeclaration));
496+
if (decl === undefined) return createUndefined();
497+
498+
const signature = checker.getSignaturesOfType(type, SignatureKind.Call);
499+
if (signature === undefined) return createUndefined();
500+
501+
const func = createSignatureDeclarationFromSignature(SyntaxKind.FunctionExpression, context, quotePreference, signature[0],
502+
createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference), /*name*/ undefined, /*modifiers*/ undefined, /*optional*/ undefined, /*enclosingDeclaration*/ undefined, importAdder) as FunctionExpression | undefined;
503+
return func ?? createUndefined();
504+
}
505+
if (getObjectFlags(type) & ObjectFlags.Class) {
506+
const classDeclaration = getClassLikeDeclarationOfSymbol(type.symbol);
507+
if (classDeclaration === undefined || hasAbstractModifier(classDeclaration)) return createUndefined();
508+
509+
const constructorDeclaration = getFirstConstructorWithBody(classDeclaration);
510+
if (constructorDeclaration && length(constructorDeclaration.parameters)) return createUndefined();
511+
512+
return factory.createNewExpression(factory.createIdentifier(type.symbol.name), /*typeArguments*/ undefined, /*argumentsArray*/ undefined);
513+
}
514+
return createUndefined();
515+
}
516+
517+
function createUndefined() {
518+
return factory.createIdentifier("undefined");
519+
}
520+
521+
function isObjectLiteralType(type: Type) {
522+
return (type.flags & TypeFlags.Object) &&
523+
((getObjectFlags(type) & ObjectFlags.ObjectLiteral) || (type.symbol && tryCast(singleOrUndefined(type.symbol.declarations), isTypeLiteralNode)));
524+
}
408525
}

src/services/codefixes/helpers.ts

+22-21
Original file line numberDiff line numberDiff line change
@@ -145,27 +145,28 @@ namespace ts.codefix {
145145
}
146146

147147
function outputMethod(quotePreference: QuotePreference, signature: Signature, modifiers: NodeArray<Modifier> | undefined, name: PropertyName, body?: Block): void {
148-
const method = signatureToMethodDeclaration(context, quotePreference, signature, enclosingDeclaration, modifiers, name, optional, body, importAdder);
148+
const method = createSignatureDeclarationFromSignature(SyntaxKind.MethodDeclaration, context, quotePreference, signature, body, name, modifiers, optional, enclosingDeclaration, importAdder);
149149
if (method) addClassElement(method);
150150
}
151151
}
152152

153-
function signatureToMethodDeclaration(
153+
export function createSignatureDeclarationFromSignature(
154+
kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionExpression | SyntaxKind.ArrowFunction,
154155
context: TypeConstructionContext,
155156
quotePreference: QuotePreference,
156157
signature: Signature,
157-
enclosingDeclaration: ClassLikeDeclaration,
158-
modifiers: NodeArray<Modifier> | undefined,
159-
name: PropertyName,
160-
optional: boolean,
161158
body: Block | undefined,
162-
importAdder: ImportAdder | undefined,
163-
): MethodDeclaration | undefined {
159+
name: PropertyName | undefined,
160+
modifiers: NodeArray<Modifier> | undefined,
161+
optional: boolean | undefined,
162+
enclosingDeclaration: Node | undefined,
163+
importAdder: ImportAdder | undefined
164+
) {
164165
const program = context.program;
165166
const checker = program.getTypeChecker();
166167
const scriptTarget = getEmitScriptTarget(program.getCompilerOptions());
167168
const flags = NodeBuilderFlags.NoTruncation | NodeBuilderFlags.NoUndefinedOptionalParameterType | NodeBuilderFlags.SuppressAnyReturnType | (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : 0);
168-
const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)) as MethodDeclaration;
169+
const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, kind, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)) as ArrowFunction | FunctionExpression | MethodDeclaration;
169170
if (!signatureDeclaration) {
170171
return undefined;
171172
}
@@ -233,18 +234,18 @@ namespace ts.codefix {
233234
}
234235
}
235236

236-
return factory.updateMethodDeclaration(
237-
signatureDeclaration,
238-
/*decorators*/ undefined,
239-
modifiers,
240-
signatureDeclaration.asteriskToken,
241-
name,
242-
optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
243-
typeParameters,
244-
parameters,
245-
type,
246-
body
247-
);
237+
const questionToken = optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined;
238+
const asteriskToken = signatureDeclaration.asteriskToken;
239+
if (isFunctionExpression(signatureDeclaration)) {
240+
return factory.updateFunctionExpression(signatureDeclaration, modifiers, signatureDeclaration.asteriskToken, tryCast(name, isIdentifier), typeParameters, parameters, type, body ?? signatureDeclaration.body);
241+
}
242+
if (isArrowFunction(signatureDeclaration)) {
243+
return factory.updateArrowFunction(signatureDeclaration, modifiers, typeParameters, parameters, type, signatureDeclaration.equalsGreaterThanToken, body ?? signatureDeclaration.body);
244+
}
245+
if (isMethodDeclaration(signatureDeclaration)) {
246+
return factory.updateMethodDeclaration(signatureDeclaration, /* decorators */ undefined, modifiers, asteriskToken, name ?? factory.createIdentifier(""), questionToken, typeParameters, parameters, type, body);
247+
}
248+
return undefined;
248249
}
249250

250251
export function createSignatureDeclarationFromCallExpression(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface Foo {
4+
//// a: number;
5+
//// b: string;
6+
//// c: 1;
7+
//// d: "d";
8+
//// e: "e1" | "e2";
9+
//// f(x: number, y: number): void;
10+
//// g: (x: number, y: number) => void;
11+
//// h: number[];
12+
//// i: bigint;
13+
//// j: undefined | "special-string";
14+
//// k: `--${string}`;
15+
////}
16+
////[|const foo: Foo = {}|];
17+
18+
verify.codeFix({
19+
index: 0,
20+
description: ts.Diagnostics.Add_missing_properties.message,
21+
newRangeContent:
22+
`const foo: Foo = {
23+
a: 0,
24+
b: "",
25+
c: 1,
26+
d: "d",
27+
e: "e1",
28+
f: function(x: number, y: number): void {
29+
throw new Error("Function not implemented.");
30+
},
31+
g: function(x: number, y: number): void {
32+
throw new Error("Function not implemented.");
33+
},
34+
h: [],
35+
i: 0n,
36+
j: "special-string",
37+
k: ""
38+
}`
39+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////type T = { x: number; };
4+
////interface I {
5+
//// a: T
6+
////}
7+
////[|const foo: I = {};|]
8+
9+
verify.codeFix({
10+
index: 0,
11+
description: ts.Diagnostics.Add_missing_properties.message,
12+
newRangeContent:
13+
`const foo: I = {
14+
a: {
15+
x: 0
16+
}
17+
};`
18+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface Foo {
4+
//// a: `--${string}`;
5+
//// b: string;
6+
//// c: "a" | "b"
7+
////}
8+
////[|const foo: Foo = {}|];
9+
10+
verify.codeFix({
11+
index: 0,
12+
description: ts.Diagnostics.Add_missing_properties.message,
13+
preferences: {
14+
quotePreference: "single"
15+
},
16+
newRangeContent:
17+
`const foo: Foo = {
18+
a: '',
19+
b: '',
20+
c: 'a'
21+
}`
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface Foo {
4+
//// a: number;
5+
//// b: string;
6+
//// c: any;
7+
////}
8+
////[|class C {
9+
//// public c: Foo = {};
10+
////}|]
11+
12+
verify.codeFix({
13+
index: 0,
14+
description: ts.Diagnostics.Add_missing_properties.message,
15+
newRangeContent:
16+
`class C {
17+
public c: Foo = {
18+
a: 0,
19+
b: "",
20+
c: undefined
21+
};
22+
}`
23+
});

0 commit comments

Comments
 (0)