mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-18 07:29:16 -05:00
Support deleting all unused type parameters in a list, and deleting @template tag (#25748)
* Support deleting all unused type parameters in a list, and deleting @template tag * Support type parameter in 'infer'
This commit is contained in:
@@ -3,6 +3,7 @@ namespace ts.codefix {
|
||||
const fixName = "unusedIdentifier";
|
||||
const fixIdPrefix = "unusedIdentifier_prefix";
|
||||
const fixIdDelete = "unusedIdentifier_delete";
|
||||
const fixIdInfer = "unusedIdentifier_infer";
|
||||
const errorCodes = [
|
||||
Diagnostics._0_is_declared_but_its_value_is_never_read.code,
|
||||
Diagnostics._0_is_declared_but_never_used.code,
|
||||
@@ -10,6 +11,7 @@ namespace ts.codefix {
|
||||
Diagnostics.All_imports_in_import_declaration_are_unused.code,
|
||||
Diagnostics.All_destructured_elements_are_unused.code,
|
||||
Diagnostics.All_variables_are_unused.code,
|
||||
Diagnostics.All_type_parameters_are_unused.code,
|
||||
];
|
||||
|
||||
registerCodeFix({
|
||||
@@ -20,28 +22,42 @@ namespace ts.codefix {
|
||||
const sourceFiles = program.getSourceFiles();
|
||||
const token = getTokenAtPosition(sourceFile, context.span.start);
|
||||
|
||||
if (isJSDocTemplateTag(token)) {
|
||||
return [createDeleteFix(textChanges.ChangeTracker.with(context, t => t.delete(sourceFile, token)), Diagnostics.Remove_template_tag)];
|
||||
}
|
||||
if (token.kind === SyntaxKind.LessThanToken) {
|
||||
const changes = textChanges.ChangeTracker.with(context, t => deleteTypeParameters(t, sourceFile, token));
|
||||
return [createDeleteFix(changes, Diagnostics.Remove_type_parameters)];
|
||||
}
|
||||
const importDecl = tryGetFullImport(token);
|
||||
if (importDecl) {
|
||||
const changes = textChanges.ChangeTracker.with(context, t => t.delete(sourceFile, importDecl));
|
||||
return [createCodeFixAction(fixName, changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)], fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
|
||||
return [createDeleteFix(changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)])];
|
||||
}
|
||||
const delDestructure = textChanges.ChangeTracker.with(context, t =>
|
||||
tryDeleteFullDestructure(token, t, sourceFile, checker, sourceFiles, /*isFixAll*/ false));
|
||||
if (delDestructure.length) {
|
||||
return [createCodeFixAction(fixName, delDestructure, Diagnostics.Remove_destructuring, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
|
||||
return [createDeleteFix(delDestructure, Diagnostics.Remove_destructuring)];
|
||||
}
|
||||
const delVar = textChanges.ChangeTracker.with(context, t => tryDeleteFullVariableStatement(sourceFile, token, t));
|
||||
if (delVar.length) {
|
||||
return [createCodeFixAction(fixName, delVar, Diagnostics.Remove_variable_statement, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
|
||||
return [createDeleteFix(delVar, Diagnostics.Remove_variable_statement)];
|
||||
}
|
||||
|
||||
const result: CodeFixAction[] = [];
|
||||
|
||||
const deletion = textChanges.ChangeTracker.with(context, t =>
|
||||
tryDeleteDeclaration(sourceFile, token, t, checker, sourceFiles, /*isFixAll*/ false));
|
||||
if (deletion.length) {
|
||||
const name = isComputedPropertyName(token.parent) ? token.parent : token;
|
||||
result.push(createCodeFixAction(fixName, deletion, [Diagnostics.Remove_declaration_for_Colon_0, name.getText(sourceFile)], fixIdDelete, Diagnostics.Delete_all_unused_declarations));
|
||||
if (token.kind === SyntaxKind.InferKeyword) {
|
||||
const changes = textChanges.ChangeTracker.with(context, t => changeInferToUnknown(t, sourceFile, token));
|
||||
const name = cast(token.parent, isInferTypeNode).typeParameter.name.text;
|
||||
result.push(createCodeFixAction(fixName, changes, [Diagnostics.Replace_infer_0_with_unknown, name], fixIdInfer, Diagnostics.Replace_all_unused_infer_with_unknown));
|
||||
}
|
||||
else {
|
||||
const deletion = textChanges.ChangeTracker.with(context, t =>
|
||||
tryDeleteDeclaration(sourceFile, token, t, checker, sourceFiles, /*isFixAll*/ false));
|
||||
if (deletion.length) {
|
||||
const name = isComputedPropertyName(token.parent) ? token.parent : token;
|
||||
result.push(createDeleteFix(deletion, [Diagnostics.Remove_declaration_for_Colon_0, name.getText(sourceFile)]));
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = textChanges.ChangeTracker.with(context, t => tryPrefixDeclaration(t, errorCode, sourceFile, token));
|
||||
@@ -51,7 +67,7 @@ namespace ts.codefix {
|
||||
|
||||
return result;
|
||||
},
|
||||
fixIds: [fixIdPrefix, fixIdDelete],
|
||||
fixIds: [fixIdPrefix, fixIdDelete, fixIdInfer],
|
||||
getAllCodeActions: context => {
|
||||
const { sourceFile, program } = context;
|
||||
const checker = program.getTypeChecker();
|
||||
@@ -60,21 +76,31 @@ namespace ts.codefix {
|
||||
const token = getTokenAtPosition(sourceFile, diag.start);
|
||||
switch (context.fixId) {
|
||||
case fixIdPrefix:
|
||||
if (isIdentifier(token) && canPrefix(token)) {
|
||||
tryPrefixDeclaration(changes, diag.code, sourceFile, token);
|
||||
}
|
||||
tryPrefixDeclaration(changes, diag.code, sourceFile, token);
|
||||
break;
|
||||
case fixIdDelete: {
|
||||
if (token.kind === SyntaxKind.InferKeyword) break; // Can't delete
|
||||
const importDecl = tryGetFullImport(token);
|
||||
if (importDecl) {
|
||||
changes.delete(sourceFile, importDecl);
|
||||
}
|
||||
else if (isJSDocTemplateTag(token)) {
|
||||
changes.delete(sourceFile, token);
|
||||
}
|
||||
else if (token.kind === SyntaxKind.LessThanToken) {
|
||||
deleteTypeParameters(changes, sourceFile, token);
|
||||
}
|
||||
else if (!tryDeleteFullDestructure(token, changes, sourceFile, checker, sourceFiles, /*isFixAll*/ true) &&
|
||||
!tryDeleteFullVariableStatement(sourceFile, token, changes)) {
|
||||
tryDeleteDeclaration(sourceFile, token, changes, checker, sourceFiles, /*isFixAll*/ true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case fixIdInfer:
|
||||
if (token.kind === SyntaxKind.InferKeyword) {
|
||||
changeInferToUnknown(changes, sourceFile, token);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Debug.fail(JSON.stringify(context.fixId));
|
||||
}
|
||||
@@ -82,6 +108,18 @@ namespace ts.codefix {
|
||||
},
|
||||
});
|
||||
|
||||
function changeInferToUnknown(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node): void {
|
||||
changes.replaceNode(sourceFile, token.parent, createKeywordTypeNode(SyntaxKind.UnknownKeyword));
|
||||
}
|
||||
|
||||
function createDeleteFix(changes: FileTextChanges[], diag: DiagnosticAndArguments): CodeFixAction {
|
||||
return createCodeFixAction(fixName, changes, diag, fixIdDelete, Diagnostics.Delete_all_unused_declarations);
|
||||
}
|
||||
|
||||
function deleteTypeParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node): void {
|
||||
changes.delete(sourceFile, Debug.assertDefined(cast(token.parent, isDeclarationWithTypeParameterChildren).typeParameters));
|
||||
}
|
||||
|
||||
// Sometimes the diagnostic span is an entire ImportDeclaration, so we should remove the whole thing.
|
||||
function tryGetFullImport(token: Node): ImportDeclaration | undefined {
|
||||
return token.kind === SyntaxKind.ImportKeyword ? tryCast(token.parent, isImportDeclaration) : undefined;
|
||||
@@ -110,7 +148,11 @@ namespace ts.codefix {
|
||||
|
||||
function tryPrefixDeclaration(changes: textChanges.ChangeTracker, errorCode: number, sourceFile: SourceFile, token: Node): void {
|
||||
// Don't offer to prefix a property.
|
||||
if (errorCode !== Diagnostics.Property_0_is_declared_but_its_value_is_never_read.code && isIdentifier(token) && canPrefix(token)) {
|
||||
if (errorCode === Diagnostics.Property_0_is_declared_but_its_value_is_never_read.code) return;
|
||||
if (token.kind === SyntaxKind.InferKeyword) {
|
||||
token = cast(token.parent, isInferTypeNode).typeParameter.name;
|
||||
}
|
||||
if (isIdentifier(token) && canPrefix(token)) {
|
||||
changes.replaceNode(sourceFile, token, createIdentifier(`_${token.text}`));
|
||||
}
|
||||
}
|
||||
@@ -118,6 +160,7 @@ namespace ts.codefix {
|
||||
function canPrefix(token: Identifier): boolean {
|
||||
switch (token.parent.kind) {
|
||||
case SyntaxKind.Parameter:
|
||||
case SyntaxKind.TypeParameter:
|
||||
return true;
|
||||
case SyntaxKind.VariableDeclaration: {
|
||||
const varDecl = token.parent as VariableDeclaration;
|
||||
|
||||
@@ -354,12 +354,15 @@ namespace ts.formatting {
|
||||
case SyntaxKind.ClassExpression:
|
||||
case SyntaxKind.InterfaceDeclaration:
|
||||
case SyntaxKind.TypeAliasDeclaration:
|
||||
return getListIfStartEndIsInListRange((<ClassDeclaration | ClassExpression | InterfaceDeclaration | TypeAliasDeclaration>node.parent).typeParameters, node.getStart(sourceFile), end);
|
||||
case SyntaxKind.JSDocTemplateTag: {
|
||||
const { typeParameters } = <ClassDeclaration | ClassExpression | InterfaceDeclaration | TypeAliasDeclaration | JSDocTemplateTag>node.parent;
|
||||
return getListIfStartEndIsInListRange(typeParameters, node.getStart(sourceFile), end);
|
||||
}
|
||||
case SyntaxKind.NewExpression:
|
||||
case SyntaxKind.CallExpression: {
|
||||
const start = node.getStart(sourceFile);
|
||||
return getListIfStartEndIsInListRange((<CallExpression>node.parent).typeArguments, start, end) ||
|
||||
getListIfStartEndIsInListRange((<CallExpression>node.parent).arguments, start, end);
|
||||
return getListIfStartEndIsInListRange((<CallExpression | NewExpression>node.parent).typeArguments, start, end) ||
|
||||
getListIfStartEndIsInListRange((<CallExpression | NewExpression>node.parent).arguments, start, end);
|
||||
}
|
||||
case SyntaxKind.VariableDeclarationList:
|
||||
return getListIfStartEndIsInListRange((<VariableDeclarationList>node.parent).declarations, node.getStart(sourceFile), end);
|
||||
|
||||
@@ -1240,7 +1240,7 @@ namespace ts.refactor.extractSymbol {
|
||||
return scope.members;
|
||||
}
|
||||
else {
|
||||
assertTypeIsNever(scope);
|
||||
assertType<never>(scope);
|
||||
}
|
||||
|
||||
return emptyArray;
|
||||
|
||||
@@ -213,7 +213,7 @@ namespace ts.textChanges {
|
||||
private readonly changes: Change[] = [];
|
||||
private readonly newFiles: { readonly oldFile: SourceFile, readonly fileName: string, readonly statements: ReadonlyArray<Statement> }[] = [];
|
||||
private readonly classesWithNodesInsertedAtStart = createMap<ClassDeclaration>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
|
||||
private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node }[] = [];
|
||||
private readonly deletedNodes: { readonly sourceFile: SourceFile, readonly node: Node | NodeArray<TypeParameterDeclaration> }[] = [];
|
||||
|
||||
public static fromContext(context: TextChangesContext): ChangeTracker {
|
||||
return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext);
|
||||
@@ -233,8 +233,8 @@ namespace ts.textChanges {
|
||||
return this;
|
||||
}
|
||||
|
||||
delete(sourceFile: SourceFile, node: Node): void {
|
||||
this.deletedNodes.push({ sourceFile, node, });
|
||||
delete(sourceFile: SourceFile, node: Node | NodeArray<TypeParameterDeclaration>): void {
|
||||
this.deletedNodes.push({ sourceFile, node });
|
||||
}
|
||||
|
||||
public deleteModifier(sourceFile: SourceFile, modifier: Modifier): void {
|
||||
@@ -661,7 +661,12 @@ namespace ts.textChanges {
|
||||
const deletedNodesInLists = new NodeSet(); // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
|
||||
for (const { sourceFile, node } of this.deletedNodes) {
|
||||
if (!this.deletedNodes.some(d => d.sourceFile === sourceFile && rangeContainsRangeExclusive(d.node, node))) {
|
||||
deleteDeclaration.deleteDeclaration(this, deletedNodesInLists, sourceFile, node);
|
||||
if (isArray(node)) {
|
||||
this.deleteRange(sourceFile, rangeOfTypeParameters(node));
|
||||
}
|
||||
else {
|
||||
deleteDeclaration.deleteDeclaration(this, deletedNodesInLists, sourceFile, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1001,7 +1006,7 @@ namespace ts.textChanges {
|
||||
}
|
||||
|
||||
namespace deleteDeclaration {
|
||||
export function deleteDeclaration(changes: ChangeTracker, deletedNodesInLists: NodeSet, sourceFile: SourceFile, node: Node): void {
|
||||
export function deleteDeclaration(changes: ChangeTracker, deletedNodesInLists: NodeSet<Node>, sourceFile: SourceFile, node: Node): void {
|
||||
switch (node.kind) {
|
||||
case SyntaxKind.Parameter: {
|
||||
const oldFunction = node.parent;
|
||||
@@ -1052,33 +1057,9 @@ namespace ts.textChanges {
|
||||
deleteVariableDeclaration(changes, deletedNodesInLists, sourceFile, node as VariableDeclaration);
|
||||
break;
|
||||
|
||||
case SyntaxKind.TypeParameter: {
|
||||
const typeParam = node as TypeParameterDeclaration;
|
||||
switch (typeParam.parent.kind) {
|
||||
case SyntaxKind.JSDocTemplateTag:
|
||||
changes.deleteRange(sourceFile, getRangeToDeleteJsDocTag(typeParam.parent, sourceFile));
|
||||
break;
|
||||
case SyntaxKind.InferType:
|
||||
// TODO: GH#25594
|
||||
break;
|
||||
default: {
|
||||
const typeParameters = getEffectiveTypeParameterDeclarations(typeParam.parent);
|
||||
if (typeParameters.length === 1) {
|
||||
const { pos, end } = cast(typeParameters, isNodeArray);
|
||||
const previousToken = getTokenAtPosition(sourceFile, pos - 1);
|
||||
const nextToken = getTokenAtPosition(sourceFile, end);
|
||||
Debug.assert(previousToken.kind === SyntaxKind.LessThanToken);
|
||||
Debug.assert(nextToken.kind === SyntaxKind.GreaterThanToken);
|
||||
|
||||
changes.deleteNodeRange(sourceFile, previousToken, nextToken);
|
||||
}
|
||||
else {
|
||||
deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
case SyntaxKind.TypeParameter:
|
||||
deleteNodeInList(changes, deletedNodesInLists, sourceFile, node);
|
||||
break;
|
||||
}
|
||||
|
||||
case SyntaxKind.ImportSpecifier:
|
||||
const namedImports = (node as ImportSpecifier).parent;
|
||||
@@ -1144,7 +1125,7 @@ namespace ts.textChanges {
|
||||
}
|
||||
}
|
||||
|
||||
function deleteVariableDeclaration(changes: ChangeTracker, deletedNodesInLists: NodeSet, sourceFile: SourceFile, node: VariableDeclaration): void {
|
||||
function deleteVariableDeclaration(changes: ChangeTracker, deletedNodesInLists: NodeSet<Node>, sourceFile: SourceFile, node: VariableDeclaration): void {
|
||||
const { parent } = node;
|
||||
|
||||
if (parent.kind === SyntaxKind.CatchClause) {
|
||||
@@ -1177,12 +1158,6 @@ namespace ts.textChanges {
|
||||
Debug.assertNever(gp);
|
||||
}
|
||||
}
|
||||
|
||||
function getRangeToDeleteJsDocTag(node: JSDocTag, sourceFile: SourceFile): TextRange {
|
||||
const { parent } = node;
|
||||
const toDelete = parent.kind === SyntaxKind.JSDocComment && parent.comment === undefined && parent.tags!.length === 1 ? parent : node;
|
||||
return createTextRangeFromNode(toDelete, sourceFile);
|
||||
}
|
||||
}
|
||||
|
||||
/** Warning: This deletes comments too. See `copyComments` in `convertFunctionToEs6Class`. */
|
||||
@@ -1193,7 +1168,7 @@ namespace ts.textChanges {
|
||||
changes.deleteRange(sourceFile, { pos: startPosition, end: endPosition });
|
||||
}
|
||||
|
||||
function deleteNodeInList(changes: ChangeTracker, deletedNodesInLists: NodeSet, sourceFile: SourceFile, node: Node): void {
|
||||
function deleteNodeInList(changes: ChangeTracker, deletedNodesInLists: NodeSet<Node>, sourceFile: SourceFile, node: Node): void {
|
||||
const containingList = Debug.assertDefined(formatting.SmartIndenter.getContainingList(node, sourceFile));
|
||||
const index = indexOfNode(containingList, node);
|
||||
Debug.assert(index !== -1);
|
||||
|
||||
@@ -374,7 +374,7 @@ namespace ts {
|
||||
case SpecialPropertyAssignmentKind.Prototype:
|
||||
return ScriptElementKind.localClassElement;
|
||||
default: {
|
||||
assertTypeIsNever(kind);
|
||||
assertType<never>(kind);
|
||||
return ScriptElementKind.unknown;
|
||||
}
|
||||
}
|
||||
@@ -1358,63 +1358,6 @@ namespace ts {
|
||||
return getPropertySymbolsFromBaseTypes(memberSymbol.parent!, memberSymbol.name, checker, _ => true) || false;
|
||||
}
|
||||
|
||||
export interface ReadonlyNodeSet {
|
||||
has(node: Node): boolean;
|
||||
forEach(cb: (node: Node) => void): void;
|
||||
some(pred: (node: Node) => boolean): boolean;
|
||||
}
|
||||
|
||||
export class NodeSet implements ReadonlyNodeSet {
|
||||
private map = createMap<Node>();
|
||||
|
||||
add(node: Node): void {
|
||||
this.map.set(String(getNodeId(node)), node);
|
||||
}
|
||||
has(node: Node): boolean {
|
||||
return this.map.has(String(getNodeId(node)));
|
||||
}
|
||||
forEach(cb: (node: Node) => void): void {
|
||||
this.map.forEach(cb);
|
||||
}
|
||||
some(pred: (node: Node) => boolean): boolean {
|
||||
return forEachEntry(this.map, pred) || false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReadonlyNodeMap<TNode extends Node, TValue> {
|
||||
get(node: TNode): TValue | undefined;
|
||||
has(node: TNode): boolean;
|
||||
}
|
||||
|
||||
export class NodeMap<TNode extends Node, TValue> implements ReadonlyNodeMap<TNode, TValue> {
|
||||
private map = createMap<{ node: TNode, value: TValue }>();
|
||||
|
||||
get(node: TNode): TValue | undefined {
|
||||
const res = this.map.get(String(getNodeId(node)));
|
||||
return res && res.value;
|
||||
}
|
||||
|
||||
getOrUpdate(node: TNode, setValue: () => TValue): TValue {
|
||||
const res = this.get(node);
|
||||
if (res) return res;
|
||||
const value = setValue();
|
||||
this.set(node, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
set(node: TNode, value: TValue): void {
|
||||
this.map.set(String(getNodeId(node)), { node, value });
|
||||
}
|
||||
|
||||
has(node: TNode): boolean {
|
||||
return this.map.has(String(getNodeId(node)));
|
||||
}
|
||||
|
||||
forEach(cb: (value: TValue, node: TNode) => void): void {
|
||||
this.map.forEach(({ node, value }) => cb(value, node));
|
||||
}
|
||||
}
|
||||
|
||||
export function getParentNodeInSpan(node: Node | undefined, file: SourceFile, span: TextSpan): Node | undefined {
|
||||
if (!node) return undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user