Skip to content

Commit 1294517

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

14 files changed

+396
-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

+110-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,85 @@ 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
439+
? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration))
440+
: undefined;
441+
return factory.createPropertyAssignment(prop.name, initializer ?? createUndefined());
442+
});
443+
changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true));
444+
}
445+
446+
function tryGetInitializerValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type): Expression | undefined {
447+
if (type.flags & TypeFlags.AnyOrUnknown) {
448+
return createUndefined();
449+
}
450+
if (type.flags & TypeFlags.String) {
451+
return factory.createStringLiteral("", /* isSingleQuote */ quotePreference === QuotePreference.Single);
452+
}
453+
if (type.flags & TypeFlags.Number) {
454+
return factory.createNumericLiteral(0);
455+
}
456+
if (type.flags & TypeFlags.BigInt) {
457+
return factory.createBigIntLiteral("0n");
458+
}
459+
if (type.flags & TypeFlags.Boolean) {
460+
return factory.createFalse();
461+
}
462+
if (type.flags & TypeFlags.EnumLike) {
463+
const enumMember = type.symbol.exports ? firstOrUndefined(arrayFrom(type.symbol.exports.values())) : type.symbol;
464+
const name = checker.symbolToExpression(type.symbol.parent ? type.symbol.parent : type.symbol, SymbolFlags.Value, /*enclosingDeclaration*/ undefined, /*flags*/ undefined);
465+
return enumMember === undefined || name === undefined ? factory.createNumericLiteral(0) : factory.createPropertyAccessExpression(name, checker.symbolToString(enumMember));
466+
}
467+
if (type.flags & TypeFlags.NumberLiteral) {
468+
return factory.createNumericLiteral((type as NumberLiteralType).value);
469+
}
470+
if (type.flags & TypeFlags.BigIntLiteral) {
471+
return factory.createBigIntLiteral((type as BigIntLiteralType).value);
472+
}
473+
if (type.flags & TypeFlags.StringLiteral) {
474+
return factory.createStringLiteral((type as StringLiteralType).value, /* isSingleQuote */ quotePreference === QuotePreference.Single);
475+
}
476+
if (type.flags & TypeFlags.BooleanLiteral) {
477+
return (type === checker.getFalseType() || type === checker.getFalseType(/*fresh*/ true)) ? factory.createFalse() : factory.createTrue();
478+
}
479+
if (type.flags & TypeFlags.Null) {
480+
return factory.createNull();
481+
}
482+
if (type.flags & TypeFlags.Union) {
483+
return firstDefined((type as UnionType).types, t => tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, t));
484+
}
485+
if (checker.isArrayLikeType(type)) {
486+
return factory.createArrayLiteralExpression();
487+
}
488+
if (getObjectFlags(type) & ObjectFlags.Anonymous) {
489+
const decl = find(type.symbol.declarations || emptyArray, or(isFunctionTypeNode, isMethodSignature, isMethodDeclaration));
490+
if (decl === undefined) return createUndefined();
491+
492+
const signature = checker.getSignaturesOfType(type, SignatureKind.Call);
493+
if (signature === undefined) return createUndefined();
494+
495+
return createSignatureDeclarationFromSignature(SyntaxKind.FunctionExpression, context, quotePreference, signature[0],
496+
createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference), /*name*/ undefined, /*modifiers*/ undefined, /*optional*/ undefined, /*enclosingDeclaration*/ undefined, importAdder) as FunctionExpression | undefined;
497+
}
498+
if (getObjectFlags(type) & ObjectFlags.Class) {
499+
const classDeclaration = getClassLikeDeclarationOfSymbol(type.symbol);
500+
if (classDeclaration === undefined || hasAbstractModifier(classDeclaration)) return createUndefined();
501+
502+
const constructorDeclaration = getFirstConstructorWithBody(classDeclaration);
503+
if (constructorDeclaration && length(constructorDeclaration.parameters)) return createUndefined();
504+
505+
return factory.createNewExpression(factory.createIdentifier(type.symbol.name), /*typeArguments*/ undefined, /*argumentsArray*/ undefined);
506+
}
507+
return createUndefined();
508+
}
509+
510+
function createUndefined() {
511+
return factory.createIdentifier("undefined");
512+
}
408513
}

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,33 @@
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+
////}
13+
////[|const foo: Foo = {}|];
14+
15+
verify.codeFix({
16+
index: 0,
17+
description: ts.Diagnostics.Add_missing_properties.message,
18+
newRangeContent:
19+
`const foo: Foo = {
20+
a: 0,
21+
b: "",
22+
c: 1,
23+
d: "d",
24+
e: "e1",
25+
f: function(x: number, y: number): void {
26+
throw new Error("Function not implemented.");
27+
},
28+
g: function(x: number, y: number): void {
29+
throw new Error("Function not implemented.");
30+
},
31+
h: []
32+
}`
33+
});
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+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface Foo {
4+
//// a: number;
5+
//// b: string;
6+
////}
7+
////[|function fn(foo: Foo = {}) {
8+
////}|]
9+
10+
verify.codeFix({
11+
index: 0,
12+
description: ts.Diagnostics.Add_missing_properties.message,
13+
newRangeContent:
14+
`function fn(foo: Foo = {
15+
a: 0,
16+
b: ""
17+
}) {
18+
}`
19+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface Foo {
4+
//// a: number;
5+
//// b: string;
6+
////}
7+
////[|const foo: Foo = { a: 10 }|];
8+
9+
verify.codeFix({
10+
index: 0,
11+
description: ts.Diagnostics.Add_missing_properties.message,
12+
newRangeContent:
13+
`const foo: Foo = {
14+
a: 10,
15+
b: ""
16+
}`
17+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////type T = {
4+
//// a: null;
5+
////}
6+
////
7+
////[|const foo: T = {}|];
8+
9+
verify.codeFix({
10+
index: 0,
11+
description: ts.Diagnostics.Add_missing_properties.message,
12+
newRangeContent:
13+
`const foo: T = {
14+
a: null
15+
}`
16+
});

0 commit comments

Comments
 (0)