Skip to content

Commit aa854ba

Browse files
azzsalSiddhi-agg
andauthored
Add Support for unwrapped libraries (#107)
* Add unwrapped library flag to api_diff command line tool * Refactor Api_diff module * Add more elaborate error messages for the api_diff CLI tool * Update cram tests to match api-diff error messages * Add Diff.library to diff entire libraries * Add Library.load_unwrapped and make Library.load match it * Format the changes * Make changes to use load_unwrapped * Extract warning printing and fix tests * Extract Library.t and fix Library.load_unwrappped * Add cram test for unwrapped libs * Fix the cram test for unwrapped library Signed-off-by: Siddhi-agg <siddhiagrawalcool@gmail.com> * Minor fix * Add changelog entry --------- Signed-off-by: Siddhi-agg <siddhiagrawalcool@gmail.com> Co-authored-by: Siddhi-agg <siddhiagrawalcool@gmail.com>
1 parent f99f00f commit aa854ba

File tree

9 files changed

+309
-44
lines changed

9 files changed

+309
-44
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Add detection of module_type declarations changes (#93, @NchamJosephMuam)
77
- Add detection of classes addition and removal (#90, @marcndo)
88
- Add detection of addition and removal of class type declarations (#103, @azzsal)
9+
- Add initial support for unwrapped libraries (#107, @Siddhi-agg, @azzsal)
910

1011
### Changed
1112

bin/api_diff.ml

Lines changed: 83 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,83 @@
11
let tool_name = "api-diff"
22

3-
let run (`Main_module main_module) (`Ref_cmi reference) (`Current_cmi current) =
3+
type mode = Unwrapped | Wrapped of string | Cmi
4+
5+
let both_directories reference current =
6+
match (Sys.is_directory reference, Sys.is_directory current) with
7+
| true, true -> Ok true
8+
| false, false -> Ok false
9+
| _ ->
10+
Error
11+
"Arguments must either both be directories or both single .cmi files."
12+
13+
let print_warning main_module unwrapped =
14+
match (main_module, unwrapped) with
15+
| None, false -> ()
16+
| Some _, false ->
17+
Printf.eprintf
18+
"%s: --main-module is ignored when diffing single .cmi files\n"
19+
tool_name
20+
| None, true ->
21+
Printf.eprintf
22+
"%s: --unwrapped is ignored when diffing single .cmi files\n" tool_name
23+
| Some _, true ->
24+
Printf.eprintf
25+
"%s: --main-module and --unwrapped are ignored when diffing single \
26+
.cmi files\n"
27+
tool_name
28+
29+
let mode ~reference ~current ~main_module ~unwrapped =
430
let open CCResult.Infix in
5-
let* reference_sig, current_sig, module_name =
6-
match
7-
(Sys.is_directory reference, Sys.is_directory current, main_module)
8-
with
9-
| true, true, Some main_module ->
31+
let* both_dirs = both_directories reference current in
32+
match (both_dirs, main_module, unwrapped) with
33+
| true, Some main_module, false -> Ok (Wrapped main_module)
34+
| true, None, true -> Ok Unwrapped
35+
| false, main_module, unwrapped ->
36+
print_warning main_module unwrapped;
37+
Ok Cmi
38+
| true, _, _ ->
39+
Error
40+
"Either --main-module or --unwrapped must be provided when diffing \
41+
entire libraries."
42+
43+
let run (`Main_module main_module) (`Unwrapped_library unwrapped)
44+
(`Ref_cmi reference) (`Current_cmi current) =
45+
let open CCResult.Infix in
46+
let* reference_map, current_map =
47+
let* curr_mode = mode ~reference ~current ~main_module ~unwrapped in
48+
match curr_mode with
49+
| Wrapped main_module ->
1050
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
13-
let module_name = String.capitalize_ascii main_module in
14-
(reference_sig, current_sig, module_name)
15-
| false, false, main_module ->
16-
let () =
17-
match main_module with
18-
| None -> ()
19-
| Some _ ->
20-
Printf.eprintf
21-
"%s: --main-module ignored when diffing single .cmi files\n"
22-
tool_name
23-
in
51+
let+ reference_map = Api_watch.Library.load ~main_module reference
52+
and+ current_map = Api_watch.Library.load ~main_module current in
53+
(reference_map, current_map)
54+
| Unwrapped ->
55+
let+ reference_map = Api_watch.Library.load_unwrapped reference
56+
and+ current_map = Api_watch.Library.load_unwrapped current in
57+
(reference_map, current_map)
58+
| Cmi ->
2459
let+ reference_cmi, _ = Api_watch.Library.load_cmi reference
2560
and+ current_cmi, module_name = Api_watch.Library.load_cmi current in
26-
(reference_cmi, current_cmi, module_name)
27-
| true, false, _ | false, true, _ ->
28-
Error
29-
"Arguments must either both be directories or both single .cmi files."
30-
| true, true, None ->
31-
Error "--main-module must be provided when diffing entire libraries."
61+
let reference_map =
62+
Api_watch.String_map.singleton module_name reference_cmi
63+
in
64+
let current_map =
65+
Api_watch.String_map.singleton module_name current_cmi
66+
in
67+
(reference_map, current_map)
3268
in
33-
let diff =
34-
Api_watch.Diff.interface ~module_name ~reference:reference_sig
35-
~current:current_sig
69+
let diff_map =
70+
Api_watch.Diff.library ~reference:reference_map ~current:current_map
71+
|> Api_watch.String_map.bindings
72+
|> List.filter_map (fun (_, v) -> v)
3673
in
37-
match diff with
38-
| None -> Ok 0
39-
| Some diff ->
74+
let has_changes = not (List.is_empty diff_map) in
75+
List.iter
76+
(fun diff ->
4077
let text_diff = Api_watch.Text_diff.from_diff diff in
41-
Api_watch.Text_diff.With_colors.pp Format.std_formatter text_diff;
42-
Ok 1
78+
Api_watch.Text_diff.With_colors.pp Format.std_formatter text_diff)
79+
diff_map;
80+
if has_changes then Ok 1 else Ok 0
4381

4482
let named f = Cmdliner.Term.(app (const f))
4583

@@ -54,6 +92,15 @@ let main_module =
5492
Cmdliner.Arg.(
5593
value & opt (some string) None & info ~doc ~docv [ "main-module" ])
5694

95+
let unwrapped_library =
96+
let doc =
97+
"Loads a library without a main module. Ignored when diffing single \
98+
$(b,.cmi) files."
99+
in
100+
named
101+
(fun x -> `Unwrapped_library x)
102+
Cmdliner.Arg.(value & flag & info ~doc [ "unwrapped" ])
103+
57104
let ref_cmi =
58105
let docv = "REF_CMI_FILES" in
59106
let doc =
@@ -79,7 +126,9 @@ let info =
79126
Cmd.info tool_name ~version:"%%VERSION%%" ~exits:Cmd.Exit.defaults
80127
~doc:"List API changes between two versions of a library"
81128

82-
let term = Cmdliner.Term.(const run $ main_module $ ref_cmi $ current_cmi)
129+
let term =
130+
Cmdliner.Term.(
131+
const run $ main_module $ unwrapped_library $ ref_cmi $ current_cmi)
83132

84133
let () =
85134
Fmt_tty.setup_std_outputs ();

dev-tools/print_api.ml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ let run (`Main_module main_module) (`Input fn) =
2121
List.iter print_cmi cmi_files;
2222
Ok ()
2323
| true, Some main_module ->
24-
let+ sig_ = Api_watch.Library.load ~main_module fn in
24+
let+ sig_map = Api_watch.Library.load ~main_module fn in
25+
let sig_ = Api_watch.String_map.find main_module sig_map in
2526
Printtyp.signature Format.std_formatter sig_;
2627
Format.printf "\n"
2728

lib/diff.ml

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ type ('item, 'diff) t = Added of 'item | Removed of 'item | Modified of 'diff
44

55
type 'a atomic_modification = { reference : 'a; current : 'a }
66
(** The simplest diff representation for the modification of a value of type 'a.
7-
[reference] is the value before and [current] is the value after the change occured.
8-
Use this type when there is no better representation available. *)
7+
[reference] is the value before and [current] is the value after the change
8+
occured. Use this type when there is no better representation available. *)
99

1010
type value = {
1111
vname : string;
@@ -235,3 +235,55 @@ let interface ~module_name ~reference ~current =
235235
let typing_env = Env.empty in
236236
let sig_out = signatures ~typing_env ~reference ~current in
237237
Option.map (fun mdiff -> { mname = module_name; mdiff }) sig_out
238+
239+
let library ~reference ~current =
240+
let mod_dec_of_sig sign =
241+
{
242+
md_type = Mty_signature sign;
243+
md_attributes = [];
244+
md_loc = Location.none;
245+
md_uid = Uid.internal_not_actually_unique;
246+
}
247+
in
248+
String_map.merge
249+
(fun module_name ref_sig_opt cur_sig_opt ->
250+
match (ref_sig_opt, cur_sig_opt) with
251+
| None, None -> None
252+
| Some ref_sig, None ->
253+
Some
254+
(Some
255+
{
256+
mname = module_name;
257+
mdiff =
258+
Modified
259+
(Supported
260+
[
261+
Module
262+
{
263+
mname = module_name;
264+
mdiff = Removed (mod_dec_of_sig ref_sig);
265+
};
266+
]);
267+
})
268+
| None, Some cur_sig ->
269+
Some
270+
(Some
271+
{
272+
mname = module_name;
273+
mdiff =
274+
Modified
275+
(Supported
276+
[
277+
Module
278+
{
279+
mname = module_name;
280+
mdiff = Added (mod_dec_of_sig cur_sig);
281+
};
282+
]);
283+
})
284+
| Some ref_sig, Some cur_sig -> (
285+
let module_diff =
286+
interface ~module_name ~reference:ref_sig ~current:cur_sig
287+
in
288+
match module_diff with None -> None | Some _ -> Some module_diff))
289+
reference current

lib/diff.mli

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@ val interface :
5151
reference:Types.signature ->
5252
current:Types.signature ->
5353
module_ option
54+
55+
val library :
56+
reference:Library.t -> current:Library.t -> module_ option String_map.t

lib/library.ml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,17 @@ let rec expand_sig ~library_modules sig_ =
178178
| _ -> Ok item)
179179
sig_
180180

181-
let load ~main_module project_path =
181+
type t = Types.signature String_map.t
182+
183+
let load_unwrapped project_path : (t, string) result =
184+
let open CCResult.Infix in
185+
let* library_modules = collect_modules project_path in
186+
let module_map =
187+
String_map.map (fun sig_ -> Lazy.force sig_) library_modules
188+
in
189+
Ok module_map
190+
191+
let load ~main_module project_path : (t, string) result =
182192
let open CCResult.Infix in
183193
let* library_modules = collect_modules project_path in
184194
let* main_sig =
@@ -189,4 +199,9 @@ let load ~main_module project_path =
189199
(Printf.sprintf "Could not find main module %s in %s" main_module
190200
project_path)
191201
in
192-
expand_sig ~library_modules main_sig
202+
let expanded_main_sig =
203+
match expand_sig ~library_modules main_sig with
204+
| Ok expanded_sig -> expanded_sig
205+
| Error e -> failwith e
206+
in
207+
Ok (String_map.singleton main_module expanded_main_sig)

lib/library.mli

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,16 @@
11
val load_cmi : string -> (Types.signature * string, string) result
2-
val load : main_module:string -> string -> (Types.signature, string) result
2+
3+
type t = Types.signature String_map.t
4+
(** Type representing a library's root modules.
5+
For wrapped libraries, contains only the main module.
6+
For unwrapped libraries, contains all root-level modules. *)
7+
8+
val load_unwrapped : string -> (t, string) result
9+
(** Load an unwrapped library, returning all root-level modules.
10+
Each module in the returned map represents a .cmi file at the root
11+
of the project path. *)
12+
13+
val load : main_module:string -> string -> (t, string) result
14+
(** Load a wrapped library, returning only its main module.
15+
The returned map will contain a single entry for the main module,
16+
with all its dependencies expanded. *)

tests/api-diff/errors.t

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,30 @@ diff a .cmi file with another .cmi file or a directory with a directory
77
api-diff: Arguments must either both be directories or both single .cmi files.
88
[123]
99

10-
When diffing all libraries, the --main-module argument is mandatory
10+
When diffing all libraries, the Either --main-module or --unwrapped must be specified
1111

1212
$ mkdir test2
1313
$ api-diff test test2
14-
api-diff: --main-module must be provided when diffing entire libraries.
14+
api-diff: Either --main-module or --unwrapped must be provided when diffing entire libraries.
1515
[123]
1616

17-
When passing --main-module while diffing single .cmi files, the user will be warn
17+
When passing --main-module and/or --unwrapped while diffing single .cmi files, the user will be warn
1818
that it is ignored
1919

2020
$ touch test2.cmi
2121
$ api-diff --main-module main test.cmi test2.cmi
22-
api-diff: --main-module ignored when diffing single .cmi files
22+
api-diff: --main-module is ignored when diffing single .cmi files
23+
api-diff: Cmi_format.Error(_)
24+
[123]
25+
26+
$ touch test2.cmi
27+
$ api-diff --unwrapped test.cmi test2.cmi
28+
api-diff: --unwrapped is ignored when diffing single .cmi files
29+
api-diff: Cmi_format.Error(_)
30+
[123]
31+
32+
$ touch test2.cmi
33+
$ api-diff --main-module main --unwrapped test.cmi test2.cmi
34+
api-diff: --main-module and --unwrapped are ignored when diffing single .cmi files
2335
api-diff: Cmi_format.Error(_)
2436
[123]

0 commit comments

Comments
 (0)