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