namespace ts . refactor . convertToOptionalChainExpression { const refactorName = "Convert to optional chain expression" ; const convertToOptionalChainExpressionMessage = getLocaleSpecificMessage ( Diagnostics . Convert_to_optional_chain_expression ) ; const toOptionalChainAction = { name : refactorName , description : convertToOptionalChainExpressionMessage , kind : "refactor.rewrite.expression.optionalChain" , } ; registerRefactor ( refactorName , { kinds : [ toOptionalChainAction . kind ] , getEditsForAction : getRefactorEditsToConvertToOptionalChain , getAvailableActions : getRefactorActionsToConvertToOptionalChain , } ) ; function getRefactorActionsToConvertToOptionalChain ( context ) { const info = getInfo ( context , context . triggerReason === "invoked" ) ; if ( ! info ) return emptyArray ; if ( ! isRefactorErrorInfo ( info ) ) { return [ { name : refactorName , description : convertToOptionalChainExpressionMessage , actions : [ toOptionalChainAction ] , } ] ; } if ( context . preferences . provideRefactorNotApplicableReason ) { return [ { name : refactorName , description : convertToOptionalChainExpressionMessage , actions : [ { ... toOptionalChainAction , notApplicableReason : info . error } ] , } ] ; } return emptyArray ; } function getRefactorEditsToConvertToOptionalChain ( context , actionName ) { const info = getInfo ( context ) ; Debug . assert ( info && ! isRefactorErrorInfo ( info ) , "Expected applicable refactor info" ) ; const edits = textChanges . ChangeTracker . with ( context , t => doChange ( context . file , context . program . getTypeChecker ( ) , t , info , actionName ) ) ; return { edits , renameFilename : undefined , renameLocation : undefined } ; } type Occurrence = PropertyAccessExpression | ElementAccessExpression | Identifier ; interface OptionalChainInfo { finalExpression : PropertyAccessExpression | ElementAccessExpression | CallExpression ; occurrences : Occurrence [ ] ; expression : ValidExpression ; } ; type ValidExpressionOrStatement = ValidExpression | ValidStatement ; /**
     * Types for which a "Convert to optional chain refactor" are offered.
     */ type ValidExpression = BinaryExpression | ConditionalExpression ; /**
     * Types of statements which are likely to include a valid expression for extraction.
     */ type ValidStatement = ExpressionStatement | ReturnStatement | VariableStatement ; function isValidExpression ( node ) { return isBinaryExpression ( node ) || isConditionalExpression ( node ) ; } function isValidStatement ( node ) { return isExpressionStatement ( node ) || isReturnStatement ( node ) || isVariableStatement ( node ) ; } function isValidExpressionOrStatement ( node ) { return isValidExpression ( node ) || isValidStatement ( node ) ; } function getInfo ( context , considerEmptySpans = true ) { const { file , program } = context ; const span = getRefactorContextSpan ( context ) ; const forEmptySpan = span . length === 0 ; if ( forEmptySpan && ! considerEmptySpans ) return undefined ; const startToken = getTokenAtPosition ( file , span . start ) ; const endToken = findTokenOnLeftOfPosition ( file , span . start + span . length ) ; const adjustedSpan = createTextSpanFromBounds ( startToken . pos , endToken && endToken . end >= startToken . pos ? endToken . getEnd ( ) : startToken . getEnd ( ) ) ; const parent = forEmptySpan ? getValidParentNodeOfEmptySpan ( startToken ) : getValidParentNodeContainingSpan ( startToken , adjustedSpan ) ; const expression = parent && isValidExpressionOrStatement ( parent ) ? getExpression ( parent ) : undefined ; if ( ! expression ) return { error : getLocaleSpecificMessage ( Diagnostics . Could_not_find_convertible_access_expression ) } ; const checker = program . getTypeChecker ( ) ; return isConditionalExpression ( expression ) ? getConditionalInfo ( expression , checker ) : getBinaryInfo ( expression ) ; } function getConditionalInfo ( expression , checker ) { const condition = expression . condition ; const finalExpression = getFinalExpressionInChain ( expression . whenTrue ) ; if ( ! finalExpression || checker . isNullableType ( checker . getTypeAtLocation ( finalExpression ) ) ) { return { error : getLocaleSpecificMessage ( Diagnostics . Could_not_find_convertible_access_expression ) } ; } if ( ( isPropertyAccessExpression ( condition ) || isIdentifier ( condition ) ) && getMatchingStart ( condition , finalExpression . expression ) ) { return { finalExpression , occurrences : [ condition ] , expression } ; } else if ( isBinaryExpression ( condition ) ) { const occurrences = getOccurrencesInExpression ( finalExpression . expression , condition ) ; return occurrences ? { finalExpression , occurrences , expression } : { error : getLocaleSpecificMessage ( Diagnostics . Could_not_find_matching_access_expressions ) } ; } } function getBinaryInfo ( expression ) { if ( expression . operatorToken . kind !== SyntaxKind . AmpersandAmpersandToken ) { return { error : getLocaleSpecificMessage ( Diagnostics . Can_only_convert_logical_AND_access_chains ) } ; } ; const finalExpression = getFinalExpressionInChain ( expression . right ) ; if ( ! finalExpression ) return { error : getLocaleSpecificMessage ( Diagnostics . Could_not_find_convertible_access_expression ) } ; const occurrences = getOccurrencesInExpression ( finalExpression . expression , expression . left ) ; return occurrences ? { finalExpression , occurrences , expression } : { error : getLocaleSpecificMessage ( Diagnostics . Could_not_find_matching_access_expressions ) } ; } /**
     * Gets a list of property accesses that appear in matchTo and occur in sequence in expression.
     */ function getOccurrencesInExpression ( matchTo , expression ) { const occurrences = [ ] ; while ( isBinaryExpression ( expression ) && expression . operatorToken . kind === SyntaxKind . AmpersandAmpersandToken ) { const match = getMatchingStart ( skipParentheses ( matchTo ) , skipParentheses ( expression . right ) ) ; if ( ! match ) { break ; } occurrences . push ( match ) ; matchTo = match ; expression = expression . left ; } const finalMatch = getMatchingStart ( matchTo , expression ) ; if ( finalMatch ) { occurrences . push ( finalMatch ) ; } return occurrences . length > 0 ? occurrences : undefined ; } /**
     * Returns subchain if chain begins with subchain syntactically.
     */ function getMatchingStart ( chain , subchain ) { if ( ! isIdentifier ( subchain ) && ! isPropertyAccessExpression ( subchain ) && ! isElementAccessExpression ( subchain ) ) { return undefined ; } return chainStartsWith ( chain , subchain ) ? subchain : undefined ; } /**
     * Returns true if chain begins with subchain syntactically.
     */ function chainStartsWith ( chain , subchain ) { while ( isCallExpression ( chain ) || isPropertyAccessExpression ( chain ) || isElementAccessExpression ( chain ) ) { if ( getTextOfChainNode ( chain ) === getTextOfChainNode ( subchain ) ) break ; chain = chain . expression ; } while ( ( isPropertyAccessExpression ( chain ) && isPropertyAccessExpression ( subchain ) ) || ( isElementAccessExpression ( chain ) && isElementAccessExpression ( subchain ) ) ) { if ( getTextOfChainNode ( chain ) !== getTextOfChainNode ( subchain ) ) return false ; chain = chain . expression ; subchain = subchain . expression ; } return isIdentifier ( chain ) && isIdentifier ( subchain ) && chain . getText ( ) === subchain . getText ( ) ; } function getTextOfChainNode ( node ) { if ( isIdentifier ( node ) || isStringOrNumericLiteralLike ( node ) ) { return node . getText ( ) ; } if ( isPropertyAccessExpression ( node ) ) { return getTextOfChainNode ( node . name ) ; } if ( isElementAccessExpression ( node ) ) { return getTextOfChainNode ( node . argumentExpression ) ; } return undefined ; } /**
     * Find the least ancestor of the input node that is a valid type for extraction and contains the input span.
     */ function getValidParentNodeContainingSpan ( node , span ) { while ( node . parent ) { if ( isValidExpressionOrStatement ( node ) && span . length !== 0 && node . end >= span . start + span . length ) { return node ; } node = node . parent ; } return undefined ; } /**
     * Finds an ancestor of the input node that is a valid type for extraction, skipping subexpressions.
     */ function getValidParentNodeOfEmptySpan ( node ) { while ( node . parent ) { if ( isValidExpressionOrStatement ( node ) && ! isValidExpressionOrStatement ( node . parent ) ) { return node ; } node = node . parent ; } return undefined ; } /**
     * Gets an expression of valid extraction type from a valid statement or expression.
     */ function getExpression ( node ) { if ( isValidExpression ( node ) ) { return node ; } if ( isVariableStatement ( node ) ) { const variable = getSingleVariableOfVariableStatement ( node ) ; const initializer = variable ?. initializer ; return initializer && isValidExpression ( initializer ) ? initializer : undefined ; } return node . expression && isValidExpression ( node . expression ) ? node . expression : undefined ; } param node function getFinalExpressionInChain ( node ) { node = skipParentheses ( node ) ; if ( isBinaryExpression ( node ) ) { return getFinalExpressionInChain ( node . left ) ; } else if ( ( isPropertyAccessExpression ( node ) || isElementAccessExpression ( node ) || isCallExpression ( node ) ) && ! isOptionalChain ( node ) ) { return node ; } return undefined ; } /**
     * Creates an access chain from toConvert with '?.' accesses at expressions appearing in occurrences.
     */ function convertOccurrences ( checker , toConvert , occurrences ) { if ( isPropertyAccessExpression ( toConvert ) || isElementAccessExpression ( toConvert ) || isCallExpression ( toConvert ) ) { const chain = convertOccurrences ( checker , toConvert . expression , occurrences ) ; const lastOccurrence = occurrences . length > 0 ? occurrences [ occurrences . length - 1 ] : undefined ; const isOccurrence = lastOccurrence ?. getText ( ) === toConvert . expression . getText ( ) ; if ( isOccurrence ) occurrences . pop ( ) ; if ( isCallExpression ( toConvert ) ) { return isOccurrence ? factory . createCallChain ( chain , factory . createToken ( SyntaxKind . QuestionDotToken ) , toConvert . typeArguments , toConvert . arguments ) : factory . createCallChain ( chain , toConvert . questionDotToken , toConvert . typeArguments , toConvert . arguments ) ; } else if ( isPropertyAccessExpression ( toConvert ) ) { return isOccurrence ? factory . createPropertyAccessChain ( chain , factory . createToken ( SyntaxKind . QuestionDotToken ) , toConvert . name ) : factory . createPropertyAccessChain ( chain , toConvert . questionDotToken , toConvert . name ) ; } else if ( isElementAccessExpression ( toConvert ) ) { return isOccurrence ? factory . createElementAccessChain ( chain , factory . createToken ( SyntaxKind . QuestionDotToken ) , toConvert . argumentExpression ) : factory . createElementAccessChain ( chain , toConvert . questionDotToken , toConvert . argumentExpression ) ; } } return toConvert ; } function doChange ( sourceFile , checker , changes , info , _actionName ) { const { finalExpression , occurrences , expression } = info ; const firstOccurrence = occurrences [ occurrences . length - 1 ] ; const convertedChain = convertOccurrences ( checker , finalExpression , occurrences ) ; if ( convertedChain && ( isPropertyAccessExpression ( convertedChain ) || isElementAccessExpression ( convertedChain ) || isCallExpression ( convertedChain ) ) ) { if ( isBinaryExpression ( expression ) ) { changes . replaceNodeRange ( sourceFile , firstOccurrence , finalExpression , convertedChain ) ; } else if ( isConditionalExpression ( expression ) ) { changes . replaceNode ( sourceFile , expression , factory . createBinaryExpression ( convertedChain , factory . createToken ( SyntaxKind . QuestionQuestionToken ) , expression . whenFalse ) ) ; } } } }