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