You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
377 lines
12 KiB
377 lines
12 KiB
(*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*)
|
|
|
|
open! Core
|
|
module F = Format
|
|
|
|
type copyright_event = CopyrightMalformed | CopyrightModified
|
|
|
|
exception CopyrightEvent of copyright_event
|
|
|
|
let exit_code_of_event = function CopyrightModified -> 1 | CopyrightMalformed -> 3
|
|
|
|
type comment_style =
|
|
| Line of string * bool
|
|
(** line comments, eg "#" for shell, and whether there should be a
|
|
newline before the copyright notice *)
|
|
| Block of string * string * string * bool (** block comments, eg ("(*", "*", "*)") for ocaml *)
|
|
[@@deriving compare]
|
|
|
|
let equal_comment_style = [%compare.equal: comment_style]
|
|
|
|
let comment_style_al = Line ("//", false)
|
|
|
|
let comment_style_c = Block ("/*", "*", "*/", false)
|
|
|
|
let comment_style_llvm = Line (";", true)
|
|
|
|
let comment_style_m4 = Line ("dnl", false)
|
|
|
|
let comment_style_make = Line ("#", false)
|
|
|
|
let comment_style_ocaml = Block ("(*", "*", "*)", false)
|
|
|
|
let comment_style_php = Line ("//", true)
|
|
|
|
let comment_style_python = Line ("#", false)
|
|
|
|
let comment_style_shell = Line ("#", true)
|
|
|
|
let comment_styles_lang =
|
|
[ (comment_style_al, "AL")
|
|
; (comment_style_c, "C")
|
|
; (comment_style_llvm, "LLVM")
|
|
; (comment_style_m4, "M4")
|
|
; (comment_style_make, "Makefile")
|
|
; (comment_style_ocaml, "OCaml")
|
|
; (comment_style_php, "PHP")
|
|
; (comment_style_python, "python")
|
|
; (comment_style_shell, "shell") ]
|
|
|
|
|
|
let lang_of_comment_style style =
|
|
List.Assoc.find_exn ~equal:equal_comment_style comment_styles_lang style
|
|
|
|
|
|
let comment_styles = List.map comment_styles_lang ~f:fst
|
|
|
|
let starts_with_newline = function
|
|
| Line (_, starts_with_newline) | Block (_, _, _, starts_with_newline) ->
|
|
starts_with_newline
|
|
|
|
|
|
let default_start_line_of_com_style style = if starts_with_newline style then 1 else 0
|
|
|
|
let indent_of_comment_style = function
|
|
| Line _ ->
|
|
""
|
|
| Block (_, inter, _, _) ->
|
|
String.make (String.length inter) ' '
|
|
|
|
|
|
let keep_going = ref false
|
|
|
|
(** If true, update the copyright message of the files. *)
|
|
let update_files = ref false
|
|
|
|
let show_diff = ref false
|
|
|
|
let line_contains_copyright line = String.is_substring ~substring:"opyright " line
|
|
|
|
let find_copyright_line lines =
|
|
List.findi lines ~f:(fun _ line -> line_contains_copyright line) |> Option.map ~f:fst
|
|
|
|
|
|
let array_rev_find_mapi_from array ~from ~f =
|
|
let i = ref from in
|
|
let found = ref None in
|
|
while !i >= 0 && Option.is_none !found do
|
|
found := f !i array.(!i) ;
|
|
decr i
|
|
done ;
|
|
!found
|
|
|
|
|
|
let find_comment_start_and_style lines n =
|
|
(* are we in a line comment? *)
|
|
let cur_line_comment =
|
|
List.find comment_styles ~f:(function
|
|
| Line (s, starts_with_newline) when String.is_prefix ~prefix:s lines.(n) ->
|
|
if starts_with_newline then n <> 0 else true
|
|
| _ ->
|
|
false )
|
|
in
|
|
let is_start i line =
|
|
match cur_line_comment with
|
|
| Some (Line _) ->
|
|
cur_line_comment
|
|
| Some (Block _) | None ->
|
|
List.find comment_styles ~f:(function
|
|
| Block (s, _, _, starts_with_newline) when String.is_substring ~substring:s line ->
|
|
if starts_with_newline then i <> 0 else true
|
|
| _ ->
|
|
false )
|
|
in
|
|
let find_in_line i line = is_start i line |> Option.map ~f:(fun style -> (i, style)) in
|
|
array_rev_find_mapi_from lines ~from:n ~f:find_in_line
|
|
|
|
|
|
let find_comment_end lines n com_style =
|
|
let is_end line =
|
|
match com_style with
|
|
| Line (s, _) ->
|
|
(not (String.is_prefix ~prefix:s line), `After)
|
|
| Block (_, _, s, _) ->
|
|
(String.is_substring ~substring:s line, `OnIt)
|
|
in
|
|
let i = ref (n + 1) in
|
|
let len = Array.length lines in
|
|
let found = ref (len - 1) in
|
|
while !i < len && !found = len - 1 do
|
|
( match is_end lines.(!i) with
|
|
| true, `OnIt ->
|
|
found := !i
|
|
| true, `After ->
|
|
found := !i - 1
|
|
| false, _ ->
|
|
() ) ;
|
|
incr i
|
|
done ;
|
|
match com_style with Line _ -> !found | Block _ -> !found
|
|
|
|
|
|
(** Heuristic to check if this looks like a copyright message. *)
|
|
let looks_like_copyright_message cstart cend lines =
|
|
let max_len = 100 in
|
|
let check_len () =
|
|
let ok = ref true in
|
|
for i = cstart to cend do
|
|
if String.length lines.(i) > max_len then ok := false
|
|
done ;
|
|
!ok
|
|
in
|
|
cstart >= 0 && cend - cstart <= 10 && check_len ()
|
|
|
|
|
|
let contains_string ~substring cstart cend lines =
|
|
let found = ref false in
|
|
for i = cstart to cend do
|
|
if String.is_substring ~substring lines.(i) then found := true
|
|
done ;
|
|
!found
|
|
|
|
|
|
let contains_monoidics cstart cend lines = contains_string ~substring:"Monoidics" cstart cend lines
|
|
|
|
let contains_ropas cstart cend lines = contains_string ~substring:"ROPAS" cstart cend lines
|
|
|
|
let pp_copyright ~monoidics ~ropas com_style fmt =
|
|
let running_comment = match com_style with Line (s, _) | Block (_, s, _, _) -> s in
|
|
let indent = indent_of_comment_style com_style in
|
|
let pp_line str =
|
|
F.kfprintf (fun fmt -> F.fprintf fmt "@\n") fmt ("%s%s" ^^ str) indent running_comment
|
|
in
|
|
let pp_start () =
|
|
match com_style with Line _ -> () | Block (start, _, _, _) -> F.fprintf fmt "%s@\n" start
|
|
in
|
|
let pp_end () =
|
|
match com_style with
|
|
| Line _ ->
|
|
()
|
|
| Block (_, _, finish, _) ->
|
|
F.fprintf fmt "%s%s@\n" indent finish
|
|
in
|
|
pp_start () ;
|
|
if ropas then (
|
|
pp_line " Copyright (c) 2016-present, Programming Research Laboratory (ROPAS)" ;
|
|
pp_line " Seoul National University, Korea" )
|
|
else if monoidics then pp_line " Copyright (c) 2009-2013, Monoidics ltd." ;
|
|
pp_line " Copyright (c) Facebook, Inc. and its affiliates." ;
|
|
pp_line "" ;
|
|
pp_line " This source code is licensed under the MIT license found in the" ;
|
|
pp_line " LICENSE file in the root directory of this source tree." ;
|
|
pp_end ()
|
|
|
|
|
|
let copyright_has_changed fname lines ~notice_range:(cstart, cend) ~monoidics ~ropas com_style =
|
|
let old_copyright =
|
|
let r = ref "" in
|
|
for i = cstart to cend do
|
|
r := !r ^ lines.(i) ^ "\n"
|
|
done ;
|
|
!r
|
|
in
|
|
let new_copyright = Format.asprintf "%t" (pp_copyright ~monoidics ~ropas com_style) in
|
|
let changed = not (String.equal old_copyright new_copyright) in
|
|
if !show_diff && changed then (
|
|
let with_suffix fname suff = Filename.basename fname ^ suff in
|
|
let orig_fname = with_suffix fname ".orig" in
|
|
let new_fname = with_suffix fname ".new" in
|
|
Out_channel.write_lines orig_fname [old_copyright] ;
|
|
Out_channel.write_lines new_fname [new_copyright] ;
|
|
() ) ;
|
|
changed
|
|
|
|
|
|
let com_style_of_lang =
|
|
[ (".ac", comment_style_m4)
|
|
; (".al", comment_style_al)
|
|
; (".atd", comment_style_ocaml)
|
|
; (".c", comment_style_c)
|
|
; (".cpp", comment_style_c)
|
|
; (".h", comment_style_c)
|
|
; (".inc", comment_style_c)
|
|
; (".java", comment_style_c)
|
|
; (".ll", comment_style_llvm)
|
|
; (".m", comment_style_c)
|
|
; (".m4", comment_style_m4)
|
|
; (".make", comment_style_make)
|
|
; (".mk", comment_style_make)
|
|
; (".ml", comment_style_ocaml)
|
|
; (".mli", comment_style_ocaml)
|
|
; (".mll", comment_style_ocaml)
|
|
; (".mly", comment_style_c)
|
|
; (".mm", comment_style_c)
|
|
; (".php", comment_style_php)
|
|
; (".py", comment_style_python)
|
|
; (".re", comment_style_c)
|
|
; (".rei", comment_style_c)
|
|
; (".sh", comment_style_shell)
|
|
; ("dune.in", comment_style_ocaml)
|
|
; ("dune.common.in", comment_style_ocaml)
|
|
; ("dune-common.in", comment_style_ocaml)
|
|
; ("dune-workspace.in", comment_style_llvm)
|
|
; ("Makefile", comment_style_make) ]
|
|
|
|
|
|
let comment_style_of_filename fname =
|
|
List.Assoc.find com_style_of_lang ~equal:Filename.check_suffix fname
|
|
|
|
|
|
let output_diff ~fname lines ?notice_range ?(monoidics = false) ?(ropas = false) com_style =
|
|
let lang = lang_of_comment_style com_style in
|
|
let pp_range_opt fmt = function
|
|
| None ->
|
|
F.pp_print_string fmt "none"
|
|
| Some (s, e) ->
|
|
F.fprintf fmt "%d-%d" s e
|
|
in
|
|
F.eprintf "%s (lang:%s notice:%a monoidics:%b ropas:%b)\n%!" fname lang pp_range_opt notice_range
|
|
monoidics ropas ;
|
|
let pp_newfile fmt =
|
|
let copy_lines_before, copy_lines_after =
|
|
match notice_range with
|
|
| Some (s, e) ->
|
|
(s - 1, e + 1)
|
|
| None ->
|
|
let insert_notice_at = default_start_line_of_com_style com_style in
|
|
(insert_notice_at - 1, insert_notice_at)
|
|
in
|
|
for i = 0 to copy_lines_before do
|
|
F.fprintf fmt "%s\n" lines.(i)
|
|
done ;
|
|
if
|
|
starts_with_newline com_style && copy_lines_before > 0 && lines.(copy_lines_before - 1) <> ""
|
|
then F.fprintf fmt "@\n" ;
|
|
pp_copyright ~monoidics ~ropas com_style fmt ;
|
|
for i = copy_lines_after to Array.length lines - 1 do
|
|
F.fprintf fmt "%s\n" lines.(i)
|
|
done ;
|
|
F.fprintf fmt "%!"
|
|
in
|
|
if !update_files then
|
|
Out_channel.with_file fname ~f:(fun cout ->
|
|
let fmt = F.formatter_of_out_channel cout in
|
|
pp_newfile fmt )
|
|
else pp_newfile F.std_formatter
|
|
|
|
|
|
let check_copyright fname =
|
|
let lines_list = In_channel.read_lines fname in
|
|
let lines = Array.of_list lines_list in
|
|
match (find_copyright_line lines_list, comment_style_of_filename fname) with
|
|
| None, None ->
|
|
()
|
|
| None, Some com_style ->
|
|
output_diff ~fname lines com_style ;
|
|
raise (CopyrightEvent CopyrightModified)
|
|
| Some n, fname_com_style ->
|
|
let cstart, contents_com_style =
|
|
find_comment_start_and_style lines n |> Option.value ~default:(0, Line ("#", false))
|
|
in
|
|
let com_style =
|
|
match fname_com_style with
|
|
| None ->
|
|
contents_com_style
|
|
| Some fname_com_style ->
|
|
let inferred_styles_agree =
|
|
match (fname_com_style, contents_com_style) with
|
|
| Line (fs, _), Line (cs, _) ->
|
|
String.equal fs cs
|
|
| Block (fstart, fbody, fend, _), Block (cstart, cbody, cend, _) ->
|
|
String.equal fstart cstart && String.equal fbody cbody && String.equal fend cend
|
|
| _ ->
|
|
false
|
|
in
|
|
if not inferred_styles_agree then (
|
|
F.eprintf
|
|
"Inferred comment style doesn't match the filename '%s':@\n\
|
|
From the filename I was expecting the %s comment style, but looking inside the \
|
|
file I found %s instead.@."
|
|
fname
|
|
(lang_of_comment_style fname_com_style)
|
|
(lang_of_comment_style contents_com_style) ;
|
|
raise (CopyrightEvent CopyrightMalformed) ) ;
|
|
fname_com_style
|
|
in
|
|
(* hack to detect shebangs regardless of the inferred style *)
|
|
let com_style =
|
|
match com_style with
|
|
| Line ("#", false) when String.is_prefix ~prefix:"#!" lines.(0) ->
|
|
comment_style_shell
|
|
| _ ->
|
|
com_style
|
|
in
|
|
let cend = find_comment_end lines n com_style in
|
|
if not (looks_like_copyright_message cstart cend lines) then (
|
|
F.eprintf "Copyright not recognized: %s@." fname ;
|
|
raise (CopyrightEvent CopyrightMalformed) ) ;
|
|
let monoidics = contains_monoidics cstart cend lines in
|
|
let ropas = contains_ropas cstart cend lines in
|
|
if copyright_has_changed fname lines ~notice_range:(cstart, cend) ~monoidics ~ropas com_style
|
|
then (
|
|
output_diff ~fname lines ~notice_range:(cstart, cend) ~monoidics ~ropas com_style ;
|
|
raise (CopyrightEvent CopyrightModified) )
|
|
|
|
|
|
let speclist =
|
|
[ ("-i", Arg.Set update_files, "Update copyright notice in-place")
|
|
; ("--inplace", Arg.Set update_files, "Update copyright notice in-place")
|
|
; ("-k", Arg.Set keep_going, "Exit with code 0 no matter what")
|
|
; ("--keep-going", Arg.Set keep_going, "Exit with code 0 no matter what")
|
|
; ( "--show-diff"
|
|
, Arg.Set show_diff
|
|
, "Write file.orig and file.new files to inspect the differences found" ) ]
|
|
|
|
|
|
let usage_msg = "checkCopyright [-i] [-k] [--show-diff] file1 ..."
|
|
|
|
let () =
|
|
let to_check = ref [] in
|
|
let add_file_to_check fname =
|
|
(* hack: LICENSE looks copyrightable but is not... *)
|
|
if not (String.is_prefix ~prefix:"LICENSE" (Filename.basename fname)) then
|
|
to_check := fname :: !to_check
|
|
in
|
|
Arg.parse (Arg.align speclist) add_file_to_check usage_msg ;
|
|
let to_check = List.rev !to_check in
|
|
let exit_code = ref 0 in
|
|
List.iter to_check ~f:(fun file ->
|
|
try check_copyright file
|
|
with CopyrightEvent event -> if not !keep_going then exit_code := exit_code_of_event event ) ;
|
|
exit !exit_code
|