diff --git a/nix/frama-c-lint.nix b/nix/frama-c-lint.nix index d33b25870079d5d24d2ba1a685ac8b1b3a203d82..dfb9e89512a7e6ac4d8908c4709f27593cf355da 100644 --- a/nix/frama-c-lint.nix +++ b/nix/frama-c-lint.nix @@ -6,6 +6,8 @@ , gitignoreSource , ocaml , ocp-indent +, ppx_deriving_yojson +, yojson } : stdenv.mkDerivation rec { @@ -24,6 +26,8 @@ stdenv.mkDerivation rec { findlib ocaml ocp-indent + ppx_deriving_yojson + yojson ]; configurePhase = '' diff --git a/tools/lint/README.md b/tools/lint/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b5ab4cfe03061fe8370eb9f4cc9a59bd2f25c2b9 --- /dev/null +++ b/tools/lint/README.md @@ -0,0 +1,83 @@ +## frama-c-lint tool + +## Usage + + +``` +> frama-c-lint -help +Usage: git ls-files -z | git check-attr --stdin -z -a | _build/install/default/bin/frama-c-lint [options] + +Checks or updates files in relation to lint constraints specified by these git attributes: + check-eoleof, check-syntax, check-utf8 and check-indent. + +Options: + --version Prints tool version + -c <json-config-file> Reads the JSON configuration file (allows to overload the default configuration) + -e Prints default JSON configuration + -s Considers warnings as errors for the exit value + -u Update ill-formed files (does not handle UTF8 update) + -v Verbose mode + -help Display this list of options + --help Display this list of options +``` + +## Managed Git Attributes + +The tool manage the following `git` attributes: +`check-eoleof`, `check-syntax`, `check-utf8`, `check-indent`. + +All of them can be `set` or `unset`, but a tool name can also be assignable to the `check-indent` attribute. + +The check/update commands related to `check-eoleof`, `check-syntax` and `check-utf8` attributes are not overloadable and cannot be parametrized. +Only the commands related to `check-indent` attribute can be parametrized. + +## The Parametrizable Configuration + +The command `frama-c-lint -e` pretty prints the JSON description equivalent to the default parametrizable configuration related to `check-indent` attribute. + +``` +> frama-c-lint -e +Default JSON configuration: +[ + { + "kind": "C", + "extensions": [ ".c", ".h" ], + "name": "clang-format", + "available_cmd": "clang-format --version > /dev/null 2> /dev/null", + "check_cmd": "clang-format --dry-run -Werror", + "update_cmd": "clang-format -i" + }, + { + "kind": "Python", + "extensions": [ ".py" ], + "name": "black", + "available_cmd": "black --version > /dev/null 2> /dev/null", + "check_cmd": "black --quiet --line-length 100 --check", + "update_cmd": "black --quiet --line-length 100" + } +] +``` + +This description defines `black` and `clang-format` as the assignable values of the `git` attribute `check-indent`. It also specifies the system command to use for checking the availability of the tool to use for checking/updating the indentation. The check and update commands are also specified. + +When the `check-indent` attribute is set without a value, the description specifies the tool to use from the extension of the file to check/update. + +There is also a built-in configuration for `.ml` and `.mli` files based of `ocp-indent` tool (from a directly use of `ocp-indent` library to improve the efficiency of the tool) that can be overloaded if necessary. +That means there is an implicit overloadable JSON description: +``` +[ + { + "kind": "OCaml", + "extensions": [ ".ml", ".mli" ], + "name": "ocp-indent", + "available_cmd": "...", + "check_cmd": "...", + "update_cmd": "..." + } +] +``` + +The option `-c <json-confi g-file>` allows to extend and/or overload the default configuration. + +When the `available_cmd` field is set to an empty string, that disable the check/update with the related tool. +An empty string can also be set to the field `check_cmd` (resp. `update_cmd`) when the related tool does not offer check (resp. update) command. diff --git a/tools/lint/dune b/tools/lint/dune index 1d2b1c4f05f25e66ac27035c716f10657484b5a2..cff48d122fdfd851d113eb40f14a7f5b1bc38f09 100644 --- a/tools/lint/dune +++ b/tools/lint/dune @@ -24,5 +24,6 @@ (public_name frama-c-lint) (name lint) (modules lint UTF8) - (libraries unix ocp-indent.lexer ocp-indent.lib ocp-indent.dynlink) + (preprocess (pps ppx_deriving_yojson)) + (libraries unix yojson ocp-indent.lexer ocp-indent.lib ocp-indent.dynlink) ) diff --git a/tools/lint/lint.ml b/tools/lint/lint.ml index 3b6a33d13dd0ccd080933768d44ce8712d05794f..e08be5eb7821545c3530ad9ad365b7a1300c489f 100644 --- a/tools/lint/lint.ml +++ b/tools/lint/lint.ml @@ -20,6 +20,38 @@ (* *) (**************************************************************************) +type tool_cmds = + { kind: string ; + extensions: string list ; + name: string ; + available_cmd: string ; (* leaves it empty to set it as unavailable *) + check_cmd: string ; (* leaves it empty if there is no check command *) + update_cmd: string (* leaves it empty if there is no updating command *) + } +[@@deriving yojson] + +(**************************************************************************) +(** The only part to modify for adding a new external formatters *) + +(** Supported indent formatters *) +let external_formatters = [ + { kind = "C"; + extensions = [ ".c" ; ".h" ]; + name = "clang-format"; + available_cmd = "clang-format --version > /dev/null 2> /dev/null"; + check_cmd = "clang-format --dry-run -Werror" ; + update_cmd = "clang-format -i" + } + ; + { kind = "Python"; + extensions = [ ".py" ]; + name = "black"; + available_cmd = "black --version > /dev/null 2> /dev/null"; + check_cmd = "black --quiet --line-length 100 --check" ; + update_cmd = "black --quiet --line-length 100" + } +] + (**************************************************************************) (* Warning/Error *) @@ -65,36 +97,46 @@ let lines_from_in channel = List.rev acc (**************************************************************************) -(* Supported indent formatter *) - -type formatter_cmds = +type available_tools = { mutable is_available: bool option ; - kind: string ; - name: string ; - available_cmd: string ; - check_cmd: string ; - update_cmd: string (* leaves it empty if there is no updating command *) + tool_cmds: tool_cmds } -let c_indent_formatter = - { is_available = None ; - kind = "C"; - name = "clang-format"; - available_cmd = "clang-format --version > /dev/null 2> /dev/null"; - check_cmd = "clang-format --dry-run -Werror" ; - update_cmd = "clang-format -i" - } +type indent_formatter = Ocp_indent | Tool of available_tools -let python_indent_formatter = - { is_available = None ; - kind = "Python"; - name = "black"; - available_cmd = "black --version > /dev/null 2> /dev/null"; - check_cmd = "black --quiet --line-length 100 --check" ; - update_cmd = "black --quiet --line-length 100" - } +(* from formatter name *) +let external_tbl = Hashtbl.create 13 + +(* from file extension *) +let default_tbl = Hashtbl.create 13 -type indent_formatter = Ocp_indent | Tool of formatter_cmds +let updates_tbl external_tools = + List.iter (fun formatter -> + let tool = Tool { is_available = None; tool_cmds = formatter } in + List.iter (fun extension -> + Hashtbl.replace default_tbl extension tool) + formatter.extensions; + Hashtbl.add external_tbl formatter.name tool) + external_tools + +let () = updates_tbl external_formatters + +type tools = tool_cmds list +[@@deriving yojson] + +let parse_config config_file = + if config_file <> "" then + let config_tools = + try + tools_of_yojson (Yojson.Safe.from_file config_file) + with Yojson.Json_error txt -> + Error txt + in match config_tools with + | Result.Ok external_tools -> updates_tbl external_tools + | Result.Error txt -> + warn "Parse error:%s:%s@." config_file txt + +(************************) let ml_indent_formatter = Ocp_indent @@ -103,12 +145,16 @@ type indent_check = NoCheck | Check of indent_formatter option let parse_indent_formatter ~file ~attr ~value = match value with | "unset" -> NoCheck | "set" -> Check None (* use the default formatter *) - | "ocp-indent" -> Check (Some ml_indent_formatter) - | "clang-format" -> Check (Some (Tool c_indent_formatter)) - | "black" -> Check (Some (Tool python_indent_formatter)) - | _ -> warn "Unsupported indent formatter: %s %s=%s@." - file attr value; - NoCheck + | _ -> + match Hashtbl.find_opt external_tbl value with + | None -> + if value = "ocp-indent" then + (* "ocp-indent" is not overloaded: using the built-in configuration *) + Check (Some ml_indent_formatter) + else (warn "Unsupported indent formatter: %s %s=%s@." + file attr value; + NoCheck) + | res -> Check res (**************************************************************************) (* Available Checks and corresponding attributes *) @@ -131,7 +177,7 @@ let add_attr ~file ~attr ~value checks = let is_set = function | "set" -> true | "unset" -> false - | _ -> warn "Invalid attribute value: %s %s=%s" file attr value ; false + | _ -> warn "Invalid attribute value: %s %s=%s@." file attr value ; false in match attr with | "check-eoleof" -> { checks with eoleof = is_set value } @@ -139,7 +185,7 @@ let add_attr ~file ~attr ~value checks = | "check-utf8" -> { checks with utf8 = is_set value } | "check-indent" -> { checks with indent = parse_indent_formatter ~file ~attr ~value } - | _ -> warn "Unknown attribute: %s %s=%s" file attr value; + | _ -> warn "Unknown attribute: %s %s=%s@." file attr value; checks let handled_attr s = @@ -284,11 +330,13 @@ let check_ml_indent ~update file = let is_formatter_available ~file indent_formatter = match indent_formatter.is_available with | None -> - let is_available = (0 = Sys.command indent_formatter.available_cmd) in + let is_available = + let cmd = indent_formatter.tool_cmds.available_cmd in + (cmd <> "") && (0 = Sys.command cmd) in indent_formatter.is_available <- Some is_available ; if not is_available then warn "%s is unavailable for checking indentation of some %s files (i.e. %s)@." - indent_formatter.name indent_formatter.kind file; + indent_formatter.tool_cmds.name indent_formatter.tool_cmds.kind file; is_available | Some is_available -> is_available @@ -298,20 +346,23 @@ let check_indent ~indent_formatter ~update file = let tool = match indent_formatter with | Some tool -> tool | None -> (* uses the default formatter *) - match Filename.extension file with - | ".c" | ".h" -> Tool c_indent_formatter - | ".ml" | ".mli" -> ml_indent_formatter - | ".py" -> Tool python_indent_formatter - | _ -> raise Bad_ext + let extension = Filename.extension file in + match Hashtbl.find_opt default_tbl extension with + | Some tool -> tool + | None -> match extension with + | ".ml" | ".mli" -> ml_indent_formatter + | _ -> raise Bad_ext in match tool with | Ocp_indent -> check_ml_indent ~update file | Tool indent_formatter -> + let do_cmd cmd = + (cmd = "") || (0 = Sys.command (Format.sprintf "%s \"%s\"" cmd file)) + in if not @@ is_formatter_available ~file indent_formatter then true else if not update then - 0 = Sys.command (Format.sprintf "%s \"%s\"" indent_formatter.check_cmd file) - else if indent_formatter.update_cmd <> "" then - 0 = Sys.command (Format.sprintf "%s \"%s\"" indent_formatter.update_cmd file) - else true (* there no updating command *) + do_cmd indent_formatter.tool_cmds.check_cmd + else + do_cmd indent_formatter.tool_cmds.update_cmd (* Main checks *) @@ -371,13 +422,25 @@ let check ~verbose ~update file params = (* Options *) let exec_name = Sys.argv.(0) + +let version= "1.0" + +let version () = + Format.printf "%s version %s@." (Filename.basename exec_name) version; + exit 0 + let update = ref false let verbose = ref false +let config_file = ref "" +let extract_config = ref false let argspec = [ - "-u", Arg.Set update, " update ill-formed files (does not handle UTF8 update)" ; - "-v", Arg.Set verbose, " verbose mode" ; - "-s", Arg.Set strict, " considers warnings as errors for the exit value" ; + "-u", Arg.Set update, " Update ill-formed files (does not handle UTF8 update)" ; + "-v", Arg.Set verbose, " Verbose mode" ; + "-s", Arg.Set strict, " Considers warnings as errors for the exit value" ; + "-c", Arg.String (fun s -> config_file := s), "<json-config-file> Reads the JSON configuration file (allows to overload the default configuration)" ; + "-e", Arg.Set extract_config, " Prints default JSON configuration" ; + "--version", Arg.Unit version, " Prints tool version" ; ] let sort argspec = List.sort (fun (name1, _, _) (name2, _, _) -> String.compare name1 name2) @@ -390,7 +453,17 @@ let () = Arg.parse (Arg.align (sort argspec)) (fun s -> warn "Unknown argument: %s@." s) - ("Usage: git ls-files -z | git check-attr --stdin -z -a | " ^ exec_name ^ " [options]"); - collect @@ lines_from_in stdin ; - Hashtbl.iter (check ~verbose:!verbose ~update:!update) table ; - if not !res then exit 1 + ("Usage: git ls-files -z | git check-attr --stdin -z -a | " ^ exec_name ^ " [options]\n" + ^"\nChecks or updates files in relation to lint constraints specified by these git attributes:\n" + ^" check-eoleof, check-syntax, check-utf8 and check-indent.\n" + ^"\nOptions:"); + if !extract_config then + Format.printf "Default JSON configuration:@.%a@." + (Yojson.Safe.pretty_print ~std:false) (tools_to_yojson external_formatters) + else begin + updates_tbl external_formatters ; + parse_config !config_file; + collect @@ lines_from_in stdin ; + Hashtbl.iter (check ~verbose:!verbose ~update:!update) table ; + if not !res then exit 1 + end