Skip to content

Commit df32709

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

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
@@ -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,21 +150,34 @@ 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+
return length(properties) ? { kind: InfoKind.ObjectLiteral, token: param.name, properties, indentation: 0, parentDeclaration: parent } : undefined;;
158174
}
159175

160-
const { parent } = token;
176+
if (!isMemberName(token)) return undefined;
177+
161178
if (isIdentifier(token) && hasInitializer(parent) && parent.initializer && isObjectLiteralExpression(parent.initializer)) {
162179
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-
}
180+
return length(properties) ? { kind: InfoKind.ObjectLiteral, token, properties, indentation: undefined, parentDeclaration: parent.initializer } : undefined;
166181
}
167182

168183
if (isIdentifier(token) && isJsxOpeningLikeElement(token.parent)) {
@@ -176,15 +191,11 @@ namespace ts.codefix {
176191
return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile };
177192
}
178193

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

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

189200
if (isIdentifier(token) && isCallExpression(parent.parent)) {
190201
const moduleDeclaration = find(symbol.declarations, isModuleDeclaration);
@@ -194,9 +205,7 @@ namespace ts.codefix {
194205
}
195206

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

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

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

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

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

481491
function tryGetValueFromType(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)