Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
- Refactor analysis CLI helpers to use source input. https://github.com/rescript-lang/rescript/pull/8466
- Include syntax, gentype, analysis, tools, and reanalyze tests in coverage reports. https://github.com/rescript-lang/rescript/pull/8467
- Remove the unreachable `Longident.Lapply` constructor (OCaml's applicative-functor path syntax `F(X).t`, which ReScript's grammar cannot produce). https://github.com/rescript-lang/rescript/pull/8469
- Refactor analysis for server side use. https://github.com/rescript-lang/rescript/pull/8478

# 13.0.0-alpha.4

Expand Down
90 changes: 52 additions & 38 deletions analysis/src/codemod.ml
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,57 @@ let rec collect_patterns p =
| Ppat_or (p1, p2) -> collect_patterns p1 @ [p2]
| _ -> [p]

let transform ~source ~pos ~debug ~typ ~hint =
let structure, print_expr, _, _ = Xform.parse_implementation ~source in
match typ with
| AddMissingCases -> (
let source = "let " ^ hint ^ " = ()" in
let {Res_driver.parsetree = hint_structure} =
Res_driver.parse_implementation_from_source ~for_printer:false
~display_filename:"<none>" ~source
in
match hint_structure with
| [{pstr_desc = Pstr_value (_, [{pvb_pat = pattern}])}] -> (
let cases =
collect_patterns pattern
|> List.map (fun (p : Parsetree.pattern) ->
Ast_helper.Exp.case p (Type_utils.Codegen.mk_fail_with_exp ()))
let transform_opt ~source ~pos ~debug ~typ ~hint =
let log message = if debug then print_endline message in
try
let structure, print_expr, _, _ = Xform.parse_implementation ~source in
match typ with
| AddMissingCases -> (
let source = "let " ^ hint ^ " = ()" in
let {Res_driver.parsetree = hint_structure} =
Res_driver.parse_implementation_from_source ~for_printer:false
~display_filename:"<none>" ~source
in
let result = ref None in
let mk_iterator ~pos ~result =
let expr (iterator : Ast_iterator.iterator) (exp : Parsetree.expression)
=
match exp.pexp_desc with
| Pexp_match (e, existing_cases)
when Pos.of_lexing exp.pexp_loc.loc_start = pos ->
result :=
Some {exp with pexp_desc = Pexp_match (e, existing_cases @ cases)}
| _ -> Ast_iterator.default_iterator.expr iterator exp
match hint_structure with
| [{pstr_desc = Pstr_value (_, [{pvb_pat = pattern}])}] -> (
let cases =
collect_patterns pattern
|> List.map (fun (p : Parsetree.pattern) ->
Ast_helper.Exp.case p (Type_utils.Codegen.mk_fail_with_exp ()))
in
{Ast_iterator.default_iterator with expr}
in
let iterator = mk_iterator ~pos ~result in
iterator.structure iterator structure;
match !result with
| None ->
if debug then print_endline "Found no result";
exit 1
| Some switch_expr ->
print_expr ~range:(Loc.range_of_loc switch_expr.pexp_loc) switch_expr)
| _ ->
if debug then print_endline "Mismatch in expected structure";
exit 1)
let result = ref None in
let mk_iterator ~pos ~result =
let expr (iterator : Ast_iterator.iterator)
(exp : Parsetree.expression) =
match exp.pexp_desc with
| Pexp_match (e, existing_cases)
when Pos.of_lexing exp.pexp_loc.loc_start = pos ->
result :=
Some
{exp with pexp_desc = Pexp_match (e, existing_cases @ cases)}
| _ -> Ast_iterator.default_iterator.expr iterator exp
in
{Ast_iterator.default_iterator with expr}
in
let iterator = mk_iterator ~pos ~result in
iterator.structure iterator structure;
match !result with
| None ->
log "Found no result";
None
| Some switch_expr ->
Some
(print_expr
~range:(Loc.range_of_loc switch_expr.pexp_loc)
switch_expr))
| _ ->
log "Mismatch in expected structure";
None)
with exn ->
log ("Codemod failed: " ^ Printexc.to_string exn);
None

let transform ~source ~pos ~debug ~typ ~hint =
match transform_opt ~source ~pos ~debug ~typ ~hint with
| Some result -> result
| None -> exit 1
19 changes: 10 additions & 9 deletions analysis/src/document_symbol.ml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ let get_symbols ~source ~kind_file =
then
let range = Utils.cmt_loc_to_range loc in
let symbol =
Lsp.Types.DocumentSymbol.create ~name ~range ~selectionRange:range
~children:[] ~kind ()
Lsp.Types.DocumentSymbol.create ~name ~range ~selectionRange:range ~kind
()
in
symbols := symbol :: !symbols
in
Expand Down Expand Up @@ -165,13 +165,14 @@ let get_symbols ~source ~kind_file =
| [] -> [symbol]
| last :: rest ->
if is_inside symbol last then
match last.children with
| Some c ->
let new_last =
{last with children = Some (c |> add_symbol_to_children ~symbol)}
in
new_last :: rest
| _ -> rest
let children = last.children |> Option.value ~default:[] in
let new_last =
{
last with
children = Some (children |> add_symbol_to_children ~symbol);
}
in
new_last :: rest
else symbol :: children
in
let rec add_sorted_symbols_to_children ~sorted_symbols children =
Expand Down
14 changes: 14 additions & 0 deletions analysis/src/packages.ml
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,26 @@ let new_bs_package ~root_path =
|> List.rev_append opens_from_compiler_flags
|> List.map (fun path -> path @ ["place holder"])
in
let dependencies =
match
( config
|> Yojson_helpers.get "dependencies"
|> bind Yojson_helpers.to_list_opt,
config
|> Yojson_helpers.get "bs-dependencies"
|> bind Yojson_helpers.to_list_opt )
with
| None, None -> []
| Some deps, None | _, Some deps ->
deps |> List.filter_map Yojson_helpers.string_opt

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve qualified dependency entries

When rescript.json uses the supported qualified dependency form like { "name": "pkg", "features": [...] }, this filter keeps only JSON strings and silently omits that dependency from the analysis package state. The build config accepts both shorthand and qualified dependency entries (rewatch/src/config.rs defines Dependency as either Shorthand or Qualified and wires it to dependencies), so server-side consumers of this new metadata will see an incomplete dependency list for feature-gated packages. Please extract the name field from object entries instead of dropping them.

Useful? React with 👍 / 👎.

@aspeddro aspeddro Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently supported, @fhammerschmidt?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No clue, but feel free to create another issue for it.

in
{
generic_jsx_module;
suffix;
rescript_version;
root_path;
project_files;
dependencies;
dependencies_files;
paths_for_module;
opens;
Expand Down
106 changes: 106 additions & 0 deletions analysis/src/shared_types.ml
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ and package = {
suffix: string;
root_path: file_path;
project_files: File_set.t;
dependencies: string list;
dependencies_files: File_set.t;
paths_for_module: (file, paths) Hashtbl.t;
namespace: string option;
Expand Down Expand Up @@ -965,3 +966,108 @@ let extract_exp_apply_args ~args =
| [] -> List.rev acc
in
args |> process_args ~acc:[]

let state_to_yojson (state : state) =
let option_to_yojson f = function
| None -> `Null
| Some value -> f value
in

let string_set_to_yojson set =
`List (set |> File_set.elements |> List.map (fun value -> `String value))
in

let path_to_yojson path = `List (List.map (fun item -> `String item) path) in

let paths_to_yojson = function
| Impl {cmt; res} ->
`Assoc
[("kind", `String "Impl"); ("cmt", `String cmt); ("res", `String res)]
| Namespace {cmt} ->
`Assoc [("kind", `String "Namespace"); ("cmt", `String cmt)]
| IntfAndImpl {cmti; resi; cmt; res} ->
`Assoc
[
("kind", `String "IntfAndImpl");
("cmti", `String cmti);
("resi", `String resi);
("cmt", `String cmt);
("res", `String res);
]
in

let paths_for_module_to_yojson paths_for_module =
paths_for_module |> Hashtbl.to_seq
|> Seq.map (fun (file, paths) -> (file, paths_to_yojson paths))
|> List.of_seq
|> fun fields -> `Assoc fields
in

let autocomplete_to_yojson autocomplete =
autocomplete |> Misc.String_map.bindings
|> List.map (fun (name, files) ->
(name, `List (List.map (fun file -> `String file) files)))
|> fun fields -> `Assoc fields
in

let package_to_yojson (package : package) =
let major, minor = package.rescript_version in
`Assoc
[
( "generic_jsx_module",
option_to_yojson
(fun value -> `String value)
package.generic_jsx_module );
("suffix", `String package.suffix);
("root_path", `String package.root_path);
("project_files", string_set_to_yojson package.project_files);
( "dependencies",
`List (package.dependencies |> List.map (fun x -> `String x)) );
("dependencies_files", string_set_to_yojson package.dependencies_files);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include package dependencies in serialized state

When state_to_yojson serializes each package, it never emits the new package.dependencies field populated in Packages.new_bs_package; it jumps from project_files to dependencies_files. Server-side consumers calling this new JSON API can inspect dependency file paths but cannot see the dependency names/metadata this change adds, so packages with entries like "dependencies": ["@rescript/react"] are indistinguishable from packages with no dependency list. Please serialize package.dependencies here as its own field.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

("paths_for_module", paths_for_module_to_yojson package.paths_for_module);
( "namespace",
option_to_yojson (fun value -> `String value) package.namespace );
("opens", `List (List.map path_to_yojson package.opens));
( "rescript_version",
`Assoc [("major", `Int major); ("minor", `Int minor)] );
("autocomplete", autocomplete_to_yojson package.autocomplete);
]
in

let file_to_yojson (file : File.t) =
`Assoc
[
("uri", `String (file.uri |> Lsp.Uri.to_string));
("module_name", `String file.module_name);
("stamps_count", `Int (List.length (Stamps.get_entries file.stamps)));
("structure_name", `String file.structure.name);
( "structure_docstring",
`List (List.map (fun value -> `String value) file.structure.docstring)
);
("structure_items_count", `Int (List.length file.structure.items));
]
in

let cmt_cache =
state.cmt_cache |> Hashtbl.to_seq
|> Seq.map (fun (file_path, file) -> (file_path, file_to_yojson file))
|> List.of_seq
in

let root_for_uri =
state.root_for_uri |> Hashtbl.to_seq |> List.of_seq
|> List.map (fun (uri, str) -> [(Lsp.Uri.to_string uri, `String str)])
|> List.flatten
in

let packages_by_root =
state.packages_by_root |> Hashtbl.to_seq |> List.of_seq
|> List.map (fun (root, package) -> (root, package_to_yojson package))
in

`Assoc
[
("cmt_cache", `Assoc cmt_cache);
("root_for_uri", `Assoc root_for_uri);
("packages_by_root", `Assoc packages_by_root);
]
11 changes: 0 additions & 11 deletions tests/analysis_tests/tests/src/expected/DocumentSymbol.res.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
DocumentSymbol src/DocumentSymbol.res
[
{
"children": [],
"kind": 16,
"name": "zzz",
"range": {
Expand All @@ -18,7 +17,6 @@ DocumentSymbol src/DocumentSymbol.res
{
"children": [
{
"children": [],
"kind": 12,
"name": "make",
"range": {
Expand Down Expand Up @@ -55,7 +53,6 @@ DocumentSymbol src/DocumentSymbol.res
}
},
{
"children": [],
"kind": 16,
"name": "fa",
"range": {
Expand All @@ -70,7 +67,6 @@ DocumentSymbol src/DocumentSymbol.res
{
"children": [
{
"children": [],
"kind": 12,
"name": "abd",
"range": {
Expand All @@ -83,7 +79,6 @@ DocumentSymbol src/DocumentSymbol.res
}
},
{
"children": [],
"kind": 12,
"name": "abc",
"range": {
Expand All @@ -96,7 +91,6 @@ DocumentSymbol src/DocumentSymbol.res
}
},
{
"children": [],
"kind": 26,
"name": "t",
"range": {
Expand All @@ -121,7 +115,6 @@ DocumentSymbol src/DocumentSymbol.res
}
},
{
"children": [],
"kind": 13,
"name": "op",
"range": {
Expand All @@ -136,7 +129,6 @@ DocumentSymbol src/DocumentSymbol.res
{
"children": [
{
"children": [],
"kind": 12,
"name": "next",
"range": {
Expand All @@ -149,7 +141,6 @@ DocumentSymbol src/DocumentSymbol.res
}
},
{
"children": [],
"kind": 12,
"name": "foo",
"range": {
Expand All @@ -176,7 +167,6 @@ DocumentSymbol src/DocumentSymbol.res
{
"children": [
{
"children": [],
"kind": 12,
"name": "customDouble",
"range": {
Expand All @@ -201,7 +191,6 @@ DocumentSymbol src/DocumentSymbol.res
}
},
{
"children": [],
"kind": 2,
"name": "MyList",
"range": {
Expand Down
Loading