Skip to content
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

x/tools/cmd/deadcode: add a way to treat exposed APIs as reachable #66073

Open
mvdan opened this issue Mar 2, 2024 · 7 comments
Open

x/tools/cmd/deadcode: add a way to treat exposed APIs as reachable #66073

mvdan opened this issue Mar 2, 2024 · 7 comments
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Tools This label describes issues relating to any tools in the x/tools repository.
Milestone

Comments

@mvdan
Copy link
Member

mvdan commented Mar 2, 2024

@alandonovan the deadcode tool is great, thank you so much for working on it! It's being really useful as an extension of staticcheck's unused analysis, which is easier and faster to run, but only works on a per-package basis and doesn't consider exported declarations.

When I run it on a public Go module, such as https://github.com/mvdan/sh, I see a lot of output that doesn't seem right to me:

$ deadcode ./...
fileutil/file.go:24:6: unreachable func: HasShebang
fileutil/file.go:64:6: unreachable func: CouldBeScript
interp/api.go:332:6: unreachable func: CallHandler
interp/api.go:343:6: unreachable func: ExecHandler
interp/api.go:364:6: unreachable func: ExecHandlers
interp/api.go:380:6: unreachable func: OpenHandler
interp/api.go:390:6: unreachable func: ReadDirHandler
interp/api.go:408:6: unreachable func: ReadDirHandler2
interp/api.go:416:6: unreachable func: StatHandler
interp/handler.go:212:6: unreachable func: LookPath
interp/handler.go:322:6: unreachable func: DefaultReadDirHandler
shell/expand.go:26:6: unreachable func: Expand
shell/expand.go:48:6: unreachable func: Fields
[...]

It's true in some way that these funcs are not reachable; there are only a couple of main packages under cmd, and neither use any of those funcs. However, this is a public and stable Go module (https://pkg.go.dev/mvdan.cc/sh/v3) and all of the APIs listed above are exposed on purpose for library users, and cannot be deleted in a backwards compatibility manner even if I wanted to.

I wish there were a way to run the deadcode tool in a "library" mode, so that any exposed API would automatically be marked as reachable. I think that would mean any exported names in packages which are not declared as internal.

One could argue that this should happen automatically, for example if the tool can detect it is running on a module which is already a stable semver version like v1 or v3. However, I think being in control would be most useful. Taking my own sh/v3 module as an example again, if I'm starting to consider what I would like to do with the API for a future v4, running the tool in its current mode can be a good way to spot if I want to drop any of the APIs I don't myself need, if I think they're no longer very useful to external users. And, on the other hand, some v0 modules are on the way to a v1, so they still try pretty hard to not break backwards compatibility.

@gopherbot gopherbot added the Tools This label describes issues relating to any tools in the x/tools repository. label Mar 2, 2024
@gopherbot gopherbot added this to the Unreleased milestone Mar 2, 2024
@mvdan
Copy link
Member Author

mvdan commented Mar 2, 2024

I should also note that deadcode -test ./... is not what I want: sometimes, due to the nature of refactors, an exported function in an internal package suddenly becomes unreachable - but it is still tested in its own package. I still want to be told about those cases even if I run the tool in "library mode".

@adonovan
Copy link
Member

adonovan commented Mar 2, 2024

We could define an -exported flag that added all public symbols from non-internal packages of the initial module to the analysis' set of reachable functions. But what can we conclude about func and interface values among the parameters to those functions? Consider func F(p I) where I is an interface type: a call to a method p.m() might not reach any callees because there are no live types that implement I, even though there would be in a real program. I worry that the results could be quite confusing.

I should also note that deadcode -test ./... is not what I want: sometimes, due to the nature of refactors, an exported function in an internal package suddenly becomes unreachable - but it is still tested in its own package. I still want to be told about those cases even if I run the tool in "library mode".

I'm confused: if it is still tested in its own package, wouldn't this be included in -test ./...?

@mvdan
Copy link
Member Author

mvdan commented Mar 2, 2024

But what can we conclude about func and interface values among the parameters to those functions?

I'm not sure. I'd be fine with this mode having documented caveats if there's no good way to deal with those situations. Would there be a way to skew towards being conservative in cases like this, leaning towards false negatives rather than false positives?

I'm confused: if it is still tested in its own package, wouldn't this be included in -test ./...?

My understanding and limited experience with the tool is that deadcode -test ./... will never warn about an exported function if it is called from any test function. In any case, all I meant to say is that I thought about using deadcode -test ./... for my libraries but it's not a good approximation of what I want to accomplish here.

@adonovan
Copy link
Member

adonovan commented Mar 2, 2024

But what can we conclude about func and interface values among the parameters to those functions?

I'm not sure. I'd be fine with this mode having documented caveats if there's no good way to deal with those situations. Would there be a way to skew towards being conservative in cases like this, leaning towards false negatives rather than false positives?

Let's define the terms to ensure we're on the same page: in this context "conservative" means silent and a "positive" is an "unreachable" diagnostic.

I think you're asking for the -exported flag to treat all implementations of I as reachable. It could do that, but I think it would seriously degrade precision.

My understanding and limited experience with the tool is that deadcode -test ./... will never warn about an exported function if it is called from any test function.

That's correct.

In any case, all I meant to say is that I thought about using deadcode -test ./... for my libraries but it's not a good approximation of what I want to accomplish here.

Obviously the tool can't safely report a function as dead if it's needed by a test. Are you saying that -test generates unwanted "coverage", and that you want to know which functions are unreachable from the exported API, ignoring tests?

I might still argue that the tool is pointing to a gap in your test suite (though pragmatically I get that a broad shallow API doesn't always need exhaustive test coverage).

@mvdan
Copy link
Member Author

mvdan commented Mar 2, 2024

Let's define the terms to ensure we're on the same page: in this context "conservative" means silent and a "positive" is an "unreachable" diagnostic.

Correct.

I think you're asking for the -exported flag to treat all implementations of I as reachable. It could do that, but I think it would seriously degrade precision.

Fair enough. I personally wouldn't mind false positives (unreachable diagnostics where I can't remove a declaration without breaking users) assuming they would be relatively rare. I think they should be relatively rare, because the types implementing a public interface are usually also public themselves.

Are you saying that -test generates unwanted "coverage", and that you want to know which functions are unreachable from the exported API, ignoring tests?

I worry that my bringing up of -test is derailing the thread a bit :) As a library author, I want to know what code I can delete without breaking my main packages or changing the API as shown by go doc or pkg.go.dev (excluding internal packages). Whether or not I have done a good job at covering my public APIs with tests should not matter in this analysis, which is why I was trying to say I've discarded using -test in any way for my purpose here.

One tangential thought is that runnable examples could matter in this -exported mode, because they are rendered as part of the documentation, so they are part of the contract between the library author and the library user in terms of what is supported and meant to continue working in the future. If runnable examples are reachable with -exported, but not tests, then perhaps the way to resolve false positives with interfaces would be to write one more example which reaches those implementation types.

@H0llyW00dzZ
Copy link

Note

Deadcode tool is quite useful, particularly when modularizing the codebase. It assists in breaking down complex functions into smaller, more manageable parts.

@mknyszek mknyszek added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Mar 4, 2024
@timothy-king
Copy link
Contributor

We could define an -exported flag that added all public symbols from non-internal packages of the initial module to the analysis' set of reachable functions. But what can we conclude about func and interface values among the parameters to those functions? Consider func F(p I) where I is an interface type: a call to a method p.m() might not reach any callees because there are no live types that implement I, even though there would be in a real program. I worry that the results could be quite confusing.

@adonovan What if we additionally add all types reachable from exported types in the transitive reachable packages as live. Would we still have an example that could end up confusing? In particular for the tool to report some F as dead in one set of packages, but not in another? It may be a lack of imagination, but I am struggling to come up with such an example.

(TypeParams seem horrible without a whole program though.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Tools This label describes issues relating to any tools in the x/tools repository.
Projects
None yet
Development

No branches or pull requests

6 participants