Skip to content

Commit e132ab5

Browse files
authored
Add option to ignore class member implementations (#1174)
1 parent 8deecde commit e132ab5

File tree

3 files changed

+29
-8
lines changed

3 files changed

+29
-8
lines changed

packages/knip/fixtures/class-members/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import { MyClass } from './members';
33
import { AbstractClassGen, ExtendedClassGen } from './iterator-generator';
44
import { AbstractClass, ExtendedClass } from './iterator';
55

6-
AbstractClassGen;
7-
ExtendedClassGen;
86
AbstractClass;
9-
ExtendedClass;
107

118
const instance = new MyClass();
129
const some = new SomeClass();
10+
const instance2 = new ExtendedClass();
11+
const instance3: AbstractClassGen = new ExtendedClassGen();
1312

1413
export class Parent {
1514
instance: MyClass;
@@ -27,3 +26,5 @@ instance.bUsedExternal;
2726
instance.cUsedExternal;
2827
instance.dUsedExternal();
2928
MyClass.eUsedExternal;
29+
instance2.implemented;
30+
instance3.implemented;

packages/knip/src/ProjectPrincipal.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export class ProjectPrincipal {
7878
};
7979

8080
findReferences?: ts.LanguageService['findReferences'];
81+
getImplementationAtPosition?: ts.LanguageService['getImplementationAtPosition'];
8182

8283
constructor({
8384
compilerOptions,
@@ -260,14 +261,30 @@ export class ProjectPrincipal {
260261
}
261262

262263
public findUnusedMembers(filePath: string, members: ExportMember[]) {
263-
if (!this.findReferences) {
264+
if (!this.findReferences || !this.getImplementationAtPosition) {
264265
const languageService = ts.createLanguageService(this.backend.languageServiceHost, ts.createDocumentRegistry());
265266
this.findReferences = timerify(languageService.findReferences);
267+
this.getImplementationAtPosition = timerify(languageService.getImplementationAtPosition);
266268
}
267269

268270
return members.filter(member => {
269271
if (member.jsDocTags.has(PUBLIC_TAG)) return false;
270-
const referencedSymbols = this.findReferences?.(filePath, member.pos) ?? [];
272+
const implementations =
273+
this.getImplementationAtPosition?.(filePath, member.pos)?.filter(
274+
impl => impl.fileName !== filePath || impl.textSpan.start !== member.pos
275+
) ?? [];
276+
277+
const referencedSymbols =
278+
this.findReferences?.(filePath, member.pos)?.filter(
279+
sym =>
280+
!implementations.some(
281+
impl =>
282+
impl.fileName === sym.definition.fileName &&
283+
impl.textSpan.start === sym.definition.textSpan.start &&
284+
impl.textSpan.length === sym.definition.textSpan.length
285+
)
286+
) ?? [];
287+
271288
const refs = referencedSymbols.flatMap(refs => refs.references).filter(ref => !ref.isDefinition);
272289
return refs.length === 0;
273290
});
@@ -276,9 +293,10 @@ export class ProjectPrincipal {
276293
public hasExternalReferences(filePath: string, exportedItem: Export) {
277294
if (exportedItem.jsDocTags.has(PUBLIC_TAG)) return false;
278295

279-
if (!this.findReferences) {
296+
if (!this.findReferences || !this.getImplementationAtPosition) {
280297
const languageService = ts.createLanguageService(this.backend.languageServiceHost, ts.createDocumentRegistry());
281298
this.findReferences = timerify(languageService.findReferences);
299+
this.getImplementationAtPosition = timerify(languageService.getImplementationAtPosition);
282300
}
283301

284302
const referencedSymbols = this.findReferences(filePath, exportedItem.pos);

packages/knip/test/class-members.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ test('Find unused class members', async () => {
1515

1616
assert.equal(Object.keys(issues.classMembers['members.ts']).length, 6);
1717
assert(issues.classMembers['iterator-generator.ts']['AbstractClassGen.unimplemented']);
18+
assert(issues.classMembers['iterator.ts']['AbstractClass.implemented']);
1819
assert(issues.classMembers['members.ts']['MyClass.bUnusedPublic']);
1920
assert(issues.classMembers['members.ts']['MyClass.cUnusedProp']);
2021
assert(issues.classMembers['members.ts']['MyClass.dUnusedMember']);
@@ -24,7 +25,7 @@ test('Find unused class members', async () => {
2425

2526
assert.deepEqual(counters, {
2627
...baseCounters,
27-
classMembers: 7,
28+
classMembers: 8,
2829
processed: 5,
2930
total: 5,
3031
});
@@ -39,6 +40,7 @@ test('Find unused class members (isIncludeEntryExports)', async () => {
3940

4041
assert.equal(Object.keys(issues.classMembers['members.ts']).length, 6);
4142
assert(issues.classMembers['iterator-generator.ts']['AbstractClassGen.unimplemented']);
43+
assert(issues.classMembers['iterator.ts']['AbstractClass.implemented']);
4244
assert(issues.classMembers['index.ts']['Parent.unusedMemberInEntry']);
4345
assert(issues.classMembers['members.ts']['MyClass.bUnusedPublic']);
4446
assert(issues.classMembers['members.ts']['MyClass.cUnusedProp']);
@@ -49,7 +51,7 @@ test('Find unused class members (isIncludeEntryExports)', async () => {
4951

5052
assert.deepEqual(counters, {
5153
...baseCounters,
52-
classMembers: 8,
54+
classMembers: 9,
5355
processed: 5,
5456
total: 5,
5557
});

0 commit comments

Comments
 (0)