Skip to content

Commit eda9cb9

Browse files
authored
Merge pull request #5 from Julow/lwt_log_to_logs
Tool for migrating from Lwt_log to Logs
2 parents 8c6b39d + 59e76f4 commit eda9cb9

File tree

15 files changed

+1265
-17
lines changed

15 files changed

+1265
-17
lines changed

bin/lwt_log_to_logs/dune

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(executable
2+
(public_name lwt-log-to-logs)
3+
(package lwt_log_to_logs)
4+
(name main)
5+
(libraries cmdliner migrate_utils ocamlformat_utils))

bin/lwt_log_to_logs/main.ml

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
open Ocamlformat_utils.Parsing
2+
open Asttypes
3+
open Parsetree
4+
open Ast_helper
5+
open Ocamlformat_utils.Ast_utils
6+
module Occ = Migrate_utils.Occ
7+
8+
let add_comment state ?loc text =
9+
Migrate_utils.add_comment state ?loc ("TODO: lwt-log-to-logs: " ^ text)
10+
11+
let mk_lwt_return_unit = Exp.ident (mk_longident [ "Lwt"; "return_unit" ])
12+
13+
let mk_log section cmd args =
14+
(* Logs.$cmd ~src:$section (fun fmt -> fmt $arg) *)
15+
let msgf =
16+
let fmt_pat = Pat.var (mk_loc "fmt") and fmt_exp = mk_exp_var "fmt" in
17+
let body = Exp.apply fmt_exp args in
18+
Exp.function_ [ mk_function_param fmt_pat ] None (Pfunction_body body)
19+
in
20+
let src_arg =
21+
match section with
22+
| Some (section, `Lbl) -> [ (mk_lbl "src", section) ]
23+
| Some (section, `Opt) -> [ (mk_lblopt "src", section) ]
24+
| None -> []
25+
in
26+
mk_apply_ident [ "Logs"; cmd ] (src_arg @ [ (Nolabel, msgf) ])
27+
28+
let mk_set_level section lvl =
29+
mk_apply_simple [ "Logs"; "Src"; "set_level" ] [ section; lvl ]
30+
31+
let mk_format_reporter_of_channel channel =
32+
mk_let_var "logs_formatter"
33+
(mk_apply_simple [ "Format"; "formatter_of_out_channel" ] [ channel ])
34+
@@ fun fmt ->
35+
mk_apply_ident
36+
[ "Logs"; "format_reporter" ]
37+
[ (mk_lbl "app", fmt); (mk_lbl "dst", fmt); (Nolabel, mk_unit_val) ]
38+
39+
let mk_channel ~state ~template ~close_mode channel =
40+
if Option.is_some template then
41+
add_comment state
42+
"Lwt_log.channel: The [~template] argument is unsupported. Use \
43+
[~pp_header] instead.";
44+
(match close_mode.pexp_desc with
45+
| Pexp_variant (cstr, None) when cstr.txt.txt = "Keep" -> ()
46+
| _ ->
47+
add_comment state
48+
"Lwt_log.channel: The [~close_mode] argument has been dropped. The \
49+
behavior is always [`Keep].");
50+
add_comment state
51+
"Format.formatter_of_out_channel: Argument is a [Lwt_io.output_channel] \
52+
but a [out_channel] is expected.";
53+
mk_format_reporter_of_channel channel
54+
55+
let mk_file ~state ~mode ~perm ~file_name =
56+
let mk_mode append_mode =
57+
Exp.list
58+
[
59+
append_mode;
60+
mk_constr_exp [ "Open_wronly" ];
61+
mk_constr_exp [ "Open_creat" ];
62+
mk_constr_exp [ "Open_text" ];
63+
]
64+
in
65+
let mode =
66+
mk_let_var "append_mode"
67+
(Exp.match_
68+
(value_of_lblopt ~default:(mk_variant_exp "Append") mode)
69+
[
70+
Exp.case (mk_variant_pat "Append") (mk_constr_exp [ "Open_append" ]);
71+
Exp.case (mk_variant_pat "Truncate") (mk_constr_exp [ "Open_trunc" ]);
72+
])
73+
mk_mode
74+
in
75+
let perm = value_of_lblopt ~default:(mk_const_int "0o640") perm in
76+
add_comment state "[file]: Channel is never closed.";
77+
mk_format_reporter_of_channel
78+
(mk_apply_simple [ "open_out_gen" ] [ mode; perm; file_name ])
79+
80+
let mk_reporter report_f =
81+
let report =
82+
let open Mk_function in
83+
mk_function
84+
(return report_f $ arg "src" $ arg "level" $ arg ~lbl:`Lbl "over"
85+
$ arg "k" $ arg "msgf")
86+
in
87+
Exp.record [ (mk_longident [ "Logs"; "report" ], None, Some report) ] None
88+
89+
let call_reporter r src level over k msgf =
90+
Exp.apply
91+
(Exp.field r (mk_longident [ "Logs"; "report" ]))
92+
[
93+
(Nolabel, src);
94+
(Nolabel, level);
95+
(mk_lbl "over", over);
96+
(Nolabel, k);
97+
(Nolabel, msgf);
98+
]
99+
100+
let mk_broadcast ~state:_ loggers =
101+
mk_let_var "broadcast_reporters" loggers @@ fun reporters_var ->
102+
mk_reporter (fun src level over k msgf ->
103+
let f k r _unit = call_reporter r src level over k msgf in
104+
let f =
105+
let open Mk_function in
106+
mk_function (return f $ arg "k" $ arg "r" $ arg "_unit")
107+
in
108+
mk_apply_simple [ "List"; "fold_left" ]
109+
[ f; k; reporters_var; mk_unit_val ])
110+
111+
let mk_dispatch ~state:_ dispatch_f =
112+
mk_let_var "dispatch_f" dispatch_f @@ fun dispatch_f ->
113+
mk_reporter (fun src level ->
114+
call_reporter
115+
(Exp.apply dispatch_f [ (Nolabel, src); (Nolabel, level) ])
116+
src level)
117+
118+
let mk_syslog ~state ~paths ~facility =
119+
let socket_arg =
120+
match paths with
121+
| Some (paths, lbl) ->
122+
let lbl = match lbl with `Lbl -> mk_lbl | `Opt -> mk_lblopt in
123+
(match paths.pexp_desc with
124+
| Pexp_list [ _ ] -> ()
125+
| _ ->
126+
add_comment state
127+
"[Logs_syslog_unix.unix_reporter] take a single path but a list \
128+
is passed.");
129+
[ (lbl "socket", paths) ]
130+
| None -> []
131+
in
132+
add_comment state
133+
"Add dependency on library [logs-syslog.unix] and package [logs-syslog].";
134+
add_comment state "The [~facility] argument is not of the right type.";
135+
Exp.apply
136+
(mk_exp_ident [ "Logs_syslog_unix"; "unix_reporter" ])
137+
(socket_arg @ [ (mk_lbl "facility", facility); (Nolabel, mk_unit_val) ])
138+
139+
(** Whether an expression can be used as a format spec. *)
140+
let format_safe exp =
141+
match exp.pexp_desc with
142+
| Pexp_constant { pconst_desc = Pconst_string (s, _, _); _ } ->
143+
String.for_all (function '%' | '@' -> false | _ -> true) s
144+
| _ -> false
145+
146+
let rewrite_apply_lwt_log ~state (unit, ident) args =
147+
let open Unpack_apply in
148+
let ignore_lblarg ?(cmt = "") arg k =
149+
take_lblopt arg @@ fun value ->
150+
(match value with
151+
| Some (_, kind) ->
152+
let prefix = match kind with `Lbl -> '~' | `Opt -> '?' in
153+
Printf.ksprintf (add_comment state)
154+
"Labelled argument %c%s was dropped.%s" prefix arg cmt
155+
| None -> ());
156+
k
157+
in
158+
let logf ~ident ~mk_log logs_name =
159+
take_lblopt "section" @@ fun section ->
160+
take_lblopt "exn" @@ fun exn ->
161+
ignore_lblarg "location" @@ ignore_lblarg "logger" @@ take
162+
@@ fun fmt_arg ->
163+
take_all @@ fun args ->
164+
let fmt_arg, args =
165+
(* Log calls that don't end in [_f] use a ["%s"] format string to avoid
166+
any typing and escaping issues. *)
167+
if String.ends_with ~suffix:"_f" ident || format_safe fmt_arg then
168+
(fmt_arg, args)
169+
else (mk_const_string "%s", (Nolabel, fmt_arg) :: args)
170+
in
171+
let fmt_arg, args =
172+
(* Print the [exn] argument. *)
173+
match exn with
174+
| Some (exn, lbl) ->
175+
if lbl = `Opt then
176+
add_comment state
177+
"Last argument is a [exception option] while [exception] is \
178+
expected.";
179+
( Exp.infix (mk_loc "^^") fmt_arg (mk_const_string "@\n%s"),
180+
args
181+
@ [ (Nolabel, mk_apply_simple [ "Printexc"; "to_string" ] [ exn ]) ]
182+
)
183+
| None -> (fmt_arg, args)
184+
in
185+
Some (mk_log section logs_name ((Nolabel, fmt_arg) :: args))
186+
in
187+
let log_unit ~ident n = logf ~ident ~mk_log n in
188+
let log_lwt ~ident n =
189+
let mk_log section n args =
190+
Exp.sequence (mk_log section n args) mk_lwt_return_unit
191+
in
192+
logf ~ident ~mk_log n
193+
in
194+
let mk_level cstr = return (Some (mk_constr_exp [ "Logs"; cstr ])) in
195+
196+
unapply args
197+
@@
198+
match unit with
199+
| "Lwt_log_core" -> (
200+
match ident with
201+
| "ign_debug" | "ign_debug_f" -> log_unit ~ident "debug"
202+
| "ign_info" | "ign_info_f" -> log_unit ~ident "info"
203+
| "ign_notice" | "ign_notice_f" -> log_unit ~ident "app"
204+
| "ign_warning" | "ign_warning_f" -> log_unit ~ident "warn"
205+
| "ign_error" | "ign_error_f" -> log_unit ~ident "err"
206+
| "ign_fatal" | "ign_fatal_f" ->
207+
add_comment state "This message was previously on the [fatal] level.";
208+
log_unit ~ident "err"
209+
| "debug" | "debug_f" -> log_lwt ~ident "debug"
210+
| "info" | "info_f" -> log_lwt ~ident "info"
211+
| "notice" | "notice_f" -> log_lwt ~ident "app"
212+
| "warning" | "warning_f" -> log_lwt ~ident "warn"
213+
| "error" | "error_f" -> log_lwt ~ident "err"
214+
| "fatal" | "fatal_f" ->
215+
add_comment state "This message was previously on the [fatal] level.";
216+
log_lwt ~ident "err"
217+
| "make" ->
218+
(* [Lwt_log.Section.make] is detected as [("Lwt_log_core", "make")]. *)
219+
take @@ fun name ->
220+
return (Some (mk_apply_simple [ "Logs"; "Src"; "create" ] [ name ]))
221+
| "set_level" ->
222+
take @@ fun section ->
223+
take @@ fun lvl ->
224+
return (Some (mk_set_level section (mk_exp_some lvl)))
225+
| "reset_level" ->
226+
take @@ fun section ->
227+
return (Some (mk_set_level section mk_exp_none))
228+
| "null" -> return (Some (mk_exp_ident [ "Logs"; "nop_reporter" ]))
229+
| "!default" ->
230+
return (Some (mk_apply_simple [ "Logs"; "reporter" ] [ mk_unit_val ]))
231+
| "default :=" ->
232+
take @@ fun r ->
233+
return (Some (mk_apply_simple [ "Logs"; "set_reporter" ] [ r ]))
234+
| "default" ->
235+
add_comment state "Use [Logs.set_reporter : reporter -> unit].";
236+
return None
237+
| "broadcast" ->
238+
take @@ fun loggers -> return (Some (mk_broadcast ~state loggers))
239+
| "dispatch" -> take @@ fun f -> return (Some (mk_dispatch ~state f))
240+
| "Debug" -> mk_level "Debug"
241+
| "Info" -> mk_level "Info"
242+
| "Notice" -> mk_level "App"
243+
| "Warning" -> mk_level "Warning"
244+
| "Error" -> mk_level "Error"
245+
| "Fatal" -> mk_level "Error"
246+
| "add_rule" | "close" ->
247+
add_comment state (ident ^ " is no longer supported.");
248+
return None
249+
| _ -> return None)
250+
| "Lwt_log" -> (
251+
match ident with
252+
| "channel" ->
253+
take_lblopt "template" @@ fun template ->
254+
take_lbl "close_mode" @@ fun close_mode ->
255+
take_lbl "channel" @@ fun channel ->
256+
take @@ fun _unit ->
257+
return (Some (mk_channel ~state ~template ~close_mode channel))
258+
| "syslog" ->
259+
ignore_lblarg "template" @@ take_lblopt "paths"
260+
@@ fun paths ->
261+
take_lbl "facility" @@ fun facility ->
262+
take @@ fun _unit -> return (Some (mk_syslog ~state ~paths ~facility))
263+
| "file" ->
264+
ignore_lblarg "template" @@ take_lblopt "mode"
265+
@@ fun mode ->
266+
take_lblopt "perm" @@ fun perm ->
267+
take_lbl "file_name" @@ fun file_name ->
268+
take @@ fun _unit ->
269+
return (Some (mk_file ~state ~mode ~perm ~file_name))
270+
| _ -> return None)
271+
| _ -> return None
272+
273+
let rewrite_expression ~state exp =
274+
match
275+
rewrite_apply exp (fun lid args ->
276+
Occ.may_rewrite state lid (fun ident ->
277+
rewrite_apply_lwt_log ~state ident args))
278+
with
279+
| Some _ as x -> x
280+
| None -> (
281+
(* Rewrite uses of [Lwt_log.default]. *)
282+
match exp.pexp_desc with
283+
| Pexp_prefix ({ txt = "!"; _ }, { pexp_desc = Pexp_ident lid; _ }) ->
284+
Occ.may_rewrite state lid (fun (unit, ident) ->
285+
rewrite_apply_lwt_log ~state (unit, "!" ^ ident) [])
286+
| Pexp_infix ({ txt = ":="; _ }, { pexp_desc = Pexp_ident lid; _ }, rhs)
287+
->
288+
Occ.may_rewrite state lid (fun (unit, ident) ->
289+
rewrite_apply_lwt_log ~state
290+
(unit, ident ^ " :=")
291+
[ (Nolabel, rhs) ])
292+
| _ -> None)
293+
294+
let rewrite_type ~state typ =
295+
match typ.ptyp_desc with
296+
| Ptyp_constr (lid, params) ->
297+
Occ.may_rewrite state lid (fun ident ->
298+
match (ident, params) with
299+
| ("Lwt_log_core", "section"), [] ->
300+
Some (mk_typ_constr [ "Logs"; "src" ])
301+
| ("Lwt_log_core", "level"), [] ->
302+
Some (mk_typ_constr [ "Logs"; "level" ])
303+
| _ -> None)
304+
| _ -> None
305+
306+
let rewrite_pat ~state pat =
307+
let mk_level cstr =
308+
Some (Pat.construct (mk_longident [ "Logs"; cstr ]) None)
309+
in
310+
match pat.ppat_desc with
311+
| Ppat_construct (lid, arg) ->
312+
Occ.may_rewrite state lid (fun (unit, ident) ->
313+
match unit with
314+
| "Lwt_log_core" -> (
315+
match (ident, arg) with
316+
| "Debug", None -> mk_level "Debug"
317+
| "Info", None -> mk_level "Info"
318+
| "Notice", None -> mk_level "App"
319+
| "Warning", None -> mk_level "Warning"
320+
| "Error", None -> mk_level "Error"
321+
| "Fatal", None -> mk_level "Error"
322+
| _ -> None)
323+
| _ -> None)
324+
| _ -> None
325+
326+
let mapper ~state =
327+
let default = Ast_mapper.default_mapper in
328+
let rec call_rewrite ~default ~loc f m x =
329+
Migrate_utils.set_default_comment_loc state loc;
330+
(* Apply the rewrite again if it succeed *)
331+
match f x with
332+
| Some x -> call_rewrite ~default ~loc f m x
333+
| None -> default m x
334+
in
335+
let expr m x =
336+
call_rewrite ~default:default.expr ~loc:x.pexp_loc
337+
(rewrite_expression ~state)
338+
m x
339+
in
340+
let typ m x =
341+
call_rewrite ~default:default.typ ~loc:x.ptyp_loc (rewrite_type ~state) m x
342+
in
343+
let pat m x =
344+
call_rewrite ~default:default.pat ~loc:x.ppat_loc (rewrite_pat ~state) m x
345+
in
346+
{ default with expr; typ; pat }
347+
348+
let modify_ast ~fname:_ =
349+
let structure state str =
350+
let m = mapper ~state in
351+
m.structure m str
352+
in
353+
let signature state sg =
354+
let m = mapper ~state in
355+
m.signature m sg
356+
in
357+
{ Migrate_utils.structure; signature }
358+
359+
let main migrate =
360+
let units = function
361+
| "Lwt_log" | "Lwt_daemon" | "Lwt_log_core" | "Lwt_log_rules" -> true
362+
| _ -> false
363+
in
364+
let packages = [ "lwt_log"; "lwt_log.core" ] in
365+
if migrate then Migrate_utils.migrate ~packages ~units ~modify_ast
366+
else Migrate_utils.print_occurrences ~packages ~units
367+
368+
open Cmdliner
369+
370+
let opt_migrate =
371+
let doc =
372+
"Modify the source code instead of printing occurrences of Lwt_log."
373+
in
374+
Arg.(value & flag & info ~doc [ "migrate" ])
375+
376+
let cmd =
377+
let doc = "Migrate your codebase from Lwt_log to Logs." in
378+
let info = Cmd.info "lwt-log-to-logs" ~version:"%%VERSION%%" ~doc in
379+
Cmd.v info Term.(const main $ opt_migrate)
380+
381+
let () = exit (Cmd.eval cmd)

0 commit comments

Comments
 (0)