diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 35d35f7234f9c..5f29748d4ef82 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -5755,6 +5755,50 @@ "category": "Message", "code": 95126 }, + "Could not find a containing arrow function": { + "category": "Message", + "code": 95127 + }, + "Containing function is not an arrow function": { + "category": "Message", + "code": 95128 + }, + "Could not find export statement": { + "category": "Message", + "code": 95129 + }, + "This file already has a default export": { + "category": "Message", + "code": 95130 + }, + "Could not find import clause": { + "category": "Message", + "code": 95131 + }, + "Could not find namespace import or named imports": { + "category": "Message", + "code": 95132 + }, + "Selection is not a valid type node": { + "category": "Message", + "code": 95133 + }, + "No type could be extracted from this type node": { + "category": "Message", + "code": 95134 + }, + "Could not find property for which to generate accessor": { + "category": "Message", + "code": 95135 + }, + "Name is not valid": { + "category": "Message", + "code": 95136 + }, + "Can only convert property with modifier": { + "category": "Message", + "code": 95137 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d6c06ebec5ff0..97fe4f408fa50 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -8024,6 +8024,7 @@ namespace ts { readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js"; readonly allowTextChangesInNewFiles?: boolean; readonly providePrefixAndSuffixTextForRename?: boolean; + readonly provideRefactorNotApplicableReason?: boolean; } /** Represents a bigint literal value without requiring bigint support */ diff --git a/src/services/codefixes/generateAccessors.ts b/src/services/codefixes/generateAccessors.ts index 75e3c9b9d24b2..518278d041c12 100644 --- a/src/services/codefixes/generateAccessors.ts +++ b/src/services/codefixes/generateAccessors.ts @@ -16,12 +16,20 @@ namespace ts.codefix { readonly renameAccessor: boolean; } + type InfoOrError = { + info: Info, + error?: never + } | { + info?: never, + error: string + }; + export function generateAccessorFromProperty(file: SourceFile, start: number, end: number, context: textChanges.TextChangesContext, _actionName: string): FileTextChanges[] | undefined { const fieldInfo = getAccessorConvertiblePropertyAtPosition(file, start, end); - if (!fieldInfo) return undefined; + if (!fieldInfo || !fieldInfo.info) return undefined; const changeTracker = textChanges.ChangeTracker.fromContext(context); - const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo; + const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo.info; suppressLeadingAndTrailingTrivia(fieldName); suppressLeadingAndTrailingTrivia(accessorName); @@ -104,29 +112,47 @@ namespace ts.codefix { return modifierFlags; } - export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, start: number, end: number, considerEmptySpans = true): Info | undefined { + export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, start: number, end: number, considerEmptySpans = true): InfoOrError | undefined { const node = getTokenAtPosition(file, start); const cursorRequest = start === end && considerEmptySpans; const declaration = findAncestor(node.parent, isAcceptedDeclaration); // make sure declaration have AccessibilityModifier or Static Modifier or Readonly Modifier const meaning = ModifierFlags.AccessibilityModifier | ModifierFlags.Static | ModifierFlags.Readonly; - if (!declaration || !(nodeOverlapsWithStartEnd(declaration.name, file, start, end) || cursorRequest) - || !isConvertibleName(declaration.name) || (getEffectiveModifierFlags(declaration) | meaning) !== meaning) return undefined; + + if (!declaration || (!(nodeOverlapsWithStartEnd(declaration.name, file, start, end) || cursorRequest))) { + return { + error: getLocaleSpecificMessage(Diagnostics.Could_not_find_property_for_which_to_generate_accessor) + }; + } + + if (!isConvertibleName(declaration.name)) { + return { + error: getLocaleSpecificMessage(Diagnostics.Name_is_not_valid) + }; + } + + if ((getEffectiveModifierFlags(declaration) | meaning) !== meaning) { + return { + error: getLocaleSpecificMessage(Diagnostics.Can_only_convert_property_with_modifier) + }; + } const name = declaration.name.text; const startWithUnderscore = startsWithUnderscore(name); const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name); const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name); return { - isStatic: hasStaticModifier(declaration), - isReadonly: hasEffectiveReadonlyModifier(declaration), - type: getTypeAnnotationNode(declaration), - container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent, - originalName: (declaration.name).text, - declaration, - fieldName, - accessorName, - renameAccessor: startWithUnderscore + info: { + isStatic: hasStaticModifier(declaration), + isReadonly: hasEffectiveReadonlyModifier(declaration), + type: getTypeAnnotationNode(declaration), + container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent, + originalName: (declaration.name).text, + declaration, + fieldName, + accessorName, + renameAccessor: startWithUnderscore + } }; } diff --git a/src/services/refactors/addOrRemoveBracesToArrowFunction.ts b/src/services/refactors/addOrRemoveBracesToArrowFunction.ts index d451a10e2fdab..a2a14152da738 100644 --- a/src/services/refactors/addOrRemoveBracesToArrowFunction.ts +++ b/src/services/refactors/addOrRemoveBracesToArrowFunction.ts @@ -15,33 +15,61 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { addBraces: boolean; } + type InfoOrError = { + info: Info, + error?: never + } | { + info?: never, + error: string + }; + function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { const { file, startPosition, triggerReason } = context; const info = getConvertibleArrowFunctionAtPosition(file, startPosition, triggerReason === "invoked"); if (!info) return emptyArray; - return [{ - name: refactorName, - description: refactorDescription, - actions: [ - info.addBraces ? - { - name: addBracesActionName, - description: addBracesActionDescription - } : { - name: removeBracesActionName, - description: removeBracesActionDescription - } - ] - }]; + if (info.error === undefined) { + return [{ + name: refactorName, + description: refactorDescription, + actions: [ + info.info.addBraces ? + { + name: addBracesActionName, + description: addBracesActionDescription + } : { + name: removeBracesActionName, + description: removeBracesActionDescription + } + ] + }]; + } + + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: refactorName, + description: refactorDescription, + actions: [{ + name: addBracesActionName, + description: addBracesActionDescription, + notApplicableReason: info.error + }, { + name: removeBracesActionName, + description: removeBracesActionDescription, + notApplicableReason: info.error + }] + }]; + } + + return emptyArray; } function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined { const { file, startPosition } = context; const info = getConvertibleArrowFunctionAtPosition(file, startPosition); - if (!info) return undefined; + if (!info || !info.info) return undefined; - const { expression, returnStatement, func } = info; + const { expression, returnStatement, func } = info.info; let body: ConciseBody; @@ -70,28 +98,45 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction { return { renameFilename: undefined, renameLocation: undefined, edits }; } - function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true): Info | undefined { + function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true): InfoOrError | undefined { const node = getTokenAtPosition(file, startPosition); const func = getContainingFunction(node); - // Only offer a refactor in the function body on explicit refactor requests. - if (!func || !isArrowFunction(func) || (!rangeContainsRange(func, node) - || (rangeContainsRange(func.body, node) && !considerFunctionBodies))) return undefined; + + if (!func) { + return { + error: getLocaleSpecificMessage(Diagnostics.Could_not_find_a_containing_arrow_function) + }; + } + + if (!isArrowFunction(func)) { + return { + error: getLocaleSpecificMessage(Diagnostics.Containing_function_is_not_an_arrow_function) + }; + } + + if ((!rangeContainsRange(func, node) || rangeContainsRange(func.body, node) && !considerFunctionBodies)) { + return undefined; + } if (isExpression(func.body)) { return { - func, - addBraces: true, - expression: func.body + info: { + func, + addBraces: true, + expression: func.body + } }; } else if (func.body.statements.length === 1) { const firstStatement = first(func.body.statements); if (isReturnStatement(firstStatement)) { return { - func, - addBraces: false, - expression: firstStatement.expression, - returnStatement: firstStatement + info: { + func, + addBraces: false, + expression: firstStatement.expression, + returnStatement: firstStatement + } }; } } diff --git a/src/services/refactors/convertExport.ts b/src/services/refactors/convertExport.ts index 9bfcfb2f8263a..1ab52fadc9bd4 100644 --- a/src/services/refactors/convertExport.ts +++ b/src/services/refactors/convertExport.ts @@ -3,17 +3,30 @@ namespace ts.refactor { const refactorName = "Convert export"; const actionNameDefaultToNamed = "Convert default export to named export"; const actionNameNamedToDefault = "Convert named export to default export"; + registerRefactor(refactorName, { getAvailableActions(context): readonly ApplicableRefactorInfo[] { const info = getInfo(context, context.triggerReason === "invoked"); if (!info) return emptyArray; - const description = info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message; - const actionName = info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault; - return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; + + if (info.error === undefined) { + const description = info.info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message; + const actionName = info.info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault; + return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; + } + + if (context.preferences.provideRefactorNotApplicableReason) { + return [ + { name: refactorName, description: Diagnostics.Convert_default_export_to_named_export.message, actions: [{ name: actionNameDefaultToNamed, description: Diagnostics.Convert_default_export_to_named_export.message, notApplicableReason: info.error }] }, + { name: refactorName, description: Diagnostics.Convert_named_export_to_default_export.message, actions: [{ name: actionNameNamedToDefault, description: Diagnostics.Convert_named_export_to_default_export.message, notApplicableReason: info.error }] }, + ]; + } + + return emptyArray; }, getEditsForAction(context, actionName): RefactorEditInfo { Debug.assert(actionName === actionNameDefaultToNamed || actionName === actionNameNamedToDefault, "Unexpected action name"); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.checkDefined(getInfo(context), "context must have info"), t, context.cancellationToken)); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.checkDefined(getInfo(context)?.info, "context must have info"), t, context.cancellationToken)); return { edits, renameFilename: undefined, renameLocation: undefined }; }, }); @@ -27,13 +40,21 @@ namespace ts.refactor { readonly exportingModuleSymbol: Symbol; } - function getInfo(context: RefactorContext, considerPartialSpans = true): Info | undefined { + type InfoOrError = { + info: Info, + error?: never + } | { + info?: never, + error: string + }; + + function getInfo(context: RefactorContext, considerPartialSpans = true): InfoOrError | undefined { const { file } = context; const span = getRefactorContextSpan(context); const token = getTokenAtPosition(file, span.start); const exportNode = !!(token.parent && getSyntacticModifierFlags(token.parent) & ModifierFlags.Export) && considerPartialSpans ? token.parent : getParentNodeInSpan(token, file, span); if (!exportNode || (!isSourceFile(exportNode.parent) && !(isModuleBlock(exportNode.parent) && isAmbientModule(exportNode.parent.parent)))) { - return undefined; + return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_export_statement) }; } const exportingModuleSymbol = isSourceFile(exportNode.parent) ? exportNode.parent.symbol : exportNode.parent.parent.symbol; @@ -42,7 +63,7 @@ namespace ts.refactor { const wasDefault = !!(flags & ModifierFlags.Default); // If source file already has a default export, don't offer refactor. if (!(flags & ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(InternalSymbolName.Default)) { - return undefined; + return { error: getLocaleSpecificMessage(Diagnostics.This_file_already_has_a_default_export) }; } switch (exportNode.kind) { @@ -53,7 +74,7 @@ namespace ts.refactor { case SyntaxKind.TypeAliasDeclaration: case SyntaxKind.ModuleDeclaration: { const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration; - return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined; + return node.name && isIdentifier(node.name) ? { info: { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } } : undefined; } case SyntaxKind.VariableStatement: { const vs = exportNode as VariableStatement; @@ -64,7 +85,7 @@ namespace ts.refactor { const decl = first(vs.declarationList.declarations); if (!decl.initializer) return undefined; Debug.assert(!wasDefault, "Can't have a default flag here"); - return isIdentifier(decl.name) ? { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } : undefined; + return isIdentifier(decl.name) ? { info: { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } } : undefined; } default: return undefined; diff --git a/src/services/refactors/convertImport.ts b/src/services/refactors/convertImport.ts index 9d33f086aa844..5ab2b316d5cc8 100644 --- a/src/services/refactors/convertImport.ts +++ b/src/services/refactors/convertImport.ts @@ -3,30 +3,61 @@ namespace ts.refactor { const refactorName = "Convert import"; const actionNameNamespaceToNamed = "Convert namespace import to named imports"; const actionNameNamedToNamespace = "Convert named imports to namespace import"; + + type NamedImportBindingsOrError = { + info: NamedImportBindings, + error?: never + } | { + info?: never, + error: string + }; + registerRefactor(refactorName, { getAvailableActions(context): readonly ApplicableRefactorInfo[] { const i = getImportToConvert(context, context.triggerReason === "invoked"); if (!i) return emptyArray; - const description = i.kind === SyntaxKind.NamespaceImport ? Diagnostics.Convert_namespace_import_to_named_imports.message : Diagnostics.Convert_named_imports_to_namespace_import.message; - const actionName = i.kind === SyntaxKind.NamespaceImport ? actionNameNamespaceToNamed : actionNameNamedToNamespace; - return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; + + if (i.error === undefined) { + const description = i.info.kind === SyntaxKind.NamespaceImport ? Diagnostics.Convert_namespace_import_to_named_imports.message : Diagnostics.Convert_named_imports_to_namespace_import.message; + const actionName = i.info.kind === SyntaxKind.NamespaceImport ? actionNameNamespaceToNamed : actionNameNamedToNamespace; + return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; + } + + if (context.preferences.provideRefactorNotApplicableReason) { + return [ + { name: refactorName, description: Diagnostics.Convert_namespace_import_to_named_imports.message, actions: [{ name: actionNameNamespaceToNamed, description: Diagnostics.Convert_namespace_import_to_named_imports.message, notApplicableReason: i.error }] }, + { name: refactorName, description: Diagnostics.Convert_named_imports_to_namespace_import.message, actions: [{ name: actionNameNamedToNamespace, description: Diagnostics.Convert_named_imports_to_namespace_import.message, notApplicableReason: i.error }] } + ]; + } + + return emptyArray; }, getEditsForAction(context, actionName): RefactorEditInfo { Debug.assert(actionName === actionNameNamespaceToNamed || actionName === actionNameNamedToNamespace, "Unexpected action name"); - const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, Debug.checkDefined(getImportToConvert(context), "Context must provide an import to convert"))); + const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, Debug.checkDefined(getImportToConvert(context)?.info, "Context must provide an import to convert"))); return { edits, renameFilename: undefined, renameLocation: undefined }; } }); // Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`. - function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindings | undefined { + function getImportToConvert(context: RefactorContext, considerPartialSpans = true): NamedImportBindingsOrError | undefined { const { file } = context; const span = getRefactorContextSpan(context); const token = getTokenAtPosition(file, span.start); const importDecl = considerPartialSpans ? findAncestor(token, isImportDeclaration) : getParentNodeInSpan(token, file, span); - if (!importDecl || !isImportDeclaration(importDecl) || (importDecl.getEnd() < span.start + span.length)) return undefined; + if (!importDecl || !isImportDeclaration(importDecl)) return { error: "Selection is not an import declaration." }; + if (importDecl.getEnd() < span.start + span.length) return undefined; + const { importClause } = importDecl; - return importClause && importClause.namedBindings; + if (!importClause) { + return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_import_clause) }; + } + + if (!importClause.namedBindings) { + return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_namespace_import_or_named_imports) }; + } + + return { info: importClause.namedBindings }; } function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void { diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index fcccf5a72a382..2fce499024a0e 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -12,7 +12,28 @@ namespace ts.refactor.extractSymbol { const targetRange = rangeToExtract.targetRange; if (targetRange === undefined) { - return emptyArray; + if (!rangeToExtract.errors || rangeToExtract.errors.length === 0 || !context.preferences.provideRefactorNotApplicableReason) { + return emptyArray; + } + + return [{ + name: refactorName, + description: getLocaleSpecificMessage(Diagnostics.Extract_function), + actions: [{ + description: getLocaleSpecificMessage(Diagnostics.Extract_function), + name: "function_extract_error", + notApplicableReason: getStringError(rangeToExtract.errors) + }] + }, + { + name: refactorName, + description: getLocaleSpecificMessage(Diagnostics.Extract_constant), + actions: [{ + description: getLocaleSpecificMessage(Diagnostics.Extract_constant), + name: "constant_extract_error", + notApplicableReason: getStringError(rangeToExtract.errors) + }] + }]; } const extractions = getPossibleExtractions(targetRange, context); @@ -23,18 +44,19 @@ namespace ts.refactor.extractSymbol { const functionActions: RefactorActionInfo[] = []; const usedFunctionNames: Map = createMap(); + let innermostErrorFunctionAction: RefactorActionInfo | undefined; const constantActions: RefactorActionInfo[] = []; const usedConstantNames: Map = createMap(); + let innermostErrorConstantAction: RefactorActionInfo | undefined; let i = 0; for (const { functionExtraction, constantExtraction } of extractions) { - // Skip these since we don't have a way to report errors yet + const description = functionExtraction.description; if (functionExtraction.errors.length === 0) { // Don't issue refactorings with duplicated names. // Scopes come back in "innermost first" order, so extractions will // preferentially go into nearer scopes - const description = functionExtraction.description; if (!usedFunctionNames.has(description)) { usedFunctionNames.set(description, true); functionActions.push({ @@ -43,6 +65,13 @@ namespace ts.refactor.extractSymbol { }); } } + else if (!innermostErrorFunctionAction) { + innermostErrorFunctionAction = { + description, + name: `function_scope_${i}`, + notApplicableReason: getStringError(functionExtraction.errors) + }; + } // Skip these since we don't have a way to report errors yet if (constantExtraction.errors.length === 0) { @@ -58,6 +87,13 @@ namespace ts.refactor.extractSymbol { }); } } + else if (!innermostErrorConstantAction) { + innermostErrorConstantAction = { + description, + name: `constant_scope_${i}`, + notApplicableReason: getStringError(constantExtraction.errors) + }; + } // *do* increment i anyway because we'll look for the i-th scope // later when actually doing the refactoring if the user requests it @@ -66,6 +102,21 @@ namespace ts.refactor.extractSymbol { const infos: ApplicableRefactorInfo[] = []; + if (functionActions.length) { + infos.push({ + name: refactorName, + description: getLocaleSpecificMessage(Diagnostics.Extract_function), + actions: functionActions + }); + } + else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorFunctionAction) { + infos.push({ + name: refactorName, + description: getLocaleSpecificMessage(Diagnostics.Extract_function), + actions: [ innermostErrorFunctionAction ] + }); + } + if (constantActions.length) { infos.push({ name: refactorName, @@ -73,16 +124,23 @@ namespace ts.refactor.extractSymbol { actions: constantActions }); } - - if (functionActions.length) { + else if (context.preferences.provideRefactorNotApplicableReason && innermostErrorConstantAction) { infos.push({ name: refactorName, - description: getLocaleSpecificMessage(Diagnostics.Extract_function), - actions: functionActions + description: getLocaleSpecificMessage(Diagnostics.Extract_constant), + actions: [ innermostErrorConstantAction ] }); } return infos.length ? infos : emptyArray; + + function getStringError(errors: readonly Diagnostic[]) { + let error = errors[0].messageText; + if (typeof error !== "string") { + error = error.messageText; + } + return error; + } } /* Exported for tests */ diff --git a/src/services/refactors/extractType.ts b/src/services/refactors/extractType.ts index 4824958fe3395..ef922bce633b8 100644 --- a/src/services/refactors/extractType.ts +++ b/src/services/refactors/extractType.ts @@ -9,21 +9,37 @@ namespace ts.refactor { const info = getRangeToExtract(context, context.triggerReason === "invoked"); if (!info) return emptyArray; - return [{ - name: refactorName, - description: getLocaleSpecificMessage(Diagnostics.Extract_type), - actions: info.isJS ? [{ - name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef) - }] : append([{ - name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias) - }], info.typeElements && { - name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface) - }) - }]; + if (info.error === undefined) { + return [{ + name: refactorName, + description: getLocaleSpecificMessage(Diagnostics.Extract_type), + actions: info.info.isJS ? [{ + name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef) + }] : append([{ + name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias) + }], info.info.typeElements && { + name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface) + }) + }]; + } + + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: refactorName, + description: getLocaleSpecificMessage(Diagnostics.Extract_type), + actions: [ + { name: extractToTypeDef, description: getLocaleSpecificMessage(Diagnostics.Extract_to_typedef), notApplicableReason: info.error }, + { name: extractToTypeAlias, description: getLocaleSpecificMessage(Diagnostics.Extract_to_type_alias), notApplicableReason: info.error }, + { name: extractToInterface, description: getLocaleSpecificMessage(Diagnostics.Extract_to_interface), notApplicableReason: info.error }, + ] + }]; + } + + return emptyArray; }, getEditsForAction(context, actionName): RefactorEditInfo { const { file, } = context; - const info = Debug.checkDefined(getRangeToExtract(context), "Expected to find a range to extract"); + const info = Debug.checkDefined(getRangeToExtract(context)?.info, "Expected to find a range to extract"); const name = getUniqueName("NewType", file); const edits = textChanges.ChangeTracker.with(context, changes => { @@ -57,8 +73,15 @@ namespace ts.refactor { } type Info = TypeAliasInfo | InterfaceInfo; - - function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): Info | undefined { + type InfoOrError = { + info: Info, + error?: never + } | { + info?: never, + error: string + }; + + function getRangeToExtract(context: RefactorContext, considerEmptySpans = true): InfoOrError | undefined { const { file, startPosition } = context; const isJS = isSourceFileJS(file); const current = getTokenAtPosition(file, startPosition); @@ -67,15 +90,15 @@ namespace ts.refactor { const selection = findAncestor(current, (node => node.parent && isTypeNode(node) && !rangeContainsSkipTrivia(range, node.parent, file) && (cursorRequest || nodeOverlapsWithStartEnd(current, file, range.pos, range.end)))); - if (!selection || !isTypeNode(selection)) return undefined; + if (!selection || !isTypeNode(selection)) return { error: getLocaleSpecificMessage(Diagnostics.Selection_is_not_a_valid_type_node) }; const checker = context.program.getTypeChecker(); const firstStatement = Debug.checkDefined(findAncestor(selection, isStatement), "Should find a statement"); const typeParameters = collectTypeParameters(checker, selection, firstStatement, file); - if (!typeParameters) return undefined; + if (!typeParameters) return { error: getLocaleSpecificMessage(Diagnostics.No_type_could_be_extracted_from_this_type_node) }; const typeElements = flattenTypeLiteralNodeReference(checker, selection); - return { isJS, selection, firstStatement, typeParameters, typeElements }; + return { info: { isJS, selection, firstStatement, typeParameters, typeElements } }; } function flattenTypeLiteralNodeReference(checker: TypeChecker, node: TypeNode | undefined): readonly TypeElement[] | undefined { diff --git a/src/services/refactors/generateGetAccessorAndSetAccessor.ts b/src/services/refactors/generateGetAccessorAndSetAccessor.ts index 53bdf728fbe94..7ac211ed76515 100644 --- a/src/services/refactors/generateGetAccessorAndSetAccessor.ts +++ b/src/services/refactors/generateGetAccessorAndSetAccessor.ts @@ -6,31 +6,48 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor { getEditsForAction(context, actionName) { if (!context.endPosition) return undefined; const info = codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.startPosition, context.endPosition); - if (!info) return undefined; + if (!info || !info.info) return undefined; const edits = codefix.generateAccessorFromProperty(context.file, context.startPosition, context.endPosition, context, actionName); if (!edits) return undefined; const renameFilename = context.file.fileName; - const nameNeedRename = info.renameAccessor ? info.accessorName : info.fieldName; + const nameNeedRename = info.info.renameAccessor ? info.info.accessorName : info.info.fieldName; const renameLocationOffset = isIdentifier(nameNeedRename) ? 0 : -1; - const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(info.declaration)); + const renameLocation = renameLocationOffset + getRenameLocation(edits, renameFilename, nameNeedRename.text, /*preferLastLocation*/ isParameter(info.info.declaration)); return { renameFilename, renameLocation, edits }; }, getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] { if (!context.endPosition) return emptyArray; - if (!codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.startPosition, context.endPosition, context.triggerReason === "invoked")) return emptyArray; + const info = codefix.getAccessorConvertiblePropertyAtPosition(context.file, context.startPosition, context.endPosition, context.triggerReason === "invoked"); + if (!info) return emptyArray; - return [{ - name: actionName, - description: actionDescription, - actions: [ - { + if (!info.error) { + return [{ + name: actionName, + description: actionDescription, + actions: [ + { + name: actionName, + description: actionDescription + } + ] + }]; + } + + if (context.preferences.provideRefactorNotApplicableReason) { + return [{ + name: actionName, + description: actionDescription, + actions: [{ name: actionName, - description: actionDescription - } - ] - }]; + description: actionDescription, + notApplicableReason: info.error + }] + }]; + } + + return emptyArray; } }); } diff --git a/src/services/types.ts b/src/services/types.ts index 757122aa2a309..006a6355c72d9 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -730,6 +730,12 @@ namespace ts { * so this description should make sense by itself if the parent is inlineable=true */ description: string; + + /** + * A message to show to the user if the refactoring cannot be applied in + * the current context. + */ + notApplicableReason?: string; } /** diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 30b789890b3e7..c528d3738bc8f 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3789,6 +3789,7 @@ declare namespace ts { readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js"; readonly allowTextChangesInNewFiles?: boolean; readonly providePrefixAndSuffixTextForRename?: boolean; + readonly provideRefactorNotApplicableReason?: boolean; } /** Represents a bigint literal value without requiring bigint support */ export interface PseudoBigInt { @@ -5660,6 +5661,11 @@ declare namespace ts { * so this description should make sense by itself if the parent is inlineable=true */ description: string; + /** + * A message to show to the user if the refactoring cannot be applied in + * the current context. + */ + notApplicableReason?: string; } /** * A set of edits to make in response to a refactor action, plus an optional diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index c3149e9b7d17e..f5c952615866a 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3789,6 +3789,7 @@ declare namespace ts { readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js"; readonly allowTextChangesInNewFiles?: boolean; readonly providePrefixAndSuffixTextForRename?: boolean; + readonly provideRefactorNotApplicableReason?: boolean; } /** Represents a bigint literal value without requiring bigint support */ export interface PseudoBigInt { @@ -5660,6 +5661,11 @@ declare namespace ts { * so this description should make sense by itself if the parent is inlineable=true */ description: string; + /** + * A message to show to the user if the refactoring cannot be applied in + * the current context. + */ + notApplicableReason?: string; } /** * A set of edits to make in response to a refactor action, plus an optional