Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Added regression coverage for dependency graph dependent-index updates
- Added ReScript 12 support for value usage analysis (`-vb`) by decoding typed trees with the layout of the compiler that wrote each `.cmt` file
- Added ReScript 12 fixture projects (standard and namespaced) with dependency graph and value usage regression tests
- Added ReScript 13 support: dependency analysis for the 13.0 alphas, and value usage analysis (`-vb`) for 13.0.0-alpha.1 to alpha.3, alpha.5 and alpha.6. `-vb` reports `-6` for compilers it cannot decode, such as 13.0.0-alpha.4
- Added ReScript 13 fixture projects and self-contained compiler identification tests
- Added a supported ReScript versions table to the README

### Changed
- Organized typed tree decoding into one backend per compiler release (`Compiler_v11`, `Compiler_v12`, `Compiler_v13`) selected by the CMT magic number, the `cmt_infos` layout and the compiler version in `lib/bs/compiler-info.json`, with decoders skipping constructors added by later releases instead of misreading them
- `.cmt` files written by unknown compilers now stop the analysis with an error instead of silently producing an empty graph
- Improved dependency graph performance by maintaining a reverse dependent index instead of scanning the full graph for each dependent lookup
- Reduced VS Code inline value usage analysis work with debounced requests, short-lived result caching, and JSON output parsing

### Fixed
- Prevented stale VS Code inline value usage decorations from appearing after source edits or outdated CLI responses
- Improved inline value usage cache invalidation when ReScript build artifacts change
- Fixed a segmentation fault when counting value usage (`-vb`) in projects built with ReScript 12
- Fixed namespaced projects so imports are filtered by each module's `.ast` file again; ReScript 12 runtime modules (`Stdlib_*`, `Primitive_*`) and unused imports no longer appear as dependencies, and the generated namespace module is no longer shown as a node
- Fixed value usage analysis (`-vb`) in namespaced projects
- Read the ReScript 12 `dependencies` and `dev-dependencies` fields of `rescript.json` when resolving external module paths
- Read `.ast` headers by their byte length so data from the binary AST is never taken as module references
- Aligned the vendored ReScript 11 typedtree with the compiler's constructor layout

## [0.1.2] - 2026-04-22
### Fixed
Expand Down
53 changes: 49 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ dune build --profile static
You can run tests using the following methods:

```bash
# Run the CI-equivalent test suite (requires pnpm 8.x)
# Run the CI-equivalent test suite (requires pnpm 10.x)
make test

# Run specific tests:
dune exec test/test_cmt_values.exe # Test value dependency information
dune exec test/test_cmt_imports.exe # Test import information
dune exec test/test_ast.exe # Test reading module references from .ast files
dune exec test/test_rescriptdep.exe # Test dependency analysis on sample projects
dune exec test/test_value_usage.exe # Test value usage counts (-vb) on sample projects
dune exec test/test_namespace.exe # Test namespace handling (self-contained)
```

### Test Structure
Expand All @@ -35,7 +38,11 @@ The test code is located in the `test/` directory and includes:

- `test_cmt_imports.ml`: Tests the import information from cmt files
- `test_cmt_values.ml`: Tests the value dependency information from cmt files
- `test_ast.ml`: Tests reading module references from compiler-generated .ast files
- `test_rescriptdep.ml`: Tests the dependency analysis on sample ReScript projects
- `test_value_usage.ml`: Tests value usage counts on ReScript 11 and 12 projects
- `test_namespace.ml`: Tests namespace handling without compiled fixtures
- `test_compilers.ml`: Tests compiler identification and typed tree decoder guards without compiled fixtures
- `benchmark.ml`: Performance benchmark for the parser

### Test Fixtures
Expand All @@ -46,15 +53,27 @@ The test system uses two types of fixtures:

The dependency analysis tests use JSON fixtures to ensure consistent behavior:

- `test/fixtures/rescript.json`: Reference output for the rescript test project
- `test/fixtures/rescript.json`: Reference output for the ReScript 11 test project
- `test/fixtures/rescript12_basic.json`: Reference output for the ReScript 12 test project
- `test/fixtures/rescript12_namespaced.json`: Reference output for the namespaced ReScript 12 test project
- `test/fixtures/rescript13_basic.json`, `test/fixtures/rescript13_namespaced.json`: The same for ReScript 13

These fixtures are automatically generated on first run and serve as reference points for future test runs. If the dependency analysis output differs from these fixtures, the test will fail and show a diff of the changes.

#### Test Projects

The test directory contains sample ReScript projects used for testing the dependency analyzer:

- `test/rescript/`: Sample ReScript project with standard ReScript setup
- `test/rescript/`: Sample ReScript 11 project with standard ReScript setup
- `test/rescript12/`: pnpm workspace with ReScript 12 projects built from the same sources:
`basic/` (standard setup) and `namespaced/` (`"namespace": true`). `math.res` is
named `mathUtil.res` there because ReScript 12 resolves `Math` to `Stdlib.Math`.
- `test/rescript13/`: The same workspace for ReScript 13, with `Belt` calls replaced by
`Stdlib` ones since ReScript 13 no longer ships `Belt`, plus `loops.res`, which uses syntax
only ReScript 13 has (`for...of`, `continue`, template literals, object literals).

The packages of a workspace share their compiled dependencies, so `res:build` builds them
one at a time (`--workspace-concurrency=1`); parallel builds corrupt the shared `.cmi` files.

Each test project contains:
- `src/`: Source code files (.res, .resi)
Expand All @@ -69,10 +88,36 @@ When making changes that affect dependency analysis:

1. Review the test output diff carefully to ensure changes are expected
2. If the changes are intentional:
- Delete the existing fixture file: `rm test/fixtures/rescript.json`
- Delete the existing fixture file, e.g. `rm test/fixtures/rescript.json`
- Run the tests again to generate new fixtures: `dune exec test/test_rescriptdep.exe`
- Commit both the code changes and the updated fixtures

### Supporting a New ReScript Release

`.cmt` files are marshalled OCaml values without type information, so they must be read with
type definitions whose constructor order and arity match the compiler exactly. Only the typed
tree used by value usage analysis (`-vb`) differs between the supported releases; the fields of
`cmt_infos` used by the dependency graph and the `.ast` header format are shared.

`lib/compilers.ml` picks the decoder from the CMT magic number (in `compiler/ext/config.ml` of
the ReScript repository), the number of fields of the `cmt_infos` record and, where several
releases write the same CMT format, the version in `lib/bs/compiler-info.json`. For example,
ReScript 12 and 13.0.0-alpha.1 to alpha.5 all write `Caml1999T022` with 14 fields, but alpha.4
changed the typed tree in place.

To add a release:

1. Diff its `compiler/ml/typedtree.mli` against the release a decoder already reads. If only
constructors were appended, or constant constructors added, extend that decoder's vendored
typedtree and list the version in its entry in `lib/compilers.ml`.
2. Otherwise vendor the parts of its typedtree that are decoded as `vendor/typedtree_vNN.ml`,
keeping every constructor in order, add a `lib/compiler_vNN.ml` decoder with its constructor
counts, and add an entry to `all` in `lib/compilers.ml`. `test/test_compilers.ml` checks that
the counts match the vendored definitions.
3. Add a fixture workspace (`test/rescriptNN/`) or update the existing one, its reference JSON
fixtures, and the project to `test/test_rescriptdep.ml`, `test/test_value_usage.ml` and the
`Makefile`.

### Running the Benchmark

The benchmark requires a ReScript project with compiled .cmt files to analyze:
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ check-pnpm:

install-test-deps:
CI=1 pnpm --dir test/rescript install --frozen-lockfile
CI=1 pnpm --dir test/rescript12 install --frozen-lockfile
CI=1 pnpm --dir test/rescript13 install --frozen-lockfile

build-test-projects:
pnpm --dir test/rescript run res:build
pnpm --dir test/rescript12 run res:build
pnpm --dir test/rescript13 run res:build

run-ocaml-tests:
dune exec ./test/test_cmt_imports.exe
dune exec ./test/test_cmt_values.exe
dune exec ./test/test_ast.exe
dune exec ./test/test_rescriptdep.exe
dune exec ./test/test_value_usage.exe
dune exec ./test/test_namespace.exe
dune exec ./test/test_compilers.exe
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,39 @@ rescriptdep [options] files_or_dirs
# -vl, --value-line <line> : Line number of the value binding (1-based, for disambiguation when multiple bindings have the same name; use with -vb)
```

`files_or_dirs` are compiled `.cmt` files or directories that contain them, usually the project's `lib/bs`.

With `-vb`, each module reports its usage count, or a negative code when it could not be analyzed:
`-2` no implementation, `-3` unreadable `.cmt` file, `-4` no `.cmt` file, `-5` no file path,
`-6` the compiler that built the module is not supported by value usage analysis.

## Supported ReScript Versions

| ReScript | Dependency graph | Value usage (`-vb`) |
|---|---|---|
| 11.0.0 - 11.1.4 | Yes | Yes |
| 12.0.0 - 12.3.1 | Yes | Yes |
| 13.0.0-alpha.1 - alpha.3, alpha.5, alpha.6 | Yes | Yes |
| 13.0.0-alpha.4 | Yes | No (`-6`), see below |

rescriptdep identifies the compiler from each `.cmt` file and, for ReScript 12 and later, from the
version rewatch records in `lib/bs/compiler-info.json`. Files in formats no supported compiler
writes are rejected with an error instead of being misread. Other versions of a supported format
(for example a later 13.0 prerelease) get the dependency graph, and `-6` from `-vb` until their
typed tree has been checked.

### ReScript 13.0.0-alpha.4

Value usage analysis (`-vb`, the inline "Used N times" count in the VS Code extension) is not
supported for projects built with 13.0.0-alpha.4, and every module reports `-6`. The dependency
graph works as usual.

`-vb` reads the typed tree stored in `.cmt` files, which depends on the exact layout of the
compiler's data types. 13.0.0-alpha.4 inserted the `for...of` constructors in the middle of the
expression type, a layout no other release uses: 13.0.0-alpha.5 moved them to the end of the type.
Rather than keep a decoder for that single prerelease, rescriptdep skips it. Upgrade to
13.0.0-alpha.5 or later to use value usage analysis.

## Development

For detailed information about building the project, running tests, and contributing, please see [CONTRIBUTING.md](CONTRIBUTING.md).
Expand All @@ -53,7 +86,7 @@ For detailed information about building the project, running tests, and contribu
# Build the project
make build

# Run the CI-equivalent test suite (requires pnpm 8.x)
# Run the CI-equivalent test suite (requires pnpm 10.x)
make test
```

Expand Down
3 changes: 2 additions & 1 deletion bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ let main () =
Printf.eprintf "[BENCH] Total execution time: %.4f seconds\n" total_time;
exit 0)
with
| Rescriptdep.Parser.Invalid_cmt_file msg ->
| Rescriptdep.Parser.Invalid_cmt_file msg
| Rescriptdep.Compilers.Unsupported msg ->
Printf.eprintf "Error: %s\n" msg;
exit 1
| Sys_error msg ->
Expand Down
104 changes: 104 additions & 0 deletions lib/compiler_v11.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
(* compiler_v11.ml - Typed tree decoder for ReScript 11 *)

open Stdlib
open Typed_ast
open Typedtree_v11

(* Constructor counts (constant, non-constant) of the decoded variants *)
let expression_desc_constructors = (1, 30)
let module_expr_desc_constructors = (0, 6)
let structure_item_desc_constructors = (0, 14)

let rec expr e =
if not (has_known_constructor expression_desc_constructors e.exp_desc) then
Node []
else
match e.exp_desc with
| Texp_ident (path, _, _) -> Ident (path_to_list path)
| Texp_let (_, vbs, body) -> Let (List.map binding vbs, expr body)
| Texp_function { cases; _ } -> Node (List.concat_map case cases)
| Texp_apply (f, args) ->
Node
(expr f :: List.filter_map (fun (_, arg) -> Option.map expr arg) args)
| Texp_match (e, cases, exn_cases, _) ->
Node (expr e :: List.concat_map case (cases @ exn_cases))
| Texp_try (e, cases) -> Node (expr e :: List.concat_map case cases)
| Texp_tuple es | Texp_array es | Texp_construct (_, _, es) ->
Node (List.map expr es)
| Texp_variant (_, eo) -> Node (option_to_list expr eo)
| Texp_record { fields; extended_expression; _ } ->
let overridden =
Array.to_list fields
|> List.filter_map (function
| _, Overridden (_, e) -> Some (expr e)
| _, Kept _ -> None)
in
Node (overridden @ option_to_list expr extended_expression)
| Texp_field (e, _, _)
| Texp_assert e
| Texp_lazy e
| Texp_letexception (_, e) ->
Node [ expr e ]
| Texp_setfield (e1, _, _, e2) | Texp_sequence (e1, e2) | Texp_while (e1, e2)
->
Node [ expr e1; expr e2 ]
| Texp_ifthenelse (e1, e2, eo) ->
Node (expr e1 :: expr e2 :: option_to_list expr eo)
| Texp_for (_, _, e1, e2, _, e3) -> Node [ expr e1; expr e2; expr e3 ]
| Texp_send (e, _, eo) -> Node (expr e :: option_to_list expr eo)
| Texp_letmodule (_, _, m, body) -> Node [ module_expr m; expr body ]
| Texp_pack m -> module_expr m
| Texp_constant _ | Texp_new _ | Texp_instvar _ | Texp_setinstvar _
| Texp_override _ | Texp_object _ | Texp_unreachable
| Texp_extension_constructor _ ->
Node []

and case c = option_to_list expr c.c_guard @ [ expr c.c_rhs ]

and binding vb =
let name =
match vb.vb_pat.pat_desc with
| Tpat_var (id, _) -> Some (Ident.name id)
| _ -> None
in
{ name; line = vb.vb_pat.pat_loc.loc_start.pos_lnum; expr = expr vb.vb_expr }

and module_expr m =
if not (has_known_constructor module_expr_desc_constructors m.mod_desc) then
Node []
else
match m.mod_desc with
| Tmod_ident _ -> Node []
| Tmod_structure s -> Struct (structure s)
| Tmod_functor (_, _, _, body) | Tmod_constraint (body, _, _, _) ->
module_expr body
| Tmod_apply (f, arg, _) -> Node [ module_expr f; module_expr arg ]
| Tmod_unpack (e, _) -> expr e

and module_items m =
match module_expr m with Struct items -> items | e -> [ Eval e ]

and structure s =
List.concat_map
(fun item ->
if
not
(has_known_constructor structure_item_desc_constructors item.str_desc)
then []
else
match item.str_desc with
| Tstr_eval (e, _) -> [ Eval (expr e) ]
| Tstr_value (_, vbs) -> [ Values (List.map binding vbs) ]
| Tstr_module mb ->
[ Module (Ident.name mb.mb_id, module_items mb.mb_expr) ]
| Tstr_recmodule mbs ->
List.map
(fun mb -> Module (Ident.name mb.mb_id, module_items mb.mb_expr))
mbs
| Tstr_include (incl : include_declaration) ->
[ Include (module_items incl.incl_mod) ]
| _ -> [])
s.str_items

(* Decode the [Implementation] annotation of a CMT file *)
let implementation (tree : Obj.t) = structure (Obj.obj tree : structure)
Loading
Loading