├── .gitignore ├── swift.ml ├── parser.mly ├── README.md ├── lexer.mll ├── utils.ml ├── graph.ml └── main.ml /.gitignore: -------------------------------------------------------------------------------- 1 | Pods 2 | -------------------------------------------------------------------------------- /swift.ml: -------------------------------------------------------------------------------- 1 | type swift_token = 2 | | Type of string 3 | | Word of string 4 | | Protocol 5 | | Class 6 | | Extension 7 | | Enum 8 | | Struct 9 | | Colon 10 | | Comma 11 | | Dot 12 | | OpenCurlyBrace 13 | | CloseCurlyBrace 14 | 15 | let string_of_swift_token token = 16 | match token with 17 | | Type name -> 18 | "TYPE(" ^ name ^ ")" 19 | | Word name -> 20 | "WORD(" ^ name ^ ")" 21 | | Protocol -> 22 | "PROTOCOL" 23 | | Class -> 24 | "CLASS" 25 | | Extension -> 26 | "EXTENSION" 27 | | Enum -> 28 | "ENUM" 29 | | Struct -> 30 | "STRUCT" 31 | | Colon -> 32 | "COLON" 33 | | Comma -> 34 | "COMMA" 35 | | Dot -> 36 | "DOT" 37 | | OpenCurlyBrace -> 38 | "OPEN_CURLY_BRACE\n" 39 | | CloseCurlyBrace -> 40 | "CLOSE_CURLY_BRACE\n" 41 | -------------------------------------------------------------------------------- /parser.mly: -------------------------------------------------------------------------------- 1 | %token TYPE 2 | %token WORD 3 | %token PROTOCOL 4 | %token CLASS 5 | %token EXTENSION 6 | %token ENUM 7 | %token STRUCT 8 | %token COLON 9 | %token COMMA 10 | %token DOT 11 | %token OPEN_CURLY_BRACE 12 | %token CLOSE_CURLY_BRACE 13 | %token EOF 14 | 15 | %{ 16 | open Lexer 17 | open Swift 18 | %} 19 | 20 | %start main 21 | 22 | %% 23 | 24 | main: 25 | | expressions=list(expr) EOF { expressions } 26 | expr: 27 | | t=TYPE { Type(t) } 28 | | w=WORD { Word(w) } 29 | | PROTOCOL { Protocol } 30 | | CLASS { Class } 31 | | EXTENSION { Extension } 32 | | ENUM { Enum } 33 | | STRUCT { Struct } 34 | | COLON { Colon } 35 | | COMMA { Comma } 36 | | DOT { Dot } 37 | | OPEN_CURLY_BRACE { OpenCurlyBrace } 38 | | CLOSE_CURLY_BRACE { CloseCurlyBrace } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DependenciesGraph 2 | Generates dependency graph for a folder containing .swift files. 3 | 4 | # Libraries used 5 | [Menhir](http://gallium.inria.fr/~fpottier/menhir/) 6 | [vis.js](http://visjs.org) 7 | 8 | # How to use 9 | Download the [release](https://github.com/kovtun1/DependenciesGraph/releases) 10 | `cd ` 11 | `./main.native ` 12 | Open index.html 13 | Select graph (Inheritance or Usage) 14 | 15 | You can specify types that shouldn't be added to the graph in `types_to_ignore.txt`. One type per line. 16 | 17 | # How to compile 18 | `` eval `opam config env` `` 19 | `ocamlbuild -package unix -package str -use-menhir -menhir "menhir --external-tokens Lexer" main.native` 20 | 21 | # Examples 22 | [Alamofire](https://github.com/Alamofire/Alamofire) 23 | ![](https://i.imgur.com/a87SPe8.png) 24 | 25 | [Nuke](https://github.com/kean/Nuke) 26 | ![](https://i.imgur.com/9QEf26G.png) 27 | -------------------------------------------------------------------------------- /lexer.mll: -------------------------------------------------------------------------------- 1 | { 2 | open Lexing 3 | 4 | type token = 5 | | TYPE of string 6 | | WORD of string 7 | | PROTOCOL 8 | | CLASS 9 | | EXTENSION 10 | | ENUM 11 | | STRUCT 12 | | COLON 13 | | COMMA 14 | | DOT 15 | | OPEN_CURLY_BRACE 16 | | CLOSE_CURLY_BRACE 17 | | EOF 18 | } 19 | 20 | let word = ['a'-'z' 'A'-'Z'] ['a'-'z' 'A'-'Z' '0'-'9' '_']* 21 | 22 | rule lex = parse 23 | | [' ' '\t' '\n'] { lex lexbuf } 24 | | "protocol" { PROTOCOL } 25 | | "class" { CLASS } 26 | | "extension" { EXTENSION } 27 | | "enum" { ENUM } 28 | | "struct" { STRUCT } 29 | | ":" { COLON } 30 | | "," { COMMA } 31 | | "." { DOT } 32 | | "{" { OPEN_CURLY_BRACE } 33 | | "}" { CLOSE_CURLY_BRACE } 34 | | "//" { skip_line lexbuf 35 | ; lex lexbuf 36 | } 37 | | "/*" { skip_block_comment 0 lexbuf 38 | ; lex lexbuf 39 | } 40 | | "\"" { skip_string false lexbuf 41 | ; lex lexbuf 42 | } 43 | | "\".*\"" { lex lexbuf } 44 | | "import" { skip_line lexbuf 45 | ; lex lexbuf 46 | } 47 | | eof { EOF } 48 | | word as s { if String.capitalize_ascii s = s then 49 | TYPE(s) 50 | else 51 | WORD(s) } 52 | | _ { lex lexbuf } 53 | and skip_line = parse 54 | | ('\n' | eof) { () } 55 | | _ { skip_line lexbuf } 56 | and skip_block_comment level = parse 57 | | "*/" { if level = 0 then 58 | () 59 | else 60 | skip_block_comment (level - 1) lexbuf } 61 | | "/*" { skip_block_comment (level + 1) lexbuf } 62 | | eof { () } 63 | | _ { skip_block_comment level lexbuf } 64 | and skip_string prev_char_is_backslash = parse 65 | | ("\"" | eof) { if prev_char_is_backslash then 66 | skip_string false lexbuf 67 | else 68 | () 69 | } 70 | | "\\" { skip_string true lexbuf } 71 | | "\\\\" { skip_string false lexbuf } 72 | | _ { skip_string false lexbuf } 73 | -------------------------------------------------------------------------------- /utils.ml: -------------------------------------------------------------------------------- 1 | let rec sort list = 2 | match list with 3 | | [] -> 4 | [] 5 | | element :: tl -> 6 | insert element (sort tl) 7 | and insert element_to_insert list = 8 | match list with 9 | | [] -> 10 | [element_to_insert] 11 | | element :: tl -> 12 | if element_to_insert < element then 13 | element_to_insert :: element :: tl 14 | else 15 | element :: insert element_to_insert tl 16 | 17 | let count element list = 18 | let rec count_aux list n = 19 | match list with 20 | | hd :: tl -> 21 | if hd = element then 22 | count_aux tl (n + 1) 23 | else 24 | count_aux tl n 25 | | [] -> 26 | n in 27 | count_aux list 0 28 | 29 | let remove_duplicates list = 30 | let rec remove_duplicates_aux list new_list = 31 | match list with 32 | | hd :: tl -> 33 | if count hd new_list = 0 then 34 | remove_duplicates_aux tl (hd :: new_list) 35 | else 36 | remove_duplicates_aux tl new_list 37 | | [] -> 38 | List.rev new_list in 39 | remove_duplicates_aux list [] 40 | 41 | let find_unique src_list dst_list = 42 | let rec find_unique_aux src_list unique_src_sublist = 43 | match src_list with 44 | | hd :: tl -> 45 | if List.mem hd dst_list then 46 | find_unique_aux tl unique_src_sublist 47 | else 48 | find_unique_aux tl (hd :: unique_src_sublist) 49 | | [] -> 50 | List.rev unique_src_sublist in 51 | find_unique_aux src_list [] 52 | 53 | let unwrap_optionals in_list = 54 | let rec unwrap_optionals_aux in_list out_list = 55 | match in_list with 56 | | hd :: tl -> 57 | begin 58 | match hd with 59 | | Some x -> 60 | unwrap_optionals_aux tl (x :: out_list) 61 | | None -> 62 | unwrap_optionals_aux tl out_list 63 | end 64 | | [] -> 65 | List.rev out_list 66 | in 67 | unwrap_optionals_aux in_list [] 68 | 69 | let remove_duplicates list = 70 | let rec remove_duplicates_aux in_list out_list = 71 | match in_list with 72 | | element :: tl -> 73 | if List.mem element out_list then 74 | remove_duplicates_aux tl out_list 75 | else 76 | remove_duplicates_aux tl (element :: out_list) 77 | | [] -> 78 | List.rev out_list 79 | in 80 | remove_duplicates_aux list [] 81 | -------------------------------------------------------------------------------- /graph.ml: -------------------------------------------------------------------------------- 1 | open Utils 2 | 3 | type node = 4 | { id : int 5 | ; label : string 6 | } 7 | 8 | type edge = 9 | { node_from_id : int 10 | ; node_to_id : int 11 | } 12 | 13 | type graph = 14 | { nodes: node list 15 | ; edges: edge list 16 | } 17 | 18 | type subgraph = 19 | { node_ids: int list 20 | ; edge_ids: string list 21 | } 22 | 23 | let get_edge_id edge = 24 | Printf.sprintf "%d-%d" edge.node_from_id edge.node_to_id 25 | 26 | let get_child_node_ids node_ids graph = 27 | List.map 28 | ( 29 | fun node_id -> 30 | let node_edges = 31 | List.filter 32 | ( 33 | fun edge -> 34 | edge.node_from_id = node_id && edge.node_to_id != node_id 35 | ) 36 | graph.edges in 37 | List.map (fun edge -> edge.node_to_id) node_edges 38 | ) 39 | node_ids 40 | |> List.flatten |> remove_duplicates 41 | 42 | let rec find_node_by_id node_id nodes = 43 | match nodes with 44 | | node :: tl -> 45 | if node.id = node_id then 46 | Some node 47 | else 48 | find_node_by_id node_id tl 49 | | [] -> 50 | None 51 | 52 | let find_edges node_from_id edges = 53 | let rec find_edges_aux edges found_edges = 54 | match edges with 55 | | edge :: tl -> 56 | if edge.node_from_id = node_from_id then 57 | find_edges_aux tl (edge :: found_edges) 58 | else 59 | find_edges_aux tl found_edges 60 | | [] -> 61 | List.rev found_edges 62 | in 63 | find_edges_aux edges [] 64 | 65 | let find_connected_nodes root_node_id graph = 66 | let rec find_connected_node_ids node_ids connected_node_ids = 67 | let child_node_ids = get_child_node_ids node_ids graph in 68 | let unique_node_ids = find_unique child_node_ids connected_node_ids in 69 | if List.length unique_node_ids > 0 then 70 | find_connected_node_ids 71 | unique_node_ids 72 | (connected_node_ids @ unique_node_ids) 73 | else 74 | connected_node_ids 75 | in 76 | let connected_node_ids = 77 | find_connected_node_ids [root_node_id] [root_node_id] in 78 | List.map 79 | (fun node_id -> find_node_by_id node_id graph.nodes) 80 | connected_node_ids 81 | |> unwrap_optionals 82 | 83 | (* let find_edges node_ids graph = 84 | let rec find_edges_aux node_ids edges = 85 | List.map (fun node_id -> ) 86 | in 87 | find_edges_aux node_ids [] *) 88 | 89 | let rec find_node_by_label label nodes = 90 | match nodes with 91 | | node :: tl -> 92 | if node.label = label then 93 | Some node 94 | else 95 | find_node_by_label label tl 96 | | [] -> 97 | None 98 | 99 | let find_nodes_by_labels labels nodes = 100 | let rec find_nodes_aux labels found_nodes = 101 | match labels with 102 | | label :: tl -> 103 | begin 104 | match find_node_by_label label nodes with 105 | | Some found_node -> 106 | find_nodes_aux tl (found_node :: found_nodes) 107 | | None -> 108 | find_nodes_aux tl found_nodes 109 | end 110 | | [] -> 111 | List.rev found_nodes 112 | in 113 | find_nodes_aux labels [] -------------------------------------------------------------------------------- /main.ml: -------------------------------------------------------------------------------- 1 | open Swift 2 | open Str 3 | open Unix 4 | open Graph 5 | open Utils 6 | 7 | type swift_type = 8 | { name : string 9 | ; inherited_types : string list 10 | ; types_in_body : string list 11 | } 12 | 13 | let tokenize file_path = 14 | let input = open_in file_path in 15 | let tokens = Parser.main Lexer.lex (Lexing.from_channel input) in 16 | close_in input; 17 | tokens 18 | 19 | let read_file file_path = 20 | let rec read_file_aux channel lines = 21 | try 22 | let line = input_line channel in 23 | read_file_aux channel (line :: lines) 24 | with 25 | | End_of_file -> 26 | close_in channel; 27 | List.rev lines in 28 | let channel = open_in file_path in 29 | read_file_aux channel [] 30 | 31 | let rec find_type_declaration tokens = 32 | match tokens with 33 | | Protocol :: Type name :: tl 34 | | Class :: Type name :: tl 35 | | Extension :: Type name :: tl 36 | | Enum :: Type name :: tl 37 | | Struct :: Type name :: tl -> 38 | Some (name, tl) 39 | | _ :: tl -> 40 | find_type_declaration tl 41 | | [] -> 42 | None 43 | 44 | let find_inherited_types tokens = 45 | let rec find_inherited_types_aux tokens inherited_types = 46 | match tokens with 47 | | OpenCurlyBrace :: tl -> 48 | (inherited_types, tokens) 49 | | token :: tl -> 50 | begin 51 | match token with 52 | | Type name -> 53 | find_inherited_types_aux tl (name :: inherited_types) 54 | | _ -> 55 | find_inherited_types_aux tl inherited_types 56 | end 57 | | [] -> 58 | (inherited_types, []) 59 | in 60 | find_inherited_types_aux tokens [] 61 | 62 | exception Curly_braces_mismatch 63 | let find_types_in_type_body tokens = 64 | let rec find_types_in_type_body_aux tokens level found_types = 65 | match tokens with 66 | | Type name :: tl -> 67 | find_types_in_type_body_aux tl level (name :: found_types) 68 | | OpenCurlyBrace :: tl -> 69 | find_types_in_type_body_aux tl (level + 1) found_types 70 | | CloseCurlyBrace :: tl -> 71 | let new_level = level - 1 in 72 | if new_level = 0 then 73 | (List.rev found_types, tl) 74 | else 75 | find_types_in_type_body_aux tl new_level found_types 76 | | _ :: tl -> 77 | find_types_in_type_body_aux tl level found_types 78 | | [] -> 79 | raise Curly_braces_mismatch 80 | in 81 | find_types_in_type_body_aux tokens 0 [] 82 | 83 | let find_dependencies tokens swift_types_to_ignore = 84 | let rec find_dependencies_aux tokens swift_types = 85 | match find_type_declaration tokens with 86 | | Some (type_name, tokens_tl) -> 87 | if List.mem type_name swift_types_to_ignore then 88 | find_dependencies_aux tokens_tl swift_types 89 | else 90 | let (inherited_types, tokens_tl) = find_inherited_types tokens_tl in 91 | let (types_in_type_body, tokens_tl) = find_types_in_type_body tokens_tl in 92 | let inherited_types = 93 | List.filter 94 | (fun swift_type -> not (List.mem swift_type swift_types_to_ignore)) 95 | inherited_types in 96 | let types_in_type_body = 97 | List.filter 98 | (fun swift_type -> not (List.mem swift_type swift_types_to_ignore)) 99 | types_in_type_body in 100 | let swift_type = { name=type_name 101 | ; inherited_types=inherited_types 102 | ; types_in_body=types_in_type_body 103 | } in 104 | find_dependencies_aux tokens_tl (swift_type :: swift_types) 105 | | None -> 106 | swift_types 107 | in 108 | find_dependencies_aux tokens [] 109 | 110 | (* Foo.Bar.Buzz -> Foo *) 111 | let reduce_type_tokens tokens = 112 | let rec reduce_type_tokens_aux tokens reduced_tokens = 113 | match tokens with 114 | | Dot :: Type _ :: tl -> 115 | reduce_type_tokens_aux tl reduced_tokens 116 | | token :: tl -> 117 | reduce_type_tokens_aux tl (token :: reduced_tokens) 118 | | [] -> 119 | List.rev reduced_tokens 120 | in 121 | reduce_type_tokens_aux tokens [] 122 | 123 | let walk_directory_tree dir pattern = 124 | let tests_regexp = Str.regexp ".+Tests\\.swift" in 125 | let re = Str.regexp pattern in 126 | let file_path_matches_pattern str = Str.string_match re str 0 in 127 | let rec walk acc = function 128 | | [] -> 129 | acc 130 | | dir :: tl -> 131 | let contents = Array.to_list (Sys.readdir dir) in 132 | let contents = 133 | List.filter 134 | ( 135 | fun name -> 136 | if Str.string_match tests_regexp name 0 then 137 | false 138 | else 139 | name <> "Pods" && name <> ".git" 140 | ) 141 | contents in 142 | let contents = List.rev_map (Filename.concat dir) contents in 143 | let dirs, files = 144 | List.fold_left 145 | ( 146 | fun (dirs, files) f -> 147 | match (Unix.stat f).st_kind with 148 | | S_REG -> 149 | (dirs, f :: files) 150 | | S_DIR -> 151 | (f :: dirs, files) 152 | | _ -> 153 | (dirs, files) 154 | ) 155 | ([], []) 156 | contents in 157 | let matched = List.filter file_path_matches_pattern files in 158 | walk (matched @ acc) (dirs @ tl) 159 | in 160 | walk [] [dir] 161 | 162 | let edges_of_swift_types_in_file swift_types_in_file nodes get_related_types = 163 | List.map 164 | ( 165 | fun swift_type -> 166 | match find_node_by_label swift_type.name nodes with 167 | | Some from_node -> 168 | let related_types = get_related_types swift_type in 169 | let label_nodes = find_nodes_by_labels related_types nodes in 170 | List.map 171 | ( 172 | fun label_node -> 173 | { node_from_id=from_node.id 174 | ; node_to_id=label_node.id 175 | } 176 | ) 177 | label_nodes 178 | | None -> 179 | [] 180 | ) 181 | swift_types_in_file 182 | 183 | let get_nodes swift_types_in_files = 184 | let node_labels = 185 | List.map 186 | ( 187 | fun swift_types_in_file -> 188 | List.map (fun swift_type -> swift_type.name) swift_types_in_file 189 | ) 190 | swift_types_in_files 191 | |> List.flatten |> remove_duplicates |> sort in 192 | List.mapi (fun index label -> { id=index; label=label }) node_labels 193 | 194 | let get_edges nodes swift_types_in_files get_related_types = 195 | List.map 196 | ( 197 | fun swift_types_in_file -> 198 | edges_of_swift_types_in_file swift_types_in_file nodes get_related_types 199 | ) 200 | swift_types_in_files 201 | |> List.flatten |> List.flatten 202 | 203 | let js_node_str_of_node node node_color = 204 | Printf.sprintf 205 | "{ id: '%d', label: '%s', shape: 'dot', size: 14, color: '%s' }" 206 | node.id 207 | node.label 208 | node_color 209 | 210 | let js_edge_str_of_edge edge = 211 | Printf.sprintf 212 | "{ id: '%s', from: %d, to: %d, arrows: 'to' }" 213 | (get_edge_id edge) 214 | edge.node_from_id 215 | edge.node_to_id 216 | 217 | let print_nodes output_channel nodes node_color = 218 | let js_node_strs = 219 | List.map (fun node -> js_node_str_of_node node node_color) nodes in 220 | let js_nodes_str = (String.concat ",\n" js_node_strs) in 221 | Printf.fprintf output_channel "var nodes = [\n%s\n];" js_nodes_str 222 | 223 | let print_edges output_channel edges = 224 | let js_edge_strs = List.map (fun edge -> js_edge_str_of_edge edge) edges in 225 | let js_edges_str = (String.concat ",\n" js_edge_strs) in 226 | Printf.fprintf output_channel "[\n%s\n]" js_edges_str 227 | 228 | let find_subgraph graph root_node_id = 229 | let connected_nodes = find_connected_nodes root_node_id graph in 230 | let connected_edges = 231 | List.map 232 | (fun node -> find_edges node.id graph.edges) 233 | connected_nodes 234 | |> List.flatten |> remove_duplicates in 235 | let node_ids = List.map (fun node -> node.id) connected_nodes in 236 | let edge_ids = List.map get_edge_id connected_edges in 237 | { node_ids=node_ids; edge_ids=edge_ids } 238 | 239 | let print_subgraphs output_channel graph = 240 | List.iteri 241 | ( 242 | fun index node -> 243 | let step_str = 244 | Printf.sprintf 245 | "%d/%d %s" 246 | index 247 | (List.length graph.nodes) 248 | node.label in 249 | print_endline step_str; 250 | let subgraph = find_subgraph graph node.id in 251 | let node_id_strs = 252 | List.map 253 | (fun node_id -> Printf.sprintf "'%d'" node_id) 254 | subgraph.node_ids in 255 | let edge_id_strs = 256 | List.map 257 | (fun edge_id -> Printf.sprintf "'%s'" edge_id) 258 | subgraph.edge_ids in 259 | let node_ids_str = String.concat ", " node_id_strs in 260 | let edge_ids_str = String.concat ", " edge_id_strs in 261 | Printf.fprintf 262 | output_channel 263 | "\"%s\": {\nnodeIds: [%s],\nedgeIds: [%s]\n}" 264 | node.label 265 | node_ids_str 266 | edge_ids_str; 267 | if index < List.length graph.nodes - 1 then 268 | Printf.fprintf output_channel ",\n" 269 | else 270 | () 271 | ) 272 | graph.nodes 273 | 274 | let () = 275 | try 276 | let folder_path = Sys.argv.(1) in 277 | let swift_file_paths = walk_directory_tree folder_path ".*\\.swift" in 278 | let swift_types_to_ignore = read_file "types_to_ignore.txt" in 279 | let swift_types_in_files = 280 | List.map 281 | ( 282 | fun file_path -> 283 | try 284 | let tokens = file_path |> tokenize |> reduce_type_tokens in 285 | find_dependencies tokens swift_types_to_ignore 286 | with 287 | | _ -> 288 | print_endline ("Failed to parse: " ^ file_path); 289 | exit 0 290 | ) 291 | swift_file_paths in 292 | let output_channel = open_out "./data.js" in 293 | let node_default_color = "0FC3FF" in 294 | Printf.fprintf 295 | output_channel 296 | "var nodeDefaultColor = '%s';\n" 297 | node_default_color; 298 | Printf.fprintf output_channel "var nodeSelectedColor = 'FF2700';\n"; 299 | let nodes = get_nodes swift_types_in_files in 300 | print_nodes output_channel nodes node_default_color; 301 | Printf.fprintf output_channel "\nvar graphs = [\n"; 302 | (* usage graph *) 303 | let usage_edges = 304 | get_edges 305 | nodes 306 | swift_types_in_files 307 | (fun swift_type -> swift_type.types_in_body) 308 | |> remove_duplicates in 309 | let usage_graph = { nodes=nodes; edges=usage_edges } in 310 | Printf.fprintf output_channel "{\nname: \"Usage\",\nedges: "; 311 | print_edges output_channel usage_edges; 312 | Printf.fprintf output_channel ",\nsubgraphs: {\n"; 313 | print_subgraphs output_channel usage_graph; 314 | Printf.fprintf output_channel "\n}\n}"; 315 | (* inheritance graph *) 316 | let inheritance_edges = 317 | get_edges 318 | nodes 319 | swift_types_in_files 320 | (fun swift_type -> swift_type.inherited_types) 321 | |> remove_duplicates in 322 | let inheritance_graph = { nodes=nodes; edges=inheritance_edges } in 323 | Printf.fprintf output_channel ",\n{\nname: \"Inheritance\",\nedges: "; 324 | print_edges output_channel inheritance_edges; 325 | Printf.fprintf output_channel ",\nsubgraphs: {\n"; 326 | print_subgraphs output_channel inheritance_graph; 327 | Printf.fprintf output_channel "\n}\n}"; 328 | Printf.fprintf output_channel "\n];"; 329 | close_out output_channel 330 | with 331 | | Invalid_argument _ -> 332 | print_endline "Expected folder path as argument" 333 | --------------------------------------------------------------------------------