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