Skip to content

Commit

Permalink
add support of codefix for Strict Class Initialization (#21528)
Browse files Browse the repository at this point in the history
* add support of add undefined type to propertyDeclaration

* add support of add Definite Assignment Assertions to propertyDeclaration

* add support of add Initializer to propertyDeclaration

* remove useless parameter

* fix PropertyDeclaration emit missing exclamationToken

* merge fixes and fix

* fix unnecessary type assert
  • Loading branch information
Kingwl authored and Andy committed Feb 23, 2018
1 parent e8fb587 commit 30a96ba
Show file tree
Hide file tree
Showing 23 changed files with 640 additions and 6 deletions.
8 changes: 4 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15889,7 +15889,7 @@ namespace ts {

// Referencing abstract properties within their own constructors is not allowed
if ((flags & ModifierFlags.Abstract) && isThisProperty(node) && symbolHasNonMethodDeclaration(prop)) {
const declaringClassDeclaration = <ClassLikeDeclaration>getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop));
const declaringClassDeclaration = getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop));
if (declaringClassDeclaration && isNodeWithinConstructorOfClass(node, declaringClassDeclaration)) {
error(errorNode, Diagnostics.Abstract_property_0_in_class_1_cannot_be_accessed_in_the_constructor, symbolToString(prop), getTextOfIdentifierOrLiteral(declaringClassDeclaration.name));
return false;
Expand All @@ -15905,7 +15905,7 @@ namespace ts {

// Private property is accessible if the property is within the declaring class
if (flags & ModifierFlags.Private) {
const declaringClassDeclaration = <ClassLikeDeclaration>getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop));
const declaringClassDeclaration = getClassLikeDeclarationOfSymbol(getParentOfSymbol(prop));
if (!isNodeWithinClass(node, declaringClassDeclaration)) {
error(errorNode, Diagnostics.Property_0_is_private_and_only_accessible_within_class_1, symbolToString(prop), typeToString(getDeclaringClass(prop)));
return false;
Expand Down Expand Up @@ -17627,7 +17627,7 @@ namespace ts {
return true;
}

const declaringClassDeclaration = <ClassLikeDeclaration>getClassLikeDeclarationOfSymbol(declaration.parent.symbol);
const declaringClassDeclaration = getClassLikeDeclarationOfSymbol(declaration.parent.symbol);
const declaringClass = <InterfaceType>getDeclaredTypeOfSymbol(declaration.parent.symbol);

// A private or protected constructor can only be instantiated within its own class (or a subclass, for protected)
Expand Down Expand Up @@ -23115,7 +23115,7 @@ namespace ts {
if (signatures.length) {
const declaration = signatures[0].declaration;
if (declaration && hasModifier(declaration, ModifierFlags.Private)) {
const typeClassDeclaration = <ClassLikeDeclaration>getClassLikeDeclarationOfSymbol(type.symbol);
const typeClassDeclaration = getClassLikeDeclarationOfSymbol(type.symbol);
if (!isNodeWithinClass(node, typeClassDeclaration)) {
error(node, Diagnostics.Cannot_extend_a_class_0_Class_constructor_is_marked_as_private, getFullyQualifiedName(type.symbol));
}
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3965,5 +3965,17 @@
"Convert to ES6 module": {
"category": "Message",
"code": 95017
},
"Add 'undefined' type to property '{0}'": {
"category": "Message",
"code": 95018
},
"Add initializer to property '{0}'": {
"category": "Message",
"code": 95019
},
"Add definite assignment assertion to property '{0}'": {
"category": "Message",
"code": 95020
}
}
1 change: 1 addition & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,7 @@ namespace ts {
emitModifiers(node, node.modifiers);
emit(node.name);
emitIfPresent(node.questionToken);
emitIfPresent(node.exclamationToken);
emitTypeAnnotation(node.type);
emitInitializer(node.initializer);
writeSemicolon();
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3738,7 +3738,7 @@ namespace ts {
return false;
}

export function getClassLikeDeclarationOfSymbol(symbol: Symbol): Declaration | undefined {
export function getClassLikeDeclarationOfSymbol(symbol: Symbol): ClassLikeDeclaration | undefined {
return find(symbol.declarations, isClassLike);
}

Expand Down
142 changes: 142 additions & 0 deletions src/services/codefixes/fixStrictClassInitialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/* @internal */
namespace ts.codefix {
const fixIdAddDefiniteAssignmentAssertions = "addMissingPropertyDefiniteAssignmentAssertions";
const fixIdAddUndefinedType = "addMissingPropertyUndefinedType";
const fixIdAddInitializer = "addMissingPropertyInitializer";
const errorCodes = [Diagnostics.Property_0_has_no_initializer_and_is_not_definitely_assigned_in_the_constructor.code];
registerCodeFix({
errorCodes,
getCodeActions: (context) => {
const propertyDeclaration = getPropertyDeclaration(context.sourceFile, context.span.start);
if (!propertyDeclaration) return;

const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);
const result = [
getActionForAddMissingUndefinedType(context, propertyDeclaration),
getActionForAddMissingDefiniteAssignmentAssertion(context, propertyDeclaration, newLineCharacter)
];

append(result, getActionForAddMissingInitializer(context, propertyDeclaration, newLineCharacter));

return result;
},
fixIds: [fixIdAddDefiniteAssignmentAssertions, fixIdAddUndefinedType, fixIdAddInitializer],
getAllCodeActions: context => {
const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);

return codeFixAll(context, errorCodes, (changes, diag) => {
const propertyDeclaration = getPropertyDeclaration(diag.file, diag.start);
if (!propertyDeclaration) return;

switch (context.fixId) {
case fixIdAddDefiniteAssignmentAssertions:
addDefiniteAssignmentAssertion(changes, diag.file, propertyDeclaration, newLineCharacter);
break;
case fixIdAddUndefinedType:
addUndefinedType(changes, diag.file, propertyDeclaration);
break;
case fixIdAddInitializer:
const checker = context.program.getTypeChecker();
const initializer = getInitializer(checker, propertyDeclaration);
if (!initializer) return;

addInitializer(changes, diag.file, propertyDeclaration, initializer, newLineCharacter);
break;
default:
Debug.fail(JSON.stringify(context.fixId));
}
});
},
});

function getPropertyDeclaration (sourceFile: SourceFile, pos: number): PropertyDeclaration | undefined {
const token = getTokenAtPosition(sourceFile, pos, /*includeJsDocComment*/ false);
return isIdentifier(token) ? cast(token.parent, isPropertyDeclaration) : undefined;
}

function getActionForAddMissingDefiniteAssignmentAssertion (context: CodeFixContext, propertyDeclaration: PropertyDeclaration, newLineCharacter: string): CodeFixAction {
const description = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_definite_assignment_assertion_to_property_0), [propertyDeclaration.getText()]);
const changes = textChanges.ChangeTracker.with(context, t => addDefiniteAssignmentAssertion(t, context.sourceFile, propertyDeclaration, newLineCharacter));
return { description, changes, fixId: fixIdAddDefiniteAssignmentAssertions };
}

function addDefiniteAssignmentAssertion(changeTracker: textChanges.ChangeTracker, propertyDeclarationSourceFile: SourceFile, propertyDeclaration: PropertyDeclaration, newLineCharacter: string): void {
const property = updateProperty(
propertyDeclaration,
propertyDeclaration.decorators,
propertyDeclaration.modifiers,
propertyDeclaration.name,
createToken(SyntaxKind.ExclamationToken),
propertyDeclaration.type,
propertyDeclaration.initializer
);
changeTracker.replaceNode(propertyDeclarationSourceFile, propertyDeclaration, property, { suffix: newLineCharacter });
}

function getActionForAddMissingUndefinedType (context: CodeFixContext, propertyDeclaration: PropertyDeclaration): CodeFixAction {
const description = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_undefined_type_to_property_0), [propertyDeclaration.name.getText()]);
const changes = textChanges.ChangeTracker.with(context, t => addUndefinedType(t, context.sourceFile, propertyDeclaration));
return { description, changes, fixId: fixIdAddUndefinedType };
}

function addUndefinedType(changeTracker: textChanges.ChangeTracker, propertyDeclarationSourceFile: SourceFile, propertyDeclaration: PropertyDeclaration): void {
const undefinedTypeNode = createKeywordTypeNode(SyntaxKind.UndefinedKeyword);
const types = isUnionTypeNode(propertyDeclaration.type) ? propertyDeclaration.type.types.concat(undefinedTypeNode) : [propertyDeclaration.type, undefinedTypeNode];
changeTracker.replaceNode(propertyDeclarationSourceFile, propertyDeclaration.type, createUnionTypeNode(types));
}

function getActionForAddMissingInitializer (context: CodeFixContext, propertyDeclaration: PropertyDeclaration, newLineCharacter: string): CodeFixAction | undefined {
const checker = context.program.getTypeChecker();
const initializer = getInitializer(checker, propertyDeclaration);
if (!initializer) return undefined;

const description = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_initializer_to_property_0), [propertyDeclaration.name.getText()]);
const changes = textChanges.ChangeTracker.with(context, t => addInitializer(t, context.sourceFile, propertyDeclaration, initializer, newLineCharacter));
return { description, changes, fixId: fixIdAddInitializer };
}

function addInitializer (changeTracker: textChanges.ChangeTracker, propertyDeclarationSourceFile: SourceFile, propertyDeclaration: PropertyDeclaration, initializer: Expression, newLineCharacter: string): void {
const property = updateProperty(
propertyDeclaration,
propertyDeclaration.decorators,
propertyDeclaration.modifiers,
propertyDeclaration.name,
propertyDeclaration.questionToken,
propertyDeclaration.type,
initializer
);
changeTracker.replaceNode(propertyDeclarationSourceFile, propertyDeclaration, property, { suffix: newLineCharacter });
}

function getInitializer(checker: TypeChecker, propertyDeclaration: PropertyDeclaration): Expression | undefined {
return getDefaultValueFromType(checker, checker.getTypeFromTypeNode(propertyDeclaration.type));
}

function getDefaultValueFromType (checker: TypeChecker, type: Type): Expression | undefined {
if (type.flags & TypeFlags.String) {
return createLiteral("");
}
else if (type.flags & TypeFlags.Number) {
return createNumericLiteral("0");
}
else if (type.flags & TypeFlags.Boolean) {
return createFalse();
}
else if (type.flags & TypeFlags.Literal) {
return createLiteral((<LiteralType>type).value);
}
else if (type.flags & TypeFlags.Union) {
return firstDefined((<UnionType>type).types, t => getDefaultValueFromType(checker, t));
}
else if (getObjectFlags(type) & ObjectFlags.Class) {
const classDeclaration = getClassLikeDeclarationOfSymbol(type.symbol);
if (!classDeclaration || hasModifier(classDeclaration, ModifierFlags.Abstract)) return undefined;

const constructorDeclaration = find<ClassElement, ConstructorDeclaration>(classDeclaration.members, (m): m is ConstructorDeclaration => isConstructorDeclaration(m) && !!m.body)!;
if (constructorDeclaration && constructorDeclaration.parameters.length) return undefined;

return createNew(createIdentifier(type.symbol.name), /*typeArguments*/ undefined, /*argumentsArray*/ undefined);
}
return undefined;
}
}
2 changes: 1 addition & 1 deletion src/services/codefixes/fixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
/// <reference path='helpers.ts' />
/// <reference path='inferFromUsage.ts' />
/// <reference path="fixInvalidImportSyntax.ts" />
/// <reference path="fixStrictClassInitialization.ts" />
40 changes: 40 additions & 0 deletions tests/cases/fourslash/codeFixClassPropertyInitialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/// <reference path='fourslash.ts' />

// @strict: true

//// abstract class A { abstract a (); }
////
//// class TT { constructor () {} }
////
//// class AT extends A { a () {} }
////
//// class Foo {}
////
//// class T {
////
//// a: string;
////
//// static b: string;
////
//// private c: string;
////
//// d: number | undefined;
////
//// e: string | number;
////
//// f: 1;
////
//// g: "123" | "456";
////
//// h: boolean;
////
//// i: TT;
////
//// j: A;
////
//// k: AT;
////
//// l: Foo;
//// }

verify.codeFixAvailable()
15 changes: 15 additions & 0 deletions tests/cases/fourslash/codeFixClassPropertyInitialization1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />

// @strict: true

//// class T {
//// a: string;
//// }

verify.codeFix({
description: `Add 'undefined' type to property 'a'`,
newFileContent: `class T {
a: string | undefined;
}`,
index: 0
})
15 changes: 15 additions & 0 deletions tests/cases/fourslash/codeFixClassPropertyInitialization10.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />

// @strict: true

//// class T {
//// a: "a" | 2;
//// }

verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class T {
a: "a" | 2 = "a";
}`,
index: 2
})
19 changes: 19 additions & 0 deletions tests/cases/fourslash/codeFixClassPropertyInitialization11.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference path='fourslash.ts' />

// @strict: true

//// class TT { constructor () {} }
////
//// class T {
//// a: TT;
//// }

verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class TT { constructor () {} }
class T {
a: TT = new TT;
}`,
index: 2
})
23 changes: 23 additions & 0 deletions tests/cases/fourslash/codeFixClassPropertyInitialization12.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference path='fourslash.ts' />

// @strict: true

//// abstract class A { abstract a (); }
////
//// class AT extends A { a () {} }
////
//// class T {
//// a: AT;
//// }

verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `abstract class A { abstract a (); }
class AT extends A { a () {} }
class T {
a: AT = new AT;
}`,
index: 2
})
19 changes: 19 additions & 0 deletions tests/cases/fourslash/codeFixClassPropertyInitialization13.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference path='fourslash.ts' />

// @strict: true

//// class TT { }
////
//// class T {
//// a: TT;
//// }

verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class TT { }
class T {
a: TT = new TT;
}`,
index: 2
})
15 changes: 15 additions & 0 deletions tests/cases/fourslash/codeFixClassPropertyInitialization2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />

// @strict: true

//// class T {
//// a: string;
//// }

verify.codeFix({
description: `Add definite assignment assertion to property 'a: string;'`,
newFileContent: `class T {
a!: string;
}`,
index: 1
})
15 changes: 15 additions & 0 deletions tests/cases/fourslash/codeFixClassPropertyInitialization3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />

// @strict: true

//// class T {
//// a: string;
//// }

verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class T {
a: string = "";
}`,
index: 2
})
Loading

0 comments on commit 30a96ba

Please sign in to comment.