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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
_build
_opam
deps.dot
deps.json
.DS_Store
.rescriptdep_cache.marshal
.rescriptdep_cache.marshal
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Added regression coverage for dependency graph dependent-index updates

### Changed
- 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

## [0.1.2] - 2026-04-22
### Fixed
Expand Down
68 changes: 56 additions & 12 deletions lib/dependency_graph.ml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,66 @@ type module_metadata = { path : string option }
(* Graph representation including module metadata *)
type t = {
dependencies : string list StringMap.t;
dependents : string list StringMap.t;
metadata : module_metadata StringMap.t;
}

(* Empty graph *)
let empty = { dependencies = StringMap.empty; metadata = StringMap.empty }
let empty =
{
dependencies = StringMap.empty;
dependents = StringMap.empty;
metadata = StringMap.empty;
}

let add_dependent dependents dependency module_name =
let current =
try StringMap.find dependency dependents with Not_found -> []
in
let updated =
if List.mem module_name current then current else module_name :: current
in
StringMap.add dependency updated dependents

let remove_dependent dependents dependency module_name =
match StringMap.find_opt dependency dependents with
| None -> dependents
| Some current ->
let updated = List.filter (fun m -> m <> module_name) current in
if updated = [] then StringMap.remove dependency dependents
else StringMap.add dependency updated dependents

let build_dependents dependencies =
StringMap.fold
(fun module_name deps dependents ->
List.fold_left
(fun acc dep -> add_dependent acc dep module_name)
dependents deps)
dependencies StringMap.empty

let make dependencies metadata =
{ dependencies; dependents = build_dependents dependencies; metadata }

(* Add a module and its dependencies to the graph *)
let add graph module_name dependencies path =
let metadata = { path } in
let dependencies = List.sort_uniq String.compare dependencies in
let dependents =
match StringMap.find_opt module_name graph.dependencies with
| None -> graph.dependents
| Some old_dependencies ->
List.fold_left
(fun acc dep -> remove_dependent acc dep module_name)
graph.dependents old_dependencies
in
let dependents =
List.fold_left
(fun acc dep -> add_dependent acc dep module_name)
dependents dependencies
in
{
dependencies = StringMap.add module_name dependencies graph.dependencies;
dependents;
metadata = StringMap.add module_name metadata graph.metadata;
}

Expand Down Expand Up @@ -53,9 +102,9 @@ let get_modules graph = StringMap.bindings graph.dependencies |> List.map fst

(* Find direct dependents of a module (modules that depend on it) *)
let find_dependents graph module_name =
StringMap.fold
(fun m deps acc -> if List.mem module_name deps then m :: acc else acc)
graph.dependencies []
match StringMap.find_opt module_name graph.dependents with
| Some dependents -> List.sort_uniq String.compare dependents
| None -> []

(* Check if a cycle exists in the dependency graph starting from a module *)
let has_cycle graph start_module =
Expand Down Expand Up @@ -274,14 +323,14 @@ let create_filtered_graph graph =
let filtered_deps = create_subgraph_preserve_deps graph project_modules in

(* Create a new graph with the filtered dependencies and original metadata *)
{ dependencies = filtered_deps; metadata = graph.metadata }
make filtered_deps graph.metadata

(* Create a focused graph centered around a specific module *)
let create_focused_graph graph center_module =
(* Check if the module exists *)
if not (StringMap.mem center_module graph.dependencies) then
(* Return empty graph if module doesn't exist *)
{ dependencies = StringMap.empty; metadata = StringMap.empty }
empty
else
(* 1. Get the center module dependencies *)
let center_deps = get_dependencies graph center_module in
Expand All @@ -295,12 +344,7 @@ let create_focused_graph graph center_module =
(* 3. Start building a new graph with just the center module *)
(* Preserve metadata - add center module *)
let center_metadata = get_module_metadata graph center_module in
let result =
{
dependencies = StringMap.singleton center_module center_deps;
metadata = StringMap.singleton center_module center_metadata;
}
in
let result = add empty center_module center_deps center_metadata.path in

(* 4. Add dependency modules and their metadata *)
let result =
Expand Down
22 changes: 22 additions & 0 deletions test/test_namespace.ml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,27 @@ let test_focus_with_namespaced_input () =
(modules = [ "Dep01"; "NS_alias" ])
"Expected focus mode to resolve namespaced input to the existing module"

let test_dependency_graph_dependents_index_updates () =
let graph =
Rescriptdep.Dependency_graph.empty
|> fun graph ->
Rescriptdep.Dependency_graph.add graph "Consumer" [ "Dep01" ] None
|> fun graph -> Rescriptdep.Dependency_graph.add graph "Dep01" [] None
in
assert_true
(Rescriptdep.Dependency_graph.find_dependents graph "Dep01" = [ "Consumer" ])
"Expected dependents index to include direct dependent";

let graph =
Rescriptdep.Dependency_graph.add graph "Consumer" [ "Dep02" ] None
in
assert_true
(Rescriptdep.Dependency_graph.find_dependents graph "Dep01" = [])
"Expected dependents index to drop stale dependencies after module update";
assert_true
(Rescriptdep.Dependency_graph.find_dependents graph "Dep02" = [ "Consumer" ])
"Expected dependents index to include updated dependency"

let test_batch_check_matches_canonical_and_qualified_ast_entries () =
let source_file = Filename.temp_file "rescriptdep-namespace" ".res" in
let ast_path = Filename.remove_extension source_file ^ ".ast" in
Expand Down Expand Up @@ -135,5 +156,6 @@ let () =
test_is_valid_module_name ();
test_extract_dependencies_from_namespaced_imports ();
test_focus_with_namespaced_input ();
test_dependency_graph_dependents_index_updates ();
test_batch_check_matches_canonical_and_qualified_ast_entries ();
print_endline "Namespace handling tests passed"
Loading
Loading