Search code examples
kotlinstatic-analysislintgradle-plugindetekt

How to count references to a variable or function in Kotlin?


The question is about static compile time analysis. Let's say there is a file-level const val:

const val SOME_CONST_VAL = "SOME_CONST_VAL"

How is it possible to get the number of usages for this SOME_CONST_VAL?

To clarify:

  1. It's something IDEs provide by their "Find Usages" features, but achieved with the code. No exact places of usages are needed, just the number of references and the name of entity being referenced.
  2. Any kinds of runtime checks are out of the interest.
  3. It's better to avoid tying with any particular IDE, a Gradle plugin way is more preferable.

Checked:

  1. Various lint plugins - could not find such features provided out-of-the-box, investigating if it's possible to be done by writing a custom rule for them.
  2. KSP seems like having no such features at all, it doesn't prepare dependency graphs for various usages.
  3. Compiler plugins seem like the most promising but time-consuming way.

What is the preferable direction here?


Solution

  • After a while of trial and error (mostly error 🙂) I can partially answer my question.

    I tried to avoid the path with compiler plugins because there are no lots of materials on it. The official Kotlin documentation provides only examples of such plugins - no overview or at least basic conceptual notes, various 3rd-party articles mostly recommend to guide yourself by reviewing existing compiler plugins. At the same time the corresponding APIs can tend to change, so I decided to postpone this way.

    My focus was concentrated on attempts to extend detekt for this. Here are some observations:

    1. The scope of visitors used to analyze the code is restricted to separate source KtFiles. Unsure if can reference some detekt documentation page stating this, but it can be inferred from its APIs (also, could find the direct answer stating it in the corresponding GitHub discussions).

    2. The consequence of #1 is that there is no way to write such rule: there are no methods for rule implementations to do some processing after all files have been visited. And if we do the required check on each file visited, we won't have enough information to state whether some variable is used in the entire codebase or not. Of course there can be attempts to do dirty workarounds - for example by using static collections to accumulate visited references and trigger their whole verification in the end, but it doesn't seem stable.

    3. It can seem possible to write a custom processor instead as it has a callback triggered when all files have been visited. But in this case we are encountering limitations in the way how detekt allows to report for processors - it provides only means of quantitative reporting. Of course it's possible to include everything we want to report into the ProjectMetric::type string, but I guess it can be restricted one day.

    4. There is no way to operate with something resembling a dependency tree for all variable and various other references. The code analysis is more like a token-based string reading. I tried to play with some heuristics based on the usage of FullQualifiedNameGuesser, but it doesn't provide stable results on attempts to find a declaration of some usage.

    5. Even if all the points above can be solved with some workaround, it's going to have a huge performance overkill as we essentially collect all declarations and all references throughout the entire codebase and match them eventually.

    To sum up: I think that extending detekt by its available APIs doesn't allow to solve the problem described in the question. Going to check something else.

    Update (20.4.23) - tried Qodana, the UnusedSymbol inspection does something similar (a little bit from the opposite side), but it's not very extensible (by the code means) and requires Docker to run. It's also possible to use Structural Search and export its templates to be run with Qodana, but again it seems not quite something I need.

    Update (22.5.23) - well, as it was assumed originally, the Kotlin compiler plugin path turns out to be the most suitable for the task described in the initial question. It allows to solve the drawbacks related to linters described above and fits all target conditions stated in the question. A couple of notes:

    1. It seems like modules represent natural scopes for such compiler plugins. If you apply a compiler plugin to some Gradle module, it will process only this module's sources, no other modules will be involved (even the ones used as dependencies for this module) until explicitly enabling the plugin for them as well.
    2. There are some limitations in getting the information about declarations for references to something declared in other modules. For example, I couldn't find a way to read annotations of such declarations. Of course it's possible to return (save) such information while processing a particular module and use it as input information for another, dependent module, but it requires more configurations.
    3. It's quite obvious, but Kotlin compiler plugins miss proper documentation (I would even say, miss any documentation at all) and prone to updates now. So this path can be quite thorny, requiring kinds of reverse engineering and delving into other projects' code bases.

    I think the original question is closed now, see no reasons to add any implementation details as it was formulated as: "What is the preferable direction here?"