diff --git a/bin/forester/main.ml b/bin/forester/main.ml index 759622b..f1b50a2 100644 --- a/bin/forester/main.ml +++ b/bin/forester/main.ml @@ -67,7 +67,7 @@ let build ~env _ config_path dev no_theme emit_legacy_xml = m "Parsed config file %s" config_path); let start_time = Unix.gettimeofday () in let forest = Forester.build_forest ~progress ~env ~dev ~config () in - State.iter_diagnostics (fun _ d -> List.iter Error.print d) forest; + State.iter_diagnostics (fun _ d -> List.iter (Error.print ~config) d) forest; if not no_theme then begin let theme_dir = Option.value @@ -76,7 +76,7 @@ let build ~env _ config_path dev no_theme emit_legacy_xml = in match Eio_util.path_of_dir ~env theme_dir with | Ok path -> Forester.copy_contents_of_dir ~env ~forest path - | Error exn -> Error.(print @@ io_error exn) + | Error exn -> Error.(print ~config @@ io_error exn) end; Forester.render_forest ~progress ~forest ~emit_legacy_xml (); let elapsed = Unix.gettimeofday () -. start_time in @@ -137,7 +137,9 @@ let index_tree_str = |} let init ~env dir = - let report = Result.iter_error Error.(io_error >>> print) in + let report = + Result.iter_error Error.(io_error >>> print ~config:(Config.default ())) + in let cwd = match dir with | None -> Eio.Stdenv.cwd env diff --git a/flake.lock b/flake.lock index 4b6b45b..8ead11a 100644 --- a/flake.lock +++ b/flake.lock @@ -70,11 +70,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772542754, - "narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=", + "lastModified": 1784796856, + "narHash": "sha256-wWFrV5/Qbm+lyt5x20E/bSbfJiGKMo4RCxZV8cl/WZI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4", + "rev": "e2587caef70cea85dd97d7daab492899902dbf5d", "type": "github" }, "original": { @@ -84,6 +84,54 @@ "type": "github" } }, + "nixpkgs-llvm17": { + "locked": { + "lastModified": 1723734425, + "narHash": "sha256-GwSfzmTMpM+gAJHURHNRle6AX9nhhdZnC41tC4AkvO8=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "35e0ed8d1875d2303fab9930c6eb205654a2f6a3", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "35e0ed8d1875d2303fab9930c6eb205654a2f6a3", + "type": "github" + } + }, + "nixpkgs-python38": { + "locked": { + "lastModified": 1708815994, + "narHash": "sha256-hL7N/ut2Xu0NaDxDMsw2HagAjgDskToGiyZOWriiLYM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9a9dae8f6319600fa9aebde37f340975cab4b8c0", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "9a9dae8f6319600fa9aebde37f340975cab4b8c0", + "type": "github" + } + }, + "nixpkgs-python39": { + "locked": { + "lastModified": 1743938762, + "narHash": "sha256-UgFYn8sGv9B8PoFpUfCa43CjMZBl1x/ShQhRDHBFQdI=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "74a40410369a1c35ee09b8a1abee6f4acbedc059", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "74a40410369a1c35ee09b8a1abee6f4acbedc059", + "type": "github" + } + }, "opam-nix": { "inputs": { "flake-compat": "flake-compat", @@ -92,6 +140,9 @@ "nixpkgs": [ "nixpkgs" ], + "nixpkgs-llvm17": "nixpkgs-llvm17", + "nixpkgs-python38": "nixpkgs-python38", + "nixpkgs-python39": "nixpkgs-python39", "opam-overlays": "opam-overlays", "opam-repository": [ "opam-repository" @@ -99,11 +150,11 @@ "opam2json": "opam2json" }, "locked": { - "lastModified": 1771067167, - "narHash": "sha256-XSw8dQIkdr+6eLvbUHo3cJPtTU7o5SMODz3qlnzmGpQ=", + "lastModified": 1782476416, + "narHash": "sha256-oFdLuHny6fe6kSOrOTN5Bfls+GisFiM8ytYrBQpGRog=", "owner": "tweag", "repo": "opam-nix", - "rev": "2e20bbbe8130d1880338291446fd4e710a4db9a1", + "rev": "583fb2ed4db44fcda4f6222c554949503d50a352", "type": "github" }, "original": { @@ -131,11 +182,11 @@ "opam-repository": { "flake": false, "locked": { - "lastModified": 1772577116, - "narHash": "sha256-2asop4QmRveafi6E4DzzLe9DgwZYmcQc6OBfP2sHFkU=", + "lastModified": 1785014707, + "narHash": "sha256-dubArXq3YZOnTZ/+F8tHL/6BLR3I7HsnRZaf4sgpj6Y=", "owner": "ocaml", "repo": "opam-repository", - "rev": "cbed368dbe42bcb41584964f7e491ddb3d77cc96", + "rev": "f15dbe6fae81b4f8d4c1e2d6a78e50ce2b723b12", "type": "github" }, "original": { @@ -153,11 +204,11 @@ "systems": "systems_3" }, "locked": { - "lastModified": 1749457947, - "narHash": "sha256-+QVm+HOYikF3wUhqSIV8qJbE/feSG+p48fgxIosbHS0=", + "lastModified": 1782276984, + "narHash": "sha256-rBGN9TERADPXiehNe1/9emO6QqYPrTwSoMdB+BVEWpM=", "owner": "tweag", "repo": "opam2json", - "rev": "0ecd66fc2bfb25d910522c990dd36412259eac1f", + "rev": "88b2a71f6e2df38d3304d3900ee129f4e83048f8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 7e96fbd..ac1e952 100644 --- a/flake.nix +++ b/flake.nix @@ -42,7 +42,7 @@ on = opam-nix.lib.${system}; devPackagesQuery = { ocaml-lsp-server = "*"; - ocamlformat = "*"; + ocamlformat = "0.29.0"; alcotest = "*"; odoc = "*"; }; @@ -50,7 +50,10 @@ ocaml-system = "*"; }; mkScopes = pkgs: isStatic: rec { - scope = on.buildDuneProject { inherit pkgs; } package ./. query; + scope = on.buildDuneProject { + inherit pkgs; + resolveArgs.env.sys-ocaml-version = pkgs.ocaml.version; + } package ./. query; overlay = final: prev: { ${package} = prev.${package}.overrideAttrs ( _: @@ -60,14 +63,14 @@ // ( if isStatic then { - buildPhase = ''dune build -p ${package} --profile static -j $NIX_BUILD_CORES''; + buildPhase = "dune build -p ${package} --profile static -j $NIX_BUILD_CORES"; } else { } ) ); ocamlgraph = prev.ocamlgraph.overrideAttrs (_: { - buildPhase = ''dune build -p ocamlgraph -j $NIX_BUILD_CORES''; + buildPhase = "dune build -p ocamlgraph -j $NIX_BUILD_CORES"; }); conf-gmp = prev.conf-gmp.overrideAttrs (_: { nativeBuildInputs = [ pkgs.pkgsBuildHost.stdenv.cc ]; @@ -108,7 +111,10 @@ with pkgs; devPackages ++ [ - esbuild + difftastic + libxslt + htmlhint + prettier tex reuse watchexec diff --git a/lib/compiler/Driver.ml b/lib/compiler/Driver.ml index 69ff92a..e0e25b7 100644 --- a/lib/compiler/Driver.ml +++ b/lib/compiler/Driver.ml @@ -23,7 +23,7 @@ let update ?(bail = true) ?(progress = Phases.no_progress) (action : Action.t) (Query_results r, forest) | Query_results _ -> (Done, forest) | Report_errors (errors, next_action) -> - List.iter Error.print errors; + List.iter (Error.print ~config:forest.config) errors; if Error.any_fatal errors then (Quit Fail, forest) else (next_action, forest) | Load_configured_dirs -> Phases.load_configured_dirs ~forest ~progress; @@ -94,7 +94,7 @@ let batch_run ?(progress = Phases.no_progress) ~env ~(config : Config.t) ~dev () let report action = match action with | Report_errors (e, _) -> - List.iter Error.print e; + List.iter (Error.print ~config) e; if Error.any_fatal e then exit 1 | _ -> () in diff --git a/lib/compiler/Error.ml b/lib/compiler/Error.ml index 20a30c1..1551be8 100644 --- a/lib/compiler/Error.ml +++ b/lib/compiler/Error.ml @@ -5,6 +5,23 @@ open struct module T = Types end +type code = + [ `Eval_error + | `Type_error + | `Expand_error + | `Parse_error + | `Io_error + | `Config_error + | `LaTeX_error + | `Duplicate_tree + | `Unlinked_attribution + | `Broken_link + | `Broken_transclusion + | `Foreign_blob_error + | `Unresolved_import + | `Invalid_date + | `Invalid_uri ] + type t = | Eval_error of Eval_error.t | Expand_error of Expand_error.t @@ -24,17 +41,14 @@ type t = and latex_error = {range: Grace.Range.t; msg: string} -and unlinked_attribution_warning = { - role: T.attribution_role; - range: Range.t option; -} +and unlinked_attribution_warning = T.(content attribution) let tex_range {range; _} = range let latex_error ~range msg = {range; msg} let of_tex_error e = LaTeX_error e -let unlinked_attribution_warning ~range role = - Unlinked_attribution_warning {range; role} +let unlinked_attribution_warning attribution = + Unlinked_attribution_warning attribution let broken_transclusion t = Broken_transclusion t let broken_link t = Broken_link t let duplicate_tree ~uri = Duplicate_tree uri @@ -103,8 +117,6 @@ let render_type_error ~range Eval_error.{got; expected} = [Label.createf ~range ~priority:Primary "this is %s" (show_value got)] | None -> [empty_label ~range] in - (*let got_note = [Message.createf "this is %s" (show_value got)] in*) - let got_note = [] in let notes = match expected with | [] -> [] @@ -117,37 +129,62 @@ let render_type_error ~range Eval_error.{got; expected} = ] in let msg = Message.createf "mismatched type" in - Diagnostic.create Error ~code:`Type_error ~labels ~notes:(got_note @ notes) - msg - -(* - Diagnostic.Label.createf ~range ~priority:Diagnostic.Priority.Primary - "this value is of type %s, but expected something of type %a" got - (Format.pp_print_list Eval_error.pp_expected_value) - expected; - *) + Diagnostic.create Error ~code:`Type_error ~labels ~notes msg let render_eval_error Eval_error.{error; range} = let default_labels = [empty_label ~range] in + let label s = Diagnostic.Label.createf ~range ~priority:Primary "%s" s in match error with | Type_error err -> render_type_error ~range err - | Invalid_URI _ -> Diagnostic.createf Error ~labels:[] ~notes:[] "" + | Invalid_URI s -> + Diagnostic.createf ~code:`Invalid_uri Error ~labels:[label s] "Invalid URI" | Invalid_date _ -> - Diagnostic.createf ~labels:default_labels Error "Invalid date" - | Unbound_variable _ -> - Diagnostic.createf ~labels:default_labels Error "unbound variable" + Diagnostic.createf ~code:`Invalid_date + ~labels:[label "expected a date in the form of YYYY-MM-DD."] + Error "Failed to parse date" + | Unbound_variable name -> + Diagnostic.createf ~code:`Eval_error Error + ~labels: + [ + Label.createf ~range ~priority:Primary + "no binding named %s is in scope" name; + ] + "unbound variable" | No_current_uri -> - Diagnostic.createf ~labels:default_labels Error "no current uri" - | Missing_arguments _ -> - Diagnostic.createf ~labels:default_labels Error "missing arguments" - | Unbound_method _ -> - Diagnostic.createf ~labels:default_labels Error "unbound method" - | Unbound_fluid_symbol _ -> - Diagnostic.createf ~labels:default_labels Error "unbound fluid symbol" - | Asset_error _ -> - Diagnostic.createf ~labels:default_labels Error "asset error" + Diagnostic.createf ~code:`Eval_error ~labels:default_labels Error + "no current uri" + | Missing_arguments n -> + Diagnostic.createf ~code:`Eval_error Error + ~labels: + [ + Label.createf ~range ~priority:Primary + "this function expected %d more argument%s" n + (if n = 1 then "" else "s"); + ] + "missing arguments" + | Unbound_method name -> + Diagnostic.createf ~code:`Eval_error Error + ~labels:[Label.createf ~range ~priority:Primary "no method named %s" name] + "unbound method" + | Unbound_fluid_symbol sym -> + Diagnostic.createf ~code:`Eval_error Error + ~labels: + [ + Label.createf ~range ~priority:Primary "no fluid binding for %s" + (Symbol.show sym); + ] + "unbound fluid symbol" + | Asset_error err -> + let describe : Asset_router.Error.error -> string = function + | Not_found path -> Format.asprintf "no such file: %s" path + | No_content_address -> "this asset has no content address" + | Failed_to_hash -> "failed to hash this asset's content" + in + Diagnostic.createf ~code:`Eval_error Error + ~labels:[Label.createf ~range ~priority:Primary "%s" (describe err)] + "asset error" | Unresolved_identifier {suggestions; path} -> - Diagnostic.createf Error + Diagnostic.createf ~code:`Eval_error Error ~labels:[empty_label ~range] ~notes:suggestions "unresolved identifier %a" Trie.pp_path path @@ -165,33 +202,42 @@ let render_io_error (e : Eio_util.IO_error.t) = | File_already_exists f -> "file " ^ f ^ " already exists" | IO_error s -> s in - Diagnostic.createf Error "io_error: %s" msg + Diagnostic.createf ~code:`Io_error Error "%s" msg -let render_config_error (error : string list Config_error.t) = +let render_config_error (error : string list Config_error.t) : code Diagnostic.t + = + let pp_path = + Format.( + pp_print_list ~pp_sep:(fun out _ -> fprintf out ".") pp_print_string) + in match error with - | Parse_error {range = _; msg = _} -> Diagnostic.createf Error "parse_error" - | Using_default (_, _) -> Diagnostic.createf Error "using default" + | Parse_error {range; msg} -> + Diagnostic.createf ~code:`Config_error Error + ~labels:[empty_label ~range] + "%s" msg + | Using_default (path, default) -> + Diagnostic.createf ~code:`Config_error Warning + "using default %a for option %a" pp_path default pp_path path | Unknown_option option -> - Diagnostic.createf Warning ~code:`Config_error "unknown options %a" - Format.( - pp_print_list ~pp_sep:(fun out _ -> fprintf out ".") pp_print_string) + Diagnostic.createf Warning ~code:`Config_error "unknown option %a" pp_path option - | Invalid_url _ -> Diagnostic.createf Error "invalid url" + | Invalid_url url -> + Diagnostic.createf ~code:`Config_error Error "invalid url %s" url | No_path_in_foreign_table -> - Diagnostic.createf Error "no path in foreign table" + Diagnostic.createf ~code:`Config_error Error "no path in foreign table" | Missing_dir s -> Diagnostic.createf ~code:`Config_error Error "directory %s does not exist" s | Missing_file s -> Diagnostic.createf ~code:`Config_error Error "file %s does not exist" s let render_latex_error ({range; msg} : latex_error) = - Diagnostic.create Error + Diagnostic.create Error ~code:`LaTeX_error ~labels:[Label.createf ~range ~priority:Primary ""] Message.(create msg) let render_broken_transclusion (t : T.transclusion) = Diagnostic.( - createf Warning + createf Warning ~code:`Broken_transclusion ~labels: (Option.fold t.range ~none:[] ~some:(fun range -> [Label.createf ~range ~priority:Primary "This tree was not found"])) @@ -199,30 +245,45 @@ let render_broken_transclusion (t : T.transclusion) = let render_broken_link (link : _ T.link) = Diagnostic.( - createf Warning + createf Warning ~code:`Broken_link ~labels: (Option.fold link.range ~none:[] ~some:(fun range -> [Label.createf ~range ~priority:Primary "This tree was not found"])) "Broken link %a" URI.pp link.href) let render_unlinked_attribution_warning - ({range; role} : unlinked_attribution_warning) = + ({role; range; vertex} : unlinked_attribution_warning) = let corrected_attribution_code = match role with | Author -> "\\author/literal" | Contributor -> "\\contributor/literal" in - Diagnostic.createf Error + let arg = + match vertex with + | T.Uri_vertex uri -> + Option.value (URI.name uri) ~default:(Format.asprintf "%a" URI.pp uri) + | T.Content_vertex _ -> "..." + in + let suggestion = Format.asprintf "%s{%s}" corrected_attribution_code arg in + let notes = + [ + Message.createf + "If there is a tree trees/%s.tree, you may use the shorthand %s \ + instead of a full URI." + arg arg; + ] + in + Diagnostic.createf Warning ~code:`Unlinked_attribution ~notes ~labels: (fold_range ~range Message.( createf "Expected valid URI in attribution. Use `%s` instead if you \ intend an unlinked attribution." - corrected_attribution_code)) - "Unlinked attribution" + suggestion)) + "Did you mean %s?" suggestion -let render = +let render ~(config : Config.t) = let open Grace.Diagnostic in function | Cant_eval_anonymous_tree -> failwith "todo: can't eval" @@ -233,7 +294,8 @@ let render = | Configuration_error error -> render_config_error error | LaTeX_error error -> render_latex_error error | Duplicate_tree uri -> - Diagnostic.createf Error "duplicate tree %a" URI.pp uri + Diagnostic.createf Error ~code:`Duplicate_tree "duplicate tree %a" URI.pp + uri | Broken_link link -> render_broken_link link | Broken_transclusion t -> render_broken_transclusion t | Unlinked_attribution_warning w -> render_unlinked_attribution_warning w @@ -246,8 +308,8 @@ let render = you are using."; ] in - createf ~labels ~notes Error "Failed to load foreign blob %s: %s" s.path - s.msg + createf ~labels ~notes Error ~code:`Foreign_blob_error + "Failed to load foreign blob %s: %s" s.path s.msg | Failed_to_parse_foreign_blob s -> let labels = [] in let notes = @@ -257,40 +319,51 @@ let render = you are using."; ] in - createf ~labels ~notes Error "Failed to parse foreign blob %s: %s" s.path - s.msg + createf ~labels ~notes Error ~code:`Foreign_blob_error + "Failed to parse foreign blob %s: %s" s.path s.msg | Failed_to_add_edge (_, _) -> failwith "failed to add edge" | Failed_to_add_vertex (range, uri) -> let name = Option.get @@ URI.(name uri) in let labels = + [Label.createf ~range ~priority:Primary "No file %s.tree found." name] + in + let notes = [ - Label.create ~range ~priority:Primary - Message.(createf "No file %s.tree found." name); + Message.createf "checked the following directories: %a" + Format.( + pp_print_list + ~pp_sep:(fun out _ -> fprintf out ", ") + pp_print_string) + config.trees; ] in - createf ~labels Error "Unresolved import" + createf ~labels ~notes Error ~code:`Unresolved_import "" -(* - let corrected = "\\tag/content" in - Diagnostic.createf Error - "Expected valid URI in tag. Use `%s` instead if you intend an unlinked \ - attribution." - corrected - *) +let code_to_string : code -> string = function + | `Eval_error -> "eval_error" + | `Type_error -> "type_error" + | `Expand_error -> "expand_error" + | `Parse_error -> "parse_error" + | `Io_error -> "io_error" + | `Config_error -> "config_error" + | `LaTeX_error -> "latex_error" + | `Duplicate_tree -> "duplicate_tree" + | `Unlinked_attribution -> "unlinked_attribution" + | `Broken_link -> "broken_link" + | `Broken_transclusion -> "broken_transclusion" + | `Foreign_blob_error -> "foreign" + | `Unresolved_import -> "unresolved_import" + | `Invalid_date -> "invalid_date" + | `Invalid_uri -> "invalid_uri" let print_diagnostic = let config = Grace_ansi_renderer.Config.{default with num_contextual_lines = 3} in - let code_to_string = function - | `Config_error -> "config_error" - | `Type_error -> "type_error" - | `Parse_error -> "parse_error" - in Format.eprintf "%a@." Grace_ansi_renderer.(pp_diagnostic ~config ~code_to_string) -let print error = print_diagnostic (render error) +let print ~config error = print_diagnostic (render ~config error) let is_fatal = function | Broken_link _ | Broken_transclusion _ -> false @@ -304,4 +377,4 @@ let is_fatal = function let any_fatal = List.fold_left (fun acc x -> acc || is_fatal x) false -let print_config_error error = print @@ config_error error +let print_config_error error = print_diagnostic (render_config_error error) diff --git a/lib/compiler/Error.mli b/lib/compiler/Error.mli index 9b5a89a..bfa4ae1 100644 --- a/lib/compiler/Error.mli +++ b/lib/compiler/Error.mli @@ -5,10 +5,24 @@ module T := Types type latex_error -type unlinked_attribution_warning = { - role: T.attribution_role; - range: Range.t option; -} +type code = + [ `Eval_error + | `Type_error + | `Expand_error + | `Parse_error + | `Io_error + | `Config_error + | `LaTeX_error + | `Duplicate_tree + | `Unlinked_attribution + | `Broken_link + | `Broken_transclusion + | `Foreign_blob_error + | `Unresolved_import + | `Invalid_date + | `Invalid_uri ] + +type unlinked_attribution_warning = T.(content attribution) type t = | Eval_error of Eval_error.t @@ -34,15 +48,14 @@ val any_fatal : t list -> bool val tex_range : latex_error -> Range.t val latex_error : range:Grace.Range.t -> string -> latex_error -val unlinked_attribution_warning : - range:Range.t option -> T.attribution_role -> t +val unlinked_attribution_warning : unlinked_attribution_warning -> t val broken_transclusion : T.transclusion -> t val broken_link : T.(content link) -> t val cant_eval_anonymous_tree : t val failed_to_load_foreign_blob : msg:string -> path:string -> t val failed_to_parse_foreign_blob : msg:string -> path:string -> t -val print : t -> unit +val print : config:Config.t -> t -> unit val config_error : string list Config_error.t -> t val parse_error : Parse.error -> t val io_error : Eio_util.IO_error.t -> t @@ -57,8 +70,9 @@ val yield : t -> unit val yield_eval_error : Eval_error.t -> unit val yield_expand_error : Expand_error.t -> unit -val render : t -> [`Type_error | `Config_error | `Parse_error] Diagnostic.t -val print_diagnostic : - [`Type_error | `Config_error | `Parse_error] Diagnostic.t -> unit +val render : config:Config.t -> t -> code Diagnostic.t +val render_config_error : string list Config_error.t -> code Diagnostic.t +val code_to_string : code -> string +val print_diagnostic : code Diagnostic.t -> unit val print_config_error : string list Config_error.t -> unit diff --git a/lib/compiler/Eval.ml b/lib/compiler/Eval.ml index 8e5b584..0475bfe 100644 --- a/lib/compiler/Eval.ml +++ b/lib/compiler/Eval.ml @@ -270,8 +270,8 @@ and eval_node ~env ({value; range} as node) : (Value.t, Eval_error.t) result = focus_clo ~env ~range env.lex_env (List.map (fun (info, x) -> (info, Some x)) xs) body - | Ref -> begin - match eval_pop_arg ~env ~range |> Result.bind @~ extract_uri ~env with + | Ref -> + begin match eval_pop_arg ~env ~range |> Result.bind @~ extract_uri ~env with | Ok {value = href; range} -> let content = T.Content @@ -286,10 +286,17 @@ and eval_node ~env ({value; range} as node) : (Value.t, Eval_error.t) result = let got = None in let expected = [URI] in type_error ~got ~expected ~range - end + end | Link {title; dest} -> + let range_of_nodes : Syn.t -> Range.t option = function + | [] -> None + | first :: _ as nodes -> + let last = List.nth nodes (List.length nodes - 1) in + Some (Range.merge first.range last.range) + in let* value = eval_tape ~env dest in - let dest = {node with value} in + let dest_range = Option.value (range_of_nodes dest) ~default:range in + let dest = Range.{value; range = dest_range} in let* {value = href; range} = extract_uri ~env dest in let* content = match title with @@ -690,7 +697,7 @@ and eval_node ~env ({value; range} as node) : (Value.t, Eval_error.t) result = | `Uri -> begin let* {value = uri; _} = extract_uri ~env arg in ok @@ T.Uri_vertex uri - end + end in focus ~env ~range:node.range @@ Dx_const const | Dx_execute -> @@ -715,7 +722,7 @@ and focus ~env ~range = function | Content content' -> ok @@ Value.Content (T.concat_compressed_content content content') | value -> ok value - end + end | ( Sym _ | Obj _ | Dx_prop _ | Dx_sequent _ | Dx_query _ | Dx_var _ | Dx_const _ ) as v -> begin let* c = process_tape ~env in @@ -725,7 +732,7 @@ and focus ~env ~range = function let expected = [Content] in let got = Some v' in type_error ~got ~expected ~range - end + end and focus_clo ~env ~range rho (xs : string option binding list) body = match xs with @@ -733,8 +740,8 @@ and focus_clo ~env ~range rho (xs : string option binding list) body = Result.bind (eval_tape ~env:{env with lex_env = rho} body) (focus ~env ~range) - | (info, y) :: ys -> begin - match pop_arg_opt ~env with + | (info, y) :: ys -> + begin match pop_arg_opt ~env with | Some arg -> let* yval = match info with @@ -751,8 +758,8 @@ and focus_clo ~env ~range rho (xs : string option binding list) body = | Content nodes when T.strip_whitespace nodes = T.Content [] -> ok @@ Value.Clo (rho, xs, body) | _ -> missing_argument ~range (List.length xs) + end end - end and emit_content_nodes ~env ~range content = focus ~env ~range @@ Content (T.Content (T.compress_nodes content)) diff --git a/lib/compiler/Expand.ml b/lib/compiler/Expand.ml index 2b30fac..fa372c0 100644 --- a/lib/compiler/Expand.ml +++ b/lib/compiler/Expand.ml @@ -200,12 +200,12 @@ let rec expand_eff ~(forest : State.t) : Code.t -> Syn.t = function | Call (obj, meth) -> let obj = expand_eff ~forest obj in {node with value = Call (obj, meth)} :: expand_eff ~forest rest - | Import (vis, dep) -> + | Import (vis, {value = dep; range = dep_range}) -> let dep_uri = URI.named_uri ~base:forest.config.url dep in begin match forest./{dep_uri} with | None -> - let range = node.range in - Error.yield_expand_error @@ Expand_error.import_not_found ~range dep_uri + Error.yield_expand_error + @@ Expand_error.import_not_found ~range:dep_range dep_uri | Some tree -> begin match vis with | Public -> Sc.include_subtree [] tree diff --git a/lib/compiler/Expand_error.ml b/lib/compiler/Expand_error.ml index 6f180e8..a3d2283 100644 --- a/lib/compiler/Expand_error.ml +++ b/lib/compiler/Expand_error.ml @@ -24,11 +24,17 @@ let render_unresolved_xmlns ~range prefix = ] in let labels = [Label.createf ~range ~priority:Primary ""] in - Diagnostic.createf Error ~labels ~notes "Unresolved XML namespace" + Diagnostic.createf Error ~code:`Expand_error ~labels ~notes + "Unresolved XML namespace" let render {range; error} = match error with - | Import_not_found _uri -> Diagnostic.createf Error {|import not found |} + | Import_not_found uri -> + Diagnostic.createf Error ~code:`Expand_error + ~labels:[Label.createf ~range ~priority:Primary "not found"] + "import not found: %a" URI.pp uri | Unresolved_identifier -> - Diagnostic.createf Error "todo: Expand_error.render" + Diagnostic.createf Error ~code:`Expand_error + ~labels:[Label.createf ~range ~priority:Primary ""] + "unresolved identifier" | Unresolved_xmlns prefix -> render_unresolved_xmlns ~range prefix diff --git a/lib/compiler/Forester_compiler.ml b/lib/compiler/Forester_compiler.ml index 2d6d3de..1b59cab 100644 --- a/lib/compiler/Forester_compiler.ml +++ b/lib/compiler/Forester_compiler.ml @@ -29,6 +29,8 @@ module Expand = Expand {{!Forester_core.Forest_graph.topo_fold}folding} over the {{!Forester_core.Forest_graph}import graph.}*) +module Expand_error = Expand_error + module Eval = Eval (** Transform {!Syn.tree}s into {{!Forester_core.Types.article}[articles]}.*) @@ -68,6 +70,7 @@ module Job = Job (**/**) module Config_parser = Config_parser +module Config_error = Config_error module Eio_util = Eio_util module Export_for_test = Export_for_test module Cache = Cache diff --git a/lib/compiler/Imports.ml b/lib/compiler/Imports.ml index 8656b94..719b068 100644 --- a/lib/compiler/Imports.ml +++ b/lib/compiler/Imports.ml @@ -43,11 +43,11 @@ and analyse_code ~env ~root (code : Code.t) = and analyse_node ~env ~root (node : Code.node Range.located) : unit = let config = env.forest.config in match node.value with - | Import (_, dep) -> + | Import (_, {value = dep; range = dep_range}) -> let dep_uri = URI.named_uri ~base:config.url dep in let dependency = T.Uri_vertex dep_uri in let target = T.Uri_vertex root in - begin match add_vertex ~env ~range:node.range dependency with + begin match add_vertex ~env ~range:dep_range dependency with | Ok () -> begin match add_edge env.graph dependency target with | Ok () -> () diff --git a/lib/compiler/Suggestions.ml b/lib/compiler/Suggestions.ml index e717180..020c4ca 100644 --- a/lib/compiler/Suggestions.ml +++ b/lib/compiler/Suggestions.ml @@ -32,15 +32,15 @@ let create_suggestions ~visible path : Diagnostic.Message.t list = let path, data, _ = List.hd suggestions in let location_hint = match data with - | Syn.Term ({range; _} :: _) -> begin - match Range.source range with + | Syn.Term ({range; _} :: _) -> + begin match Range.source range with | `File name | `Reader {name = Some name; _} -> [Diagnostic.Message.createf "defined in %s" name] | _ -> [] - end + end | _ -> [] in - [Diagnostic.Message.createf "Did you mean %a?" Trie.pp_path path] + [Diagnostic.Message.createf "Did you mean \\%a?" Trie.pp_path path] @ location_hint else [] in diff --git a/lib/core/Code.ml b/lib/core/Code.ml index 466b723..eae8b09 100644 --- a/lib/core/Code.ml +++ b/lib/core/Code.ml @@ -41,7 +41,7 @@ type node = | Object of t _object | Patch of t patch | Call of t * string - | Import of visibility * string + | Import of visibility * string Range.located | Def of Trie.path * string binding list * t | Decl_xmlns of string * string | Alloc of Trie.path diff --git a/lib/core/Code.mli b/lib/core/Code.mli index b378e23..edc5246 100644 --- a/lib/core/Code.mli +++ b/lib/core/Code.mli @@ -26,7 +26,7 @@ type node = | Object of t _object | Patch of t patch | Call of t * string - | Import of visibility * string + | Import of visibility * string Range.located | Def of Trie.path * string binding list * t | Decl_xmlns of string * string | Alloc of Trie.path @@ -60,8 +60,8 @@ type tree = {source_path: string option; uri: URI.t option; code: t} val parens : t -> node val squares : t -> node val braces : t -> node -val import_private : string -> node -val import_public : string -> node +val import_private : string Range.located -> node +val import_public : string Range.located -> node val inline_math : t -> node val display_math : t -> node val map : (t -> t) -> node -> node diff --git a/lib/frontend/Forester.ml b/lib/frontend/Forester.ml index 44b6925..ac32a81 100644 --- a/lib/frontend/Forester.ml +++ b/lib/frontend/Forester.ml @@ -214,7 +214,7 @@ let render_forest ?(progress = Phases.no_progress) ~emit_legacy_xml in forest.log.debug ~src:Log.Src.frontend (fun m -> m "Writing %i files to output" (List.length jobs)); - List.iter Error.print errors; + List.iter (Error.print ~config:forest.config) errors; begin (* Note: this part appears to be fast! *) let@ items = Eio.Fiber.List.iter ~max_fibers:20 @~ jobs in diff --git a/lib/frontend/Html_client.ml b/lib/frontend/Html_client.ml index 3b8bbd7..57fb24d 100644 --- a/lib/frontend/Html_client.ml +++ b/lib/frontend/Html_client.ml @@ -513,8 +513,8 @@ and render_attribution_vertex ~env (attribution : T.(content attribution)) = | Ok node -> node | Error (node, error) -> begin match error with - | `Broken_link {range; _} -> - Error.(yield @@ unlinked_attribution_warning ~range attribution.role); + | `Broken_link _ -> + Error.(yield @@ unlinked_attribution_warning attribution); node end end @@ -868,10 +868,10 @@ let render_page ~forest ?(mode = Static) | Some uri -> if URI.Set.mem uri forest.broken_links then () else begin - Error.print error; + Error.print ~config:forest.config error; forest.broken_links <- URI.Set.add uri forest.broken_links end - | None -> Error.print error) + | None -> Error.print ~config:forest.config error) errors; (Option.get !result, List.of_seq errors) diff --git a/lib/frontend/Legacy_xml_client.ml b/lib/frontend/Legacy_xml_client.ml index 472d77a..0b431b9 100644 --- a/lib/frontend/Legacy_xml_client.ml +++ b/lib/frontend/Legacy_xml_client.ml @@ -249,7 +249,7 @@ and render_transclusion ~env (transclusion : T.transclusion) : P.node list = match State.get_content_of_transclusion ~forest:env.forest transclusion with | None -> let warning = Error.broken_transclusion transclusion in - Error.print warning; + Error.print ~config:env.forest.config warning; [] | Some content -> render_content ~env content diff --git a/lib/frontend/test/config.t b/lib/frontend/test/config.t index a1bf37b..a290348 100644 --- a/lib/frontend/test/config.t +++ b/lib/frontend/test/config.t @@ -6,7 +6,7 @@ warning: option [forest.home] not set, using default index $ parse_forester_config configs/uninterpreted-fields.toml - warning[config_error]: unknown options forest.unknown + warning[config_error]: unknown option forest.unknown $ parse_forester_config configs/nonexistent.toml error[config_error]: file configs/nonexistent.toml does not exist diff --git a/lib/language_server/Analysis.ml b/lib/language_server/Analysis.ml index 03c7a41..9e80fc7 100644 --- a/lib/language_server/Analysis.ml +++ b/lib/language_server/Analysis.ml @@ -55,9 +55,9 @@ let extract_addr ({value; range} : Code.node Range.located) = match value with | Group (Braces, [{value = Text addr; _}]) | Group (Parens, [{value = Text addr; _}]) - | Text addr (* SEEEMS DODGY!! *) - | Import (_, addr) -> + | Text addr (* SEEEMS DODGY!! *) -> Some Range.{value = addr; range} + | Import (_, {value = addr; _}) -> Some Range.{value = addr; range} | Subtree (addr, _) -> Option.map (fun s -> Range.{value = s; range}) addr | _ -> None diff --git a/lib/language_server/Diagnostics.ml b/lib/language_server/Diagnostics.ml index fe53e9e..a58199c 100644 --- a/lib/language_server/Diagnostics.ml +++ b/lib/language_server/Diagnostics.ml @@ -30,4 +30,5 @@ let compute (document : Lsp.Text_document.t) = | diagnostics -> Eio.traceln "publishing %i diagnostics to %s" (List.length diagnostics) (Lsp.Uri.to_path lsp_uri); - Publish.publish lsp_uri @@ List.map Error.render diagnostics + Publish.publish lsp_uri + @@ List.map (Error.render ~config:forest.config) diagnostics diff --git a/lib/language_server/Publish.ml b/lib/language_server/Publish.ml index 1436090..074ee67 100644 --- a/lib/language_server/Publish.ml +++ b/lib/language_server/Publish.ml @@ -7,7 +7,6 @@ open Forester_core open Forester_compiler -open State.Syntax open struct module L = Lsp.Types @@ -27,32 +26,32 @@ let render_lsp_related_info (uri : L.DocumentUri.t) (label : Diagnostic.Label.t) let message = Format.asprintf "%a" Diagnostic.Message.pp label.message in L.DiagnosticRelatedInformation.create ~location ~message -let render_lsp_diagnostic (uri : L.DocumentUri.t) (diag : 'a Diagnostic.t) : - Lsp_Diagnostic.t = - let range = Lsp_shims.lsp_range_of_range @@ (List.hd diag.labels).range in +let lsp_code_of_code (code : Error.code) : Jsonrpc.Id.t = + `String (Error.code_to_string code) + +let render_lsp_diagnostic (uri : L.DocumentUri.t) + (diag : Error.code Diagnostic.t) : Lsp_Diagnostic.t = + let range = + match diag.labels with + | label :: _ -> Lsp_shims.lsp_range_of_range label.range + | [] -> + let start = L.Position.create ~line:0 ~character:0 in + L.Range.create ~start ~end_:start + in let severity = Lsp_shims.Diagnostic.lsp_severity_of_severity @@ diag.severity in - let code = - `String "todo" - (*diag.code*) - in - let source = - let Lsp_state.{forest; _} = Lsp_state.get () in - let uri = URI.of_lsp_uri ~base:forest.config.url uri in - let@ doc = Option.map @~ Option.bind forest.={uri} Tree.document in - Lsp.Text_document.text doc - in + let code = Option.map lsp_code_of_code diag.code in let message = Format.asprintf "%a" Diagnostic.Message.pp diag.message in let relatedInformation = List.map (render_lsp_related_info uri) diag.labels in - Lsp_Diagnostic.create ~range ~severity ~code ?source + Lsp_Diagnostic.create ~range ~severity ?code ~source:"forester" ~message:(`String message) ~relatedInformation () let broadcast notif = let msg = Broadcast.to_jsonrpc notif in send @@ RPC.Packet.Notification msg -let publish (uri : Lsp.Uri.t) (diagnostics : 'a Diagnostic.t list) = +let publish (uri : Lsp.Uri.t) (diagnostics : Error.code Diagnostic.t list) = let diagnostics = List.map (render_lsp_diagnostic uri) diagnostics in let params = L.PublishDiagnosticsParams.create ~uri ~diagnostics () in broadcast @@ PublishDiagnostics params diff --git a/lib/parser/Grammar.mly b/lib/parser/Grammar.mly index eb748ba..4e0b445 100644 --- a/lib/parser/Grammar.mly +++ b/lib/parser/Grammar.mly @@ -61,7 +61,7 @@ let patch_bindings := let head_node := | DEF; (~,~,~) = fun_spec; | ALLOC; ~ = ident; -| EXPORT; ~ = txt_arg; +| EXPORT; ~ = located_txt_arg; | NAMESPACE; ~ = ident; ~ = braces(code_expr); | SUBTREE; ~ = option(squares(wstext)); ~ = braces(ws_list(locate(head_node))); | FUN; ~ = binder; ~ = arg; @@ -114,11 +114,12 @@ let arg := { [{located_str with value = Code.Verbatim located_str.value}] } let txt_arg == braces(wstext) +let located_txt_arg == braces(locate(wstext)) let fun_spec == ~ = ident; ~ = binder; ~ = arg; <> let head_node_or_import := | head_node -| IMPORT; ~ = txt_arg; +| IMPORT; ~ = located_txt_arg; let main := | ~ = ws_list(locate(head_node_or_import)); EOF; <> diff --git a/lib/server/Server.ml b/lib/server/Server.ml index a9379d8..9836c3e 100644 --- a/lib/server/Server.ml +++ b/lib/server/Server.ml @@ -213,7 +213,7 @@ let config_handler ~env request_headers request_body forest = the user enters ~/forest/forest.toml *) match Config_parser.parse_forest_config_file ~env filename with | Error errs -> - let errors = List.map Error.(config_error >>> render) errs in + let errors = List.map Error.render_config_error errs in let errors = List.map (Format.asprintf "%a@." diff --git a/test/bad-foreign-blob.t b/test/bad-foreign-blob.t index d08699a..734d20d 100644 --- a/test/bad-foreign-blob.t +++ b/test/bad-foreign-blob.t @@ -17,6 +17,6 @@ > EOF $ forester build - error: Failed to parse foreign blob $TESTCASE_ROOT/bad: expected JSON text (JSON value) + error[foreign]: Failed to parse foreign blob $TESTCASE_ROOT/bad: expected JSON text (JSON value) = Make sure the blob was generated with the version of forester that you are using. [1] diff --git a/test/broken-link-gets-reported-only-once.t b/test/broken-link-gets-reported-only-once.t index 7d00773..5b0b39c 100644 --- a/test/broken-link-gets-reported-only-once.t +++ b/test/broken-link-gets-reported-only-once.t @@ -17,7 +17,7 @@ > EOF $ forester build > /dev/null - warning: Broken link https://www.my-great-forest.net/link/ - ┌─ $TESTCASE_ROOT/trees/foo.tree:1:4 + warning[broken_link]: Broken link https://www.my-great-forest.net/link/ + ┌─ $TESTCASE_ROOT/trees/foo.tree:1:13 1 │ \p{[broken](link)} - │ ^^^^^^^^^^^^^^ This tree was not found + │ ^^^^ This tree was not found diff --git a/test/eval-errors.t b/test/eval-errors.t index 23b1cc1..3621688 100644 --- a/test/eval-errors.t +++ b/test/eval-errors.t @@ -19,13 +19,13 @@ 2 │ \p{\hello} │ ^^^^^^^^ This is a function. Did you forget to provide some arguments? = expected some content - error: Invalid date + error[invalid_date]: Failed to parse date ┌─ $TESTCASE_ROOT/trees/invalid-date.tree:1:6 1 │ \date{asdf} - │ ^^^^^^ - error: unresolved identifier rel/has-taf + │ ^^^^^^ expected a date in the form of YYYY-MM-DD. + error[eval_error]: unresolved identifier rel/has-taf ┌─ $TESTCASE_ROOT/trees/unresolved-ident.tree:1:5 1 │ \p{\rel/has-taf} │ ^^^^^^^^^^^ - = Did you mean rel/has-tag? + = Did you mean \rel/has-tag? [1] diff --git a/test/link-errors.t b/test/link-errors.t index feb4b12..db30463 100644 --- a/test/link-errors.t +++ b/test/link-errors.t @@ -18,11 +18,12 @@ $ forester build > /dev/null - warning: Broken link https://www.my-great-forest.net/foo/ - ┌─ $TESTCASE_ROOT/trees/broken-link.tree:1:4 + warning[broken_link]: Broken link https://www.my-great-forest.net/foo/ + ┌─ $TESTCASE_ROOT/trees/broken-link.tree:1:28 1 │ \p{[I am pointing nowhere](foo)} - │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This tree was not found - error: Unlinked attribution + │ ^^^ This tree was not found + warning[unlinked_attribution]: Did you mean \author/literal{unknown}? ┌─ $TESTCASE_ROOT/trees/unlinked-attribution.tree:1:8 1 │ \author{unknown} - │ ^^^^^^^^^ Expected valid URI in attribution. Use `\author/literal` instead if you intend an unlinked attribution. + │ ^^^^^^^^^ Expected valid URI in attribution. Use `\author/literal{unknown}` instead if you intend an unlinked attribution. + = If there is a tree trees/unknown.tree, you may use the shorthand unknown instead of a full URI. diff --git a/test/missing-import.t b/test/missing-import.t index 220b133..484318a 100644 --- a/test/missing-import.t +++ b/test/missing-import.t @@ -4,8 +4,9 @@ > \import{foo} > EOF $ forester build - error: Unresolved import - ┌─ $TESTCASE_ROOT/trees/index.tree:1:2 + error[unresolved_import]: + ┌─ $TESTCASE_ROOT/trees/index.tree:1:9 1 │ \import{foo} - │ ^^^^^^^^^^^ No file foo.tree found. + │ ^^^ No file foo.tree found. + = checked the following directories: trees [1] diff --git a/test/unlinked-attribution.t b/test/unlinked-attribution.t index f6edf70..cb45d63 100644 --- a/test/unlinked-attribution.t +++ b/test/unlinked-attribution.t @@ -5,7 +5,8 @@ > EOF $ forester build > /dev/null - error: Unlinked attribution + warning[unlinked_attribution]: Did you mean \author/literal{Kento}? ┌─ $TESTCASE_ROOT/trees/index.tree:1:8 1 │ \author{Kento} - │ ^^^^^^^ Expected valid URI in attribution. Use `\author/literal` instead if you intend an unlinked attribution. + │ ^^^^^^^ Expected valid URI in attribution. Use `\author/literal{Kento}` instead if you intend an unlinked attribution. + = If there is a tree trees/Kento.tree, you may use the shorthand Kento instead of a full URI. diff --git a/test/xml-errors.t b/test/xml-errors.t index 24a2317..4f78057 100644 --- a/test/xml-errors.t +++ b/test/xml-errors.t @@ -7,7 +7,7 @@ > EOF $ forester build - error: Unresolved XML namespace + error[expand_error]: Unresolved XML namespace ┌─ $TESTCASE_ROOT/trees/index.tree:2:4 2 │ \ │ ^^^^^^^^^