• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts.codefix {
3    type AcceptedDeclaration = ParameterPropertyDeclaration | PropertyDeclaration | PropertyAssignment;
4    type AcceptedNameType = Identifier | StringLiteral;
5    type ContainerDeclaration = ClassLikeDeclaration | ObjectLiteralExpression;
6
7    type Info = AccessorInfo | refactor.RefactorErrorInfo;
8    interface AccessorInfo {
9        readonly container: ContainerDeclaration;
10        readonly isStatic: boolean;
11        readonly isReadonly: boolean;
12        readonly type: TypeNode | undefined;
13        readonly declaration: AcceptedDeclaration;
14        readonly fieldName: AcceptedNameType;
15        readonly accessorName: AcceptedNameType;
16        readonly originalName: string;
17        readonly renameAccessor: boolean;
18    }
19
20    export function generateAccessorFromProperty(file: SourceFile, program: Program, start: number, end: number, context: textChanges.TextChangesContext, _actionName: string): FileTextChanges[] | undefined {
21        const fieldInfo = getAccessorConvertiblePropertyAtPosition(file, program, start, end);
22        if (!fieldInfo || refactor.isRefactorErrorInfo(fieldInfo)) return undefined;
23
24        const changeTracker = textChanges.ChangeTracker.fromContext(context);
25        const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo;
26
27        suppressLeadingAndTrailingTrivia(fieldName);
28        suppressLeadingAndTrailingTrivia(accessorName);
29        suppressLeadingAndTrailingTrivia(declaration);
30        suppressLeadingAndTrailingTrivia(container);
31
32        let accessorModifiers: ModifiersArray | undefined;
33        let fieldModifiers: ModifiersArray | undefined;
34        if (isClassLike(container)) {
35            const modifierFlags = getEffectiveModifierFlags(declaration);
36            if (isSourceFileJS(file)) {
37                const modifiers = createModifiers(modifierFlags);
38                accessorModifiers = modifiers;
39                fieldModifiers = modifiers;
40            }
41            else {
42                accessorModifiers = createModifiers(prepareModifierFlagsForAccessor(modifierFlags));
43                fieldModifiers = createModifiers(prepareModifierFlagsForField(modifierFlags));
44            }
45        }
46
47        updateFieldDeclaration(changeTracker, file, declaration, type, fieldName, fieldModifiers);
48
49        const getAccessor = generateGetAccessor(fieldName, accessorName, type, accessorModifiers, isStatic, container);
50        suppressLeadingAndTrailingTrivia(getAccessor);
51        insertAccessor(changeTracker, file, getAccessor, declaration, container);
52
53        if (isReadonly) {
54            // readonly modifier only existed in classLikeDeclaration
55            const constructor = getFirstConstructorWithBody(<ClassLikeDeclaration>container);
56            if (constructor) {
57                updateReadonlyPropertyInitializerStatementConstructor(changeTracker, file, constructor, fieldName.text, originalName);
58            }
59        }
60        else {
61            const setAccessor = generateSetAccessor(fieldName, accessorName, type, accessorModifiers, isStatic, container);
62            suppressLeadingAndTrailingTrivia(setAccessor);
63            insertAccessor(changeTracker, file, setAccessor, declaration, container);
64        }
65
66        return changeTracker.getChanges();
67    }
68
69    function isConvertibleName(name: DeclarationName): name is AcceptedNameType {
70        return isIdentifier(name) || isStringLiteral(name);
71    }
72
73    function isAcceptedDeclaration(node: Node): node is AcceptedDeclaration {
74        return isParameterPropertyDeclaration(node, node.parent) || isPropertyDeclaration(node) || isPropertyAssignment(node);
75    }
76
77    function createPropertyName(name: string, originalName: AcceptedNameType) {
78        return isIdentifier(originalName) ? factory.createIdentifier(name) : factory.createStringLiteral(name);
79    }
80
81    function createAccessorAccessExpression(fieldName: AcceptedNameType, isStatic: boolean, container: ContainerDeclaration) {
82        const leftHead = isStatic ? (<ClassLikeDeclaration>container).name! : factory.createThis(); // TODO: GH#18217
83        return isIdentifier(fieldName) ? factory.createPropertyAccessExpression(leftHead, fieldName) : factory.createElementAccessExpression(leftHead, factory.createStringLiteralFromNode(fieldName));
84    }
85
86    function createModifiers(modifierFlags: ModifierFlags): ModifiersArray | undefined {
87        return modifierFlags ? factory.createNodeArray(factory.createModifiersFromModifierFlags(modifierFlags)) : undefined;
88    }
89
90    function prepareModifierFlagsForAccessor(modifierFlags: ModifierFlags): ModifierFlags {
91        modifierFlags &= ~ModifierFlags.Readonly; // avoid Readonly modifier because it will convert to get accessor
92        modifierFlags &= ~ModifierFlags.Private;
93
94        if (!(modifierFlags & ModifierFlags.Protected)) {
95            modifierFlags |= ModifierFlags.Public;
96        }
97
98        return modifierFlags;
99    }
100
101    function prepareModifierFlagsForField(modifierFlags: ModifierFlags): ModifierFlags {
102        modifierFlags &= ~ModifierFlags.Public;
103        modifierFlags &= ~ModifierFlags.Protected;
104        modifierFlags |= ModifierFlags.Private;
105        return modifierFlags;
106    }
107
108    export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, program: Program, start: number, end: number, considerEmptySpans = true): Info | undefined {
109        const node = getTokenAtPosition(file, start);
110        const cursorRequest = start === end && considerEmptySpans;
111        const declaration = findAncestor(node.parent, isAcceptedDeclaration);
112        // make sure declaration have AccessibilityModifier or Static Modifier or Readonly Modifier
113        const meaning = ModifierFlags.AccessibilityModifier | ModifierFlags.Static | ModifierFlags.Readonly;
114
115        if (!declaration || (!(nodeOverlapsWithStartEnd(declaration.name, file, start, end) || cursorRequest))) {
116            return {
117                error: getLocaleSpecificMessage(Diagnostics.Could_not_find_property_for_which_to_generate_accessor)
118            };
119        }
120
121        if (!isConvertibleName(declaration.name)) {
122            return {
123                error: getLocaleSpecificMessage(Diagnostics.Name_is_not_valid)
124            };
125        }
126
127        if ((getEffectiveModifierFlags(declaration) | meaning) !== meaning) {
128            return {
129                error: getLocaleSpecificMessage(Diagnostics.Can_only_convert_property_with_modifier)
130            };
131        }
132
133        const name = declaration.name.text;
134        const startWithUnderscore = startsWithUnderscore(name);
135        const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name);
136        const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name);
137        return {
138            isStatic: hasStaticModifier(declaration),
139            isReadonly: hasEffectiveReadonlyModifier(declaration),
140            type: getDeclarationType(declaration, program),
141            container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
142            originalName: (<AcceptedNameType>declaration.name).text,
143            declaration,
144            fieldName,
145            accessorName,
146            renameAccessor: startWithUnderscore
147        };
148    }
149
150    function generateGetAccessor(fieldName: AcceptedNameType, accessorName: AcceptedNameType, type: TypeNode | undefined, modifiers: ModifiersArray | undefined, isStatic: boolean, container: ContainerDeclaration) {
151        return factory.createGetAccessorDeclaration(
152            /*decorators*/ undefined,
153            modifiers,
154            accessorName,
155            /*parameters*/ undefined!, // TODO: GH#18217
156            type,
157            factory.createBlock([
158                factory.createReturnStatement(
159                    createAccessorAccessExpression(fieldName, isStatic, container)
160                )
161            ], /*multiLine*/ true)
162        );
163    }
164
165    function generateSetAccessor(fieldName: AcceptedNameType, accessorName: AcceptedNameType, type: TypeNode | undefined, modifiers: ModifiersArray | undefined, isStatic: boolean, container: ContainerDeclaration) {
166        return factory.createSetAccessorDeclaration(
167            /*decorators*/ undefined,
168            modifiers,
169            accessorName,
170            [factory.createParameterDeclaration(
171                /*decorators*/ undefined,
172                /*modifiers*/ undefined,
173                /*dotDotDotToken*/ undefined,
174                factory.createIdentifier("value"),
175                /*questionToken*/ undefined,
176                type
177            )],
178            factory.createBlock([
179                factory.createExpressionStatement(
180                    factory.createAssignment(
181                        createAccessorAccessExpression(fieldName, isStatic, container),
182                        factory.createIdentifier("value")
183                    )
184                )
185            ], /*multiLine*/ true)
186        );
187    }
188
189    function updatePropertyDeclaration(changeTracker: textChanges.ChangeTracker, file: SourceFile, declaration: PropertyDeclaration, type: TypeNode | undefined, fieldName: AcceptedNameType, modifiers: ModifiersArray | undefined) {
190        const property = factory.updatePropertyDeclaration(
191            declaration,
192            declaration.decorators,
193            modifiers,
194            fieldName,
195            declaration.questionToken || declaration.exclamationToken,
196            type,
197            declaration.initializer
198        );
199        changeTracker.replaceNode(file, declaration, property);
200    }
201
202    function updatePropertyAssignmentDeclaration(changeTracker: textChanges.ChangeTracker, file: SourceFile, declaration: PropertyAssignment, fieldName: AcceptedNameType) {
203        const assignment = factory.updatePropertyAssignment(declaration, fieldName, declaration.initializer);
204        changeTracker.replacePropertyAssignment(file, declaration, assignment);
205    }
206
207    function updateFieldDeclaration(changeTracker: textChanges.ChangeTracker, file: SourceFile, declaration: AcceptedDeclaration, type: TypeNode | undefined, fieldName: AcceptedNameType, modifiers: ModifiersArray | undefined) {
208        if (isPropertyDeclaration(declaration)) {
209            updatePropertyDeclaration(changeTracker, file, declaration, type, fieldName, modifiers);
210        }
211        else if (isPropertyAssignment(declaration)) {
212            updatePropertyAssignmentDeclaration(changeTracker, file, declaration, fieldName);
213        }
214        else {
215            changeTracker.replaceNode(file, declaration,
216                factory.updateParameterDeclaration(declaration, declaration.decorators, modifiers, declaration.dotDotDotToken, cast(fieldName, isIdentifier), declaration.questionToken, declaration.type, declaration.initializer));
217        }
218    }
219
220    function insertAccessor(changeTracker: textChanges.ChangeTracker, file: SourceFile, accessor: AccessorDeclaration, declaration: AcceptedDeclaration, container: ContainerDeclaration) {
221        isParameterPropertyDeclaration(declaration, declaration.parent) ? changeTracker.insertNodeAtClassStart(file, <ClassLikeDeclaration>container, accessor) :
222            isPropertyAssignment(declaration) ? changeTracker.insertNodeAfterComma(file, declaration, accessor) :
223            changeTracker.insertNodeAfter(file, declaration, accessor);
224    }
225
226    function updateReadonlyPropertyInitializerStatementConstructor(changeTracker: textChanges.ChangeTracker, file: SourceFile, constructor: ConstructorDeclaration, fieldName: string, originalName: string) {
227        if (!constructor.body) return;
228        constructor.body.forEachChild(function recur(node) {
229            if (isElementAccessExpression(node) &&
230                node.expression.kind === SyntaxKind.ThisKeyword &&
231                isStringLiteral(node.argumentExpression) &&
232                node.argumentExpression.text === originalName &&
233                isWriteAccess(node)) {
234                changeTracker.replaceNode(file, node.argumentExpression, factory.createStringLiteral(fieldName));
235            }
236            if (isPropertyAccessExpression(node) && node.expression.kind === SyntaxKind.ThisKeyword && node.name.text === originalName && isWriteAccess(node)) {
237                changeTracker.replaceNode(file, node.name, factory.createIdentifier(fieldName));
238            }
239            if (!isFunctionLike(node) && !isClassLike(node)) {
240                node.forEachChild(recur);
241            }
242        });
243    }
244
245    function getDeclarationType(declaration: AcceptedDeclaration, program: Program): TypeNode | undefined {
246        const typeNode = getTypeAnnotationNode(declaration);
247        if (isPropertyDeclaration(declaration) && typeNode && declaration.questionToken) {
248            const typeChecker = program.getTypeChecker();
249            const type = typeChecker.getTypeFromTypeNode(typeNode);
250            if (!typeChecker.isTypeAssignableTo(typeChecker.getUndefinedType(), type)) {
251                const types = isUnionTypeNode(typeNode) ? typeNode.types : [typeNode];
252                return factory.createUnionTypeNode([...types, factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword)]);
253            }
254        }
255        return typeNode;
256    }
257
258    export function getAllSupers(decl: ClassOrInterface | undefined, checker: TypeChecker): readonly ClassOrInterface[] {
259        const res: ClassLikeDeclaration[] = [];
260        while (decl) {
261            const superElement = getClassExtendsHeritageElement(decl);
262            const superSymbol = superElement && checker.getSymbolAtLocation(superElement.expression);
263            if (!superSymbol) break;
264            const symbol = superSymbol.flags & SymbolFlags.Alias ? checker.getAliasedSymbol(superSymbol) : superSymbol;
265            const superDecl = find(symbol.declarations, isClassLike);
266            if (!superDecl) break;
267            res.push(superDecl);
268            decl = superDecl;
269        }
270        return res;
271    }
272
273    export type ClassOrInterface = ClassLikeDeclaration | InterfaceDeclaration;
274}
275