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