Skip to content

Commit 80b3811

Browse files
cristianocclaude
andcommitted
Reanalyze server: invalidate cache on config change
Re-read rescript.json before each server request and recreate the reactive pipeline from scratch when the reanalyze config changes (suppress, unsuppress, analysis mode, transitive). Closes rescript-lang/rescript-vscode#1180 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f0b3b3b commit 80b3811

File tree

4 files changed

+165
-40
lines changed

4 files changed

+165
-40
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
#### :bug: Bug fix
2222

23+
- Reanalyze server: invalidate cache and recompute results when config changes in `rescript.json`. https://github.com/rescript-lang/rescript/pull/8262
24+
2325
#### :memo: Documentation
2426

2527
#### :nail_care: Polish

analysis/reanalyze/src/ReanalyzeServer.ml

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ module Server = struct
9696
let s = Gc.quick_stat () in
9797
mb_of_words s.live_words
9898

99+
type reactive_pipeline = {
100+
dce_config: DceConfig.t;
101+
reactive_collection: ReactiveAnalysis.t;
102+
reactive_merge: ReactiveMerge.t;
103+
reactive_liveness: ReactiveLiveness.t;
104+
reactive_solver: ReactiveSolver.t;
105+
}
106+
99107
type server_state = {
100108
parse_argv: string array -> string option;
101109
run_analysis:
@@ -111,12 +119,9 @@ module Server = struct
111119
unit;
112120
config: server_config;
113121
cmtRoot: string option;
114-
dce_config: DceConfig.t;
115-
reactive_collection: ReactiveAnalysis.t;
116-
reactive_merge: ReactiveMerge.t;
117-
reactive_liveness: ReactiveLiveness.t;
118-
reactive_solver: ReactiveSolver.t;
122+
mutable pipeline: reactive_pipeline;
119123
stats: server_stats;
124+
mutable config_snapshot: RunConfig.snapshot;
120125
}
121126

122127
type request_info = {
@@ -261,6 +266,32 @@ Examples:
261266
unlink_if_exists stderr_path)
262267
run
263268

269+
let create_reactive_pipeline () : reactive_pipeline =
270+
let dce_config = DceConfig.current () in
271+
let reactive_collection = ReactiveAnalysis.create ~config:dce_config in
272+
let file_data_collection =
273+
ReactiveAnalysis.to_file_data_collection reactive_collection
274+
in
275+
let reactive_merge = ReactiveMerge.create file_data_collection in
276+
let reactive_liveness = ReactiveLiveness.create ~merged:reactive_merge in
277+
let value_refs_from =
278+
if dce_config.DceConfig.run.transitive then None
279+
else Some reactive_merge.ReactiveMerge.value_refs_from
280+
in
281+
let reactive_solver =
282+
ReactiveSolver.create ~decls:reactive_merge.ReactiveMerge.decls
283+
~live:reactive_liveness.ReactiveLiveness.live
284+
~annotations:reactive_merge.ReactiveMerge.annotations ~value_refs_from
285+
~config:dce_config
286+
in
287+
{
288+
dce_config;
289+
reactive_collection;
290+
reactive_merge;
291+
reactive_liveness;
292+
reactive_solver;
293+
}
294+
264295
let init_state ~(parse_argv : string array -> string option)
265296
~(run_analysis :
266297
dce_config:DceConfig.t ->
@@ -291,39 +322,16 @@ Examples:
291322
server with editor-like args only."
292323
!Cli.churn
293324
else
294-
let dce_config = DceConfig.current () in
295-
let reactive_collection =
296-
ReactiveAnalysis.create ~config:dce_config
297-
in
298-
let file_data_collection =
299-
ReactiveAnalysis.to_file_data_collection reactive_collection
300-
in
301-
let reactive_merge = ReactiveMerge.create file_data_collection in
302-
let reactive_liveness =
303-
ReactiveLiveness.create ~merged:reactive_merge
304-
in
305-
let value_refs_from =
306-
if dce_config.DceConfig.run.transitive then None
307-
else Some reactive_merge.ReactiveMerge.value_refs_from
308-
in
309-
let reactive_solver =
310-
ReactiveSolver.create ~decls:reactive_merge.ReactiveMerge.decls
311-
~live:reactive_liveness.ReactiveLiveness.live
312-
~annotations:reactive_merge.ReactiveMerge.annotations
313-
~value_refs_from ~config:dce_config
314-
in
325+
let pipeline = create_reactive_pipeline () in
315326
Ok
316327
{
317328
parse_argv;
318329
run_analysis;
319330
config;
320331
cmtRoot;
321-
dce_config;
322-
reactive_collection;
323-
reactive_merge;
324-
reactive_liveness;
325-
reactive_solver;
332+
pipeline;
326333
stats = {request_count = 0};
334+
config_snapshot = RunConfig.snapshot ();
327335
})
328336

329337
let run_one_request (state : server_state) (_req : request) :
@@ -347,6 +355,17 @@ Examples:
347355
(* Always run from the server's project root; client cwd is not stable in VS Code. *)
348356
state.config.cwd (fun () ->
349357
capture_stdout_stderr (fun () ->
358+
(* Re-read config from rescript.json to detect changes.
359+
If changed, recreate the entire reactive pipeline from scratch. *)
360+
RunConfig.reset ();
361+
Paths.Config.processConfig ();
362+
let new_snapshot = RunConfig.snapshot () in
363+
if
364+
not
365+
(RunConfig.equal_snapshot state.config_snapshot new_snapshot)
366+
then (
367+
state.pipeline <- create_reactive_pipeline ();
368+
state.config_snapshot <- new_snapshot);
350369
Log_.Color.setup ();
351370
Timing.enabled := !Cli.timing;
352371
Reactive.set_debug !Cli.timing;
@@ -357,18 +376,18 @@ Examples:
357376
(* Match direct CLI output (a leading newline before the JSON array). *)
358377
Printf.printf "\n";
359378
EmitJson.start ();
360-
state.run_analysis ~dce_config:state.dce_config
361-
~cmtRoot:state.cmtRoot
362-
~reactive_collection:(Some state.reactive_collection)
363-
~reactive_merge:(Some state.reactive_merge)
364-
~reactive_liveness:(Some state.reactive_liveness)
365-
~reactive_solver:(Some state.reactive_solver) ~skip_file:None
379+
let p = state.pipeline in
380+
state.run_analysis ~dce_config:p.dce_config ~cmtRoot:state.cmtRoot
381+
~reactive_collection:(Some p.reactive_collection)
382+
~reactive_merge:(Some p.reactive_merge)
383+
~reactive_liveness:(Some p.reactive_liveness)
384+
~reactive_solver:(Some p.reactive_solver) ~skip_file:None
366385
~file_stats ();
367386
issue_count := Log_.Stats.get_issue_count ();
368-
let d, l = ReactiveSolver.stats ~t:state.reactive_solver in
387+
let d, l = ReactiveSolver.stats ~t:p.reactive_solver in
369388
dead_count := d;
370389
live_count := l;
371-
Log_.Stats.report ~config:state.dce_config;
390+
Log_.Stats.report ~config:p.dce_config;
372391
Log_.Stats.clear ();
373392
EmitJson.finish ())
374393
|> response_of_result)

analysis/reanalyze/src/RunConfig.ml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ let runConfig =
2121
unsuppress = [];
2222
}
2323

24+
let reset () =
25+
runConfig.dce <- false;
26+
runConfig.exception_ <- false;
27+
runConfig.suppress <- [];
28+
runConfig.termination <- false;
29+
runConfig.transitive <- false;
30+
runConfig.unsuppress <- []
31+
2432
let all () =
2533
runConfig.dce <- true;
2634
runConfig.exception_ <- true;
@@ -31,3 +39,24 @@ let exception_ () = runConfig.exception_ <- true
3139
let termination () = runConfig.termination <- true
3240

3341
let transitive b = runConfig.transitive <- b
42+
43+
type snapshot = {
44+
dce: bool;
45+
exception_: bool;
46+
suppress: string list;
47+
termination: bool;
48+
transitive: bool;
49+
unsuppress: string list;
50+
}
51+
52+
let snapshot () =
53+
{
54+
dce = runConfig.dce;
55+
exception_ = runConfig.exception_;
56+
suppress = runConfig.suppress;
57+
termination = runConfig.termination;
58+
transitive = runConfig.transitive;
59+
unsuppress = runConfig.unsuppress;
60+
}
61+
62+
let equal_snapshot (a : snapshot) (b : snapshot) = a = b

tests/analysis_tests/tests-reanalyze/deadcode/test-reactive-server.sh

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ time_end() {
5858
}
5959

6060
BACKUP_FILE="/tmp/reactive-test-backup.$$"
61+
CONFIG_BACKUP_FILE="/tmp/reactive-test-config-backup.$$"
6162
DEFAULT_SOCKET_FILE=""
63+
CONFIG_FILE=""
6264

6365
# Cleanup function
6466
cleanup() {
@@ -67,6 +69,11 @@ cleanup() {
6769
cp "$BACKUP_FILE" "$TEST_FILE"
6870
rm -f "$BACKUP_FILE"
6971
fi
72+
# Restore config file if backup exists
73+
if [[ -n "$CONFIG_FILE" && -f "$CONFIG_BACKUP_FILE" ]]; then
74+
cp "$CONFIG_BACKUP_FILE" "$CONFIG_FILE"
75+
rm -f "$CONFIG_BACKUP_FILE"
76+
fi
7077
# Stop server if running
7178
if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
7279
kill "$SERVER_PID" 2>/dev/null || true
@@ -190,6 +197,12 @@ configure_project() {
190197
log_error "Could not find test file for project: $project_name"
191198
exit 1
192199
fi
200+
201+
CONFIG_FILE="$PROJECT_DIR/rescript.json"
202+
if [[ ! -f "$CONFIG_FILE" ]]; then
203+
log_error "Could not find config file: $CONFIG_FILE"
204+
exit 1
205+
fi
193206
}
194207

195208
configure_project
@@ -214,6 +227,7 @@ time_end "initial_build"
214227

215228
# Backup the test file
216229
cp "$TEST_FILE" "$BACKUP_FILE"
230+
cp "$CONFIG_FILE" "$CONFIG_BACKUP_FILE"
217231

218232
# Start the server
219233
start_server() {
@@ -332,6 +346,24 @@ PY
332346
time_end "json_compare"
333347
}
334348

349+
set_config_suppress_all() {
350+
python3 - <<PY
351+
import json
352+
353+
path = "$CONFIG_FILE"
354+
with open(path) as f:
355+
cfg = json.load(f)
356+
357+
reanalyze = cfg.setdefault("reanalyze", {})
358+
reanalyze["suppress"] = ["src"]
359+
reanalyze["unsuppress"] = []
360+
361+
with open(path, "w") as f:
362+
json.dump(cfg, f, indent=2)
363+
f.write("\\n")
364+
PY
365+
}
366+
335367
# Add an unused value to the test file (creates +1 dead code warning)
336368
add_unused_value() {
337369
log_verbose "Adding unused value to $TEST_FILE"
@@ -487,6 +519,42 @@ run_scenario_make_live() {
487519
return 0
488520
}
489521

522+
run_scenario_config_change() {
523+
local baseline_count="$1"
524+
local server_after="/tmp/reanalyze-after-config-server-$$.json"
525+
local server_restored="/tmp/reanalyze-restored-config-server-$$.json"
526+
527+
log_edit "Updating rescript.json (reanalyze.suppress=[\"src\"])..."
528+
set_config_suppress_all
529+
530+
log_reactive "Analyzing after config change..."
531+
send_request "$server_after" "incremental"
532+
local suppressed_count
533+
suppressed_count=$(count_issues "$server_after")
534+
if [[ "$suppressed_count" -ne 0 ]]; then
535+
log_error "Expected 0 issues after suppressing src via config, got $suppressed_count"
536+
rm -f "$server_after" "$server_restored"
537+
return 1
538+
fi
539+
540+
cp "$CONFIG_BACKUP_FILE" "$CONFIG_FILE"
541+
542+
log_reactive "Analyzing after config restore..."
543+
send_request "$server_restored" "incremental"
544+
545+
local restored_count
546+
restored_count=$(count_issues "$server_restored")
547+
if [[ "$restored_count" -ne "$baseline_count" ]]; then
548+
log_error "Expected $baseline_count issues after restoring config, got $restored_count"
549+
rm -f "$server_after" "$server_restored"
550+
return 1
551+
fi
552+
553+
log "✓ Config change invalidates/recomputes server cache correctly"
554+
rm -f "$server_after" "$server_restored"
555+
return 0
556+
}
557+
490558
# Run one benchmark iteration
491559
run_iteration() {
492560
local iter="$1"
@@ -539,6 +607,14 @@ main() {
539607
baseline_count=$(count_issues "$baseline_file")
540608
log "Baseline: $baseline_count issues"
541609
log ""
610+
611+
if ! run_scenario_config_change "$baseline_count"; then
612+
log_error "Config-change scenario failed"
613+
stop_server
614+
rm -f "$baseline_file"
615+
exit 1
616+
fi
617+
log ""
542618

543619
#-----------------------------------------
544620
# BENCHMARK PHASE: Edit → Rebuild → Measure
@@ -627,4 +703,3 @@ main() {
627703
}
628704

629705
main
630-

0 commit comments

Comments
 (0)