Program analysis for ReScript and OCaml projects targeting JS (ReScript) as well as native code (dune):
- Globally dead values, redundant optional arguments, dead modules, dead types (records and variants).
- Exception analysis.
- Termination.
Early release. While the core functionality is reasonably stable, the CLI and annotations are subject to change. However, this is a tiny surface at the moment.
For correct handling of ReasonReact components in the Dead Code Analysis, bucklescript version 7.3.2 is required (fixes the location in the JSX PPX).
The rest of this document describes the dead code analysis. For the Exception Analysis, build instructions are the same, and the command-line invocation is different.
Build and run on existing projects using the Build and Try instructions below. The analysis uses .cmt[i]
files which are generated during compilation, so should be run after building your project. Remember to rebuild the project before running again.
# dead code analysis
reanalyze.exe -dce
# exception analysis
reanalyze.exe -exception
The requirement is that bsconfig.json
can be found by walking up the current directory.
# dead code analysis
reanalyze.exe -dce-cmt root/containing/cmt/files
# exception analysis
reanalyze.exe -exception-cmt root/containing/cmt/files
Subdirectories are scanned recursively looking for .cmt[i]
files.
The requirement is that the current directory is where file paths start from. So if the file path seen by the compiler is relative src/core/version.ml
then the current directory should contain src
as a subdirectory. The analysis only reports on existing files, so getting this wrong means no reporting.
The dead code analysis reports on globally dead values, redundant optional arguments, dead modules, dead types (records and variants).
A value x
is dead if it is never used, or if it is used by a value which itself is dead (transitivity). At the top level, function calls such as Js.log(x)
, or other expressions that might cause side effects, keep value x
live.
An optional argument ~argName
to a function is redundant if all the calls to the function supply the argument, or if no call does.
A module is considered dead if all the elements defined it in are dead.
The type analysis repots on variant cases, and record labels.
-
A variant case
| A(int)
is dead if a value such asA(3)
is never constructed. But it can be deconstructed via pattern matching| A(n) => ...
or checked for equalityx == A(3)
without making the caseA
live. -
A record label
x
intype r = {x:int, y:int}
is dead if it is never read (by direct accessr.x
or pattern matching| {x:n, y:m} => ...
). However, creating a valuelet r = {x:3, y:4}
does not makex
andy
live. Note that reading a valuer
does not maker.x
orr.y
live.
While dead values can be removed automatically (see below), dead types require a bit more work. A dead variant case requires changing the type definition, and the various accesses to it. A dead record label requires changing the type definition, and removing the label from any expressions that create a value of that type.
The dead code analysis supports 2 annotations:
-
@dead
suppresses reporting on the value/type, but can also be used to force the analysis to consider a value as dead. Typically used to acknowledge cases of dead code you are not planning to address right now, but can be searched easily later. -
@live
tells the analysis that the value should be considered live, even though it might appear to be dead. This is typically used in case of FFI where there are indirect ways to access values. In case of bucklescript projects usinggenType
, export annotations immediately qualify values as live, because they are potentially reachable from JS.
The main difference between @dead
and @live
is the transitive behaviour: @dead
values don't keep alive values they use, while @live
values do.
Several examples can be found in
examples/deadcode/src/DeadTest.re
Here are several unstable features, which could change substantially over time. Ask for more information.
This automatically annotates @live
all the items called foo
or bar
:
-live-names foo,bar
This automatically annotates @live
all the items in file Hello.re
:
-live-paths Hello.re
This automatically annotates @live
all the items in the src/test
and tmp
folders:
-live-paths src/test,tmp
Print debug information during the analysis
reanalyze.exe -debug ...
This overwrites your source files automatically with dead code annotations:
reanalyze.exe -write ...
There's a dead code ppx (values only, not types) in this repository. It can be used to automatically remove code annotated @dead
, as if it had been commented out.
Can be used after adding annotations automatically. The combination of automatic annotation and automatic elimination is a form of automatic dead code elimination. For projects that use a library, or that in general have code which is dead only temporarily.
There's obviously a level of risk in doing this automatic elimination. The safety net you can rely on is that the code must still compile.
npm add --save-dev reanalyze
opam switch 4.06.1
eval $(opam env)
opam install dune
dune build
# _build/default/src/Reanalyze.exe
opam install dune
dune build
# _build/default/src/Reanalyze.exe
npm run build # or whatever command to build the project
npm add --save-dev reanalyze
npx reanalyze -dce
See for example project reanalyze-esy-example:
git clone https://github.com/cristianoc/reanalyze-esy-example
cd reanalyze-esy-example
esy
esy dce
esy check-exceptions
echo "let unused = 34" > test.ml
ocamlc -c -bin-annot test.ml
reanalyze.exe -dce-cmt ./test.cmt
Warning Dead Value
test.ml 1:1-15
unused is never used
<-- line 1
let unused = 34 [@@dead "unused"]
mkdir test
echo "let unused = 34 let used = 42" > test/test.ml
echo "let _ = Test.used" > test/use.ml
cd test
ocamlc -c -bin-annot *.ml
reanalyze.exe -dce-cmt .
Warning Dead Value
test.ml 1:1-15
unused is never used
<-- line 1
let unused = 34 [@@dead "unused"] let used = 42
Warning Dead Value
use.ml 1:1-17
_ has no side effects and can be removed
<-- line 1
let _ = Test.used [@@dead "_"]
How to test on Infer :
- Make sure that
dune
builds both.cmt
and.cmti
files (see ocaml/dune#3182 as to why):
--- a/infer/src/Makefile
+++ b/infer/src/Makefile
-DUNE_BUILD = dune build --profile=$(BUILD_MODE)
+DUNE_BUILD = dune build @check @all --profile=$(BUILD_MODE)
- Build normally
make -j BUILD_MODE=dev
- Go to the right directory from which file paths start:
% cd infer/infer
- Run the analysis
% path/to/reanalyze.exe -dce-cmt _build/default/src/.InferModules.objs/byte/