Skip to content

Commit 5577315

Browse files
author
Andy Hanson
committed
Support deleting all unused type parameters in a list, and deleting @template tag
1 parent 854462d commit 5577315

35 files changed

+322
-163
lines changed

src/compiler/checker.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23659,21 +23659,43 @@ namespace ts {
2365923659
}
2366023660
}
2366123661

23662-
function checkUnusedTypeParameters(
23663-
node: ClassDeclaration | ClassExpression | FunctionDeclaration | MethodDeclaration | FunctionExpression | ArrowFunction | ConstructorDeclaration | SignatureDeclaration | InterfaceDeclaration | TypeAliasDeclaration,
23664-
addDiagnostic: AddUnusedDiagnostic,
23665-
): void {
23662+
function checkUnusedTypeParameters(node: ClassLikeDeclaration | SignatureDeclaration | InterfaceDeclaration | TypeAliasDeclaration, addDiagnostic: AddUnusedDiagnostic): void {
2366623663
// Only report errors on the last declaration for the type parameter container;
2366723664
// this ensures that all uses have been accounted for.
2366823665
const typeParameters = getEffectiveTypeParameterDeclarations(node);
23669-
if (!(node.flags & NodeFlags.Ambient) && last(getSymbolOfNode(node).declarations) === node) {
23670-
for (const typeParameter of typeParameters) {
23671-
if (!(getMergedSymbol(typeParameter.symbol).isReferenced! & SymbolFlags.TypeParameter) && !isIdentifierThatStartsWithUnderscore(typeParameter.name)) {
23672-
addDiagnostic(typeParameter, UnusedKind.Parameter, createDiagnosticForNode(typeParameter.name, Diagnostics._0_is_declared_but_its_value_is_never_read, symbolName(typeParameter.symbol)));
23666+
if (node.flags & NodeFlags.Ambient || last(getSymbolOfNode(node).declarations) !== node) return;
23667+
23668+
const seenParentsWithEveryUnused = new NodeSet<DeclarationWithTypeParameterChildren>();
23669+
23670+
for (const typeParameter of typeParameters) {
23671+
if (!isTypeParameterUnused(typeParameter)) continue;
23672+
23673+
const parent = typeParameter.parent;
23674+
if (parent.kind === SyntaxKind.InferType) return Debug.fail(); // Not possible given the input type of `node`
23675+
23676+
const name = symbolName(typeParameter.symbol);
23677+
const typeParameters = parent.typeParameters!;
23678+
if (typeParameters.every(isTypeParameterUnused)) {
23679+
if (seenParentsWithEveryUnused.tryAdd(parent)) {
23680+
const range = isJSDocTemplateTag(parent)
23681+
// Whole @template tag
23682+
? rangeOfNode(parent)
23683+
// Include the `<>` in the error message
23684+
: rangeOfTypeParameters(typeParameters);
23685+
const only = typeParameters.length === 1;
23686+
const message = only ? Diagnostics._0_is_declared_but_its_value_is_never_read : Diagnostics.All_type_parameters_are_unused;
23687+
const arg0 = only ? name : undefined;
23688+
addDiagnostic(typeParameter, UnusedKind.Parameter, createFileDiagnostic(getSourceFileOfNode(parent), range.pos, range.end - range.pos, message, arg0));
2367323689
}
2367423690
}
23691+
else {
23692+
addDiagnostic(typeParameter, UnusedKind.Parameter, createDiagnosticForNode(typeParameter, Diagnostics._0_is_declared_but_its_value_is_never_read, name));
23693+
}
2367523694
}
2367623695
}
23696+
function isTypeParameterUnused(typeParameter: TypeParameterDeclaration): boolean {
23697+
return !(getMergedSymbol(typeParameter.symbol).isReferenced! & SymbolFlags.TypeParameter) && !isIdentifierThatStartsWithUnderscore(typeParameter.name);
23698+
}
2367723699

2367823700
function addToGroup<K, V>(map: Map<[K, V[]]>, key: K, value: V, getKey: (key: K) => number | string): void {
2367923701
const keyString = String(getKey(key));

src/compiler/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2093,7 +2093,7 @@ namespace ts {
20932093
return arg => f(arg) || g(arg);
20942094
}
20952095

2096-
export function assertTypeIsNever(_: never): void { } // tslint:disable-line no-empty
2096+
export function assertType<T>(_: T): void { } // tslint:disable-line no-empty
20972097

20982098
export function singleElementArray<T>(t: T | undefined): T[] | undefined {
20992099
return t === undefined ? undefined : [t];

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3639,6 +3639,10 @@
36393639
"category": "Message",
36403640
"code": 6204
36413641
},
3642+
"All type parameters are unused": {
3643+
"category": "Error",
3644+
"code": 6205
3645+
},
36423646

36433647
"Projects to reference": {
36443648
"category": "Message",
@@ -4188,6 +4192,14 @@
41884192
"category": "Message",
41894193
"code": 90010
41904194
},
4195+
"Remove template tag": {
4196+
"category": "Message",
4197+
"code": 90011
4198+
},
4199+
"Remove type parameters": {
4200+
"category": "Message",
4201+
"code": 90012
4202+
},
41914203
"Import '{0}' from module \"{1}\"": {
41924204
"category": "Message",
41934205
"code": 90013

src/compiler/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7019,8 +7019,8 @@ namespace ts {
70197019
skipWhitespace();
70207020
const typeParameter = <TypeParameterDeclaration>createNode(SyntaxKind.TypeParameter);
70217021
typeParameter.name = parseJSDocIdentifierName(Diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces);
7022-
skipWhitespace();
70237022
finishNode(typeParameter);
7023+
skipWhitespace();
70247024
typeParameters.push(typeParameter);
70257025
} while (parseOptionalJsdoc(SyntaxKind.CommaToken));
70267026

src/compiler/tsbuild.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1263,7 +1263,7 @@ namespace ts {
12631263
// Don't report status on "solution" projects
12641264
break;
12651265
default:
1266-
assertTypeIsNever(status);
1266+
assertType<never>(status);
12671267
}
12681268
}
12691269
}

src/compiler/utilities.ts

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,19 @@ namespace ts {
666666

667667
export function isDeclarationWithTypeParameters(node: Node): node is DeclarationWithTypeParameters;
668668
export function isDeclarationWithTypeParameters(node: DeclarationWithTypeParameters): node is DeclarationWithTypeParameters {
669+
switch (node.kind) {
670+
case SyntaxKind.JSDocCallbackTag:
671+
case SyntaxKind.JSDocTypedefTag:
672+
case SyntaxKind.JSDocSignature:
673+
return true;
674+
default:
675+
assertType<DeclarationWithTypeParameterChildren>(node);
676+
return isDeclarationWithTypeParameterChildren(node);
677+
}
678+
}
679+
680+
export function isDeclarationWithTypeParameterChildren(node: Node): node is DeclarationWithTypeParameterChildren;
681+
export function isDeclarationWithTypeParameterChildren(node: DeclarationWithTypeParameterChildren): node is DeclarationWithTypeParameterChildren {
669682
switch (node.kind) {
670683
case SyntaxKind.CallSignature:
671684
case SyntaxKind.ConstructSignature:
@@ -686,12 +699,9 @@ namespace ts {
686699
case SyntaxKind.SetAccessor:
687700
case SyntaxKind.FunctionExpression:
688701
case SyntaxKind.ArrowFunction:
689-
case SyntaxKind.JSDocCallbackTag:
690-
case SyntaxKind.JSDocTypedefTag:
691-
case SyntaxKind.JSDocSignature:
692702
return true;
693703
default:
694-
assertTypeIsNever(node);
704+
assertType<never>(node);
695705
return false;
696706
}
697707
}
@@ -776,7 +786,7 @@ namespace ts {
776786
return createDiagnosticForNodeInSourceFile(sourceFile, node, message, arg0, arg1, arg2, arg3);
777787
}
778788

779-
export function createDiagnosticForNodeArray(sourceFile: SourceFile, nodes: NodeArray<Node>, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): Diagnostic {
789+
export function createDiagnosticForNodeArray(sourceFile: SourceFile, nodes: NodeArray<Node>, message: DiagnosticMessage, arg0?: string | number, arg1?: string | number, arg2?: string | number, arg3?: string | number): DiagnosticWithLocation {
780790
const start = skipTrivia(sourceFile.text, nodes.pos);
781791
return createFileDiagnostic(sourceFile, start, nodes.end - start, message, arg0, arg1, arg2, arg3);
782792
}
@@ -8101,4 +8111,75 @@ namespace ts {
81018111
}
81028112
return { min, max };
81038113
}
8114+
8115+
export interface ReadonlyNodeSet<TNode extends Node> {
8116+
has(node: TNode): boolean;
8117+
forEach(cb: (node: TNode) => void): void;
8118+
some(pred: (node: TNode) => boolean): boolean;
8119+
}
8120+
8121+
export class NodeSet<TNode extends Node> implements ReadonlyNodeSet<TNode> {
8122+
private map = createMap<TNode>();
8123+
8124+
add(node: TNode): void {
8125+
this.map.set(String(getNodeId(node)), node);
8126+
}
8127+
tryAdd(node: TNode): boolean {
8128+
if (this.has(node)) return false;
8129+
this.add(node);
8130+
return true;
8131+
}
8132+
has(node: TNode): boolean {
8133+
return this.map.has(String(getNodeId(node)));
8134+
}
8135+
forEach(cb: (node: TNode) => void): void {
8136+
this.map.forEach(cb);
8137+
}
8138+
some(pred: (node: TNode) => boolean): boolean {
8139+
return forEachEntry(this.map, pred) || false;
8140+
}
8141+
}
8142+
8143+
export interface ReadonlyNodeMap<TNode extends Node, TValue> {
8144+
get(node: TNode): TValue | undefined;
8145+
has(node: TNode): boolean;
8146+
}
8147+
8148+
export class NodeMap<TNode extends Node, TValue> implements ReadonlyNodeMap<TNode, TValue> {
8149+
private map = createMap<{ node: TNode, value: TValue }>();
8150+
8151+
get(node: TNode): TValue | undefined {
8152+
const res = this.map.get(String(getNodeId(node)));
8153+
return res && res.value;
8154+
}
8155+
8156+
getOrUpdate(node: TNode, setValue: () => TValue): TValue {
8157+
const res = this.get(node);
8158+
if (res) return res;
8159+
const value = setValue();
8160+
this.set(node, value);
8161+
return value;
8162+
}
8163+
8164+
set(node: TNode, value: TValue): void {
8165+
this.map.set(String(getNodeId(node)), { node, value });
8166+
}
8167+
8168+
has(node: TNode): boolean {
8169+
return this.map.has(String(getNodeId(node)));
8170+
}
8171+
8172+
forEach(cb: (value: TValue, node: TNode) => void): void {
8173+
this.map.forEach(({ node, value }) => cb(value, node));
8174+
}
8175+
}
8176+
8177+
export function rangeOfNode(node: Node): TextRange {
8178+
return { pos: getTokenPosOfNode(node), end: node.end };
8179+
}
8180+
8181+
export function rangeOfTypeParameters(typeParameters: NodeArray<TypeParameterDeclaration>): TextRange {
8182+
// Include the `<>`
8183+
return { pos: typeParameters.pos - 1, end: typeParameters.end + 1 };
8184+
}
81048185
}

src/services/codefixes/fixUnusedIdentifier.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace ts.codefix {
1010
Diagnostics.All_imports_in_import_declaration_are_unused.code,
1111
Diagnostics.All_destructured_elements_are_unused.code,
1212
Diagnostics.All_variables_are_unused.code,
13+
Diagnostics.All_type_parameters_are_unused.code,
1314
];
1415

1516
registerCodeFix({
@@ -20,19 +21,26 @@ namespace ts.codefix {
2021
const sourceFiles = program.getSourceFiles();
2122
const token = getTokenAtPosition(sourceFile, context.span.start);
2223

24+
if (isJSDocTemplateTag(token)) {
25+
return [createDeleteFix(textChanges.ChangeTracker.with(context, t => t.delete(sourceFile, token)), Diagnostics.Remove_template_tag)];
26+
}
27+
if (token.kind === SyntaxKind.LessThanToken) {
28+
const changes = textChanges.ChangeTracker.with(context, t => deleteTypeParameters(t, sourceFile, token));
29+
return [createDeleteFix(changes, Diagnostics.Remove_type_parameters)];
30+
}
2331
const importDecl = tryGetFullImport(token);
2432
if (importDecl) {
2533
const changes = textChanges.ChangeTracker.with(context, t => t.delete(sourceFile, importDecl));
26-
return [createCodeFixAction(fixName, changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)], fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
34+
return [createDeleteFix(changes, [Diagnostics.Remove_import_from_0, showModuleSpecifier(importDecl)])];
2735
}
2836
const delDestructure = textChanges.ChangeTracker.with(context, t =>
2937
tryDeleteFullDestructure(token, t, sourceFile, checker, sourceFiles, /*isFixAll*/ false));
3038
if (delDestructure.length) {
31-
return [createCodeFixAction(fixName, delDestructure, Diagnostics.Remove_destructuring, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
39+
return [createDeleteFix(delDestructure, Diagnostics.Remove_destructuring)];
3240
}
3341
const delVar = textChanges.ChangeTracker.with(context, t => tryDeleteFullVariableStatement(sourceFile, token, t));
3442
if (delVar.length) {
35-
return [createCodeFixAction(fixName, delVar, Diagnostics.Remove_variable_statement, fixIdDelete, Diagnostics.Delete_all_unused_declarations)];
43+
return [createDeleteFix(delVar, Diagnostics.Remove_variable_statement)];
3644
}
3745

3846
const result: CodeFixAction[] = [];
@@ -41,7 +49,7 @@ namespace ts.codefix {
4149
tryDeleteDeclaration(sourceFile, token, t, checker, sourceFiles, /*isFixAll*/ false));
4250
if (deletion.length) {
4351
const name = isComputedPropertyName(token.parent) ? token.parent : token;
44-
result.push(createCodeFixAction(fixName, deletion, [Diagnostics.Remove_declaration_for_Colon_0, name.getText(sourceFile)], fixIdDelete, Diagnostics.Delete_all_unused_declarations));
52+
result.push(createDeleteFix(deletion, [Diagnostics.Remove_declaration_for_Colon_0, name.getText(sourceFile)]));
4553
}
4654

4755
const prefix = textChanges.ChangeTracker.with(context, t => tryPrefixDeclaration(t, errorCode, sourceFile, token));
@@ -69,6 +77,12 @@ namespace ts.codefix {
6977
if (importDecl) {
7078
changes.delete(sourceFile, importDecl);
7179
}
80+
else if (isJSDocTemplateTag(token)) {
81+
changes.delete(sourceFile, token);
82+
}
83+
else if (token.kind === SyntaxKind.LessThanToken) {
84+
deleteTypeParameters(changes, sourceFile, token);
85+
}
7286
else if (!tryDeleteFullDestructure(token, changes, sourceFile, checker, sourceFiles, /*isFixAll*/ true) &&
7387
!tryDeleteFullVariableStatement(sourceFile, token, changes)) {
7488
tryDeleteDeclaration(sourceFile, token, changes, checker, sourceFiles, /*isFixAll*/ true);
@@ -82,6 +96,14 @@ namespace ts.codefix {
8296
},
8397
});
8498

99+
function createDeleteFix(changes: FileTextChanges[], diag: DiagnosticAndArguments): CodeFixAction {
100+
return createCodeFixAction(fixName, changes, diag, fixIdDelete, Diagnostics.Delete_all_unused_declarations);
101+
}
102+
103+
function deleteTypeParameters(changes: textChanges.ChangeTracker, sourceFile: SourceFile, token: Node): void {
104+
changes.delete(sourceFile, Debug.assertDefined(cast(token.parent, isDeclarationWithTypeParameterChildren).typeParameters));
105+
}
106+
85107
// Sometimes the diagnostic span is an entire ImportDeclaration, so we should remove the whole thing.
86108
function tryGetFullImport(token: Node): ImportDeclaration | undefined {
87109
return token.kind === SyntaxKind.ImportKeyword ? tryCast(token.parent, isImportDeclaration) : undefined;

src/services/formatting/smartIndenter.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -354,12 +354,15 @@ namespace ts.formatting {
354354
case SyntaxKind.ClassExpression:
355355
case SyntaxKind.InterfaceDeclaration:
356356
case SyntaxKind.TypeAliasDeclaration:
357-
return getListIfStartEndIsInListRange((<ClassDeclaration | ClassExpression | InterfaceDeclaration | TypeAliasDeclaration>node.parent).typeParameters, node.getStart(sourceFile), end);
357+
case SyntaxKind.JSDocTemplateTag: {
358+
const { typeParameters } = <ClassDeclaration | ClassExpression | InterfaceDeclaration | TypeAliasDeclaration | JSDocTemplateTag>node.parent;
359+
return getListIfStartEndIsInListRange(typeParameters, node.getStart(sourceFile), end);
360+
}
358361
case SyntaxKind.NewExpression:
359362
case SyntaxKind.CallExpression: {
360363
const start = node.getStart(sourceFile);
361-
return getListIfStartEndIsInListRange((<CallExpression>node.parent).typeArguments, start, end) ||
362-
getListIfStartEndIsInListRange((<CallExpression>node.parent).arguments, start, end);
364+
return getListIfStartEndIsInListRange((<CallExpression | NewExpression>node.parent).typeArguments, start, end) ||
365+
getListIfStartEndIsInListRange((<CallExpression | NewExpression>node.parent).arguments, start, end);
363366
}
364367
case SyntaxKind.VariableDeclarationList:
365368
return getListIfStartEndIsInListRange((<VariableDeclarationList>node.parent).declarations, node.getStart(sourceFile), end);

src/services/refactors/extractSymbol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1240,7 +1240,7 @@ namespace ts.refactor.extractSymbol {
12401240
return scope.members;
12411241
}
12421242
else {
1243-
assertTypeIsNever(scope);
1243+
assertType<never>(scope);
12441244
}
12451245

12461246
return emptyArray;

0 commit comments

Comments
 (0)