Skip to content

Commit

Permalink
Add typeMembers issue type
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Jan 12, 2025
1 parent 5a77dcc commit def96eb
Show file tree
Hide file tree
Showing 27 changed files with 605 additions and 21 deletions.
2 changes: 2 additions & 0 deletions packages/docs/src/content/docs/guides/issue-reproduction.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: Issue Reproduction
sidebar:
order: 4
---

If you encounter an issue or false positives when using Knip, you can [open an
Expand Down
2 changes: 2 additions & 0 deletions packages/docs/src/content/docs/guides/namespace-imports.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---
title: Namespace Imports
sidebar:
order: 5
---

The intention of exports used through namespace imports may not always be clear
Expand Down
143 changes: 143 additions & 0 deletions packages/docs/src/content/docs/guides/type-members.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
title: Type & Interface Members
sidebar:
label: Type Members
order: 6
badge:
text: Experimental
variant: caution
---

:::caution[Warning]

This is a highly experimental feature. Currently:

- There will be false positives.
- Only members of **exported** types and interfaces are considered
- Only members of **selected** types and interfaces are considered (see examples
below)

:::

## Usage

Reporting unused type and interface members is disabled by default. To enable
this, make sure to include the `typeMembers` issue type:

```sh
knip --include typeMembers
```

Below are code examples to get an idea of what Knip should catch. Also see the
fixture folders ([1][1] & [2][2]) for slightly more involved examples.

## Interface Members

In the next example, `Dog.wings` is reported as unused:

```ts
interface Dog {
legs: number;
wings: boolean;
}

const charlie: Dog = {
legs: 4,
};
```

## Type Members

In the next example, `Pet.fins` and `Cat.horn` are reported as unused:

```ts
type Pet = {
legs: number;
fins: boolean;
};

type Cat = {
legs: Pet['legs'];
horn: boolean;
};

const coco: Cat = {
legs: 4,
};
```

## Function Arguments

In the next example, `Args.caseB` is reported as unused:

```ts
export interface Args {
caseA: boolean;
caseB: boolean;
}

function fn(options: Args) {
if (options.caseA) return 1;
}

fn({ caseA: true });
```

## JSX Component Props

Component props that are not referenced, are reported as unused.

Note that props that are referenced inside the component, but never passed in as
part of the JSX props or argument(s), are not reported.

### Example 1: Passed prop

In the next example `unusedProp` is reported as unused:

```tsx
export interface ComponentProps {
usedProp: boolean;
unusedProp: boolean;
}

const Component: React.FC<ComponentProps> = props => null;

const App = () => (
<>
<Component usedProp={true} />;
<Component {...{ usedProp: true }} />
{ React.createElement(Component, { usedProp: true }); }
</>
);
```

### Example 2: Used prop

In the next example `deep.unusedProp` is reported as unused:

```tsx
export type ComponentProps = {
usedProp: boolean;
deep: {
unusedProp: boolean;
};
};

const Component: React.FC<ComponentProps> = props => (
<span>{props.usedProp}</span>
);

const App = () => <Component />;
```

## Closing Notes

- Only members of **exported** interfaces and types are considered.
- Knip tries to consider only **relevant** interfaces and types.
- Don't start exporting or reusing interfaces and types for the sake of Knip
detecting unused properties.

[1]:
https://github.com/webpro-nl/knip/tree/feat/unused-exported-type-members/packages/knip/fixtures/type-members
[2]:
https://github.com/webpro-nl/knip/tree/feat/unused-exported-type-members/packages/knip/fixtures/type-members2
77 changes: 77 additions & 0 deletions packages/knip/fixtures/type-members/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { MyInterface, ExtendedInterface } from './interfaces';
import { MyType, WithIntersection, WithUnion } from './types';
import type { PropsA, PropsB, PropsC, PropsD, FnArg } from './props';
import type { OnlyTypedUsage } from './interfaces';

const interfaceRef: MyInterface = {
usedInterfaceMember: true,
};

type TypeRef = {
prop: MyType['usedTypeMember'];
key: MyInterface['usedKey'];
};

class ImplementsUsage implements Pick<MyInterface, 'usedInImplements' | 'usedInImplementsInternal'> {
usedInImplements = true;
}

const interfaceUsage: ExtendedInterface = {
usedInExtends: true,
};

const intersectionUsage: WithIntersection = {
usedInIntersection: true,
};

const unionUsage: WithUnion = {
usedInUnion: true,
};

interfaceRef;
ImplementsUsage;
interfaceUsage;
intersectionUsage;
unionUsage;

declare const React: any;

declare namespace React {
type FC<P = Record<string, unknown>> = (props: P) => any;
}

const ComponentA: React.FC<PropsA> = () => null;

const ComponentB: React.FC<PropsB> = props => <div {...props} />;

const ComponentC: React.FC<PropsC> = props => <div>{props.usedPropC}</div>;

const ComponentD: React.FC<PropsD> = props => <div>{props.usedPropC}</div>;

const App = () => (
<>
<ComponentA usedProp1={true} />
<ComponentA usedProp2={true} />
<ComponentB {...{ usedPropB: true }} />
<ComponentC />
{React.createElement(ComponentD, { usedPropD: true })}
</>
);

function fn(options: FnArg) {
if (options.optionA) return 1;
// if (options.optionB) return 2;
}

fn({ optionA: true });

type TypedDocumentNode<T> = T extends string ? () => string : () => number;

export const anotherFn = async (): Promise<() => number> => {
const info = await f({
query: (() => 1) as TypedDocumentNode<OnlyTypedUsage>,
});
return info;
};

export const getQuery: TypedDocumentNode<OnlyTypedUsage> = () => 1;
31 changes: 31 additions & 0 deletions packages/knip/fixtures/type-members/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export interface MyInterface {
usedInterfaceMember?: boolean;
usedKey?: boolean;
usedInExtends?: boolean;
usedInImplements?: boolean;

usedInExtendsInternal?: boolean;
usedInImplementsInternal?: boolean;

unusedInterfaceMember?: boolean;
'unused-interface-quoted'?: boolean;
}

export interface ExtendedInterface extends Pick<MyInterface, 'usedInExtends' | 'usedInExtendsInternal'> {
boolA?: true;
}

class ImplementingClass implements Exclude<MyInterface, 'usedInExtends' | 'usedInExtendsInternal'> {
usedInImplementsInternal = true;
}

const internalInterfaceUsage: ExtendedInterface = {
usedInExtendsInternal: true,
};

ImplementingClass;
internalInterfaceUsage;

export interface OnlyTypedUsage {
id: string;
}
6 changes: 6 additions & 0 deletions packages/knip/fixtures/type-members/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@fixtures/type-members",
"knip": {
"include": ["typeMembers"]
}
}
25 changes: 25 additions & 0 deletions packages/knip/fixtures/type-members/props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface PropsA {
usedProp1?: boolean;
usedProp2?: boolean;
unusedPropA?: boolean;
}

export type PropsB = {
usedPropB: boolean;
unusedPropB?: boolean;
};

export type PropsC = {
usedPropC?: boolean;
unusedPropC?: boolean;
};

export type PropsD = {
usedPropC?: boolean;
unusedPropD?: boolean;
};

export interface FnArg {
optionA?: boolean;
optionB?: boolean;
}
25 changes: 25 additions & 0 deletions packages/knip/fixtures/type-members/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type MyType = {
usedTypeMember?: boolean;
usedInUnion?: boolean;
usedInIntersection?: boolean;

usedInIntersectionInternal?: boolean;
usedInUnionInternal?: boolean;

unusedTypeMember?: boolean;
'unused-type-quoted'?: boolean;
};

export type WithIntersection = { boolB?: boolean } & Omit<MyType, 'usedInExtends' | 'usedInExtendsInternal'>;
export type WithUnion = { boolC?: boolean } | Omit<MyType, 'usedInExtends' | 'usedInExtendsInternal'>;

const internalIntersectionUsage: WithIntersection = {
usedInIntersectionInternal: true,
};

const internalUnionUsage: WithUnion = {
usedInUnionInternal: true,
};

internalIntersectionUsage;
internalUnionUsage;
23 changes: 23 additions & 0 deletions packages/knip/fixtures/type-members2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { MyInterface } from './interfaces';
import { MyType } from './types';

// interface ExtendedInterface<T extends MyInterface> {
// get: <K extends keyof T['keyA']>(key: K) => T['keyA'][K] | undefined;
// }

const i: MyInterface = {
keyA: '',
keyB: {
subB: '',
},
};

const t: MyType = {
keyA: '',
keyB: {
subB: '',
},
};

i;
t;
7 changes: 7 additions & 0 deletions packages/knip/fixtures/type-members2/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface MyInterface {
keyA: string;
keyB: {
subA: string;
subB: string;
};
}
6 changes: 6 additions & 0 deletions packages/knip/fixtures/type-members2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@fixtures/type-members2",
"knip": {
"include": ["typeMembers"]
}
}
3 changes: 3 additions & 0 deletions packages/knip/fixtures/type-members2/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"compilerOptions": {}
}
7 changes: 7 additions & 0 deletions packages/knip/fixtures/type-members2/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type MyType = {
keyA: string;
keyB: {
subA: string;
subB: string;
};
};
1 change: 1 addition & 0 deletions packages/knip/src/ConfigurationValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const issueTypeSchema = z.union([
z.literal('duplicates'),
z.literal('enumMembers'),
z.literal('classMembers'),
z.literal('typeMembers'),
]);

const rulesSchema = z.record(issueTypeSchema, z.enum(['error', 'warn', 'off']));
Expand Down
Loading

0 comments on commit def96eb

Please sign in to comment.