open Ppxlib open Base let error ~loc fmt = Location.raise_errorf ~loc (Stdlib.( ^^ ) "ppx_base_lint:" fmt) type suspicious_id = Stdlib_submodule of string let rec iter_suspicious (id : Longident.t) ~f = match id with | Ldot (Lident "Stdlib", s) when String.( <> ) s "" && match s.[0] with | 'A' .. 'Z' -> true | _ -> false -> f (Stdlib_submodule s) | Ldot (x, _) -> iter_suspicious x ~f | Lapply (a, b) -> iter_suspicious a ~f; iter_suspicious b ~f | Lident _ -> () ;; let zero_modules () = Stdlib.Sys.readdir "." |> Array.to_list |> List.filter ~f:(fun fn -> Stdlib.Filename.check_suffix fn "0.ml") |> List.map ~f:(fun fn -> String.capitalize (String.sub fn ~pos:0 ~len:(String.length fn - 4))) |> Set.of_list (module String) ;; let check_open (id : Longident.t Asttypes.loc) = match id.txt with | Lident "Stdlib" -> error ~loc:id.loc "you are not allowed to open Stdlib inside Base" | _ -> () ;; let rec is_stdlib_dot_something : Longident.t -> bool = function | Ldot (Lident "Stdlib", _) -> true | Ldot (id, _) -> is_stdlib_dot_something id | _ -> false ;; let print_payload ppf = function | PStr x -> Pprintast.structure ppf x | PSig x -> Pprintast.signature ppf x | PTyp x -> Pprintast.core_type ppf x | PPat (x, None) -> Pprintast.pattern ppf x | PPat (x, Some w) -> Stdlib.Format.fprintf ppf "%a@ when@ %a" Pprintast.pattern x Pprintast.expression w ;; let remove_loc = object inherit Ast_traverse.map method! location _ = Location.none method! location_stack _ = [] end ;; let check current_module = let zero_modules = zero_modules () in object inherit Ast_traverse.iter as super method! longident_loc { txt = id; loc } = (* Note: we don't distinguish between module identifiers and constructors names. Since there is no [Stdlib.String], [Stdlib.Array], ... constructors this is not a problem. *) iter_suspicious id ~f:(fun (Stdlib_submodule m) -> if not (Set.mem zero_modules m) then (* We are allowed to use Stdlib modules that don't have a Foo0 version *) () else if String.equal (m ^ "0") current_module then () (* Foo0 is allowed to use Stdlib.Foo *) else ( match current_module with | "Import0" | "Base" -> () | _ -> error ~loc "you cannot use [Stdlib.%s] here, use [%s0] instead" m m)) (* We allow references to Stdlib in types. This is primarily to allow ppx-derived code to refer to Stdlib. *) method! core_type _ = () method! expression e = super#expression e; match e.pexp_desc with | Pexp_open ({ popen_expr = { pmod_desc = Pmod_ident id; _ }; _ }, _) -> check_open id | _ -> () method! open_description op = super#open_description op; check_open op.popen_expr method! module_binding mb = super#module_binding mb; match current_module with | "Import0" -> () | _ -> (match mb.pmb_expr.pmod_desc with | Pmod_ident { txt = id; _ } when is_stdlib_dot_something id -> error ~loc:mb.pmb_loc "you cannot alias [Stdlib] sub-modules, use them directly" | _ -> ()) method! attributes attrs = super#attributes attrs; let is_cold attr = String.equal attr.attr_name.txt "cold" in match List.find attrs ~f:is_cold with | None -> () | Some attr -> let expansion = Ppx_cold.expand_cold_attribute attr |> List.map ~f:(fun a -> { a with attr_name = { a.attr_name with txt = String.chop_prefix a.attr_name.txt ~prefix:"ocaml." |> Option.value ~default:a.attr_name.txt } }) in let is_part_of_expansion attr = List.exists expansion ~f:(fun a -> String.equal a.attr_name.txt attr.attr_name.txt || String.equal ("ocaml." ^ a.attr_name.txt) attr.attr_name.txt) in let new_attrs = List.concat_map attrs ~f:(fun a -> if is_cold a then a :: expansion else if is_part_of_expansion a then [] else [ a ]) in if not (Poly.equal (remove_loc#attributes attrs) (remove_loc#attributes new_attrs)) then ( (* Remove attributes written by the user that correspond to attributes in the expansion *) List.iter attrs ~f:(fun a -> if is_part_of_expansion a then Driver.register_correction ~loc:a.attr_loc ~repl:""); let attribute_level = String.make (attr.attr_name.loc.loc_start.pos_cnum - attr.attr_loc.loc_start.pos_cnum - 1) '@' in let repl = Stdlib.Format.asprintf "@[%a@]" (Stdlib.Format.pp_print_list (fun ppf x -> Stdlib.Format.fprintf ppf "[%s%s@ %a]" attribute_level x.attr_name.txt print_payload x.attr_payload)) (attr :: expansion) in Driver.register_correction ~loc:attr.attr_loc ~repl) end ;; let module_of_loc (loc : Location.t) = String.capitalize (Stdlib.Filename.chop_extension (Stdlib.Filename.basename loc.loc_start.pos_fname)) ;; let () = Ppxlib.Driver.register_transformation "base_lint" ~impl:(function | [] -> [] | { pstr_loc = loc; _ } :: _ as st -> (check (module_of_loc loc))#structure st; st) ~intf:(function | [] -> [] | { psig_loc = loc; _ } :: _ as sg -> (check (module_of_loc loc))#signature sg; sg) ;;