add support of codefix for Strict Class Initialization (#21528)

* 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
This commit is contained in:
Wenlu Wang
2018-02-23 23:25:50 +08:00
committed by Andy
parent e8fb587097
commit 30a96ba335
23 changed files with 640 additions and 6 deletions

View File

@@ -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;
@@ -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;
@@ -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)
@@ -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));
}

View File

@@ -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
}
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -17,4 +17,4 @@
/// <reference path='helpers.ts' />
/// <reference path='inferFromUsage.ts' />
/// <reference path="fixInvalidImportSyntax.ts" />
/// <reference path="fixStrictClassInitialization.ts" />

View File

@@ -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()

View File

@@ -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
})

View File

@@ -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
})

View File

@@ -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
})

View File

@@ -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
})

View File

@@ -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
})

View File

@@ -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
})

View File

@@ -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
})

View File

@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @strict: true
//// class T {
//// a: number;
//// }
verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class T {
a: number = 0;
}`,
index: 2
})

View File

@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @strict: true
//// class T {
//// a: boolean;
//// }
verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class T {
a: boolean = false;
}`,
index: 2
})

View File

@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @strict: true
//// class T {
//// a: "1";
//// }
verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class T {
a: "1" = "1";
}`,
index: 2
})

View File

@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @strict: true
//// class T {
//// a: 2;
//// }
verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class T {
a: 2 = 2;
}`,
index: 2
})

View File

@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @strict: true
//// class T {
//// a: string | number;
//// }
verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class T {
a: string | number = "";
}`,
index: 2
})

View File

@@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @strict: true
//// class T {
//// a: 1 | 2;
//// }
verify.codeFix({
description: `Add initializer to property 'a'`,
newFileContent: `class T {
a: 1 | 2 = 1;
}`,
index: 2
})

View File

@@ -0,0 +1,76 @@
/// <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.codeFixAll({
fixId: 'addMissingPropertyDefiniteAssignmentAssertions',
newFileContent: `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;
}`
});

View File

@@ -0,0 +1,76 @@
/// <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.codeFixAll({
fixId: 'addMissingPropertyUndefinedType',
newFileContent: `abstract class A { abstract a (); }
class TT { constructor () {} }
class AT extends A { a () {} }
class Foo {}
class T {
a: string | undefined;
static b: string;
private c: string | undefined;
d: number | undefined;
e: string | number | undefined;
f: 1 | undefined;
g: "123" | "456" | undefined;
h: boolean | undefined;
i: TT | undefined;
j: A | undefined;
k: AT | undefined;
l: Foo | undefined;
}`
});

View File

@@ -0,0 +1,76 @@
/// <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.codeFixAll({
fixId: 'addMissingPropertyInitializer',
newFileContent: `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 = 1;
g: "123" | "456" = "123";
h: boolean = false;
i: TT = new TT;
j: A;
k: AT = new AT;
l: Foo = new Foo;
}`
});