Skip to content

Commit 63991bb

Browse files
committed
feat(16755): show QF to declare missing properties in a call expression with an object literal argument
1 parent a7a0d25 commit 63991bb

File tree

5 files changed

+113
-31
lines changed

5 files changed

+113
-31
lines changed

src/services/codefixes/fixAddMissingMember.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ namespace ts.codefix {
1010
Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2.code,
1111
Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2.code,
1212
Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more.code,
13+
Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code,
1314
Diagnostics.Cannot_find_name_0.code
1415
];
1516

1617
registerCodeFix({
1718
errorCodes,
1819
getCodeActions(context) {
1920
const typeChecker = context.program.getTypeChecker();
20-
const info = getInfo(context.sourceFile, context.span.start, typeChecker, context.program);
21+
const info = getInfo(context.sourceFile, context.span.start, context.errorCode, typeChecker, context.program);
2122
if (!info) {
2223
return undefined;
2324
}
@@ -44,7 +45,7 @@ namespace ts.codefix {
4445

4546
return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => {
4647
eachDiagnostic(context, errorCodes, diag => {
47-
const info = getInfo(diag.file, diag.start, checker, context.program);
48+
const info = getInfo(diag.file, diag.start, diag.code, checker, context.program);
4849
if (!info || !addToSeen(seen, getNodeId(info.parentDeclaration) + "#" + info.token.text)) {
4950
return;
5051
}
@@ -135,38 +136,48 @@ namespace ts.codefix {
135136
readonly token: Identifier;
136137
readonly properties: Symbol[];
137138
readonly parentDeclaration: ObjectLiteralExpression;
139+
readonly indentation?: number;
138140
}
139141

140-
function getInfo(sourceFile: SourceFile, tokenPos: number, checker: TypeChecker, program: Program): Info | undefined {
142+
function getInfo(sourceFile: SourceFile, tokenPos: number, errorCode: number, checker: TypeChecker, program: Program): Info | undefined {
141143
// The identifier of the missing property. eg:
142144
// this.missing = 1;
143145
// ^^^^^^^
144146
const token = getTokenAtPosition(sourceFile, tokenPos);
145-
if (!isIdentifier(token) && !isPrivateIdentifier(token)) {
146-
return undefined;
147+
const parent = token.parent;
148+
149+
if (errorCode === Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code) {
150+
if (!(token.kind === SyntaxKind.OpenBraceToken && isObjectLiteralExpression(parent) && isCallExpression(parent.parent))) return undefined;
151+
152+
const argIndex = findIndex(parent.parent.arguments, arg => arg === parent);
153+
if (argIndex < 0) return undefined;
154+
155+
const signature = singleOrUndefined(checker.getSignaturesOfType(checker.getTypeAtLocation(parent.parent.expression), SignatureKind.Call));
156+
if (!(signature && signature.declaration && signature.parameters[argIndex])) return undefined;
157+
158+
const param = signature.parameters[argIndex].valueDeclaration;
159+
if (!(param && isParameter(param) && isIdentifier(param.name))) return undefined;
160+
161+
const properties = arrayFrom(checker.getUnmatchedProperties(checker.getTypeAtLocation(parent), checker.getTypeAtLocation(param), /* requireOptionalProperties */ false, /* matchDiscriminantProperties */ false));
162+
return length(properties) ? { kind: InfoKind.ObjectLiteral, token: param.name, properties, indentation: 0, parentDeclaration: parent } : undefined;;
147163
}
148164

149-
const { parent } = token;
165+
if (!isMemberName(token)) return undefined;
166+
150167
if (isIdentifier(token) && hasInitializer(parent) && parent.initializer && isObjectLiteralExpression(parent.initializer)) {
151168
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-
}
169+
return length(properties) ? { kind: InfoKind.ObjectLiteral, token, properties, indentation: undefined, parentDeclaration: parent.initializer } : undefined;
155170
}
156171

157172
if (isIdentifier(token) && isCallExpression(parent)) {
158173
return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile };
159174
}
160175

161-
if (!isPropertyAccessExpression(parent)) {
162-
return undefined;
163-
}
176+
if (!isPropertyAccessExpression(parent)) return undefined;
164177

165178
const leftExpressionType = skipConstraint(checker.getTypeAtLocation(parent.expression));
166-
const { symbol } = leftExpressionType;
167-
if (!symbol || !symbol.declarations) {
168-
return undefined;
169-
}
179+
const symbol = leftExpressionType.symbol;
180+
if (!symbol || !symbol.declarations) return undefined;
170181

171182
if (isIdentifier(token) && isCallExpression(parent.parent)) {
172183
const moduleDeclaration = find(symbol.declarations, isModuleDeclaration);
@@ -176,9 +187,7 @@ namespace ts.codefix {
176187
}
177188

178189
const moduleSourceFile = find(symbol.declarations, isSourceFile);
179-
if (sourceFile.commonJsModuleIndicator) {
180-
return;
181-
}
190+
if (sourceFile.commonJsModuleIndicator) return undefined;
182191

183192
if (moduleSourceFile && !program.isSourceFileFromExternalLibrary(moduleSourceFile)) {
184193
return { kind: InfoKind.Function, token, call: parent.parent, sourceFile: moduleSourceFile, modifierFlags: ModifierFlags.Export, parentDeclaration: moduleSourceFile };
@@ -187,17 +196,13 @@ namespace ts.codefix {
187196

188197
const classDeclaration = find(symbol.declarations, isClassLike);
189198
// Don't suggest adding private identifiers to anything other than a class.
190-
if (!classDeclaration && isPrivateIdentifier(token)) {
191-
return undefined;
192-
}
199+
if (!classDeclaration && isPrivateIdentifier(token)) return undefined;
193200

194201
// Prefer to change the class instead of the interface if they are merged
195202
const classOrInterface = classDeclaration || find(symbol.declarations, isInterfaceDeclaration);
196203
if (classOrInterface && !program.isSourceFileFromExternalLibrary(classOrInterface.getSourceFile())) {
197204
const makeStatic = ((leftExpressionType as TypeReference).target || leftExpressionType) !== checker.getDeclaredTypeOfSymbol(symbol);
198-
if (makeStatic && (isPrivateIdentifier(token) || isInterfaceDeclaration(classOrInterface))) {
199-
return undefined;
200-
}
205+
if (makeStatic && (isPrivateIdentifier(token) || isInterfaceDeclaration(classOrInterface))) return undefined;
201206

202207
const declSourceFile = classOrInterface.getSourceFile();
203208
const modifierFlags = (makeStatic ? ModifierFlags.Static : 0) | (startsWithUnderscore(token.text) ? ModifierFlags.Private : 0);
@@ -438,7 +443,12 @@ namespace ts.codefix {
438443
const initializer = prop.valueDeclaration ? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
439444
return factory.createPropertyAssignment(prop.name, initializer);
440445
});
441-
changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true));
446+
const options = {
447+
leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude,
448+
trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude,
449+
indentation: info.indentation
450+
};
451+
changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true), options);
442452
}
443453

444454
function tryGetInitializerValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type): Expression {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface Foo {
4+
//// a: number;
5+
//// b: number;
6+
////}
7+
////function f(foo: Foo) {}
8+
////[|f({})|];
9+
10+
verify.codeFix({
11+
index: 0,
12+
description: ts.Diagnostics.Add_missing_properties.message,
13+
newRangeContent:
14+
`f({
15+
a: 0,
16+
b: 0
17+
})`
18+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface Foo {
4+
//// a: number;
5+
//// b: number;
6+
//// c: () => void;
7+
////}
8+
////function f(foo: Foo) {}
9+
////[|f({ a: 10 })|];
10+
11+
verify.codeFix({
12+
index: 0,
13+
description: ts.Diagnostics.Add_missing_properties.message,
14+
newRangeContent:
15+
`f({
16+
a: 10,
17+
b: 0,
18+
c: function(): void {
19+
throw new Error("Function not implemented.");
20+
}
21+
})`
22+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface Foo {
4+
//// a: number;
5+
//// b: number;
6+
////}
7+
////function f(a: number, b: number, c: Foo) {}
8+
////[|f(1, 2, {})|];
9+
10+
verify.codeFix({
11+
index: 0,
12+
description: ts.Diagnostics.Add_missing_properties.message,
13+
newRangeContent:
14+
`f(1, 2, {
15+
a: 0,
16+
b: 0
17+
})`
18+
});

tests/cases/fourslash/codeFixAddMissingProperties_all.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
////class C {
1919
//// public c: I1 = {};
2020
////}
21-
////function fn(foo: I2 = {}) {
22-
////}
21+
////function fn1(foo: I2 = {}) {}
22+
////function fn2(a: I1) {}
23+
////fn2({});
2324

2425
verify.codeFixAll({
2526
fixId: "fixMissingProperties",
@@ -70,9 +71,22 @@ class C {
7071
}
7172
};
7273
}
73-
function fn(foo: I2 = {
74+
function fn1(foo: I2 = {
7475
a: undefined,
7576
b: undefined
76-
}) {
77-
}`
77+
}) {}
78+
function fn2(a: I1) {}
79+
fn2({
80+
a: 0,
81+
b: "",
82+
c: 1,
83+
d: "d",
84+
e: "e1",
85+
f: function(x: number, y: number): void {
86+
throw new Error("Function not implemented.");
87+
},
88+
g: function(x: number, y: number): void {
89+
throw new Error("Function not implemented.");
90+
}
91+
});`
7892
});

0 commit comments

Comments
 (0)