-
-
Notifications
You must be signed in to change notification settings - Fork 179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Detect unused JSX component props #848
base: main
Are you sure you want to change the base?
Conversation
Thanks for the PR, @SanderRonde. I really appreciate it. This feature will be appreciated by many and adds a lot of value, so let's see how we can deliver.
So, overall, I think what we need to tackle first is two things:
If we get those right, I'd expect the implementation in the main sequence to become easier:
Sorry, that's a lot to digest perhaps. I'm going to let it simmer for a bit. Happy to hear your thoughts on any or all of this and discuss further. Thanks again for pushing this. |
Some high-level thoughts on the to-tackle things:
Happy to hear what you think |
Yes, we could actually start by taking only exported interfaces and types into account. The downsides of this approach I can think of:
Since it'll be an opt-in feature (like
|
Hmm in that case I do indeed think that indeed it's not as useful. I think the pattern of exporting a React component and not its types is very common so for that use case it would indeed not be that complete. Could maybe consider requiring the components/functions that use those those interfaces/types to be exported instead, although I do admit that then you're no longer really "only checking exported values". Maybe it's worth considering dropping the exported-only requirement then. It would indeed be best for this to work perfectly on the first go. And while it's fine to improve on the feature while it's opt-in, I think it's good for the "final goal" to be as complete as possible.
Ah yeah I just forgot that it's indeed a valid case for an interface/type to be non-local to the file, meaning you would have to crawl another file. However I guess in that case it is already in the exports field in the graph so we could maybe read from there?
Yeah that should work. Indeed only caveat is that you'll need to filter out usages within the body of usages of that interface. For example a component reading from Or another (very iffy) approach might be to iterate through |
A few more things to consider perhaps:
Not sure I fully understand. Could you give an example, perhaps with code, what needs filtering out from regular logic of (not) being referenced? |
Yes, I do think that it would be really nice for both of these to work with the same implementation. But my fear is that if we're going with the approach of only checking exported interfaces/types, it might not work that well for React component props. But let's see :)
Ah yes that makes sense
Agreed!
Of course! Here's a generic case in which we'd need some filtering // date.ts
export interface DateFormatOptions {
weekday?: boolean; // <-- Unused
month?: boolean;
}
export function someDateFormatter(date: Date, options: DateFormatOptions) {
const text = [];
if (options.weekday) { // <-- This counts as a reference according to LS.findReferences and it's making this not-unused
text.push(date.toWeekDay());
}
if (options.month) {
text.push(date.toMonth());
}
return text.join(' ');
} // app.ts
const date = someDateFormatter(new Date(), { month: true }); |
Yeah we'll eventually need that unexported part too for sure, but gotta start somewhere :)
Right. Not sure I agree this should be filtered out. Did you consider that data can come from anywhere/outside the codebase as well: const data = await fetch() as OurInterface;
return <SomeExternalComponent {...data} /> Thinking we should keep it simple and see whether props are referenced in/from our own code. Also see https://knip.dev/guides/namespace-imports for how we could go about cases like this, but as mentioned before, not having all the cases clear yet. |
Hmm if they wouldn't be filtered it would be quite rare for an options-style interface to ever have any of its members marked as unused right? In the example I sent, even though the
Yeah indeed that does complicate things. Even if that data doesn't come from outside the codebase, an object spread being passed to a JSX component isn't seen as a reference by TS. For example in
Yeah agreed. Although in this case a downside of less-thorough detection in finding references means false positives (members flagged as unused even though maybe a |
A type/interface and the object passed as component props are two different things: in your example, |
Which isn't to say we should or could not implement the use case, we just need to think it through a bit more: how to opt-in, where to hook this special case into, without too much added complexity nor sacrificing performance during the default flow. Also wondering whether this information should be in the |
Implement detection of unused JSX component props. Example:
Implementation:
packages/knip/src/typescript/visitors/jsx-component/reactComponent.ts
for more details). In this process collect the name of theProps
typeJSXAttribute
(i.e.<MyComponent foo />
). If there are none, this prop member is marked as unusedLimitations:
interface Foo { unused: boolean }; interface ComponentProps extends Foo { }
unused
is not detectedReact.createElement(..., { foo: 'bar' })
or<Component {...{foo: 'bar'}} />
. This is quite simply because TS does not detect it as a reference to the props field.I have to admit that I haven't used Knip at all before yesterday and I'm not really all that knowledgeable of how error reporting/fixing works in Knip so I think there may still be some code lacking there (see TODOs in the code too), so feel free to give me some pointers.