Skip to content

Commit 30a96ba

Browse files
KingwlAndy
authored and
Andy
committed
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
1 parent e8fb587 commit 30a96ba

23 files changed

+640
-6
lines changed

src/compiler/checker.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -15889,7 +15889,7 @@ namespace ts {
1588915889

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

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

17630-
const declaringClassDeclaration = <ClassLikeDeclaration>getClassLikeDeclarationOfSymbol(declaration.parent.symbol);
17630+
const declaringClassDeclaration = getClassLikeDeclarationOfSymbol(declaration.parent.symbol);
1763117631
const declaringClass = <InterfaceType>getDeclaredTypeOfSymbol(declaration.parent.symbol);
1763217632

1763317633
// A private or protected constructor can only be instantiated within its own class (or a subclass, for protected)
@@ -23115,7 +23115,7 @@ namespace ts {
2311523115
if (signatures.length) {
2311623116
const declaration = signatures[0].declaration;
2311723117
if (declaration && hasModifier(declaration, ModifierFlags.Private)) {
23118-
const typeClassDeclaration = <ClassLikeDeclaration>getClassLikeDeclarationOfSymbol(type.symbol);
23118+
const typeClassDeclaration = getClassLikeDeclarationOfSymbol(type.symbol);
2311923119
if (!isNodeWithinClass(node, typeClassDeclaration)) {
2312023120
error(node, Diagnostics.Cannot_extend_a_class_0_Class_constructor_is_marked_as_private, getFullyQualifiedName(type.symbol));
2312123121
}

src/compiler/diagnosticMessages.json

+12
Original file line numberDiff line numberDiff line change
@@ -3965,5 +3965,17 @@
39653965
"Convert to ES6 module": {
39663966
"category": "Message",
39673967
"code": 95017
3968+
},
3969+
"Add 'undefined' type to property '{0}'": {
3970+
"category": "Message",
3971+
"code": 95018
3972+
},
3973+
"Add initializer to property '{0}'": {
3974+
"category": "Message",
3975+
"code": 95019
3976+
},
3977+
"Add definite assignment assertion to property '{0}'": {
3978+
"category": "Message",
3979+
"code": 95020
39683980
}
39693981
}

src/compiler/emitter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,7 @@ namespace ts {
10241024
emitModifiers(node, node.modifiers);
10251025
emit(node.name);
10261026
emitIfPresent(node.questionToken);
1027+
emitIfPresent(node.exclamationToken);
10271028
emitTypeAnnotation(node.type);
10281029
emitInitializer(node.initializer);
10291030
writeSemicolon();

src/compiler/utilities.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3738,7 +3738,7 @@ namespace ts {
37383738
return false;
37393739
}
37403740

3741-
export function getClassLikeDeclarationOfSymbol(symbol: Symbol): Declaration | undefined {
3741+
export function getClassLikeDeclarationOfSymbol(symbol: Symbol): ClassLikeDeclaration | undefined {
37423742
return find(symbol.declarations, isClassLike);
37433743
}
37443744

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
const fixIdAddDefiniteAssignmentAssertions = "addMissingPropertyDefiniteAssignmentAssertions";
4+
const fixIdAddUndefinedType = "addMissingPropertyUndefinedType";
5+
const fixIdAddInitializer = "addMissingPropertyInitializer";
6+
const errorCodes = [Diagnostics.Property_0_has_no_initializer_and_is_not_definitely_assigned_in_the_constructor.code];
7+
registerCodeFix({
8+
errorCodes,
9+
getCodeActions: (context) => {
10+
const propertyDeclaration = getPropertyDeclaration(context.sourceFile, context.span.start);
11+
if (!propertyDeclaration) return;
12+
13+
const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);
14+
const result = [
15+
getActionForAddMissingUndefinedType(context, propertyDeclaration),
16+
getActionForAddMissingDefiniteAssignmentAssertion(context, propertyDeclaration, newLineCharacter)
17+
];
18+
19+
append(result, getActionForAddMissingInitializer(context, propertyDeclaration, newLineCharacter));
20+
21+
return result;
22+
},
23+
fixIds: [fixIdAddDefiniteAssignmentAssertions, fixIdAddUndefinedType, fixIdAddInitializer],
24+
getAllCodeActions: context => {
25+
const newLineCharacter = getNewLineOrDefaultFromHost(context.host, context.formatContext.options);
26+
27+
return codeFixAll(context, errorCodes, (changes, diag) => {
28+
const propertyDeclaration = getPropertyDeclaration(diag.file, diag.start);
29+
if (!propertyDeclaration) return;
30+
31+
switch (context.fixId) {
32+
case fixIdAddDefiniteAssignmentAssertions:
33+
addDefiniteAssignmentAssertion(changes, diag.file, propertyDeclaration, newLineCharacter);
34+
break;
35+
case fixIdAddUndefinedType:
36+
addUndefinedType(changes, diag.file, propertyDeclaration);
37+
break;
38+
case fixIdAddInitializer:
39+
const checker = context.program.getTypeChecker();
40+
const initializer = getInitializer(checker, propertyDeclaration);
41+
if (!initializer) return;
42+
43+
addInitializer(changes, diag.file, propertyDeclaration, initializer, newLineCharacter);
44+
break;
45+
default:
46+
Debug.fail(JSON.stringify(context.fixId));
47+
}
48+
});
49+
},
50+
});
51+
52+
function getPropertyDeclaration (sourceFile: SourceFile, pos: number): PropertyDeclaration | undefined {
53+
const token = getTokenAtPosition(sourceFile, pos, /*includeJsDocComment*/ false);
54+
return isIdentifier(token) ? cast(token.parent, isPropertyDeclaration) : undefined;
55+
}
56+
57+
function getActionForAddMissingDefiniteAssignmentAssertion (context: CodeFixContext, propertyDeclaration: PropertyDeclaration, newLineCharacter: string): CodeFixAction {
58+
const description = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_definite_assignment_assertion_to_property_0), [propertyDeclaration.getText()]);
59+
const changes = textChanges.ChangeTracker.with(context, t => addDefiniteAssignmentAssertion(t, context.sourceFile, propertyDeclaration, newLineCharacter));
60+
return { description, changes, fixId: fixIdAddDefiniteAssignmentAssertions };
61+
}
62+
63+
function addDefiniteAssignmentAssertion(changeTracker: textChanges.ChangeTracker, propertyDeclarationSourceFile: SourceFile, propertyDeclaration: PropertyDeclaration, newLineCharacter: string): void {
64+
const property = updateProperty(
65+
propertyDeclaration,
66+
propertyDeclaration.decorators,
67+
propertyDeclaration.modifiers,
68+
propertyDeclaration.name,
69+
createToken(SyntaxKind.ExclamationToken),
70+
propertyDeclaration.type,
71+
propertyDeclaration.initializer
72+
);
73+
changeTracker.replaceNode(propertyDeclarationSourceFile, propertyDeclaration, property, { suffix: newLineCharacter });
74+
}
75+
76+
function getActionForAddMissingUndefinedType (context: CodeFixContext, propertyDeclaration: PropertyDeclaration): CodeFixAction {
77+
const description = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_undefined_type_to_property_0), [propertyDeclaration.name.getText()]);
78+
const changes = textChanges.ChangeTracker.with(context, t => addUndefinedType(t, context.sourceFile, propertyDeclaration));
79+
return { description, changes, fixId: fixIdAddUndefinedType };
80+
}
81+
82+
function addUndefinedType(changeTracker: textChanges.ChangeTracker, propertyDeclarationSourceFile: SourceFile, propertyDeclaration: PropertyDeclaration): void {
83+
const undefinedTypeNode = createKeywordTypeNode(SyntaxKind.UndefinedKeyword);
84+
const types = isUnionTypeNode(propertyDeclaration.type) ? propertyDeclaration.type.types.concat(undefinedTypeNode) : [propertyDeclaration.type, undefinedTypeNode];
85+
changeTracker.replaceNode(propertyDeclarationSourceFile, propertyDeclaration.type, createUnionTypeNode(types));
86+
}
87+
88+
function getActionForAddMissingInitializer (context: CodeFixContext, propertyDeclaration: PropertyDeclaration, newLineCharacter: string): CodeFixAction | undefined {
89+
const checker = context.program.getTypeChecker();
90+
const initializer = getInitializer(checker, propertyDeclaration);
91+
if (!initializer) return undefined;
92+
93+
const description = formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_initializer_to_property_0), [propertyDeclaration.name.getText()]);
94+
const changes = textChanges.ChangeTracker.with(context, t => addInitializer(t, context.sourceFile, propertyDeclaration, initializer, newLineCharacter));
95+
return { description, changes, fixId: fixIdAddInitializer };
96+
}
97+
98+
function addInitializer (changeTracker: textChanges.ChangeTracker, propertyDeclarationSourceFile: SourceFile, propertyDeclaration: PropertyDeclaration, initializer: Expression, newLineCharacter: string): void {
99+
const property = updateProperty(
100+
propertyDeclaration,
101+
propertyDeclaration.decorators,
102+
propertyDeclaration.modifiers,
103+
propertyDeclaration.name,
104+
propertyDeclaration.questionToken,
105+
propertyDeclaration.type,
106+
initializer
107+
);
108+
changeTracker.replaceNode(propertyDeclarationSourceFile, propertyDeclaration, property, { suffix: newLineCharacter });
109+
}
110+
111+
function getInitializer(checker: TypeChecker, propertyDeclaration: PropertyDeclaration): Expression | undefined {
112+
return getDefaultValueFromType(checker, checker.getTypeFromTypeNode(propertyDeclaration.type));
113+
}
114+
115+
function getDefaultValueFromType (checker: TypeChecker, type: Type): Expression | undefined {
116+
if (type.flags & TypeFlags.String) {
117+
return createLiteral("");
118+
}
119+
else if (type.flags & TypeFlags.Number) {
120+
return createNumericLiteral("0");
121+
}
122+
else if (type.flags & TypeFlags.Boolean) {
123+
return createFalse();
124+
}
125+
else if (type.flags & TypeFlags.Literal) {
126+
return createLiteral((<LiteralType>type).value);
127+
}
128+
else if (type.flags & TypeFlags.Union) {
129+
return firstDefined((<UnionType>type).types, t => getDefaultValueFromType(checker, t));
130+
}
131+
else if (getObjectFlags(type) & ObjectFlags.Class) {
132+
const classDeclaration = getClassLikeDeclarationOfSymbol(type.symbol);
133+
if (!classDeclaration || hasModifier(classDeclaration, ModifierFlags.Abstract)) return undefined;
134+
135+
const constructorDeclaration = find<ClassElement, ConstructorDeclaration>(classDeclaration.members, (m): m is ConstructorDeclaration => isConstructorDeclaration(m) && !!m.body)!;
136+
if (constructorDeclaration && constructorDeclaration.parameters.length) return undefined;
137+
138+
return createNew(createIdentifier(type.symbol.name), /*typeArguments*/ undefined, /*argumentsArray*/ undefined);
139+
}
140+
return undefined;
141+
}
142+
}

src/services/codefixes/fixes.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@
1717
/// <reference path='helpers.ts' />
1818
/// <reference path='inferFromUsage.ts' />
1919
/// <reference path="fixInvalidImportSyntax.ts" />
20-
20+
/// <reference path="fixStrictClassInitialization.ts" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @strict: true
4+
5+
//// abstract class A { abstract a (); }
6+
////
7+
//// class TT { constructor () {} }
8+
////
9+
//// class AT extends A { a () {} }
10+
////
11+
//// class Foo {}
12+
////
13+
//// class T {
14+
////
15+
//// a: string;
16+
////
17+
//// static b: string;
18+
////
19+
//// private c: string;
20+
////
21+
//// d: number | undefined;
22+
////
23+
//// e: string | number;
24+
////
25+
//// f: 1;
26+
////
27+
//// g: "123" | "456";
28+
////
29+
//// h: boolean;
30+
////
31+
//// i: TT;
32+
////
33+
//// j: A;
34+
////
35+
//// k: AT;
36+
////
37+
//// l: Foo;
38+
//// }
39+
40+
verify.codeFixAvailable()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @strict: true
4+
5+
//// class T {
6+
//// a: string;
7+
//// }
8+
9+
verify.codeFix({
10+
description: `Add 'undefined' type to property 'a'`,
11+
newFileContent: `class T {
12+
a: string | undefined;
13+
}`,
14+
index: 0
15+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @strict: true
4+
5+
//// class T {
6+
//// a: "a" | 2;
7+
//// }
8+
9+
verify.codeFix({
10+
description: `Add initializer to property 'a'`,
11+
newFileContent: `class T {
12+
a: "a" | 2 = "a";
13+
}`,
14+
index: 2
15+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @strict: true
4+
5+
//// class TT { constructor () {} }
6+
////
7+
//// class T {
8+
//// a: TT;
9+
//// }
10+
11+
verify.codeFix({
12+
description: `Add initializer to property 'a'`,
13+
newFileContent: `class TT { constructor () {} }
14+
15+
class T {
16+
a: TT = new TT;
17+
}`,
18+
index: 2
19+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @strict: true
4+
5+
//// abstract class A { abstract a (); }
6+
////
7+
//// class AT extends A { a () {} }
8+
////
9+
//// class T {
10+
//// a: AT;
11+
//// }
12+
13+
verify.codeFix({
14+
description: `Add initializer to property 'a'`,
15+
newFileContent: `abstract class A { abstract a (); }
16+
17+
class AT extends A { a () {} }
18+
19+
class T {
20+
a: AT = new AT;
21+
}`,
22+
index: 2
23+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @strict: true
4+
5+
//// class TT { }
6+
////
7+
//// class T {
8+
//// a: TT;
9+
//// }
10+
11+
verify.codeFix({
12+
description: `Add initializer to property 'a'`,
13+
newFileContent: `class TT { }
14+
15+
class T {
16+
a: TT = new TT;
17+
}`,
18+
index: 2
19+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @strict: true
4+
5+
//// class T {
6+
//// a: string;
7+
//// }
8+
9+
verify.codeFix({
10+
description: `Add definite assignment assertion to property 'a: string;'`,
11+
newFileContent: `class T {
12+
a!: string;
13+
}`,
14+
index: 1
15+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @strict: true
4+
5+
//// class T {
6+
//// a: string;
7+
//// }
8+
9+
verify.codeFix({
10+
description: `Add initializer to property 'a'`,
11+
newFileContent: `class T {
12+
a: string = "";
13+
}`,
14+
index: 2
15+
})

0 commit comments

Comments
 (0)