From a1b7016e1153c408301f7e49a79b3918674b6570 Mon Sep 17 00:00:00 2001 From: Jules Villard Date: Thu, 11 Jun 2020 06:43:55 -0700 Subject: [PATCH] [help] implement `--write-website` Summary: Write documentation for all documented issue types and all user-facing checkers in the "next" version of the documentation. Next diff shows the new website. Reviewed By: dulmarod Differential Revision: D21934370 fbshipit-source-id: 53315d2b4 --- Makefile | 24 +++-- infer/src/base/Language.ml | 2 +- infer/src/base/Language.mli | 2 +- infer/src/integration/Help.ml | 184 +++++++++++++++++++++++++++++++++- website/checkers.json | 7 ++ website/sidebars.js | 8 +- 6 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 website/checkers.json diff --git a/Makefile b/Makefile index 3ca328d6e..3011d167d 100644 --- a/Makefile +++ b/Makefile @@ -590,7 +590,7 @@ config_tests: test_build ocaml_unit_test validate-skel mod_dep $(MAKE) endtoend_test checkCopyright $(MAKE) manuals -ifneq ($(filter endtoend_test,${MAKECMDGOALS}),) +ifneq ($(filter endtoend_test,$(MAKECMDGOALS)),) checkCopyright: src_build toplevel_test: checkCopyright endif @@ -854,7 +854,7 @@ doc: src_build_common $(QUIET)$(call silent_on_success,Generating infer documentation,\ $(MAKE_SOURCE) doc) # do not call the browser if we are publishing the docs -ifeq ($(filter doc-publish,${MAKECMDGOALS}),) +ifneq ($(NO_BROWSE_DOC),yes) $(QUIET)$(call silent_on_success,Opening in browser,\ browse $(INFER_DIR)/_build/default/_doc/_html/index.html) $(QUIET)echo "Tip: you can generate the doc for all the opam dependencies of infer like this:" @@ -864,22 +864,28 @@ ifeq ($(filter doc-publish,${MAKECMDGOALS}),) endif .PHONY: doc-publish -doc-publish: $(INFER_GROFF_MANUALS) +doc-publish: +ifeq ($(IS_FACEBOOK_TREE),yes) + $(QUIET)$(call silent_on_success,Cleaning up FB-only files,\ + $(MAKE) -C $(SRC_DIR) clean; \ + $(MAKE) -C facebook clean) +endif + $(QUIET)$(call silent_on_success,Building infer and manuals,\ + $(MAKE) $(INFER_GROFF_MANUALS)) $(QUIET)$(MKDIR_P) "$(WEBSITE_DIR)"/static/man/next "$(WEBSITE_DIR)"/static/odoc/next $(QUIET)$(call silent_on_success,Copying man pages,\ $(REMOVE) "$(WEBSITE_DIR)"/static/man/*; \ for man in $(INFER_GROFF_MANUALS); do \ groff -Thtml "$$man" > "$(WEBSITE_DIR)"/static/man/next/$$(basename "$$man").html; \ done) -ifeq ($(IS_FACEBOOK_TREE),yes) - $(QUIET)$(call silent_on_success,Cleaning up FB-only files,\ - $(MAKE) -C $(SRC_DIR) clean; \ - $(MAKE) -C facebook clean) -endif $(QUIET)$(call silent_on_success,Building OCaml modules documentation,\ - $(MAKE) IS_FACEBOOK_TREE=no doc) + $(MAKE) IS_FACEBOOK_TREE=no NO_BROWSE_DOC=yes doc) $(QUIET)$(call silent_on_success,Copying OCaml modules documentation,\ rsync -a --delete $(BUILD_DIR)/default/_doc/_html/ "$(WEBSITE_DIR)"/static/odoc/next/) + $(QUIET)$(call silent_on_success,Building infer,\ + $(MAKE) src_build) + $(QUIET)$(call silent_on_success,Calling 'infer help --write-website',\ + $(INFER_BIN) help --write-website "$(WEBSITE_DIR)") # print list of targets .PHONY: show-targets diff --git a/infer/src/base/Language.ml b/infer/src/base/Language.ml index d7648c071..43356cea9 100644 --- a/infer/src/base/Language.ml +++ b/infer/src/base/Language.ml @@ -6,7 +6,7 @@ *) open! IStd -type t = Clang | Java [@@deriving compare] +type t = Clang | Java [@@deriving compare, enumerate] let equal = [%compare.equal: t] diff --git a/infer/src/base/Language.mli b/infer/src/base/Language.mli index b11b977a7..f799bd966 100644 --- a/infer/src/base/Language.mli +++ b/infer/src/base/Language.mli @@ -7,7 +7,7 @@ open! IStd -type t = Clang | Java [@@deriving compare] +type t = Clang | Java [@@deriving compare, enumerate] val equal : t -> t -> bool diff --git a/infer/src/integration/Help.ml b/infer/src/integration/Help.ml index 420d8b094..14f6b05b7 100644 --- a/infer/src/integration/Help.ml +++ b/infer/src/integration/Help.ml @@ -6,10 +6,78 @@ *) open! IStd +module F = Format module L = Logging let list_checkers () = assert false +let mk_markdown_docs_path ~website_root ~basename = website_root ^/ "docs" ^/ basename ^ ".md" + +let escape_double_quotes s = String.substr_replace_all s ~pattern:"\"" ~with_:"\\\"" + +let all_issues_basename = "all-issue-types" + +let basename_checker_prefix = "checker-" + +let basename_of_checker {Checker.id} = basename_checker_prefix ^ id + +let get_checker_web_documentation (checker : Checker.config) = + match checker.kind with + | UserFacing {title; markdown_body} -> + Some (title, markdown_body, None) + | UserFacingDeprecated {title; markdown_body; deprecation_message} -> + Some (title, markdown_body, Some deprecation_message) + | Internal | Exercise -> + None + + +let markdown_one_issue f (issue_type : IssueType.t) = + F.fprintf f "## %s@\n@\n" issue_type.unique_id ; + let checker_config = Checker.config issue_type.checker in + if Option.is_none (get_checker_web_documentation checker_config) then + L.die InternalError + "Checker %s can report user-facing issue %s but is not of type UserFacing in \ + src/base/Checker.ml. Please fix!" + checker_config.id issue_type.unique_id ; + F.fprintf f "Reported as \"%s\" by [%s](%s.md).@\n@\n" issue_type.hum checker_config.id + (basename_of_checker checker_config) ; + match issue_type.user_documentation with + | None -> + () + | Some documentation -> + F.pp_print_string f documentation + + +let all_issues_header = + {|--- +title: List of all issue types +--- + +Here is an overview of the issue types currently reported by Infer. Currently outdated and being worked on! + +|} + + +(* TODO: instead of just taking issues that have documentation, enforce that (some, eg enabled + by default) issue types always have documentation *) +let all_issues = + lazy + ( IssueType.all_issues () + |> List.filter ~f:(fun {IssueType.user_documentation} -> Option.is_some user_documentation) + |> List.sort ~compare:(fun {IssueType.unique_id= id1} {IssueType.unique_id= id2} -> + String.compare id1 id2 ) ) + + +let all_issues_website ~website_root = + let issues_to_document = Lazy.force all_issues in + Utils.with_file_out (mk_markdown_docs_path ~website_root ~basename:all_issues_basename) + ~f:(fun out_channel -> + let f = F.formatter_of_out_channel out_channel in + F.fprintf f "%s@\n%a@\n%!" all_issues_header + (Pp.seq ~sep:"\n" markdown_one_issue) + issues_to_document ) + + let list_issue_types () = L.progress "@[Format:@\n\ @@ -66,4 +134,118 @@ let show_issue_types issue_types = L.result "@]%!" -let write_website ~website_root:_ = assert false +let mk_checkers_json checkers_base_filenames = + `Assoc + [ ( "README" + , `String + (Printf.sprintf + "This is a %cgenerated file, run `make doc-publish` from the root of the infer \ + repository to generate it" + (* avoid tooling thinking this source file itself is generated because of the string _at_generated appearing in it *) + '@') ) + ; ( "doc_entries" + , `List + ( `String all_issues_basename + :: List.map checkers_base_filenames ~f:(fun filename -> `String filename) ) ) ] + + +(** Writes an index of all the checkers documentation pages. Must correspond to all the pages + written in docs/! *) +let write_checkers_json ~path = + let json = + List.filter_map Checker.all ~f:(fun checker -> + let config = Checker.config checker in + if Option.is_some (get_checker_web_documentation config) then + Some (basename_of_checker config) + else None ) + |> mk_checkers_json + in + Utils.with_file_out path ~f:(fun out_channel -> + Yojson.pretty_to_channel ~std:true out_channel json ) + + +let pp_checker_webpage_header f ~title ~short_documentation = + F.fprintf f {|--- +title: "%s" +description: "%s" +--- + +%s + +|} (escape_double_quotes title) + (escape_double_quotes short_documentation) + short_documentation + + +let pp_checker_deprecation_message f message = + F.fprintf f "**\\*\\*\\*DEPRECATED\\*\\*\\*** %s@\n@\n" message + + +let pp_checker_cli_flags f checker_config = + F.fprintf f "Activate with `--%s`.@\n@\n" checker_config.Checker.id + + +let string_of_support (support : Checker.support) = + match support with NoSupport -> "No" | ExperimentalSupport -> "Experimental" | Support -> "Yes" + + +let pp_checker_language_support f support = + F.fprintf f "Supported languages:@\n" ; + List.iter Language.all ~f:(fun language -> + F.fprintf f "- %s: %s@\n" (Language.to_string language) (string_of_support (support language)) ) ; + F.pp_print_newline f () + + +let pp_checker_issue_types f checker = + F.fprintf f "@\n@\n## List of Issue Types@\n@\n" ; + F.fprintf f "The following issue types are reported by this checker:@\n" ; + let checker_issues = + List.filter (Lazy.force all_issues) ~f:(fun {IssueType.checker= issue_checker} -> + Checker.equal issue_checker checker ) + in + let pp_issue f {IssueType.unique_id} = + F.fprintf f "- [%s](%s.md#%s)@\n" unique_id all_issues_basename (String.lowercase unique_id) + in + List.iter checker_issues ~f:(pp_issue f) + + +let write_checker_webpage ~website_root (checker : Checker.t) = + let checker_config = Checker.config checker in + match get_checker_web_documentation checker_config with + | None -> + () + | Some (title, markdown_body, deprecated_opt) -> + Utils.with_file_out + (mk_markdown_docs_path ~website_root ~basename:(basename_of_checker checker_config)) + ~f:(fun out_channel -> + let f = F.formatter_of_out_channel out_channel in + pp_checker_webpage_header f ~title ~short_documentation:checker_config.short_documentation ; + Option.iter deprecated_opt ~f:(pp_checker_deprecation_message f) ; + Option.iter checker_config.cli_flags ~f:(fun _ -> pp_checker_cli_flags f checker_config) ; + pp_checker_language_support f checker_config.support ; + F.pp_print_string f markdown_body ; + pp_checker_issue_types f checker ; + () ) + + +(** delete all files that look like they were generated by a previous invocation of + [--write-website] to avoid keeping documentation for deleted checkers around *) +let delete_checkers_website ~website_root = + Utils.directory_iter + (fun path -> + if String.is_prefix ~prefix:basename_checker_prefix (Filename.basename path) then ( + L.progress "deleting '%s'@\n" path ; + Unix.unlink path ) ) + (website_root ^/ "docs") + + +let all_checkers_website ~website_root = + delete_checkers_website ~website_root ; + List.iter Checker.all ~f:(fun checker -> write_checker_webpage ~website_root checker) + + +let write_website ~website_root = + write_checkers_json ~path:(website_root ^/ "checkers.json") ; + all_checkers_website ~website_root ; + all_issues_website ~website_root ; + () diff --git a/website/checkers.json b/website/checkers.json new file mode 100644 index 000000000..c62eaa0b5 --- /dev/null +++ b/website/checkers.json @@ -0,0 +1,7 @@ +{ + "doc_entries": [ + "checkers-bug-types", + "eradicate-warnings", + "linters-bug-types" + ] +} diff --git a/website/sidebars.js b/website/sidebars.js index 72070e3b3..6f878c029 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +const checkers = require("./checkers"); + module.exports = { docs: { "Quick Start": ["getting-started", "hello-world"], @@ -26,11 +28,7 @@ module.exports = { "separation-logic-and-bi-abduction", "limitations", ], - "Bug Types Reference": [ - "checkers-bug-types", - "eradicate-warnings", - "linters-bug-types", - ], + "Analyses and Issue Types": checkers.doc_entries, Contribute: ["absint-framework", "adding-checkers", "internal-API"], Versions: ["versions"], },