|
| 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