Skip to content

Commit db8a47c

Browse files
PranavSenthilnathanJack-Works
authored andcommitted
Add reason for a disabled code action (microsoft#37871)
* add reason for a disabled code action * add addOrRemoveBracesToArrowFunction * use user preferences * add error reason * accept baseline changes * fix lint rules * move messages to diagnosticMessages.json * rename 'error' to 'notApplicableReason' * accept baseline change * address comments
1 parent 4e94719 commit db8a47c

12 files changed

+378
-94
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5767,6 +5767,50 @@
57675767
"category": "Message",
57685768
"code": 95126
57695769
},
5770+
"Could not find a containing arrow function": {
5771+
"category": "Message",
5772+
"code": 95127
5773+
},
5774+
"Containing function is not an arrow function": {
5775+
"category": "Message",
5776+
"code": 95128
5777+
},
5778+
"Could not find export statement": {
5779+
"category": "Message",
5780+
"code": 95129
5781+
},
5782+
"This file already has a default export": {
5783+
"category": "Message",
5784+
"code": 95130
5785+
},
5786+
"Could not find import clause": {
5787+
"category": "Message",
5788+
"code": 95131
5789+
},
5790+
"Could not find namespace import or named imports": {
5791+
"category": "Message",
5792+
"code": 95132
5793+
},
5794+
"Selection is not a valid type node": {
5795+
"category": "Message",
5796+
"code": 95133
5797+
},
5798+
"No type could be extracted from this type node": {
5799+
"category": "Message",
5800+
"code": 95134
5801+
},
5802+
"Could not find property for which to generate accessor": {
5803+
"category": "Message",
5804+
"code": 95135
5805+
},
5806+
"Name is not valid": {
5807+
"category": "Message",
5808+
"code": 95136
5809+
},
5810+
"Can only convert property with modifier": {
5811+
"category": "Message",
5812+
"code": 95137
5813+
},
57705814

57715815
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
57725816
"category": "Error",

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8025,6 +8025,7 @@ namespace ts {
80258025
readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js";
80268026
readonly allowTextChangesInNewFiles?: boolean;
80278027
readonly providePrefixAndSuffixTextForRename?: boolean;
8028+
readonly provideRefactorNotApplicableReason?: boolean;
80288029
}
80298030

80308031
/** Represents a bigint literal value without requiring bigint support */

src/services/codefixes/generateAccessors.ts

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,20 @@ namespace ts.codefix {
1616
readonly renameAccessor: boolean;
1717
}
1818

19+
type InfoOrError = {
20+
info: Info,
21+
error?: never
22+
} | {
23+
info?: never,
24+
error: string
25+
};
26+
1927
export function generateAccessorFromProperty(file: SourceFile, start: number, end: number, context: textChanges.TextChangesContext, _actionName: string): FileTextChanges[] | undefined {
2028
const fieldInfo = getAccessorConvertiblePropertyAtPosition(file, start, end);
21-
if (!fieldInfo) return undefined;
29+
if (!fieldInfo || !fieldInfo.info) return undefined;
2230

2331
const changeTracker = textChanges.ChangeTracker.fromContext(context);
24-
const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo;
32+
const { isStatic, isReadonly, fieldName, accessorName, originalName, type, container, declaration } = fieldInfo.info;
2533

2634
suppressLeadingAndTrailingTrivia(fieldName);
2735
suppressLeadingAndTrailingTrivia(accessorName);
@@ -104,29 +112,47 @@ namespace ts.codefix {
104112
return modifierFlags;
105113
}
106114

107-
export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, start: number, end: number, considerEmptySpans = true): Info | undefined {
115+
export function getAccessorConvertiblePropertyAtPosition(file: SourceFile, start: number, end: number, considerEmptySpans = true): InfoOrError | undefined {
108116
const node = getTokenAtPosition(file, start);
109117
const cursorRequest = start === end && considerEmptySpans;
110118
const declaration = findAncestor(node.parent, isAcceptedDeclaration);
111119
// make sure declaration have AccessibilityModifier or Static Modifier or Readonly Modifier
112120
const meaning = ModifierFlags.AccessibilityModifier | ModifierFlags.Static | ModifierFlags.Readonly;
113-
if (!declaration || !(nodeOverlapsWithStartEnd(declaration.name, file, start, end) || cursorRequest)
114-
|| !isConvertibleName(declaration.name) || (getEffectiveModifierFlags(declaration) | meaning) !== meaning) return undefined;
121+
122+
if (!declaration || (!(nodeOverlapsWithStartEnd(declaration.name, file, start, end) || cursorRequest))) {
123+
return {
124+
error: getLocaleSpecificMessage(Diagnostics.Could_not_find_property_for_which_to_generate_accessor)
125+
};
126+
}
127+
128+
if (!isConvertibleName(declaration.name)) {
129+
return {
130+
error: getLocaleSpecificMessage(Diagnostics.Name_is_not_valid)
131+
};
132+
}
133+
134+
if ((getEffectiveModifierFlags(declaration) | meaning) !== meaning) {
135+
return {
136+
error: getLocaleSpecificMessage(Diagnostics.Can_only_convert_property_with_modifier)
137+
};
138+
}
115139

116140
const name = declaration.name.text;
117141
const startWithUnderscore = startsWithUnderscore(name);
118142
const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name);
119143
const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name);
120144
return {
121-
isStatic: hasStaticModifier(declaration),
122-
isReadonly: hasEffectiveReadonlyModifier(declaration),
123-
type: getTypeAnnotationNode(declaration),
124-
container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
125-
originalName: (<AcceptedNameType>declaration.name).text,
126-
declaration,
127-
fieldName,
128-
accessorName,
129-
renameAccessor: startWithUnderscore
145+
info: {
146+
isStatic: hasStaticModifier(declaration),
147+
isReadonly: hasEffectiveReadonlyModifier(declaration),
148+
type: getTypeAnnotationNode(declaration),
149+
container: declaration.kind === SyntaxKind.Parameter ? declaration.parent.parent : declaration.parent,
150+
originalName: (<AcceptedNameType>declaration.name).text,
151+
declaration,
152+
fieldName,
153+
accessorName,
154+
renameAccessor: startWithUnderscore
155+
}
130156
};
131157
}
132158

src/services/refactors/addOrRemoveBracesToArrowFunction.ts

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,61 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
1515
addBraces: boolean;
1616
}
1717

18+
type InfoOrError = {
19+
info: Info,
20+
error?: never
21+
} | {
22+
info?: never,
23+
error: string
24+
};
25+
1826
function getAvailableActions(context: RefactorContext): readonly ApplicableRefactorInfo[] {
1927
const { file, startPosition, triggerReason } = context;
2028
const info = getConvertibleArrowFunctionAtPosition(file, startPosition, triggerReason === "invoked");
2129
if (!info) return emptyArray;
2230

23-
return [{
24-
name: refactorName,
25-
description: refactorDescription,
26-
actions: [
27-
info.addBraces ?
28-
{
29-
name: addBracesActionName,
30-
description: addBracesActionDescription
31-
} : {
32-
name: removeBracesActionName,
33-
description: removeBracesActionDescription
34-
}
35-
]
36-
}];
31+
if (info.error === undefined) {
32+
return [{
33+
name: refactorName,
34+
description: refactorDescription,
35+
actions: [
36+
info.info.addBraces ?
37+
{
38+
name: addBracesActionName,
39+
description: addBracesActionDescription
40+
} : {
41+
name: removeBracesActionName,
42+
description: removeBracesActionDescription
43+
}
44+
]
45+
}];
46+
}
47+
48+
if (context.preferences.provideRefactorNotApplicableReason) {
49+
return [{
50+
name: refactorName,
51+
description: refactorDescription,
52+
actions: [{
53+
name: addBracesActionName,
54+
description: addBracesActionDescription,
55+
notApplicableReason: info.error
56+
}, {
57+
name: removeBracesActionName,
58+
description: removeBracesActionDescription,
59+
notApplicableReason: info.error
60+
}]
61+
}];
62+
}
63+
64+
return emptyArray;
3765
}
3866

3967
function getEditsForAction(context: RefactorContext, actionName: string): RefactorEditInfo | undefined {
4068
const { file, startPosition } = context;
4169
const info = getConvertibleArrowFunctionAtPosition(file, startPosition);
42-
if (!info) return undefined;
70+
if (!info || !info.info) return undefined;
4371

44-
const { expression, returnStatement, func } = info;
72+
const { expression, returnStatement, func } = info.info;
4573

4674
let body: ConciseBody;
4775

@@ -70,28 +98,45 @@ namespace ts.refactor.addOrRemoveBracesToArrowFunction {
7098
return { renameFilename: undefined, renameLocation: undefined, edits };
7199
}
72100

73-
function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true): Info | undefined {
101+
function getConvertibleArrowFunctionAtPosition(file: SourceFile, startPosition: number, considerFunctionBodies = true): InfoOrError | undefined {
74102
const node = getTokenAtPosition(file, startPosition);
75103
const func = getContainingFunction(node);
76-
// Only offer a refactor in the function body on explicit refactor requests.
77-
if (!func || !isArrowFunction(func) || (!rangeContainsRange(func, node)
78-
|| (rangeContainsRange(func.body, node) && !considerFunctionBodies))) return undefined;
104+
105+
if (!func) {
106+
return {
107+
error: getLocaleSpecificMessage(Diagnostics.Could_not_find_a_containing_arrow_function)
108+
};
109+
}
110+
111+
if (!isArrowFunction(func)) {
112+
return {
113+
error: getLocaleSpecificMessage(Diagnostics.Containing_function_is_not_an_arrow_function)
114+
};
115+
}
116+
117+
if ((!rangeContainsRange(func, node) || rangeContainsRange(func.body, node) && !considerFunctionBodies)) {
118+
return undefined;
119+
}
79120

80121
if (isExpression(func.body)) {
81122
return {
82-
func,
83-
addBraces: true,
84-
expression: func.body
123+
info: {
124+
func,
125+
addBraces: true,
126+
expression: func.body
127+
}
85128
};
86129
}
87130
else if (func.body.statements.length === 1) {
88131
const firstStatement = first(func.body.statements);
89132
if (isReturnStatement(firstStatement)) {
90133
return {
91-
func,
92-
addBraces: false,
93-
expression: firstStatement.expression,
94-
returnStatement: firstStatement
134+
info: {
135+
func,
136+
addBraces: false,
137+
expression: firstStatement.expression,
138+
returnStatement: firstStatement
139+
}
95140
};
96141
}
97142
}

src/services/refactors/convertExport.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,30 @@ namespace ts.refactor {
33
const refactorName = "Convert export";
44
const actionNameDefaultToNamed = "Convert default export to named export";
55
const actionNameNamedToDefault = "Convert named export to default export";
6+
67
registerRefactor(refactorName, {
78
getAvailableActions(context): readonly ApplicableRefactorInfo[] {
89
const info = getInfo(context, context.triggerReason === "invoked");
910
if (!info) return emptyArray;
10-
const description = info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message;
11-
const actionName = info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault;
12-
return [{ name: refactorName, description, actions: [{ name: actionName, description }] }];
11+
12+
if (info.error === undefined) {
13+
const description = info.info.wasDefault ? Diagnostics.Convert_default_export_to_named_export.message : Diagnostics.Convert_named_export_to_default_export.message;
14+
const actionName = info.info.wasDefault ? actionNameDefaultToNamed : actionNameNamedToDefault;
15+
return [{ name: refactorName, description, actions: [{ name: actionName, description }] }];
16+
}
17+
18+
if (context.preferences.provideRefactorNotApplicableReason) {
19+
return [
20+
{ 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 }] },
21+
{ 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 }] },
22+
];
23+
}
24+
25+
return emptyArray;
1326
},
1427
getEditsForAction(context, actionName): RefactorEditInfo {
1528
Debug.assert(actionName === actionNameDefaultToNamed || actionName === actionNameNamedToDefault, "Unexpected action name");
16-
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.checkDefined(getInfo(context), "context must have info"), t, context.cancellationToken));
29+
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, Debug.checkDefined(getInfo(context)?.info, "context must have info"), t, context.cancellationToken));
1730
return { edits, renameFilename: undefined, renameLocation: undefined };
1831
},
1932
});
@@ -27,13 +40,21 @@ namespace ts.refactor {
2740
readonly exportingModuleSymbol: Symbol;
2841
}
2942

30-
function getInfo(context: RefactorContext, considerPartialSpans = true): Info | undefined {
43+
type InfoOrError = {
44+
info: Info,
45+
error?: never
46+
} | {
47+
info?: never,
48+
error: string
49+
};
50+
51+
function getInfo(context: RefactorContext, considerPartialSpans = true): InfoOrError | undefined {
3152
const { file } = context;
3253
const span = getRefactorContextSpan(context);
3354
const token = getTokenAtPosition(file, span.start);
3455
const exportNode = !!(token.parent && getSyntacticModifierFlags(token.parent) & ModifierFlags.Export) && considerPartialSpans ? token.parent : getParentNodeInSpan(token, file, span);
3556
if (!exportNode || (!isSourceFile(exportNode.parent) && !(isModuleBlock(exportNode.parent) && isAmbientModule(exportNode.parent.parent)))) {
36-
return undefined;
57+
return { error: getLocaleSpecificMessage(Diagnostics.Could_not_find_export_statement) };
3758
}
3859

3960
const exportingModuleSymbol = isSourceFile(exportNode.parent) ? exportNode.parent.symbol : exportNode.parent.parent.symbol;
@@ -42,7 +63,7 @@ namespace ts.refactor {
4263
const wasDefault = !!(flags & ModifierFlags.Default);
4364
// If source file already has a default export, don't offer refactor.
4465
if (!(flags & ModifierFlags.Export) || !wasDefault && exportingModuleSymbol.exports!.has(InternalSymbolName.Default)) {
45-
return undefined;
66+
return { error: getLocaleSpecificMessage(Diagnostics.This_file_already_has_a_default_export) };
4667
}
4768

4869
switch (exportNode.kind) {
@@ -53,7 +74,7 @@ namespace ts.refactor {
5374
case SyntaxKind.TypeAliasDeclaration:
5475
case SyntaxKind.ModuleDeclaration: {
5576
const node = exportNode as FunctionDeclaration | ClassDeclaration | InterfaceDeclaration | EnumDeclaration | TypeAliasDeclaration | NamespaceDeclaration;
56-
return node.name && isIdentifier(node.name) ? { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } : undefined;
77+
return node.name && isIdentifier(node.name) ? { info: { exportNode: node, exportName: node.name, wasDefault, exportingModuleSymbol } } : undefined;
5778
}
5879
case SyntaxKind.VariableStatement: {
5980
const vs = exportNode as VariableStatement;
@@ -64,7 +85,7 @@ namespace ts.refactor {
6485
const decl = first(vs.declarationList.declarations);
6586
if (!decl.initializer) return undefined;
6687
Debug.assert(!wasDefault, "Can't have a default flag here");
67-
return isIdentifier(decl.name) ? { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } : undefined;
88+
return isIdentifier(decl.name) ? { info: { exportNode: vs, exportName: decl.name, wasDefault, exportingModuleSymbol } } : undefined;
6889
}
6990
default:
7091
return undefined;

0 commit comments

Comments
 (0)