Skip to content

Commit fddf558

Browse files
committed
Load libraries true API using the main-module argument
We now load the main module of the library first and then recursively replace module aliases with the actual signature if the aliased module is part of the library. Signed-off-by: Nathan Rebours <nathan.p.rebours@gmail.com>
1 parent b56cfc2 commit fddf558

File tree

7 files changed

+252
-74
lines changed

7 files changed

+252
-74
lines changed

bin/api_diff.ml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ let run (`Main_module main_module) (`Ref_cmi reference) (`Current_cmi current) =
77
(Sys.is_directory reference, Sys.is_directory current, main_module)
88
with
99
| true, true, Some main_module ->
10-
let+ reference_sig = Api_watch.Library.load reference
11-
and+ current_sig = Api_watch.Library.load current in
10+
let main_module = String.capitalize_ascii main_module in
11+
let+ reference_sig = Api_watch.Library.load ~main_module reference
12+
and+ current_sig = Api_watch.Library.load ~main_module current in
1213
let module_name = String.capitalize_ascii main_module in
1314
(reference_sig, current_sig, module_name)
1415
| false, false, main_module ->

dev-tools/dune

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
(executable
2-
(name print_cmi)
3-
(libraries cmdliner compiler-libs.common))
2+
(name print_api)
3+
(libraries api-watch cmdliner compiler-libs.common))

dev-tools/print_api.ml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
let print_cmi path =
2+
let cmi_infos = Cmi_format.read_cmi path in
3+
Format.printf "cmi_name: %s\n" cmi_infos.cmi_name;
4+
Format.printf "cmi_sign:\n";
5+
Printtyp.signature Format.std_formatter cmi_infos.cmi_sign;
6+
Format.printf "\n"
7+
8+
let all_cmi_files path =
9+
Sys.readdir path |> Array.to_list
10+
|> List.filter (fun p -> Filename.check_suffix p ".cmi")
11+
|> List.map (Filename.concat path)
12+
13+
let run (`Main_module main_module) (`Input fn) =
14+
let open CCResult.Infix in
15+
match (Sys.is_directory fn, main_module) with
16+
| false, _ ->
17+
print_cmi fn;
18+
Ok ()
19+
| true, None ->
20+
let cmi_files = all_cmi_files fn in
21+
List.iter print_cmi cmi_files;
22+
Ok ()
23+
| true, Some main_module ->
24+
let+ sig_ = Api_watch.Library.load ~main_module fn in
25+
Printtyp.signature Format.std_formatter sig_;
26+
Format.printf "\n"
27+
28+
let named f = Cmdliner.Term.(app (const f))
29+
30+
let main_module =
31+
let docv = "MAIN_MODULE_NAME" in
32+
let doc =
33+
"The name of the library's main module. Ignored when input is a $(b,.cmi) \
34+
file"
35+
in
36+
named
37+
(fun x -> `Main_module x)
38+
Cmdliner.Arg.(
39+
value & opt (some string) None & info ~doc ~docv [ "main-module" ])
40+
41+
let input_file =
42+
let docv = "PATH" in
43+
let doc =
44+
"Path to the $(b,.cmi) file or lib directory. If $(docv) is directory\n\
45+
\ and no $(b,--main-module) is provided, prints the API of all \
46+
$(b,.cmi) files.\n\
47+
\ If $(b,--main-module) is provided, prints the public API of the \
48+
library."
49+
in
50+
named
51+
(fun x -> `Input x)
52+
Cmdliner.Arg.(required & pos 0 (some file) None & info ~doc ~docv [])
53+
54+
let info =
55+
let open Cmdliner in
56+
Cmd.info "print_api" ~version:"%%VERSION%%" ~exits:Cmd.Exit.defaults
57+
~doc:"Pretty prints the API of a $(b,.cmi) file or a whole library"
58+
59+
let term = Cmdliner.Term.(const run $ main_module $ input_file)
60+
61+
let () =
62+
let exit_code = Cmdliner.Cmd.eval_result (Cmdliner.Cmd.v info term) in
63+
exit exit_code

dev-tools/print_cmi.ml

Lines changed: 0 additions & 26 deletions
This file was deleted.

lib/library.ml

Lines changed: 179 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,192 @@
1-
let collect_cmi_files dir =
1+
let mod_name file =
2+
String.capitalize_ascii Filename.(remove_extension (basename file))
3+
4+
let lazy_sig path =
5+
Lazy.from_fun (fun () ->
6+
let cmi_infos = Cmi_format.read_cmi path in
7+
cmi_infos.cmi_sign)
8+
9+
let collect_modules dir =
210
try
311
let files = Sys.readdir dir in
4-
Ok
5-
(Array.fold_left
6-
(fun acc file ->
7-
let path = Filename.concat dir file in
8-
if (not (Sys.is_directory path)) && Filename.check_suffix file ".cmi"
9-
then path :: acc
10-
else acc)
11-
[] files)
12+
let map =
13+
Array.fold_left
14+
(fun acc file ->
15+
let path = Filename.concat dir file in
16+
if (not (Sys.is_directory path)) && Filename.check_suffix file ".cmi"
17+
then String_map.add (mod_name file) (lazy_sig path) acc
18+
else acc)
19+
String_map.empty files
20+
in
21+
Ok map
1222
with Sys_error e ->
1323
Error (Printf.sprintf "Error reading directory %s: %s" dir e)
1424

25+
let get_sig modname map =
26+
Option.map Lazy.force (String_map.find_opt modname map)
27+
1528
let load_cmi file_path =
1629
try
1730
let cmi_infos = Cmi_format.read_cmi file_path in
1831
Ok (cmi_infos.cmi_sign, cmi_infos.cmi_name)
1932
with e -> Error (Printexc.to_string e)
2033

21-
let load project_path =
22-
let open CCResult.Infix in
23-
let* cmi_files = collect_cmi_files project_path in
24-
let* signatures =
25-
CCResult.map_l
26-
(fun cmi_file ->
27-
let+ signature, module_name = load_cmi cmi_file in
28-
(module_name, signature))
29-
cmi_files
34+
(* Attach a module name to its various representations, e.g. a [signature] or a
35+
[module_type].
36+
Mostly used to report lookup failures. *)
37+
type 'a named = { name : string; value : 'a }
38+
39+
module Flat_path = struct
40+
type component = Id of Ident.t | Comp of string
41+
type t = component list
42+
43+
let from_path path =
44+
match Path.flatten path with
45+
| `Contains_apply -> None
46+
| `Ok (id, comps) -> Some (Id id :: List.map (fun s -> Comp s) comps)
47+
48+
let modname_from_component = function Id id -> Ident.name id | Comp s -> s
49+
let to_string t = String.concat "." (List.map modname_from_component t)
50+
end
51+
52+
let rec path_in_module ~module_path flat_path =
53+
match flat_path with
54+
| [] -> module_path
55+
| hd :: tl ->
56+
let module_path =
57+
Path.Pdot (module_path, Flat_path.modname_from_component hd)
58+
in
59+
path_in_module ~module_path tl
60+
61+
let rewrite_mty_path mty path =
62+
let open Types in
63+
match mty with
64+
| Mty_ident _ -> Mty_ident path
65+
| Mty_alias _ -> Mty_alias path
66+
| _ -> assert false
67+
68+
let lookup_error ~path ~module_name =
69+
Error (Printf.sprintf "Could not find module %s in %s" path module_name)
70+
71+
let find_module modname sig_ =
72+
let open Types in
73+
let mty_opt =
74+
List.find_map
75+
(function
76+
| Sig_module (id, _, { md_type; _ }, _, _)
77+
when String.equal (Ident.name id) modname ->
78+
Some md_type
79+
| _ -> None)
80+
sig_.value
3081
in
31-
let merged_signature =
32-
List.fold_left
33-
(fun acc (module_name, signature) ->
34-
String_map.add module_name signature acc)
35-
String_map.empty signatures
82+
match mty_opt with
83+
| Some mty -> Ok mty
84+
| None -> lookup_error ~path:modname ~module_name:sig_.name
85+
86+
let rec find_module_in_sig ~library_modules path sig_ =
87+
let open CCResult.Infix in
88+
match (path : Flat_path.t) with
89+
| [ last ] ->
90+
let modname = Flat_path.modname_from_component last in
91+
find_module modname sig_
92+
| hd :: tl ->
93+
let modname = Flat_path.modname_from_component hd in
94+
let* mty = find_module modname sig_ in
95+
find_module_in_md_type ~library_modules tl { name = modname; value = mty }
96+
| [] -> assert false
97+
98+
and find_module_in_md_type ~library_modules path mty =
99+
let open CCResult.Infix in
100+
match mty.value with
101+
| Mty_signature s ->
102+
find_module_in_sig ~library_modules path { name = mty.name; value = s }
103+
| Mty_ident mty_path | Mty_alias mty_path -> (
104+
let* expanded =
105+
match Flat_path.from_path mty_path with
106+
| None -> Ok None
107+
| Some flat_mty_path ->
108+
find_module_in_lib ~library_modules flat_mty_path
109+
in
110+
match expanded with
111+
| Some expanded_mty ->
112+
find_module_in_md_type ~library_modules path
113+
{ name = Path.name mty_path; value = expanded_mty }
114+
| None ->
115+
let expanded_path = path_in_module ~module_path:mty_path path in
116+
Ok (rewrite_mty_path mty.value expanded_path))
117+
| _ -> lookup_error ~path:(Flat_path.to_string path) ~module_name:mty.name
118+
119+
and find_module_in_lib ~library_modules path :
120+
(Types.module_type option, string) result =
121+
let open Types in
122+
let open CCResult.Infix in
123+
match path with
124+
| [ comp ] ->
125+
let modname = Flat_path.modname_from_component comp in
126+
let sig_opt = get_sig modname library_modules in
127+
Ok (Option.map (fun s -> Mty_signature s) sig_opt)
128+
| comp :: inner_path -> (
129+
let modname = Flat_path.modname_from_component comp in
130+
match get_sig modname library_modules with
131+
| None -> Ok None
132+
| Some parent_sig -> (
133+
let* mty =
134+
find_module_in_sig ~library_modules inner_path
135+
{ name = modname; value = parent_sig }
136+
in
137+
match mty with
138+
| Mty_signature _ | Mty_functor _ -> Ok (Some mty)
139+
| Mty_ident path' | Mty_alias path' -> (
140+
match Flat_path.from_path path' with
141+
| None -> Ok (Some mty)
142+
| Some fpath -> find_module_in_lib ~library_modules fpath)))
143+
| _ -> Ok None
144+
145+
let rec expand_sig ~library_modules sig_ =
146+
let open Types in
147+
let open CCResult.Infix in
148+
CCResult.map_l
149+
(fun item ->
150+
match item with
151+
| Sig_module
152+
( id,
153+
presence,
154+
({ md_type = Mty_ident path | Mty_alias path; _ } as mod_decl),
155+
rs,
156+
vis ) -> (
157+
match Flat_path.from_path path with
158+
| None -> Ok item
159+
| Some fpath -> (
160+
let* mty_opt = find_module_in_lib ~library_modules fpath in
161+
match mty_opt with
162+
| None -> Ok item
163+
| Some mty ->
164+
let* expanded =
165+
match mty with
166+
| Mty_signature s ->
167+
let* expanded = expand_sig ~library_modules s in
168+
Ok (Mty_signature expanded)
169+
| _ -> Ok mty
170+
in
171+
let presence =
172+
match expanded with
173+
| Mty_alias _ -> presence
174+
| _ -> Mp_present
175+
in
176+
let mod_decl' = { mod_decl with md_type = expanded } in
177+
Ok (Sig_module (id, presence, mod_decl', rs, vis))))
178+
| _ -> Ok item)
179+
sig_
180+
181+
let load ~main_module project_path =
182+
let open CCResult.Infix in
183+
let* library_modules = collect_modules project_path in
184+
let* main_sig =
185+
match get_sig main_module library_modules with
186+
| Some s -> Ok s
187+
| None ->
188+
Error
189+
(Printf.sprintf "Could not find main module %s in %s" main_module
190+
project_path)
36191
in
37-
Ok
38-
(String_map.fold
39-
(fun module_name module_sig acc ->
40-
Types.Sig_module
41-
( Ident.create_local module_name,
42-
Mp_present,
43-
{
44-
md_type = Mty_signature module_sig;
45-
md_attributes = [];
46-
md_loc = Location.none;
47-
md_uid = Types.Uid.internal_not_actually_unique;
48-
},
49-
Trec_not,
50-
Exported )
51-
:: acc)
52-
merged_signature [])
192+
expand_sig ~library_modules main_sig

lib/library.mli

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
val load_cmi : string -> (Types.signature * string, string) result
2-
val load : string -> (Types.signature, string) result
2+
val load : main_module:string -> string -> (Types.signature, string) result

tests/api-diff/run.t

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -364,17 +364,17 @@ Build the second version
364364
$ cd project_v2 && dune build && cd ..
365365

366366
Run the api-diff tool on the two project versions
367-
$ api-diff --main-module myproject project_v1/_build/default/lib/.mylib.objs/byte project_v2/_build/default/lib/.mylib.objs/byte
368-
diff module Myproject.Mylib__Math:
367+
$ api-diff --main-module mylib project_v1/_build/default/lib/.mylib.objs/byte project_v2/_build/default/lib/.mylib.objs/byte
368+
diff module Mylib.Math:
369369
-val add : int -> int -> int
370370
+val add : int -> int -> int -> int
371371
+val multiply : int -> int -> int
372372
+module New_module: sig val hello : unit -> string end
373373

374-
diff module Myproject.Mylib__Math.Advanced:
374+
diff module Mylib.Math.Advanced:
375375
+val cube : int -> int
376376

377-
diff module Myproject.Mylib__Utils:
377+
diff module Mylib.Utils:
378378
+val triple : int -> int
379379

380380
[1]

0 commit comments

Comments
 (0)