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
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy