1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
open Odoc_utils
open Or_error
type named_root = string * Fs.Directory.t
module Named_roots : sig
type t
type error = NoPackage | NoRoot
type input = { name : string; dir : Fs.Directory.t }
val create : input list -> current_root:named_root option -> t
val current_root : t -> Fs.Directory.t option
val find_by_path :
?root:string -> t -> path:Fs.File.t -> (Fs.File.t option, error) result
val find_by_name :
?root:string -> t -> name:string -> (Fs.File.t list, error) result
end = struct
type flat =
| Unvisited of Fs.Directory.t
| Visited of (string, Fs.File.t) Hashtbl.t
type hierarchical = (Fs.File.t, Fs.File.t) Hashtbl.t * Fs.Directory.t
type pkg = { flat : flat; hierarchical : hierarchical }
type t = { table : (string, pkg) Hashtbl.t; current_root : named_root option }
type input = { name : string; dir : Fs.Directory.t }
type error = NoPackage | NoRoot
let hashtbl_find_opt cache package =
match Hashtbl.find cache package with
| x -> Some x
| exception Not_found -> None
let create (pkglist : input list) ~current_root =
let cache = Hashtbl.create 42 in
List.iter
(fun { name = pkgname; dir = root } ->
let flat = Unvisited root
and hierarchical = (Hashtbl.create 42, root) in
Hashtbl.add cache pkgname { flat; hierarchical })
pkglist;
{ current_root; table = cache }
let current_root t = Option.map snd t.current_root
let find_by_path ?root { table = cache; current_root; _ } ~path =
let path = Fpath.normalize path in
let root =
match (root, current_root) with
| Some pkg, _ | None, Some (pkg, _) -> Ok pkg
| None, None -> Error NoRoot
in
root >>= fun root ->
match hashtbl_find_opt cache root with
| Some { hierarchical = cache, root; _ } -> (
match hashtbl_find_opt cache path with
| Some x -> Ok (Some x)
| None ->
let full_path = Fpath.( // ) (Fs.Directory.to_fpath root) path in
if Fs.File.exists full_path then (
Hashtbl.add cache path full_path;
Ok (Some full_path))
else Ok None)
| None -> Error NoPackage
let populate_flat_namespace ~root =
let flat_namespace = Hashtbl.create 42 in
let () =
match
Fs.Directory.fold_files_rec_result
(fun () path ->
let name = Fpath.filename path in
Ok (Hashtbl.add flat_namespace name path))
() root
with
| Ok () -> ()
| Error _ -> assert false
in
flat_namespace
let find_by_name ?root { table = cache; current_root; _ } ~name =
let package =
match (root, current_root) with
| Some pkg, _ | None, Some (pkg, _) -> Ok pkg
| None, None -> Error NoRoot
in
package >>= fun package ->
match hashtbl_find_opt cache package with
| Some { flat = Visited flat; _ } -> Ok (Hashtbl.find_all flat name)
| Some ({ flat = Unvisited root; _ } as p) ->
let flat = populate_flat_namespace ~root in
Hashtbl.replace cache package { p with flat = Visited flat };
Ok (Hashtbl.find_all flat name)
| None -> Error NoPackage
end
let () = ignore Named_roots.find_by_name [@warning "-5"]
module Accessible_paths : sig
type t
val create : directories:Fs.Directory.t list -> t
val find : t -> string -> Fs.File.t list
end = struct
type t = (string, Fpath.t ) Hashtbl.t
let create ~directories =
let unit_cache = Hashtbl.create 42 in
List.iter
(fun directory ->
try
let files = Sys.readdir (Fs.Directory.to_string directory) in
Array.iter
(fun file ->
let file = Fpath.v file in
if Fpath.has_ext "odoc" file then
Hashtbl.add unit_cache
(Astring.String.Ascii.capitalize
(file |> Fpath.rem_ext |> Fpath.basename))
(Fs.File.append directory file))
files
with Sys_error _ ->
())
directories;
unit_cache
let find t name =
let name = Astring.String.Ascii.capitalize name in
Hashtbl.find_all t name
end
module Hierarchy : sig
(** Represent a file hierarchy and allow file path manipulations that do not
escape it. *)
type t
type error = [ `Escape_hierarchy ]
val make : hierarchy_root:Fs.Directory.t -> current_dir:Fs.Directory.t -> t
val resolve_relative : t -> Fs.File.t -> (Fs.File.t, error) result
(** [resolve_relative h relpath] resolve [relpath] relatively to the current
directory, making sure not to escape the hierarchy. *)
end = struct
type t = { hierarchy_root : Fs.Directory.t; current_dir : Fs.Directory.t }
type error = [ `Escape_hierarchy ]
let make ~hierarchy_root ~current_dir = { hierarchy_root; current_dir }
let resolve_relative t relpath =
let path = Fs.File.append t.current_dir relpath in
if Fs.Directory.contains ~parentdir:t.hierarchy_root path then Ok path
else Error `Escape_hierarchy
end
module StringMap = Map.Make (String)
let build_imports_map imports =
List.fold_left
(fun map import ->
match import with
| Odoc_model.Lang.Compilation_unit.Import.Unresolved (name, _) ->
StringMap.add name import map
| Odoc_model.Lang.Compilation_unit.Import.Resolved (_, name) ->
StringMap.add (Odoc_model.Names.ModuleName.to_string name) import map)
StringMap.empty imports
let root_name root = Odoc_model.Root.Odoc_file.name root.Odoc_model.Root.file
let unit_name
( Odoc_file.Unit_content { root; _ }
| Page_content { root; _ }
| Impl_content { root; _ }
| Asset_content { root; _ } ) =
root_name root
let unit_cache = Hashtbl.create 42
let load_unit_from_file path =
try Hashtbl.find unit_cache path
with Not_found ->
let r = Odoc_file.load path >>= fun u -> Ok u.content in
Hashtbl.add unit_cache path r;
r
let self = ref None
(** Load every units matching a given name. Cached. *)
let load_units_from_name =
let safe_read file acc =
match load_unit_from_file file with
| Ok u -> u :: acc
| Error (`Msg msg) ->
let warning =
Odoc_model.Error.filename_only "%s" msg (Fs.File.to_string file)
in
prerr_endline (Odoc_model.Error.to_string warning);
acc
in
let do_load ap target_name =
let paths = Accessible_paths.find ap target_name in
List.fold_right safe_read paths []
in
let check_self name =
match !self with
| Some (n, unit) -> if n = name then Some unit else None
| None -> None
in
fun ap target_name ->
match check_self target_name with
| Some unit -> [ unit ]
| None -> do_load ap target_name
let rec find_map f = function
| [] -> None
| hd :: tl -> (
match f hd with Some x -> Some (x, tl) | None -> find_map f tl)
let lookup_unit_with_digest ap target_name digest =
let unit_that_match_digest u =
match u with
| Odoc_file.Unit_content m
when Digest.compare m.Odoc_model.Lang.Compilation_unit.digest digest = 0
->
Some m
| _ -> None
in
let units = load_units_from_name ap target_name in
match find_map unit_that_match_digest units with
| Some (m, _) -> Ok (Odoc_xref2.Env.Found m)
| None -> Error `Not_found
(** Lookup a compilation unit matching a name. If there is more than one result,
report on stderr and return the first one.
TODO: Correctly propagate warnings instead of printing. *)
let lookup_unit_by_name ap target_name =
let first_unit u =
match u with
| Odoc_file.Unit_content m -> Some m
| Impl_content _ | Page_content _ | Asset_content _ -> None
in
let rec find_ambiguous tl =
match find_map first_unit tl with
| Some (m, tl) -> m :: find_ambiguous tl
| None -> []
in
let units = load_units_from_name ap target_name in
match find_map first_unit units with
| Some (m, tl) ->
(match find_ambiguous tl with
| [] -> ()
| ambiguous ->
let ambiguous = m :: ambiguous in
let ambiguous =
List.map
(fun m -> root_name m.Odoc_model.Lang.Compilation_unit.root)
ambiguous
in
let warning =
Odoc_model.Error.filename_only
"Ambiguous lookup. Possible files: %a"
Format.(pp_print_list pp_print_string)
ambiguous target_name
in
prerr_endline (Odoc_model.Error.to_string warning));
Some m
| None -> None
(** Lookup an unit. First looks into [imports_map] then searches into the paths.
*)
let lookup_unit_by_name ~important_digests ~imports_map ap target_name =
let of_option f =
match f with
| Some m -> Ok (Odoc_xref2.Env.Found m)
| None -> Error `Not_found
in
match StringMap.find target_name imports_map with
| Odoc_model.Lang.Compilation_unit.Import.Unresolved (_, Some digest) ->
lookup_unit_with_digest ap target_name digest
| Unresolved (_, None) ->
if important_digests then Ok Odoc_xref2.Env.Forward_reference
else of_option (lookup_unit_by_name ap target_name)
| Resolved (root, _) -> lookup_unit_with_digest ap target_name root.digest
| exception Not_found ->
if important_digests then Error `Not_found
else of_option (lookup_unit_by_name ap target_name)
(** Lookup a page.
TODO: Warning on ambiguous lookup. *)
let lookup_page_by_name ap target_name =
let target_name = "page-" ^ target_name in
let is_page u =
match u with
| Odoc_file.Page_content p -> Some p
| Impl_content _ | Unit_content _ | Asset_content _ -> None
in
let units = load_units_from_name ap target_name in
match find_map is_page units with
| Some (p, _) -> Ok p
| None -> Error `Not_found
(** Lookup an implementation. *)
let lookup_impl ap target_name =
let target_name = "impl-" ^ Astring.String.Ascii.uncapitalize target_name in
let is_impl u =
match u with
| Odoc_file.Impl_content p -> Some p
| Page_content _ | Unit_content _ | Asset_content _ -> None
in
let units = load_units_from_name ap target_name in
match find_map is_impl units with Some (p, _) -> Some p | None -> None
(** Add the current unit to the cache. No need to load other units with the same
name. *)
let add_unit_to_cache u =
let target_name =
(match u with
| Odoc_file.Page_content _ -> "page-"
| Impl_content _ -> "impl-"
| Unit_content _ -> ""
| Asset_content _ -> "asset-")
^ unit_name u
in
self := Some (target_name, u)
(** Resolve a path reference in the given named roots and hierarchy.
[possible_unit_names] should return a list of possible file names for the
given unit name. *)
let lookup_path ~possible_unit_names ~named_roots ~hierarchy (tag, path) :
(Odoc_file.content, [ `Not_found ]) result =
let open Odoc_utils.OptionMonad in
let option_to_result = function Some p -> Ok p | None -> Error `Not_found in
let handle_load_error = function Ok u -> Some u | Error (`Msg _) -> None in
let ref_path_to_file_path path =
match List.rev path with
| [] -> []
| name :: rest ->
List.map
(fun fname -> List.rev (fname :: rest) |> Fs.File.of_segs)
(possible_unit_names name)
in
let find_by_path ?root named_roots path =
match Named_roots.find_by_path ?root named_roots ~path with
| Ok x -> x
| Error (NoPackage | NoRoot) -> None
in
let find_in_named_roots ?root path =
named_roots >>= fun named_roots ->
find_by_path ?root named_roots path >>= fun path ->
load_unit_from_file path |> handle_load_error
in
let find_in_hierarchy path =
hierarchy >>= fun hierarchy ->
match Hierarchy.resolve_relative hierarchy path with
| Ok path -> load_unit_from_file path |> handle_load_error
| Error `Escape_hierarchy -> None
in
match tag with
| `TCurrentPackage ->
ref_path_to_file_path path
|> List.find_map find_in_named_roots
|> option_to_result
| `TAbsolutePath ->
(match path with
| root :: path ->
ref_path_to_file_path path
|> List.find_map (find_in_named_roots ~root)
| [] -> None)
|> option_to_result
| `TRelativePath ->
ref_path_to_file_path path
|> List.find_map find_in_hierarchy
|> option_to_result
let lookup_asset_by_path ~pages ~hierarchy path =
let possible_unit_names name = [ "asset-" ^ name ^ ".odoc" ] in
match lookup_path ~possible_unit_names ~named_roots:pages ~hierarchy path with
| Ok (Odoc_file.Asset_content asset) -> Ok asset
| Ok _ -> Error `Not_found
| Error _ as e -> e
let lookup_page_by_path ~pages ~hierarchy path =
let possible_unit_names name = [ "page-" ^ name ^ ".odoc" ] in
match lookup_path ~possible_unit_names ~named_roots:pages ~hierarchy path with
| Ok (Odoc_file.Page_content page) -> Ok page
| Ok _ -> Error `Not_found
| Error _ as e -> e
let lookup_unit_by_path ~libs ~hierarchy path =
let possible_unit_names name =
Astring.String.Ascii.
[ capitalize name ^ ".odoc"; uncapitalize name ^ ".odoc" ]
in
match lookup_path ~possible_unit_names ~named_roots:libs ~hierarchy path with
| Ok (Odoc_file.Unit_content u) -> Ok (Odoc_xref2.Env.Found u)
| Ok _ -> Error `Not_found
| Error _ as e -> e
let lookup_unit ~important_digests ~imports_map ap ~libs ~hierarchy = function
| `Path p -> lookup_unit_by_path ~libs ~hierarchy p
| `Name n -> lookup_unit_by_name ~important_digests ~imports_map ap n
let lookup_page ap ~pages ~hierarchy = function
| `Path p -> lookup_page_by_path ~pages ~hierarchy p
| `Name n -> lookup_page_by_name ap n
let lookup_asset ~pages ~hierarchy = function
| `Path p -> lookup_asset_by_path ~pages ~hierarchy p
| `Name _ -> failwith "TODO"
type t = {
important_digests : bool;
ap : Accessible_paths.t;
extended_ap : Accessible_paths.t;
pages : Named_roots.t option;
libs : Named_roots.t option;
open_modules : string list;
current_dir : Fs.Directory.t option;
}
type roots = {
page_roots : named_root list;
lib_roots : named_root list;
current_lib : named_root option;
current_package : named_root option;
current_dir : Fs.Directory.t;
}
let create ~important_digests ~directories ~open_modules ~roots =
let pages, libs, current_dir, directories =
match roots with
| None -> (None, None, None, directories)
| Some { page_roots; lib_roots; current_lib; current_package; current_dir }
->
let prepare roots =
List.map (fun (name, dir) -> { Named_roots.name; dir }) roots
in
let directories =
match current_package with
| None -> directories
| Some (_pkg, dir) -> dir :: directories
in
let lib_roots = prepare lib_roots in
let page_roots = prepare page_roots in
let pages = Named_roots.create ~current_root:current_package page_roots
and libs = Named_roots.create ~current_root:current_lib lib_roots in
let directories =
List.sort_uniq Fs.Directory.compare (current_dir :: directories)
in
(Some pages, Some libs, Some current_dir, directories)
in
let ap = Accessible_paths.create ~directories in
let extended_directories =
match roots with
| None -> directories
| Some { lib_roots; _ } -> directories @ List.map snd lib_roots
in
let extended_directories =
List.sort_uniq Fs.Directory.compare extended_directories
in
let extended_ap = Accessible_paths.create ~directories:extended_directories in
{ important_digests; ap; extended_ap; open_modules; pages; libs; current_dir }
(** Helpers for creating xref2 env. *)
open Odoc_xref2
let build_compile_env_for_unit
{
important_digests;
ap;
extended_ap = _;
open_modules = open_units;
pages = _;
libs = _;
current_dir = _;
} m =
add_unit_to_cache (Odoc_file.Unit_content m);
let imports_map = build_imports_map m.imports in
let lookup_unit =
lookup_unit ~important_digests ~imports_map ap ~libs:None ~hierarchy:None
and lookup_page _ = Error `Not_found
and lookup_asset _ = Error `Not_found
and lookup_impl = lookup_impl ap in
let resolver =
{ Env.open_units; lookup_unit; lookup_page; lookup_impl; lookup_asset }
in
Env.env_of_unit m ~linking:false resolver
(** [important_digests] and [imports_map] only apply to modules. *)
let build ?(imports_map = StringMap.empty) ?hierarchy_roots
{
important_digests;
ap;
extended_ap;
open_modules = open_units;
pages;
libs;
current_dir;
} =
let hierarchy =
let open OptionMonad in
current_dir >>= fun current_dir ->
hierarchy_roots >>= Named_roots.current_root >>= fun hierarchy_root ->
Some (Hierarchy.make ~hierarchy_root ~current_dir)
in
let lookup_unit =
lookup_unit ~important_digests ~imports_map extended_ap ~libs ~hierarchy
and lookup_page = lookup_page ap ~pages ~hierarchy
and lookup_asset = lookup_asset ~pages ~hierarchy
and lookup_impl = lookup_impl ap in
{ Env.open_units; lookup_unit; lookup_page; lookup_impl; lookup_asset }
let build_compile_env_for_impl t i =
let imports_map =
build_imports_map i.Odoc_model.Lang.Implementation.imports
in
let resolver = build ~imports_map t in
Env.env_of_impl i resolver
let build_link_env_for_unit t m =
add_unit_to_cache (Odoc_file.Unit_content m);
let imports_map = build_imports_map m.imports in
let resolver = build ~imports_map ?hierarchy_roots:t.libs t in
Env.env_of_unit m ~linking:true resolver
let build_link_env_for_impl t i =
let imports_map =
build_imports_map i.Odoc_model.Lang.Implementation.imports
in
let resolver = build ~imports_map t in
Env.env_of_impl i resolver
let build_env_for_page t p =
add_unit_to_cache (Odoc_file.Page_content p);
let resolver =
build ?hierarchy_roots:t.pages { t with important_digests = false }
in
Env.env_of_page p resolver
let build_env_for_reference t =
let resolver = build { t with important_digests = false } in
Env.env_for_reference resolver
let lookup_page t target_name =
match lookup_page_by_name t.ap target_name with
| Ok p -> Some p
| Error `Not_found -> None
let resolve_import t target_name =
let rec loop = function
| [] -> None
| path :: tl -> (
match Odoc_file.load_root path with
| Error _ -> loop tl
| Ok root -> (
match root.Odoc_model.Root.file with
| Compilation_unit _ -> Some root
| Impl _ | Page _ | Asset _ -> loop tl))
in
loop (Accessible_paths.find t.ap target_name)