Skip to content

Commit

Permalink
feat(28491): add QF to declare missing properties (#44576)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk authored Jun 17, 2021
1 parent 9549928 commit eee34d5
Show file tree
Hide file tree
Showing 17 changed files with 488 additions and 26 deletions.
1 change: 1 addition & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ namespace ts {
getDiagnostics,
getGlobalDiagnostics,
getRecursionIdentity,
getUnmatchedProperties,
getTypeOfSymbolAtLocation: (symbol, locationIn) => {
const location = getParseTreeNode(locationIn);
return location ? getTypeOfSymbolAtLocation(symbol, location) : errorType;
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -7048,6 +7048,14 @@
"category": "Message",
"code": 95164
},
"Add missing properties": {
"category": "Message",
"code": 95165
},
"Add all missing properties": {
"category": "Message",
"code": 95166
},

"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
"category": "Error",
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4275,6 +4275,7 @@ namespace ts {
/* @internal */ getInstantiationCount(): number;
/* @internal */ getRelationCacheSizes(): { assignable: number, identity: number, subtype: number, strictSubtype: number };
/* @internal */ getRecursionIdentity(type: Type): object | undefined;
/* @internal */ getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator<Symbol>;

/* @internal */ isArrayType(type: Type): boolean;
/* @internal */ isTupleType(type: Type): boolean;
Expand Down
127 changes: 122 additions & 5 deletions src/services/codefixes/fixAddMissingMember.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* @internal */
namespace ts.codefix {
const fixMissingMember = "fixMissingMember";
const fixMissingProperties = "fixMissingProperties";
const fixMissingFunctionDeclaration = "fixMissingFunctionDeclaration";

const errorCodes = [
Diagnostics.Property_0_does_not_exist_on_type_1.code,
Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2.code,
Expand All @@ -19,6 +21,10 @@ namespace ts.codefix {
if (!info) {
return undefined;
}
if (info.kind === InfoKind.ObjectLiteral) {
const changes = textChanges.ChangeTracker.with(context, t => addObjectLiteralProperties(t, context, info));
return [createCodeFixAction(fixMissingProperties, changes, Diagnostics.Add_missing_properties, fixMissingProperties, Diagnostics.Add_all_missing_properties)];
}
if (info.kind === InfoKind.Function) {
const changes = textChanges.ChangeTracker.with(context, t => addFunctionDeclaration(t, context, info));
return [createCodeFixAction(fixMissingFunctionDeclaration, changes, [Diagnostics.Add_missing_function_declaration_0, info.token.text], fixMissingFunctionDeclaration, Diagnostics.Add_all_missing_function_declarations)];
Expand All @@ -29,7 +35,7 @@ namespace ts.codefix {
}
return concatenate(getActionsForMissingMethodDeclaration(context, info), getActionsForMissingMemberDeclaration(context, info));
},
fixIds: [fixMissingMember, fixMissingFunctionDeclaration],
fixIds: [fixMissingMember, fixMissingFunctionDeclaration, fixMissingProperties],
getAllCodeActions: context => {
const { program, fixId } = context;
const checker = program.getTypeChecker();
Expand All @@ -48,11 +54,15 @@ namespace ts.codefix {
addFunctionDeclaration(changes, context, info);
}
}
else if (fixId === fixMissingProperties) {
if (info.kind === InfoKind.ObjectLiteral) {
addObjectLiteralProperties(changes, context, info);
}
}
else {
if (info.kind === InfoKind.Enum) {
addEnumMemberDeclaration(changes, checker, info);
}

if (info.kind === InfoKind.ClassOrInterface) {
const { parentDeclaration, token } = info;
const infos = getOrUpdate(typeDeclToMembers, parentDeclaration, () => []);
Expand Down Expand Up @@ -92,8 +102,8 @@ namespace ts.codefix {
},
});

const enum InfoKind { Enum, ClassOrInterface, Function }
type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo;
const enum InfoKind { Enum, ClassOrInterface, Function, ObjectLiteral }
type Info = EnumInfo | ClassOrInterfaceInfo | FunctionInfo | ObjectLiteralInfo;

interface EnumInfo {
readonly kind: InfoKind.Enum;
Expand All @@ -120,6 +130,13 @@ namespace ts.codefix {
readonly parentDeclaration: SourceFile | ModuleDeclaration;
}

interface ObjectLiteralInfo {
readonly kind: InfoKind.ObjectLiteral;
readonly token: Identifier;
readonly properties: Symbol[];
readonly parentDeclaration: ObjectLiteralExpression;
}

function getInfo(sourceFile: SourceFile, tokenPos: number, checker: TypeChecker, program: Program): Info | undefined {
// The identifier of the missing property. eg:
// this.missing = 1;
Expand All @@ -130,6 +147,13 @@ namespace ts.codefix {
}

const { parent } = token;
if (isIdentifier(token) && hasInitializer(parent) && parent.initializer && isObjectLiteralExpression(parent.initializer)) {
const properties = arrayFrom(checker.getUnmatchedProperties(checker.getTypeAtLocation(parent.initializer), checker.getTypeAtLocation(token), /* requireOptionalProperties */ false, /* matchDiscriminantProperties */ false));
if (length(properties)) {
return { kind: InfoKind.ObjectLiteral, token, properties, parentDeclaration: parent.initializer };
}
}

if (isIdentifier(token) && isCallExpression(parent)) {
return { kind: InfoKind.Function, token, call: parent, sourceFile, modifierFlags: ModifierFlags.None, parentDeclaration: sourceFile };
}
Expand Down Expand Up @@ -248,7 +272,7 @@ namespace ts.codefix {
}

function initializePropertyToUndefined(obj: Expression, propertyName: string) {
return factory.createExpressionStatement(factory.createAssignment(factory.createPropertyAccessExpression(obj, propertyName), factory.createIdentifier("undefined")));
return factory.createExpressionStatement(factory.createAssignment(factory.createPropertyAccessExpression(obj, propertyName), createUndefined()));
}

function createActionsForAddMissingMemberInTypeScriptFile(context: CodeFixContext, { parentDeclaration, declSourceFile, modifierFlags, token }: ClassOrInterfaceInfo): CodeFixAction[] | undefined {
Expand Down Expand Up @@ -405,4 +429,97 @@ namespace ts.codefix {
const functionDeclaration = createSignatureDeclarationFromCallExpression(SyntaxKind.FunctionDeclaration, context, importAdder, info.call, idText(info.token), info.modifierFlags, info.parentDeclaration) as FunctionDeclaration;
changes.insertNodeAtEndOfScope(info.sourceFile, info.parentDeclaration, functionDeclaration);
}

function addObjectLiteralProperties(changes: textChanges.ChangeTracker, context: CodeFixContextBase, info: ObjectLiteralInfo) {
const importAdder = createImportAdder(context.sourceFile, context.program, context.preferences, context.host);
const quotePreference = getQuotePreference(context.sourceFile, context.preferences);
const checker = context.program.getTypeChecker();
const props = map(info.properties, prop => {
const initializer = prop.valueDeclaration ? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
return factory.createPropertyAssignment(prop.name, initializer);
});
changes.replaceNode(context.sourceFile, info.parentDeclaration, factory.createObjectLiteralExpression([...info.parentDeclaration.properties, ...props], /*multiLine*/ true));
}

function tryGetInitializerValueFromType(context: CodeFixContextBase, checker: TypeChecker, importAdder: ImportAdder, quotePreference: QuotePreference, type: Type): Expression {
if (type.flags & TypeFlags.AnyOrUnknown) {
return createUndefined();
}
if (type.flags & (TypeFlags.String | TypeFlags.TemplateLiteral)) {
return factory.createStringLiteral("", /* isSingleQuote */ quotePreference === QuotePreference.Single);
}
if (type.flags & TypeFlags.Number) {
return factory.createNumericLiteral(0);
}
if (type.flags & TypeFlags.BigInt) {
return factory.createBigIntLiteral("0n");
}
if (type.flags & TypeFlags.Boolean) {
return factory.createFalse();
}
if (type.flags & TypeFlags.EnumLike) {
const enumMember = type.symbol.exports ? firstOrUndefined(arrayFrom(type.symbol.exports.values())) : type.symbol;
const name = checker.symbolToExpression(type.symbol.parent ? type.symbol.parent : type.symbol, SymbolFlags.Value, /*enclosingDeclaration*/ undefined, /*flags*/ undefined);
return enumMember === undefined || name === undefined ? factory.createNumericLiteral(0) : factory.createPropertyAccessExpression(name, checker.symbolToString(enumMember));
}
if (type.flags & TypeFlags.NumberLiteral) {
return factory.createNumericLiteral((type as NumberLiteralType).value);
}
if (type.flags & TypeFlags.BigIntLiteral) {
return factory.createBigIntLiteral((type as BigIntLiteralType).value);
}
if (type.flags & TypeFlags.StringLiteral) {
return factory.createStringLiteral((type as StringLiteralType).value, /* isSingleQuote */ quotePreference === QuotePreference.Single);
}
if (type.flags & TypeFlags.BooleanLiteral) {
return (type === checker.getFalseType() || type === checker.getFalseType(/*fresh*/ true)) ? factory.createFalse() : factory.createTrue();
}
if (type.flags & TypeFlags.Null) {
return factory.createNull();
}
if (type.flags & TypeFlags.Union) {
const expression = firstDefined((type as UnionType).types, t => tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, t));
return expression ?? createUndefined();
}
if (checker.isArrayLikeType(type)) {
return factory.createArrayLiteralExpression();
}
if (isObjectLiteralType(type)) {
const props = map(checker.getPropertiesOfType(type), prop => {
const initializer = prop.valueDeclaration ? tryGetInitializerValueFromType(context, checker, importAdder, quotePreference, checker.getTypeAtLocation(prop.valueDeclaration)) : createUndefined();
return factory.createPropertyAssignment(prop.name, initializer);
});
return factory.createObjectLiteralExpression(props, /*multiLine*/ true);
}
if (getObjectFlags(type) & ObjectFlags.Anonymous) {
const decl = find(type.symbol.declarations || emptyArray, or(isFunctionTypeNode, isMethodSignature, isMethodDeclaration));
if (decl === undefined) return createUndefined();

const signature = checker.getSignaturesOfType(type, SignatureKind.Call);
if (signature === undefined) return createUndefined();

const func = createSignatureDeclarationFromSignature(SyntaxKind.FunctionExpression, context, quotePreference, signature[0],
createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference), /*name*/ undefined, /*modifiers*/ undefined, /*optional*/ undefined, /*enclosingDeclaration*/ undefined, importAdder) as FunctionExpression | undefined;
return func ?? createUndefined();
}
if (getObjectFlags(type) & ObjectFlags.Class) {
const classDeclaration = getClassLikeDeclarationOfSymbol(type.symbol);
if (classDeclaration === undefined || hasAbstractModifier(classDeclaration)) return createUndefined();

const constructorDeclaration = getFirstConstructorWithBody(classDeclaration);
if (constructorDeclaration && length(constructorDeclaration.parameters)) return createUndefined();

return factory.createNewExpression(factory.createIdentifier(type.symbol.name), /*typeArguments*/ undefined, /*argumentsArray*/ undefined);
}
return createUndefined();
}

function createUndefined() {
return factory.createIdentifier("undefined");
}

function isObjectLiteralType(type: Type) {
return (type.flags & TypeFlags.Object) &&
((getObjectFlags(type) & ObjectFlags.ObjectLiteral) || (type.symbol && tryCast(singleOrUndefined(type.symbol.declarations), isTypeLiteralNode)));
}
}
43 changes: 22 additions & 21 deletions src/services/codefixes/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,27 +145,28 @@ namespace ts.codefix {
}

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

function signatureToMethodDeclaration(
export function createSignatureDeclarationFromSignature(
kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionExpression | SyntaxKind.ArrowFunction,
context: TypeConstructionContext,
quotePreference: QuotePreference,
signature: Signature,
enclosingDeclaration: ClassLikeDeclaration,
modifiers: NodeArray<Modifier> | undefined,
name: PropertyName,
optional: boolean,
body: Block | undefined,
importAdder: ImportAdder | undefined,
): MethodDeclaration | undefined {
name: PropertyName | undefined,
modifiers: NodeArray<Modifier> | undefined,
optional: boolean | undefined,
enclosingDeclaration: Node | undefined,
importAdder: ImportAdder | undefined
) {
const program = context.program;
const checker = program.getTypeChecker();
const scriptTarget = getEmitScriptTarget(program.getCompilerOptions());
const flags = NodeBuilderFlags.NoTruncation | NodeBuilderFlags.NoUndefinedOptionalParameterType | NodeBuilderFlags.SuppressAnyReturnType | (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : 0);
const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)) as MethodDeclaration;
const signatureDeclaration = checker.signatureToSignatureDeclaration(signature, kind, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context)) as ArrowFunction | FunctionExpression | MethodDeclaration;
if (!signatureDeclaration) {
return undefined;
}
Expand Down Expand Up @@ -233,18 +234,18 @@ namespace ts.codefix {
}
}

return factory.updateMethodDeclaration(
signatureDeclaration,
/*decorators*/ undefined,
modifiers,
signatureDeclaration.asteriskToken,
name,
optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined,
typeParameters,
parameters,
type,
body
);
const questionToken = optional ? factory.createToken(SyntaxKind.QuestionToken) : undefined;
const asteriskToken = signatureDeclaration.asteriskToken;
if (isFunctionExpression(signatureDeclaration)) {
return factory.updateFunctionExpression(signatureDeclaration, modifiers, signatureDeclaration.asteriskToken, tryCast(name, isIdentifier), typeParameters, parameters, type, body ?? signatureDeclaration.body);
}
if (isArrowFunction(signatureDeclaration)) {
return factory.updateArrowFunction(signatureDeclaration, modifiers, typeParameters, parameters, type, signatureDeclaration.equalsGreaterThanToken, body ?? signatureDeclaration.body);
}
if (isMethodDeclaration(signatureDeclaration)) {
return factory.updateMethodDeclaration(signatureDeclaration, /* decorators */ undefined, modifiers, asteriskToken, name ?? factory.createIdentifier(""), questionToken, typeParameters, parameters, type, body);
}
return undefined;
}

export function createSignatureDeclarationFromCallExpression(
Expand Down
39 changes: 39 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingProperties1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// <reference path='fourslash.ts' />

////interface Foo {
//// a: number;
//// b: string;
//// c: 1;
//// d: "d";
//// e: "e1" | "e2";
//// f(x: number, y: number): void;
//// g: (x: number, y: number) => void;
//// h: number[];
//// i: bigint;
//// j: undefined | "special-string";
//// k: `--${string}`;
////}
////[|const foo: Foo = {}|];

verify.codeFix({
index: 0,
description: ts.Diagnostics.Add_missing_properties.message,
newRangeContent:
`const foo: Foo = {
a: 0,
b: "",
c: 1,
d: "d",
e: "e1",
f: function(x: number, y: number): void {
throw new Error("Function not implemented.");
},
g: function(x: number, y: number): void {
throw new Error("Function not implemented.");
},
h: [],
i: 0n,
j: "special-string",
k: ""
}`
});
18 changes: 18 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingProperties10.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />

////type T = { x: number; };
////interface I {
//// a: T
////}
////[|const foo: I = {};|]

verify.codeFix({
index: 0,
description: ts.Diagnostics.Add_missing_properties.message,
newRangeContent:
`const foo: I = {
a: {
x: 0
}
};`
});
22 changes: 22 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingProperties11.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference path='fourslash.ts' />

////interface Foo {
//// a: `--${string}`;
//// b: string;
//// c: "a" | "b"
////}
////[|const foo: Foo = {}|];

verify.codeFix({
index: 0,
description: ts.Diagnostics.Add_missing_properties.message,
preferences: {
quotePreference: "single"
},
newRangeContent:
`const foo: Foo = {
a: '',
b: '',
c: 'a'
}`
});
23 changes: 23 additions & 0 deletions tests/cases/fourslash/codeFixAddMissingProperties2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference path='fourslash.ts' />

////interface Foo {
//// a: number;
//// b: string;
//// c: any;
////}
////[|class C {
//// public c: Foo = {};
////}|]

verify.codeFix({
index: 0,
description: ts.Diagnostics.Add_missing_properties.message,
newRangeContent:
`class C {
public c: Foo = {
a: 0,
b: "",
c: undefined
};
}`
});
Loading

0 comments on commit eee34d5

Please sign in to comment.