diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 2ad1bfb8eea50..9e07924dce005 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -5827,6 +5827,22 @@ "category": "Message", "code": 95138 }, + "Convert to optional chain expression": { + "category": "Message", + "code": 95139 + }, + "Could not find convertible access expression": { + "category": "Message", + "code": 95140 + }, + "Could not find matching access expressions": { + "category": "Message", + "code": 95141 + }, + "Can only convert logical AND access chains": { + "category": "Message", + "code": 95142 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index a6978df31b5df..59ec995b16edc 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2463,7 +2463,7 @@ namespace ts { } } - function getSingleVariableOfVariableStatement(node: Node): VariableDeclaration | undefined { + export function getSingleVariableOfVariableStatement(node: Node): VariableDeclaration | undefined { return isVariableStatement(node) ? firstOrUndefined(node.declarationList.declarations) : undefined; } diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 08ed8709fb0e9..4abeb59239535 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3252,9 +3252,9 @@ namespace FourSlash { } } - public applyRefactor({ refactorName, actionName, actionDescription, newContent: newContentWithRenameMarker }: FourSlashInterface.ApplyRefactorOptions) { + public applyRefactor({ refactorName, actionName, actionDescription, newContent: newContentWithRenameMarker, triggerReason }: FourSlashInterface.ApplyRefactorOptions) { const range = this.getSelection(); - const refactors = this.getApplicableRefactorsAtSelection(); + const refactors = this.getApplicableRefactorsAtSelection(triggerReason); const refactorsWithName = refactors.filter(r => r.name === refactorName); if (refactorsWithName.length === 0) { this.raiseError(`The expected refactor: ${refactorName} is not available at the marker location.\nAvailable refactors: ${refactors.map(r => r.name)}`); diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 876a5bc37b02b..918d92ca8bbb0 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -1483,6 +1483,7 @@ namespace FourSlashInterface { actionName: string; actionDescription: string; newContent: NewFileContent; + triggerReason?: ts.RefactorTriggerReason; } export type ExpectedCompletionEntry = string | ExpectedCompletionEntryObject; diff --git a/src/services/refactors/convertToOptionalChainExpression.ts b/src/services/refactors/convertToOptionalChainExpression.ts new file mode 100644 index 0000000000000..0333506b1e3f2 --- /dev/null +++ b/src/services/refactors/convertToOptionalChainExpression.ts @@ -0,0 +1,284 @@ +/* @internal */ +namespace ts.refactor.convertToOptionalChainExpression { + const refactorName = "Convert to optional chain expression"; + const convertToOptionalChainExpressionMessage = getLocaleSpecificMessage(Diagnostics.Convert_to_optional_chain_expression); + + registerRefactor(refactorName, { getAvailableActions, getEditsForAction }); + + function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { + const info = getInfo(context, context.triggerReason === "invoked"); + if (!info) return emptyArray; + + if (!info.error) { + return [{ + name: refactorName, + description: convertToOptionalChainExpressionMessage, + actions: [{ + name: refactorName, + description: convertToOptionalChainExpressionMessage + }] + }]; + } + + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: refactorName, + description: convertToOptionalChainExpressionMessage, + actions: [{ + name: refactorName, + description: convertToOptionalChainExpressionMessage, + notApplicableReason: info.error + }] + }]; + } + return emptyArray; + } + + function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { + const info = getInfo(context); + if (!info || !info.info) return undefined; + const edits = textChanges.ChangeTracker.with(context, t => + doChange(context.file, context.program.getTypeChecker(), t, Debug.checkDefined(info.info, "context must have info"), actionName) + ); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } + + type InfoOrError = { + info: Info, + error?: never; + } | { + info?: never, + error: string; + }; + + interface Info { + finalExpression: PropertyAccessExpression | CallExpression, + occurrences: (PropertyAccessExpression | Identifier)[], + 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: Node): node is ValidExpression { + return isBinaryExpression(node) || isConditionalExpression(node); + } + + function isValidStatement(node: Node): node is ValidStatement { + return isExpressionStatement(node) || isReturnStatement(node) || isVariableStatement(node); + } + + function isValidExpressionOrStatement(node: Node): node is ValidExpressionOrStatement { + return isValidExpression(node) || isValidStatement(node); + } + + function getInfo(context: RefactorContext, considerEmptySpans = true): InfoOrError | undefined { + const { file, program } = context; + const span = getRefactorContextSpan(context); + + const forEmptySpan = span.length === 0; + if (forEmptySpan && !considerEmptySpans) return undefined; + + // selecting fo[|o && foo.ba|]r should be valid, so adjust span to fit start and end tokens + 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: ConditionalExpression, checker: TypeChecker): InfoOrError | undefined { + 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 { info: { finalExpression, occurrences: [condition], expression } }; + } + else if (isBinaryExpression(condition)) { + const occurrences = getOccurrencesInExpression(finalExpression.expression, condition); + return occurrences ? { info: { finalExpression, occurrences, expression } } : + { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_matching_access_expressions) }; + } + } + + function getBinaryInfo(expression: BinaryExpression): InfoOrError | undefined { + 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 ? { info: { 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, expression: Expression): (PropertyAccessExpression | Identifier)[] | undefined { + const occurrences: (PropertyAccessExpression | Identifier)[] = []; + 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: Expression, subchain: Expression): PropertyAccessExpression | Identifier | undefined { + return (isIdentifier(subchain) || isPropertyAccessExpression(subchain)) && + chainStartsWith(chain, subchain) ? subchain : undefined; + } + + /** + * Returns true if chain begins with subchain syntactically. + */ + function chainStartsWith(chain: Node, subchain: Node): boolean { + // skip until we find a matching identifier. + while (isCallExpression(chain) || isPropertyAccessExpression(chain)) { + const subchainName = isPropertyAccessExpression(subchain) ? subchain.name.getText() : subchain.getText(); + if (isPropertyAccessExpression(chain) && chain.name.getText() === subchainName) break; + chain = chain.expression; + } + // check that the chains match at each access. Call chains in subchain are not valid. + while (isPropertyAccessExpression(chain) && isPropertyAccessExpression(subchain)) { + if (chain.name.getText() !== subchain.name.getText()) return false; + chain = chain.expression; + subchain = subchain.expression; + } + // check if we have reached a final identifier. + return isIdentifier(chain) && isIdentifier(subchain) && chain.getText() === subchain.getText(); + } + + /** + * Find the least ancestor of the input node that is a valid type for extraction and contains the input span. + */ + function getValidParentNodeContainingSpan(node: Node, span: TextSpan): ValidExpressionOrStatement | undefined { + 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: Node): ValidExpressionOrStatement | undefined { + 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: ValidExpressionOrStatement): ValidExpression | undefined { + 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; + } + + /** + * Gets a property access expression which may be nested inside of a binary expression. The final + * expression in an && chain will occur as the right child of the parent binary expression, unless + * it is followed by a different binary operator. + * @param node the right child of a binary expression or a call expression. + */ + function getFinalExpressionInChain(node: Expression): CallExpression | PropertyAccessExpression | undefined { + // foo && |foo.bar === 1|; - here the right child of the && binary expression is another binary expression. + // the rightmost member of the && chain should be the leftmost child of that expression. + node = skipParentheses(node); + if (isBinaryExpression(node)) { + return getFinalExpressionInChain(node.left); + } + // foo && |foo.bar()()| - nested calls are treated like further accesses. + else if ((isPropertyAccessExpression(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: TypeChecker, toConvert: Expression, occurrences: (PropertyAccessExpression | Identifier)[]): Expression { + if (isPropertyAccessExpression(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); + } + } + return toConvert; + } + + function doChange(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, info: Info, _actionName: string): void { + const { finalExpression, occurrences, expression } = info; + const firstOccurrence = occurrences[occurrences.length - 1]; + const convertedChain = convertOccurrences(checker, finalExpression, occurrences); + if (convertedChain && (isPropertyAccessExpression(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) + ); + } + } + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index e1aab52f187f1..cc24ac7f9c405 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -107,6 +107,7 @@ "codefixes/fixExpectedComma.ts", "refactors/convertExport.ts", "refactors/convertImport.ts", + "refactors/convertToOptionalChainExpression.ts", "refactors/convertOverloadListToSingleSignature.ts", "refactors/extractSymbol.ts", "refactors/extractType.ts", diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index a1f91482309c6..8997e237751ab 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -420,7 +420,7 @@ declare namespace FourSlashInterface { enableFormatting(): void; disableFormatting(): void; - applyRefactor(options: { refactorName: string, actionName: string, actionDescription: string, newContent: NewFileContent }): void; + applyRefactor(options: { refactorName: string, actionName: string, actionDescription: string, newContent: NewFileContent, triggerReason?: RefactorTriggerReason }): void; } class debug { printCurrentParameterHelp(): void; diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessCallCallReturnValue.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessCallCallReturnValue.ts new file mode 100644 index 0000000000000..3336a71ed8360 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessCallCallReturnValue.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: () => { return () => { c: 0 } } } +/////*a*/a && a.b && a.b()().c/*b*/; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: () => { return () => { c: 0 } } } +a?.b?.()().c;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessCallReturnValue.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessCallReturnValue.ts new file mode 100644 index 0000000000000..f4f2a6768aa04 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessCallReturnValue.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: () => { return { c: 0 } } } +/////*a*/a && a.b && a.b().c/*b*/; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: () => { return { c: 0 } } } +a?.b?.().c;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessThenCall.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessThenCall.ts new file mode 100644 index 0000000000000..3e6f799604784 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_AccessThenCall.ts @@ -0,0 +1,12 @@ +/// + +/////*a*/a && a.b && a.b()/*b*/; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`a?.b?.();` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_BinaryExpression.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_BinaryExpression.ts new file mode 100644 index 0000000000000..400c84adb43e8 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_BinaryExpression.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: { c: 0 } }; +/////*a*/a && a.b && a.b.c;/*b*/ + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +a?.b?.c;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_BinaryExpressionPartialSpan.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_BinaryExpressionPartialSpan.ts new file mode 100644 index 0000000000000..1495cee31cbff --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_BinaryExpressionPartialSpan.ts @@ -0,0 +1,15 @@ +/// + +////let foo = { bar: { baz: 0 } }; +////f/*a*/oo && foo.bar && foo.bar.ba/*b*/z; + +// allow partial spans +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let foo = { bar: { baz: 0 } }; +foo?.bar?.baz;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_BinaryWithCallExpression.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_BinaryWithCallExpression.ts new file mode 100644 index 0000000000000..9385a0268b7cf --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_BinaryWithCallExpression.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: { c: () => { } } }; +/////*a*/a && a.b && a.b.c();/*b*/ + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: () => { } } }; +a?.b?.c();` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_CallExpressionComparison.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_CallExpressionComparison.ts new file mode 100644 index 0000000000000..d9397ab11f2d8 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_CallExpressionComparison.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: { c: () => { } } }; +/////*a*/a && a.b && a.b.c() === 1;/*b*/ + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: () => { } } }; +a?.b?.c() === 1;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ComparisonOperator.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ComparisonOperator.ts new file mode 100644 index 0000000000000..2e61bfea4da45 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ComparisonOperator.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: { c: 0 } }; +/////*a*/a && a.b && a.b.c === 1;/*b*/ + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +a?.b?.c === 1;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalForAny.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalForAny.ts new file mode 100644 index 0000000000000..cecca1a295e40 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalForAny.ts @@ -0,0 +1,16 @@ +/// + +// @strict: true + +////interface Foo { +//// bar?:{ +//// baz?: any; +//// } +////} +////declare let foo: Foo; +/////*a*/foo.bar ? foo.bar.baz : "whenFalse";/*b*/ + +// It is reasonable to offer a refactor when baz is of type any since implicit any in strict mode +// produces an error and those with strict mode off aren't getting null checks anyway. +goTo.select("a", "b"); +verify.refactorAvailable("Convert to optional chain expression"); diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalInitialIdentifier.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalInitialIdentifier.ts new file mode 100644 index 0000000000000..7c73a2d2ee2e4 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalInitialIdentifier.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: 0 }; +/////*a*/a ? a.b : "whenFalse";/*b*/ + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: 0 }; +a?.b ?? "whenFalse";` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalNoNullish.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalNoNullish.ts new file mode 100644 index 0000000000000..eef8c58c84397 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalNoNullish.ts @@ -0,0 +1,15 @@ +/// + +// @strict: true + +////interface Foo { +//// bar?:{ +//// baz?: string | null; +//// } +////} +////declare let foo: Foo; +/////*a*/foo.bar ? foo.bar.baz : "whenFalse";/*b*/ + +// do not offer a refactor for ternary expression if type of baz is nullish +goTo.select("a", "b"); +verify.not.refactorAvailable("Convert to optional chain expression"); diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalPartialSPan.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalPartialSPan.ts new file mode 100644 index 0000000000000..e394527764325 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalPartialSPan.ts @@ -0,0 +1,15 @@ +/// + +////let foo = { bar: { baz: 0 } }; +////f/*a*/oo.bar ? foo.bar.baz : "when/*b*/False"; + +// allow partial spans +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let foo = { bar: { baz: 0 } }; +foo.bar?.baz ?? "whenFalse";` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalStrictMode.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalStrictMode.ts new file mode 100644 index 0000000000000..65f49e1ce459d --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalStrictMode.ts @@ -0,0 +1,15 @@ +/// + +// @strict: true + +////interface Foo { +//// bar?:{ +//// baz: string; +//// } +////} +////declare let foo: Foo; +/////*a*/foo.bar ? foo.bar.baz : "whenFalse";/*b*/ + +// Offer the refactor for ternary expressions if type of baz is not null, unknown, or undefined +goTo.select("a", "b"); +verify.refactorAvailable("Convert to optional chain expression"); diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalWithBinaryCondition1.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalWithBinaryCondition1.ts new file mode 100644 index 0000000000000..5ddefb61d9c08 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalWithBinaryCondition1.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: { c: 0 } }; +/////*a*/a && a.b/*b*/ ? a.b.c : "whenFalse"; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +a?.b ? a.b.c : "whenFalse";` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalWithBinaryCondition2.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalWithBinaryCondition2.ts new file mode 100644 index 0000000000000..953f44a51bfc4 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalWithBinaryCondition2.ts @@ -0,0 +1,26 @@ +/// + +// @strict: true + +////interface Foo { +//// bar:{ +//// baz: string; +//// } +////} +////declare let foo: Foo; +/////*a*/foo && foo.bar ? foo.bar.baz : "whenFalse";/*b*/ + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`interface Foo { + bar:{ + baz: string; + } +} +declare let foo: Foo; +foo?.bar?.baz ?? "whenFalse";` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalWithBinaryConditionNoNullish.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalWithBinaryConditionNoNullish.ts new file mode 100644 index 0000000000000..e1c068e281003 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ConditionalWithBinaryConditionNoNullish.ts @@ -0,0 +1,15 @@ +/// + +// @strict: true + +////interface Foo { +//// bar:{ +//// baz: string | null; +//// } +////} +////declare let foo: Foo; +/////*a*/foo && foo.bar ? foo.bar.baz : "whenFalse";/*b*/ + +// Do not offer refactor when true condition can be null. +goTo.select("a", "b"); +verify.not.refactorAvailable("Convert to optional chain expression") \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanBinaryExpression.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanBinaryExpression.ts new file mode 100644 index 0000000000000..ad68d59052080 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanBinaryExpression.ts @@ -0,0 +1,18 @@ +/// + +////let a = { b: { c: 0 } }; +////a && a.b && /*a*//*b*/a.b.c; + +// verify that the refactor is offered for empty spans in expression statements. +goTo.select("a", "b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +a?.b?.c;`, + triggerReason: "invoked" +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanBinaryReturnStatement.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanBinaryReturnStatement.ts new file mode 100644 index 0000000000000..fe4f85484dfff --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanBinaryReturnStatement.ts @@ -0,0 +1,22 @@ +/// + +////let a = { b: { c: 0 } }; +////function f(){ +//// return a && a.b && /*a*//*b*/a.b.c; +////} + +// verify that the refactor is offered for empty spans in return statements. +goTo.select("a", "b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +function f(){ + return a?.b?.c; +}`, + triggerReason: "invoked" +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanCallArgument.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanCallArgument.ts new file mode 100644 index 0000000000000..5b0a9cb22ed18 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanCallArgument.ts @@ -0,0 +1,16 @@ +/// + +////let foo = { bar: { baz: 0 } }; +////f(foo && foo.ba/*a*//*b*/r && foo.bar.baz); + +// allow for call arguments +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let foo = { bar: { baz: 0 } }; +f(foo?.bar?.baz);`, + triggerReason: "invoked" +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditional.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditional.ts new file mode 100644 index 0000000000000..caf1ccab89a12 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditional.ts @@ -0,0 +1,18 @@ +/// + +////let a = { b: { c: 0 } }; +////a.b ? /*a*//*b*/a.b.c : "whenFalse"; + +// verify that the refactor is offered for empty spans in expression statements. +goTo.select("a", "b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +a.b?.c ?? "whenFalse";`, + triggerReason: "invoked" +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditionalReturnKeyword.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditionalReturnKeyword.ts new file mode 100644 index 0000000000000..3f7b6bf10fd53 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditionalReturnKeyword.ts @@ -0,0 +1,22 @@ +/// + +////let a = { b: { c: 0 } }; +////function f(){ +//// ret/*a*//*b*/urn a.b ? a.b.c : "whenFalse"; +////} + +// verify that the refactor is offered for empty spans in return statements. +goTo.select("a", "b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +function f(){ + return a.b?.c ?? "whenFalse"; +}`, + triggerReason: "invoked" +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditionalReturnStatement.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditionalReturnStatement.ts new file mode 100644 index 0000000000000..494cb91986d68 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanConditionalReturnStatement.ts @@ -0,0 +1,22 @@ +/// + +////let a = { b: { c: 0 } }; +////function f(){ +//// return a.b ? /*a*//*b*/a.b.c : "whenFalse"; +////} + +// verify that the refactor is offered for empty spans in return statements. +goTo.select("a", "b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +function f(){ + return a.b?.c ?? "whenFalse"; +}`, + triggerReason: "invoked" +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanVarKeyword.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanVarKeyword.ts new file mode 100644 index 0000000000000..6f6bd981979ea --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanVarKeyword.ts @@ -0,0 +1,18 @@ +/// + +////let a = { b: { c: 0 } }; +////let/*a*//*b*/ x = a.b ? a.b.c : "whenFalse"; + +// verify that the refactor is offered for empty spans in variable statements. +goTo.select("a", "b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +let x = a.b?.c ?? "whenFalse";`, + triggerReason: "invoked" +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanVariableStatementBinary.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanVariableStatementBinary.ts new file mode 100644 index 0000000000000..b8233a55c4d89 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanVariableStatementBinary.ts @@ -0,0 +1,18 @@ +/// + +////let a = { b: { c: 0 } }; +////let x = a && a.b && /*a*//*b*/a.b.c; + +// verify that the refactor is offered for empty spans in variable statements. +goTo.select("a", "b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +let x = a?.b?.c;`, + triggerReason: "invoked" +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanVariableStatementConditional.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanVariableStatementConditional.ts new file mode 100644 index 0000000000000..75445c055d59f --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_EmptySpanVariableStatementConditional.ts @@ -0,0 +1,18 @@ +/// + +////let a = { b: { c: 0 } }; +////let x = a.b ? /*a*//*b*/a.b.c : "whenFalse"; + +// verify that the refactor is offered for empty spans in variable statements. +goTo.select("a", "b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +let x = a.b?.c ?? "whenFalse";`, + triggerReason: "invoked" +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ExpressionStatementValidSpans.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ExpressionStatementValidSpans.ts new file mode 100644 index 0000000000000..67eab8346ed58 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ExpressionStatementValidSpans.ts @@ -0,0 +1,19 @@ +/// + +////let a = { b: { c: 0 } }; +/////*1a*/let x1 = a && a.b && a.b.c;/*1b*/ +////let x2 = /*2a*/a && a.b && a.b.c;/*2b*/ +////let x3 = /*3a*/a && a.b && a.b.c/*3b*/; +////let x4 = /*4a*/a.b ? a.b.c : "whenFalse"/*4b*/; + +goTo.select("1a", "1b"); +verify.refactorAvailable("Convert to optional chain expression"); + +goTo.select("2a", "2b"); +verify.refactorAvailable("Convert to optional chain expression"); + +goTo.select("3a", "3b"); +verify.refactorAvailable("Convert to optional chain expression"); + +goTo.select("4a", "4b"); +verify.refactorAvailable("Convert to optional chain expression"); diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_InFunctionCall.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_InFunctionCall.ts new file mode 100644 index 0000000000000..964f0089d73ee --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_InFunctionCall.ts @@ -0,0 +1,15 @@ +/// + +////let foo = { bar: { baz: 0 } }; +////f(/*a*/foo && foo.bar && foo.bar.baz/*b*/); + +// allow for call arguments +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let foo = { bar: { baz: 0 } }; +f(foo?.bar?.baz);` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_InIfStatement.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_InIfStatement.ts new file mode 100644 index 0000000000000..03562de6639c7 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_InIfStatement.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: { c: 0 } }; +////if(/*a*/a && a.b && a.b.c/*b*/){}; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +if(a?.b?.c){};` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NoInitialIdentifier.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NoInitialIdentifier.ts new file mode 100644 index 0000000000000..29b7710dcd39f --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NoInitialIdentifier.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: { c: 0 } }; +/////*a*/a.b && a.b.c;/*b*/ + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +a.b?.c;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NoPreviousCall.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NoPreviousCall.ts new file mode 100644 index 0000000000000..24a072f572ceb --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NoPreviousCall.ts @@ -0,0 +1,15 @@ +/// + +////let a = { +//// b: () => { +//// return { +//// c: () => { +//// return { d: 0 }; +//// } +//// }; +//// } +////} +/////*a*/a && a.b() && a.b.c;/*b*/ + +goTo.select("a", "b"); +verify.not.refactorAvailable("Convert to optional chain expression"); diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NoRepeatCalls.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NoRepeatCalls.ts new file mode 100644 index 0000000000000..99f34efe1bbd1 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NoRepeatCalls.ts @@ -0,0 +1,34 @@ +/// + +////let a = { b: ()=> { +//// return { +//// c: ()=> { +//// return { +//// d: 0 +//// } +//// } +//// } +////}}; +/////*1a*//*2a*/a && a.b && a.b().c/*1b*/ && a.b().c().d;/*2b*/ + +// We should stop at the first call for b since it may not be a pure function. +goTo.select("2a", "2b"); +verify.not.refactorAvailable("Convert to optional chain expression"); + +goTo.select("1a", "1b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: ()=> { + return { + c: ()=> { + return { + d: 0 + } + } + } +}}; +a?.b?.().c && a.b().c().d;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForOptionalChain.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForOptionalChain.ts new file mode 100644 index 0000000000000..a74717ebce8c8 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForOptionalChain.ts @@ -0,0 +1,7 @@ +/// + +////let a = { b: { c: 0 } }; +/////*a*/a && a.b && a?.b?.c;/*b*/ + +goTo.select("a", "b"); +verify.not.refactorAvailable("Convert to optional chain expression"); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForOtherOperators.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForOtherOperators.ts new file mode 100644 index 0000000000000..5dffd5cc7e108 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForOtherOperators.ts @@ -0,0 +1,28 @@ +/// + +////let a = { b: { c: { d: 0 } } }; +/////*1a*/a || a.b && a.b.c && a.b.c.d;/*1b*/ +/////*2a*/a && a.b || a.b.c && a.b.c.d;/*2b*/ +/////*3a*/a && a.b && a.b.c || a.b.c.d;/*3b*/ +/////*4a*/a ?? a.b && a.b.c && a.b.c.d;/*4b*/ +/////*5a*/a && a.b ?? a.b.c || a.b.c.d;/*5b*/ +/////*6a*/a && a.b && a.b.c ?? a.b.c.d;/*6b*/ + +// Only offer refactor for && chains. +goTo.select("1a", "1b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +goTo.select("2a", "2b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +goTo.select("3a", "3b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +goTo.select("4a", "4b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +goTo.select("5a", "5b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); + +goTo.select("6a", "6b"); +verify.not.refactorAvailableForTriggerReason("implicit", "Convert to optional chain expression"); diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForOutOfOrderSequence.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForOutOfOrderSequence.ts new file mode 100644 index 0000000000000..4879612b27971 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_NotForOutOfOrderSequence.ts @@ -0,0 +1,9 @@ +/// + +////let a = { b: 0 }; +////let x = { b: 0 }; +/////*a*/a && x && a.b && x.y;/*b*/ + +// We don't currently offer a refactor for this case but should add it in the future. +goTo.select("a", "b"); +verify.not.refactorAvailable("Convert to optional chain expression"); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_OptionalInterface.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_OptionalInterface.ts new file mode 100644 index 0000000000000..1d777daa8c04c --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_OptionalInterface.ts @@ -0,0 +1,24 @@ +/// + +////interface Foo { +//// bar?:{ +//// baz?: string; +//// } +////} +////declare let foo: Foo | undefined; +/////*a*/foo && foo.bar && foo.bar.baz;/*b*/ + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`interface Foo { + bar?:{ + baz?: string; + } +} +declare let foo: Foo | undefined; +foo?.bar?.baz;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ReturnStatementBinary.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ReturnStatementBinary.ts new file mode 100644 index 0000000000000..78b39d484fdfd --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ReturnStatementBinary.ts @@ -0,0 +1,18 @@ +/// + +////let a = { b: { c: 0 } }; +////function f(){ +//// return /*a*/a && a.b && a.b.c/*b*/; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +function f(){ + return a?.b?.c; +}` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ReturnStatementConditional.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ReturnStatementConditional.ts new file mode 100644 index 0000000000000..3849741eb9e97 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ReturnStatementConditional.ts @@ -0,0 +1,18 @@ +/// + +////let a = { b: { c: 0 } }; +////function f(){ +//// return /*a*/a.b ? a.b.c : "whenFalse"/*b*/; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +function f(){ + return a.b?.c ?? "whenFalse"; +}` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ReturnStatementValidSpans.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ReturnStatementValidSpans.ts new file mode 100644 index 0000000000000..54a4c0d90a287 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_ReturnStatementValidSpans.ts @@ -0,0 +1,28 @@ +/// + +////let a = { b: { c: 0 } }; +////function f()1{ +//// /*1a*/return a && a.b && a.b.c;/*1b*/ +////} +////function f()2{ +//// return /*2a*/a && a.b && a.b.c;/*2b*/ +////} +////function f()3{ +//// return /*3a*/a && a.b && a.b.c/*3b*/; +////} +////function f()4{ +//// return /*4a*/a.b ? a.b.c : "whenFalse";/*4b*/ +////} + +// valid spans for return statement +goTo.select("1a", "1b"); +verify.refactorAvailable("Convert to optional chain expression"); + +goTo.select("2a", "2b"); +verify.refactorAvailable("Convert to optional chain expression"); + +goTo.select("3a", "3b"); +verify.refactorAvailable("Convert to optional chain expression"); + +goTo.select("4a", "4b"); +verify.refactorAvailable("Convert to optional chain expression"); diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SemicolonNotSelected.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SemicolonNotSelected.ts new file mode 100644 index 0000000000000..205c3f893347e --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SemicolonNotSelected.ts @@ -0,0 +1,14 @@ +/// + +////let a = { b: { c: 0 } }; +////let x = /*a*/a && a.b && a.b.c/*b*/; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +let x = a?.b?.c;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SparseAccess.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SparseAccess.ts new file mode 100644 index 0000000000000..13c0fac40cd3a --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SparseAccess.ts @@ -0,0 +1,15 @@ +/// + +////let a = { b: { c: { d: { e: {f: 0} } } } }; +/////*a*/a.b && a.b.c.d && a.b.c.d.e.f;/*b*/ + +// Only convert the accesses for which existence is checked. +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: { d: { e: {f: 0} } } } }; +a.b?.c.d?.e.f;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithPrefix1.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithPrefix1.ts new file mode 100644 index 0000000000000..ed6061a8e6726 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithPrefix1.ts @@ -0,0 +1,19 @@ +/// + +////let a = { b: { c: 0 } }; +////let foo; +////let bar; +////foo && bar && /*a*/a && a.b && a.b.c;/*b*/ + +// verify that we stop at an invalid prefix sequence. +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +let foo; +let bar; +foo && bar && a?.b?.c;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithSuffix1.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithSuffix1.ts new file mode 100644 index 0000000000000..ab48eb8c85dec --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithSuffix1.ts @@ -0,0 +1,19 @@ +/// + +////let a = { b: { c: 0 } }; +////let foo; +////let bar; +/////*a*/a && a.b && a.b.c/*b*/ && foo && bar; + +// verify that we stop at an invalid suffix sequence. +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: { c: 0 } }; +let foo; +let bar; +a?.b?.c && foo && bar;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithSuffix2.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithSuffix2.ts new file mode 100644 index 0000000000000..ec038ae76c67f --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionWithSuffix2.ts @@ -0,0 +1,17 @@ +/// + +////let a = { b: 0 }; +////let x = { y: 0 }; +/////*a*/a && a.b()/*b*/ && x && x.y(); + +// verify that we stop at a suffix sequence which is otherwise valid. +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: 0 }; +let x = { y: 0 }; +a?.b() && x && x.y();` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionsWithPrefix2.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionsWithPrefix2.ts new file mode 100644 index 0000000000000..15b3dbb8e70dc --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_SubexpressionsWithPrefix2.ts @@ -0,0 +1,17 @@ +/// + +////let a = { b: 0 }; +////let x = { y: 0 }; +////a && a.b && /*a*/x && x.y;/*b*/ + +// Verify that we stop at a prefix sequence that is otherwise valid. +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to optional chain expression", + actionName: "Convert to optional chain expression", + actionDescription: "Convert to optional chain expression", + newContent: +`let a = { b: 0 }; +let x = { y: 0 }; +a && a.b && x?.y;` +}); \ No newline at end of file diff --git a/tests/cases/fourslash/refactorConvertToOptionalChainExpression_UnknownSymbol.ts b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_UnknownSymbol.ts new file mode 100644 index 0000000000000..ea4b6d6c1683b --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToOptionalChainExpression_UnknownSymbol.ts @@ -0,0 +1,6 @@ +/// + +/////*a*/foo && foo.bar;/*b*/ + +goTo.select("a", "b"); +verify.refactorAvailable("Convert to optional chain expression") \ No newline at end of file