-
-
Notifications
You must be signed in to change notification settings - Fork 198
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
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 |
Yeah I think we're indeed both looking for 2 main goals here. My primary goal is/was JSX component checking (or a more generalized version if possible), yours is primarily interface checking. I think we're indeed both hoping for the "final" state to be one where Knip does both of them.
Yeah I think I'm going to have to agree here. My main reason for questioning whether the approach of doing interface checking first and then growing that into component-checking was the right one was that indeed it sounds like component-checking might not "fit" very well into the pattern of interface-checking and into Knip's current design. As you already listed:
These all sound like relatively big "issues" and not like something that would fit very well on top of something like interface member checking (which fits the current philosophy very well). For that reason I'd say it's maybe better to, if this is indeed implemented in Knip, implement it as a separate visitor and in an opt-in like state. Similar to the way done in this PR. I'm of course still willing to help you out in getting there, but it feels like the interface-member checking might be better implemented by you given that it's relatively straight-forward and I'm sure you know much more about the specifics of implementing Knip features than I do :). This PR is also relatively far from an implementation of that. I am of course always willing to further discuss and work on the JSX component props checking and to think about how to (and whether to) fit it into Knip. |
💯
|
Awesome!
Sounds good. I'm of course very curious about your thoughts so I'll just throw my general idea at you and let's see what you think :)
|
💯 Is that enough for "unused JSX component props" though? Also, totally on board with leaving out any complications re. exported object members as you mention.
A few things to consider here. The
Previously I've thought of having a separate graph/storage of sorts before for this type of functionality, but now I'm on board with extending the current
|
Sorry for the rather late response
I think it should be yeah. We could however choose to more specifically cater to JSX components and drop the generic-ness. I think approaching this generically might have too many false-positives to be worth it:
Alright that's good to know. That would essentially mean that we'd need to make a few changes. Right now the implementation in this PR is the following:
So I think the new flow should be:
What do you think? Could this work? I think autofix would/could be nice but I'm not sure if it's trivial to do. For example given:
|
Not sure, mainly because of this:
If you literally mean "all functions", I think they're not available there?
Indeed, looks like autofix seems out of range for safe removal for most (if not all) cases. Maybe there are some trivial cases we could fix, we might learn more along the way. |
Ah no I meant just all exported (so previously collected) functions.
Good point, will add some "todo: this could autofix using xyz" pointers along the way so it's easier to implement in the future once we go all the way and implement it all. |
Def... not as easy as i expected... but got an alpha version working. This should catch the original use case from the opening post (but obviously not the "runtime" case). Installation: npm i -D https://pkg.pr.new/knip@def96eb Initial docs: https://42d094e9.knip.pages.dev/guides/type-members I'll move this comment into a new issue if/when that makes sense. Would be great if you could try this out, eager to hear about findings and false positives. |
Works like a charm! A couple of comments:
I think it would indeed be better to move the change you made to another issue and get it merged :) |
Thanks for the quick feedback. Totally agree with your points. Although - at least for now - we'll have to resort to some "interesting" configuration to get the behavior you describe: https://09833a2d.knip.pages.dev/guides/type-members#closing-notes. It's in line with how Knip already works, but I also get it might be confusing. Moving to #910. |
Ahh well at least nice that there's a way to get it working anyways :) |
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.