Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(28491): add QF to declare missing properties #44576

Merged
merged 1 commit into from
Jun 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -6496,6 +6496,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 @@ -4272,6 +4272,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();
a-tarasyuk marked this conversation as resolved.
Show resolved Hide resolved
}
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();
a-tarasyuk marked this conversation as resolved.
Show resolved Hide resolved
}

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