Skip to content

Commit 69b5b2b

Browse files
authored
feat(16755): show QF to declare missing properties in a call expression with an object literal argument (microsoft#44781)
1 parent 84b0578 commit 69b5b2b

5 files changed

+117
-34
lines changed

src/services/codefixes/fixAddMissingMember.ts

+40-29
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ namespace ts.codefix {
1111
Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2.code,
1212
Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2.code,
1313
Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more.code,
14+
Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code,
1415
Diagnostics.Cannot_find_name_0.code
1516
];
1617

1718
registerCodeFix({
1819
errorCodes,
1920
getCodeActions(context) {
2021
const typeChecker = context.program.getTypeChecker();
21-
const info = getInfo(context.sourceFile, context.span.start, typeChecker, context.program);
22+
const info = getInfo(context.sourceFile, context.span.start, context.errorCode, typeChecker, context.program);
2223
if (!info) {
2324
return undefined;
2425
}
@@ -49,7 +50,7 @@ namespace ts.codefix {
4950

5051
return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => {
5152
eachDiagnostic(context, errorCodes, diag => {
52-
const info = getInfo(diag.file, diag.start, checker, context.program);
53+
const info = getInfo(diag.file, diag.start, diag.code, checker, context.program);
5354
if (!info || !addToSeen(seen, getNodeId(info.parentDeclaration) + "#" + info.token.text)) {
5455
return;
5556
}
@@ -139,6 +140,7 @@ namespace ts.codefix {
139140
readonly token: Identifier;
140141
readonly properties: Symbol[];
141142
readonly parentDeclaration: ObjectLiteralExpression;
143+
readonly indentation?: number;
142144
}
143145

144146
interface JsxAttributesInfo {
@@ -148,43 +150,53 @@ namespace ts.codefix {
148150
readonly parentDeclaration: JsxOpeningLikeElement;
149151
}
150152

151-
function getInfo(sourceFile: SourceFile, tokenPos: number, checker: TypeChecker, program: Program): Info | undefined {
153+
function getInfo(sourceFile: SourceFile, tokenPos: number, errorCode: number, checker: TypeChecker, program: Program): Info | undefined {
152154
// The identifier of the missing property. eg:
153155
// this.missing = 1;
154156
// ^^^^^^^
155157
const token = getTokenAtPosition(sourceFile, tokenPos);
156-
if (!isIdentifier(token) && !isPrivateIdentifier(token)) {
157-
return undefined;
158+
const parent = token.parent;
159+
160+
if (errorCode === Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code) {
161+
if (!(token.kind === SyntaxKind.OpenBraceToken && isObjectLiteralExpression(parent) && isCallExpression(parent.parent))) return undefined;
162+
163+
const argIndex = findIndex(parent.parent.arguments, arg => arg === parent);
164+
if (argIndex < 0) return undefined;
165+
166+
const signature = singleOrUndefined(checker.getSignaturesOfType(checker.getTypeAtLocation(parent.parent.expression), SignatureKind.Call));
167+
if (!(signature && signature.declaration && signature.parameters[argIndex])) return undefined;
168+
169+
const param = signature.parameters[argIndex].valueDeclaration;
170+
if (!(param && isParameter(param) && isIdentifier(param.name))) return undefined;
171+
172+
const properties = arrayFrom(checker.getUnmatchedProperties(checker.getTypeAtLocation(parent), checker.getTypeAtLocation(param), /* requireOptionalProperties */ false, /* matchDiscriminantProperties */ false));
173+
if (!length(properties)) return undefined;
174+
return { kind: InfoKind.ObjectLiteral, token: param.name, properties, indentation: 0, parentDeclaration: parent };
158175
}
159176

160-
const { parent } = token;
177+
if (!isMemberName(token)) return undefined;
178+
161179
if (isIdentifier(token) && hasInitializer(parent) && parent.initializer && isObjectLiteralExpression(parent.initializer)) {
162180
const properties = arrayFrom(checker.getUnmatchedProperties(checker.getTypeAtLocation(parent.initializer), checker.getTypeAtLocation(token), /* requireOptionalProperties */ false, /* matchDiscriminantProperties */ false));
163-
if (length(properties)) {
164-
return { kind: InfoKind.ObjectLiteral, token, properties, parentDeclaration: parent.initializer };
165-
}
181+
if (!length(properties)) return undefined;
182+
return { kind: InfoKind.ObjectLiteral, token, properties, indentation: undefined, parentDeclaration: parent.initializer };
166183
}
167184

168185
if (isIdentifier(token) && isJsxOpeningLikeElement(token.parent)) {
169186
const attributes = getUnmatchedAttributes(checker, token.parent);
170-
if (length(attributes)) {
171-
return { kind: InfoKind.JsxAttributes, token, attributes, parentDeclaration: token.parent };
172-
}
187+
if (!length(attributes)) return undefined;
188+
return { kind: InfoKind.JsxAttributes, token, attributes, parentDeclaration: token.parent };
173189
}
174190

175191
if (isIdentifier(token) && isCallExpression(parent)) {
176192
return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile };
177193
}
178194

179-
if (!isPropertyAccessExpression(parent)) {
180-
return undefined;
181-
}
195+
if (!isPropertyAccessExpression(parent)) return undefined;
182196

183197
const leftExpressionType = skipConstraint(checker.getTypeAtLocation(parent.expression));
184-
const { symbol } = leftExpressionType;
185-
if (!symbol || !symbol.declarations) {
186-
return undefined;
187-
}
198+
const symbol = leftExpressionType.symbol;
199+
if (!symbol || !symbol.declarations) return undefined;
188200

189201
if (isIdentifier(token) && isCallExpression(parent.parent)) {
190202
const moduleDeclaration = find(symbol.declarations, isModuleDeclaration);
@@ -194,9 +206,7 @@ namespace ts.codefix {
194206
}
195207

196208
const moduleSourceFile = find(symbol.declarations, isSourceFile);
197-
if (sourceFile.commonJsModuleIndicator) {
198-
return;
199-
}
209+
if (sourceFile.commonJsModuleIndicator) return undefined;
200210

201211
if (moduleSourceFile && !isSourceFileFromLibrary(program, moduleSourceFile)) {
202212
return { kind: InfoKind.Function, token, call: parent.parent, sourceFile: moduleSourceFile, modifierFlags: ModifierFlags.Export, parentDeclaration: moduleSourceFile };
@@ -205,17 +215,13 @@ namespace ts.codefix {
205215

206216
const classDeclaration = find(symbol.declarations, isClassLike);
207217
// Don't suggest adding private identifiers to anything other than a class.
208-
if (!classDeclaration && isPrivateIdentifier(token)) {
209-
return undefined;
210-
}
218+
if (!classDeclaration && isPrivateIdentifier(token)) return undefined;
211219

212220
// Prefer to change the class instead of the interface if they are merged
213221
const classOrInterface = classDeclaration || find(symbol.declarations, isInterfaceDeclaration);
214222
if (classOrInterface && !isSourceFileFromLibrary(program, classOrInterface.getSourceFile())) {
215223
const makeStatic = ((leftExpressionType as TypeReference).target || leftExpressionType) !== checker.getDeclaredTypeOfSymbol(symbol);
216-
if (makeStatic && (isPrivateIdentifier(token) || isInterfaceDeclaration(classOrInterface))) {
217-
return undefined;
218-
}
224+
if (makeStatic && (isPrivateIdentifier(token) || isInterfaceDeclaration(classOrInterface))) return undefined;
219225

220226
const declSourceFile = classOrInterface.getSourceFile();
221227
const modifierFlags = (makeStatic ? ModifierFlags.Static : 0) | (startsWithUnderscore(token.text) ? ModifierFlags.Private : 0);
@@ -475,7 +481,12 @@ namespace ts.codefix {
475481
const initializer = prop.valueDeclaration ? tryGetValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
476482
return factory.createPropertyAssignment(prop.name, initializer);
477483
});
478-
changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true));
484+
const options = {
485+
leadingTriviaOption: textChanges.LeadingTriviaOption.Exclude,
486+
trailingTriviaOption: textChanges.TrailingTriviaOption.Exclude,
487+
indentation: info.indentation
488+
};
489+
changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true), options);
479490
}
480491

481492
function tryGetValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type): Expression {
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+
});
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+
});
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

+19-5
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)