A few weeks ago, I announced a new version of elm-review
. In part 1, I wrote about how automatic fixes are now much, much faster than before.
In this second and final part (disappointed audience sighing), I will talk about an entirely new feature of elm-review
, which allows you to gain arbitrary insight into your codebase.
Do you wish to have an overview of your codebase? Which modules import which? Do you wish to know how complex your codebase is?
Create diagrams of how your project works?
Well, you’ll have the tools for that now.
Context is important
First, a bit of context… about context.
elm-review
is a static analysis tool that reports problems in Elm codebases according to a set of rules that you can
choose from the Elm package registry, or write yourself.
In order to make the most accurate analysis possible — report as many of the problems we possibly can while reporting no
false positives — rules need to collect information, which I call “context”. And that is because the devil is in the details.
Let’s say we wish to have a tool that reports references to Html.button
to have people use
BetterHtml.button
instead.
We could use a tool like grep
to go through the project’s source code and find the references to Html.button
, like grep -e 'Html.button' src/**/*.elm
. But that won’t work all that well.
import Html
Html.button [] []
import Html exposing (button)
button [] []
import Html as H
H.button [] []
import Html exposing (..)
button [] []
import BetterHtml as Html
Html.button [] []
someText = "I love Html.button!"
module BetterHtml exposing (button)
button =
Naively searching for Html.button
will lead to very poor results. It will miss a lot of the references we’re interested in,
and report a bunch of results that are not references or not the ones we’re interested in. If we search for button
instead of Html.button
, then we’ll find more references, but also a lot more unrelated ones.
Some tools — such as comby
and
tree-grepper
— are more
code-aware than grep
, and they will do a better a job at this task — such as not reporting references in strings or
comments —, but probably not without mistakes. I imagine tree-grepper
could potentially find references correctly, but
be limited in other kinds of analysis (finding unused type variants for instance).
What happens in one part of the file can impact what happens in another part of the file, and the same thing is true at
the codebase level as well. Stateless tools like grep
, comby
or tree-grepper
are awesome
(and very fast!) but as soon you need a bit of context or to combine pieces of information to make something of what has
been found, you’ll need to add in more logic through external tools or scripts.
If you wish to gain insight into a project, not being able to get this level of nuance can make or break the results, or
experience, of your analysis.
But this is something that elm-review
does very well. Targeting a specific function for instance is something the tool
does flawlessly and makes quite easy.
Because a lot of analysis requires information that needs to be collected, I made sure that elm-review
has a very nice
way of traversing a project and gathering that context. Which as I said before allows it to report errors very accurately.
But while you can easily collect all that information, you can only use it to report errors. And that’s kind of a shame.
Starting from jfmengels/elm-review
v2.10.0, elm-review
rules can define a “data extractor” using Rule.withDataExtractor
.
This makes it possible to transform the collected data (the “project context” in our terminology) into arbitrary JSON.
As a (too) trivial example, below is a rule (also to be found here)
that goes through the project and outputs a mapping of the module name of Elm files to their file path. This is what the
output would look like:
{
"Api": "src/Api.elm",
"Article": "src/Article.elm",
"Article.Body": "src/Article/Body.elm",
"Asset": "src/Asset.elm",
"Page.Article": "src/Page/Article.elm",
"Page.Article.Editor": "src/Page/Article/Editor.elm",
"Page.Profile": "src/Page/Profile.elm",
"...": "...and some more"
}
and the rule’s implementation:
module ModuleNameToFilePath exposing (rule)
import Dict exposing (Dict)
import Json.Encode
import Review.Rule as Rule exposing (Rule)
rule : Rule
rule =
Rule.newProjectRuleSchema "ModuleNameToFilePath" initialContext
|> Rule.withModuleVisitor (schema -> schema |> Rule.withSimpleModuleDefinitionVisitor (always []))
|> Rule.withModuleContextUsingContextCreator
{ fromModuleToProject = fromModuleToProject
, fromProjectToModule = Rule.initContextCreator (_ -> ())
, foldProjectContexts = foldProjectContexts
}
|> Rule.withDataExtractor dataExtractor
|> Rule.fromProjectRuleSchema
type alias ProjectContext =
Dict String String
initialContext : ProjectContext
initialContext =
Dict.empty
fromModuleToProject : Rule.ContextCreator () ProjectContext
fromModuleToProject =
Rule.initContextCreator
(moduleName filePath () ->
Dict.singleton (String.join "." moduleName) filePath
)
|> Rule.withModuleName
|> Rule.withFilePath
foldProjectContexts : ProjectContext -> ProjectContext -> ProjectContext
foldProjectContexts =
Dict.union
dataExtractor : ProjectContext -> Json.Encode.Value
dataExtractor projectContext =
Json.Encode.dict identity Json.Encode.string projectContext
This is likely not a rule that you will end up using as its utility is limited, but I hope this shows the general feel.
(That said, this example is maybe a bit weird. Because it doesn’t really visit Elm files, it does look a bit alien even to me)
If you’re familiar with writing elm-review
rules, it’s going to be exactly the same but with an additional data extractor function.
If you’re familiar with Elm but not elm-review
, it’s going to be a new API to learn, but it will be very Elm-like,
especially when compared to learning a new DSL like the alternatives I mentioned.
Usage
The way to run the rule above and get the extracted information is by adding the rule to your configuration and then running
the CLI with the following flags:
elm-review --report=json --extract
Without these flags, elm-review
will not call the data extractor function. Since it wouldn’t make sense to view the JSON
output of a rule when looking at the regular output (at least, I haven’t figured a good way), you need the --report=json
flag.
And since that reporting format is used by IDEs, the --extract
option is opt-in to avoid them incurring a performance penalty.
Running the above will result in JSON that looks like the following:
{
"errors": [],
"extracts": {
"Name.Of.A.Rule": "arbitrary string",
"Name.Of.Other.Rule": {
"arbitrary": "json"
},
"...": "..."
}
}
To access the output of a rule named ModuleNameToFilePath
, you will want to read the value under extracts
then under
ModuleNameToFilePath
. My current approach which works quite well is to pipe the result of elm-review
into jq
, a tool to manipulate JSON through the command-line, like this:
elm-review --report=json --extract | jq -r '.extracts.ModuleName