objc-dependency-tree-generator 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,13 +1,16 @@
1
1
  # encoding: UTF-8
2
+
2
3
  require 'optparse'
3
4
  require 'yaml'
4
5
  require 'json'
5
- require 'objc_dependency_tree_generator_helper'
6
- require 'swift_dependencies_generator'
7
- require 'objc_dependencies_generator'
8
-
9
- class ObjCDependencyTreeGenerator
10
-
6
+ require 'helpers/objc_dependency_tree_generator_helper'
7
+ require 'swift/swift_dependencies_generator'
8
+ require 'objc/objc_dependencies_generator'
9
+ require 'sourcekitten/sourcekitten_dependencies_generator'
10
+ require 'dependency_tree'
11
+ require 'tree_serializer'
12
+
13
+ class DependencyTreeGenerator
11
14
  def initialize(options)
12
15
  @options = options
13
16
  @options[:derived_data_project_pattern] = '*-*' unless @options[:derived_data_project_pattern]
@@ -19,125 +22,139 @@ class ObjCDependencyTreeGenerator
19
22
  def self.parse_command_line_options
20
23
  options = {}
21
24
 
22
- #Defaults
25
+ # Defaults
23
26
  options[:derived_data_paths] = ['~/Library/Developer/Xcode/DerivedData', '~/Library/Caches/appCode*/DerivedData']
24
27
  options[:project_name] = ''
25
28
  options[:output_format] = 'json'
29
+ options[:verbose] = true
30
+ options[:swift_ast_show_parsed_tree] = false
31
+ options[:ignore_primitive_types] = true
32
+ options[:show_inheritance_only] = false
26
33
 
27
-
28
- parser = OptionParser.new do |o|
34
+ OptionParser.new do |o|
29
35
  o.separator 'General options:'
30
- o.on('-p PATH', '--path' ,'Path to directory where are your .o files were placed by the compiler', Array) { |directory|
36
+ o.on('-p PATH', '--path', 'Path to directory where are your .o files were placed by the compiler', Array) do |directory|
31
37
  options[:search_directories] = Array(options[:search_directories]) | Array(directory)
32
- }
33
- o.on('-D DERIVED_DATA', 'Path to directory where DerivedData is') { |derived_data|
38
+ end
39
+ o.on('-D DERIVED_DATA', 'Path to directory where DerivedData is') do |derived_data|
34
40
  options[:derived_data_paths] = [derived_data]
35
41
  options[:derived_data_project_pattern] = '*'
36
- }
37
- o.on('-s PROJECT_NAME', 'Search project .o files by specified project name') { |project_name|
42
+ end
43
+ o.on('-output PROJECT_NAME', 'Search project .o files by specified project name') do |project_name|
38
44
  options[:project_name] = project_name
39
- }
40
- o.on('-t TARGET_NAME', '--target' 'Target of project', Array) { |target_name|
45
+ end
46
+ o.on('-t TARGET_NAME', '--target', 'Target of project', Array) do |target_name|
41
47
  options[:target_names] = Array(options[:target_names]) | Array(target_name)
42
- }
43
- o.on('-e PREFIXES', "Prefixes of classes those will be exсluded from visualization. \n\t\t\t\t\tNS|UI\n\t\t\t\t\tUI|CA|MF") { |exclusion_prefixes|
48
+ end
49
+ o.on('-e PREFIXES', "Prefixes of classes those will be exсluded from visualization. \n\t\t\t\t\tNS|UI\n\t\t\t\t\tUI|CA|MF") do |exclusion_prefixes|
44
50
  options[:exclusion_prefixes] = exclusion_prefixes
45
- }
51
+ end
46
52
 
47
- o.on('-d', '--use-dwarf-info', 'Use DWARF Information also') { |v|
53
+ o.on('-d', '--use-dwarf-info', 'Use DWARF Information also') do |v|
48
54
  options[:use_dwarf] = v
49
- }
50
- o.on('-w', '--swift-dependencies', 'Generate swift project dependencies') { |v|
55
+ end
56
+
57
+ o.on('-w', '--swift-dependencies', 'Generate swift project dependencies') do |v|
51
58
  options[:swift_dependencies] = v
52
- }
53
- o.on('-f FORMAT', 'Output format. json by default. Possible values are [dot|json-pretty|json|json-var|yaml]') { |f|
59
+ end
60
+ o.on('-k FILENAME', 'Generate dependencies from source kitten output (json)') do |v|
61
+ options[:sourcekitten_dependencies_file] = v
62
+ end
63
+
64
+ o.on('--ast-file FILENAME', 'Generate dependencies from the swift ast dump output (ast)') do |v|
65
+ options[:swift_ast_dump_file] = v
66
+ end
67
+
68
+ o.on('--ast-show-parsed-tree', 'Show ast parsing info (for swift ast parser only)') do |_v|
69
+ options[:swift_ast_show_parsed_tree] = true
70
+ end
71
+
72
+ o.on('--inheritance-only', 'Show only inheritance dependencies') do
73
+ options[:show_inheritance_only] = true
74
+ end
75
+
76
+ o.on('-f FORMAT', 'Output format. json by default. Possible values are [dot|json-pretty|json|json-var|yaml]') do |f|
54
77
  options[:output_format] = f
55
- }
78
+ end
79
+ o.on('-o OUTPUT_FILE', '--output', 'target of output') do |f|
80
+ options[:target_file_name] = f
81
+ end
56
82
 
57
83
  o.separator 'Common options:'
58
- o.on_tail('-h', 'Prints this help') { puts o; exit }
84
+ o.on_tail('-h', 'Prints this help') do
85
+ puts o
86
+ exit
87
+ end
59
88
  o.parse!
60
-
61
89
  end
62
90
 
63
91
  options
92
+ end
64
93
 
94
+ def find_objecte_files_directories
95
+ find_project_output_directory(
96
+ @options[:derived_data_paths],
97
+ @options[:project_name],
98
+ @options[:derived_data_project_pattern],
99
+ @options[:target_names],
100
+ @options[:verbose]
101
+ )
65
102
  end
66
103
 
67
- def find_dependencies
68
- if !@options or @options.empty?
69
- return {}
70
- end
104
+ def build_dependency_tree
105
+ tree = generate_depdendency_tree
106
+ tree.filter { |item, _| is_valid_dest?(item, @exclusion_prefixes) } if @options[:ignore_primitive_types]
107
+ tree.filter_links { |_ , _ , type | type == DependencyLinkType::INHERITANCE } if @options[:show_inheritance_only]
108
+ tree
109
+ end
71
110
 
72
- unless @object_files_directories
73
- @object_files_directories =
74
- find_project_output_directory(@options[:derived_data_paths],
75
- @options[:project_name],
76
- @options[:derived_data_project_pattern],
77
- @options[:target_names])
78
- return {} unless @object_files_directories
79
- end
111
+ def generate_depdendency_tree
112
+ return build_sourcekitten_dependency_tree if @options[:sourcekitten_dependencies_file]
113
+ return build_ast_dependency_tree if @options[:swift_ast_dump_file]
114
+ return tree_from_object_files_directory
115
+ end
80
116
 
81
- unless @object_files_directories
82
- puts parser.help
83
- exit 1
84
- end
117
+ def tree_from_object_files_directory
118
+ tree = DependencyTree.new
85
119
 
120
+ return tree if !@options || @options.empty?
121
+ @object_files_directories ||= find_objecte_files_directories
122
+ return tree unless @object_files_directories
86
123
 
87
- links = {}
88
- links_block = lambda { |source, dest|
89
- links[source] = {} unless links[source]
90
- if source != dest and is_valid_dest?(dest, @exclusion_prefixes)
91
- links[source][dest] = 'set up'
92
- end
93
- }
94
-
124
+ update_tree_block = lambda { |source, target| tree.add(source, target) }
95
125
  if @options[:swift_dependencies]
96
- SwiftDependenciesGenerator.new.generate_dependencies(@object_files_directories, &links_block)
126
+ SwiftDependenciesGenerator.new.generate_dependencies(@object_files_directories, &update_tree_block)
97
127
  else
98
- ObjcDependenciesGenerator.new.generate_dependencies(@object_files_directories, @options[:use_dwarf], &links_block)
128
+ ObjcDependenciesGenerator.new.generate_dependencies(@object_files_directories, @options[:use_dwarf], &update_tree_block)
99
129
  end
130
+ tree
131
+ end
132
+
133
+ def build_ast_dependency_tree
134
+ require_relative 'swift-ast-dump/swift_ast_dependencies_generator'
135
+ generator = SwiftAstDependenciesGenerator.new(
136
+ @options[:swift_ast_dump_file],
137
+ @options[:swift_ast_show_parsed_tree]
138
+ )
139
+ generator.generate_dependencies
140
+ end
100
141
 
101
- links
142
+ def build_sourcekitten_dependency_tree
143
+ generator = SourcekittenDependenciesGenerator.new(
144
+ @options[:sourcekitten_dependencies_file]
145
+ )
146
+ generator.generate_dependencies
102
147
  end
103
148
 
104
149
  def dependencies_to_s
105
- links = find_dependencies
106
- s = ''
107
- if @options[:output_format] == 'json-var'
108
- s+= <<-THEEND
109
- var dependencies =
110
- THEEND
111
- end
150
+ tree = build_dependency_tree
151
+ serializer = TreeSerializer.new(tree)
152
+ output = serializer.serialize(@options[:output_format])
112
153
 
113
- json_result = {}
114
- json_links = []
115
-
116
- links_count = 0
117
- links.each do |source, dest_hash|
118
- links_count = links_count + dest_hash.length
119
- dest_hash.each do |dest, _|
120
- json_links += [{'source' => source, 'dest' => dest}]
121
- end
122
- end
123
- json_result['links'] = json_links
124
- json_result['source_files_count'] = links.length
125
- json_result['links_count'] = links_count
126
-
127
- if @options[:output_format] == 'dot'
128
- indent = "\t"
129
- s = "digraph dependencies {\n#{indent}node [fontname=monospace, fontsize=9, shape=box, style=rounded]\n"
130
- json_links.each do |link|
131
- s += "#{indent}\"#{link['source']}\" -> \"#{link['dest']}\"\n"
132
- end
133
- s += "}\n"
154
+ if @options[:target_file_name]
155
+ File.open(@options[:target_file_name], 'w').write(output.to_s)
134
156
  else
135
- s = s + JSON.pretty_generate(json_result) if @options[:output_format] == 'json-pretty'
136
- s = s + json_result.to_json if @options[:output_format] == 'json' || @options[:output_format] == 'json-var'
137
- s = s + json_result.to_yaml if @options[:output_format] == 'yaml'
157
+ output
138
158
  end
139
- s
140
159
  end
141
-
142
-
143
- end
160
+ end
@@ -0,0 +1,229 @@
1
+ require 'json'
2
+ require 'helpers/objc_dependency_tree_generator_helper'
3
+ require 'rexml/document'
4
+ require 'helpers/swift_primitives'
5
+
6
+ module SKDeclarationType
7
+ SWIFT_EXTENSION = 'source.lang.swift.decl.extension'.freeze
8
+ SWIFT_PROTOCOL = 'source.lang.swift.decl.protocol'.freeze
9
+ SWIFT_STRUCT = 'source.lang.swift.decl.struct'.freeze
10
+ SWIFT_CLASS = 'source.lang.swift.decl.class'.freeze
11
+ OBJC_PROTOCOL = 'sourcekitten.source.lang.objc.decl.protocol'.freeze
12
+ OBJC_STRUCT = 'sourcekitten.source.lang.objc.decl.struct'.freeze
13
+ OBJC_CLASS = 'sourcekitten.source.lang.objc.decl.class'.freeze
14
+
15
+
16
+ INSTANCE_VARIABLE = 'source.lang.swift.decl.var.instance'.freeze
17
+ STATIC_VARIABLE = 'source.lang.swift.decl.var.static'.freeze
18
+ INSTANCE_METHOD = 'source.lang.swift.decl.function.method.instance'.freeze
19
+ CLASS_METHOD = 'source.lang.swift.decl.function.method.class'.freeze
20
+ CALL = 'source.lang.swift.expr.call'.freeze
21
+ ARGUMENT = 'source.lang.swift.expr.argument'.freeze
22
+ DICTIONARY = 'source.lang.swift.expr.dictionary'.freeze
23
+ ARRAY = 'source.lang.swift.expr.array'.freeze
24
+ end
25
+
26
+ module SKKey
27
+ SUBSTRUCTURE = 'key.substructure'.freeze
28
+ KIND = 'key.kind'.freeze
29
+ INHERITED_TYPES = 'key.inheritedtypes'.freeze
30
+ NAME = 'key.name'.freeze
31
+ TYPE_NAME = 'key.typename'.freeze
32
+ ANNOTATED_DECLARATION = 'key.annotated_decl'.freeze
33
+ FULLY_ANNOTATED_DECLARATION = 'key.fully_annotated_decl'.freeze
34
+ end
35
+
36
+
37
+ class SourcekittenDependenciesGenerator
38
+
39
+ def initialize(source_kitten_json)
40
+ @source_kitten_json = source_kitten_json
41
+ end
42
+ # @return [DependencyTree]
43
+ def generate_dependencies
44
+
45
+ @tree = DependencyTree.new
46
+ @context = []
47
+
48
+ file = File.read(@source_kitten_json)
49
+ parsed_files = JSON.parse(file)
50
+
51
+ parsed_files.each do |parsed_file|
52
+ parsed_file.each do |_path, contents|
53
+ substructures = contents[SKKey::SUBSTRUCTURE] || next
54
+ substructures.each { |substructure| parse_structure(substructure) }
55
+ end
56
+ end
57
+
58
+ @tree
59
+ end
60
+
61
+ def in_context(name)
62
+ @context.push(name)
63
+ yield
64
+ @context.pop
65
+ end
66
+
67
+ # @param [Hash] item array of sourcekitten substructure of class, protocol, whatever
68
+ # @param [DependencyItemType] type type of items to register
69
+ def register_item(item, type)
70
+ item_name = item[SKKey::NAME]
71
+ @tree.register(item_name, type)
72
+
73
+ inherited_types = item[SKKey::INHERITED_TYPES] || return
74
+
75
+ inherited_types.each do |inherited_type|
76
+ inherited_type_name = inherited_type[SKKey::NAME]
77
+ @tree.add(item_name, inherited_type_name)
78
+ end
79
+
80
+ end
81
+
82
+ # @param [Hash] element SourceKitten source
83
+ def parse_structure(element)
84
+
85
+ kind = element[SKKey::KIND]
86
+ item_name = element[SKKey::NAME]
87
+
88
+ case kind
89
+ when SKDeclarationType::SWIFT_EXTENSION
90
+ process_element(element, DependencyItemType::UNKNOWN)
91
+
92
+ when SKDeclarationType::SWIFT_PROTOCOL, SKDeclarationType::OBJC_PROTOCOL
93
+ process_element(element, DependencyItemType::PROTOCOL)
94
+
95
+ when SKDeclarationType::SWIFT_STRUCT, SKDeclarationType::OBJC_STRUCT
96
+ process_element(element, DependencyItemType::STRUCTURE)
97
+
98
+ when SKDeclarationType::SWIFT_CLASS, SKDeclarationType::OBJC_CLASS
99
+ process_element(element, DependencyItemType::CLASS)
100
+ register_types_from_annotated_declaration(element)
101
+
102
+ when SKDeclarationType::INSTANCE_VARIABLE, SKDeclarationType::INSTANCE_METHOD, SKDeclarationType::STATIC_VARIABLE, SKDeclarationType::CLASS_METHOD
103
+ @context.each do |el_name|
104
+ register_types_from_annotated_declaration(element, el_name)
105
+ end
106
+ parse_substructures(element)
107
+
108
+ when SKDeclarationType::CALL
109
+ if item_name
110
+ object_names = potential_object_names(item_name)
111
+ object_names.each { |on| @context.each { |el_name| @tree.add(el_name, on, DependencyLinkType::CALL)} }
112
+ end
113
+ parse_substructures(element)
114
+
115
+ when SKDeclarationType::ARGUMENT, SKDeclarationType::DICTIONARY, SKDeclarationType::ARRAY
116
+ parse_substructures(element)
117
+
118
+ else
119
+ # do nothing
120
+ end
121
+ end
122
+
123
+ # @param [Hash] item array of sourcekitten substructure of class, protocol, whatever
124
+ # @param [DependencyItemType] type type of items to register
125
+ def process_element(element, type)
126
+ register_item(element, type)
127
+ item_name = element[SKKey::NAME] || return
128
+ link_items_from_context(item_name)
129
+ in_context(item_name) do
130
+ parse_substructures(element)
131
+ end
132
+ end
133
+
134
+ def parse_substructures(element)
135
+ sub_structures = element[SKKey::SUBSTRUCTURE] || return
136
+ sub_structures.each { |it| parse_structure(it) }
137
+ end
138
+
139
+ def link_items_from_context(item_name)
140
+ @context.each { |el_name| @tree.add(el_name, item_name) }
141
+ end
142
+
143
+ def register_types_from_annotated_declaration(element, name = nil)
144
+ item_name = name || element[SKKey::NAME] || return
145
+ annotated_decl = element[SKKey::ANNOTATED_DECLARATION] || return
146
+
147
+ doc = REXML::Document.new(annotated_decl)
148
+
149
+ has_typealiases = false
150
+ doc.each_element('//Declaration/Type') do |el|
151
+ dependency_type = el.text.to_s
152
+
153
+ # get el type
154
+ attribute_el = el.attribute('usr') || next
155
+
156
+ if is_typealias(element, dependency_type)
157
+ has_typealiases = true
158
+ next
159
+ end
160
+
161
+ attribute = attribute_el.to_s
162
+ if attribute.start_with? 's:P'
163
+ @tree.add(item_name, dependency_type)
164
+ @tree.register(dependency_type, DependencyItemType::PROTOCOL)
165
+ elsif attribute.start_with? 'c:objc(pl)'
166
+ @tree.add(item_name, dependency_type)
167
+ @tree.register(dependency_type, DependencyItemType::PROTOCOL)
168
+ elsif attribute.start_with? 'c:objc(cs)'
169
+ @tree.add(item_name, dependency_type)
170
+ @tree.register(dependency_type, DependencyItemType::CLASS)
171
+ elsif attribute.start_with? 's:C'
172
+ @tree.add(item_name, dependency_type)
173
+ @tree.register(dependency_type, DependencyItemType::CLASS)
174
+ end
175
+ end
176
+
177
+ register_types_from_type_name(element, item_name) if has_typealiases
178
+ end
179
+
180
+ def register_types_from_type_name(element, item_name)
181
+ type_name_string = element[SKKey::TYPE_NAME] || return
182
+
183
+ generics = generic_parameters(element)
184
+ type_names(type_name_string)
185
+ .select { |type_name| !generics.include?(type_name) }
186
+ .each { |type_name| @tree.add(item_name, type_name) }
187
+
188
+ end
189
+
190
+ # @return [Array<String>] array of generic names for element
191
+ def generic_parameters(element)
192
+ fully_annotated_decl = element[SKKey::FULLY_ANNOTATED_DECLARATION]
193
+ return [] if fully_annotated_decl.nil?
194
+ generics = []
195
+ doc = REXML::Document.new(fully_annotated_decl)
196
+ doc.each_element('//decl.generic_type_param.name') do |el|
197
+ generics.push(el.text.to_s)
198
+ end
199
+ generics
200
+ end
201
+
202
+ def is_typealias(element, name)
203
+ fully_annotated_decl = element[SKKey::FULLY_ANNOTATED_DECLARATION]
204
+ return false if fully_annotated_decl.nil?
205
+ doc = REXML::Document.new(fully_annotated_decl)
206
+ doc.each_element('//ref.typealias') do |el|
207
+ return true if el.text.to_s == name
208
+ end
209
+
210
+ false
211
+ end
212
+
213
+ # Returns an array of strings, which represents type names
214
+ # @param [String] type_name_string sourcekitten type name
215
+ # @return [Array<String>] array of types, found in this type
216
+ def type_names(type_name_string)
217
+ type_name_string
218
+ .split(/\W+/)
219
+ .select { |t| !t.empty? }
220
+ .select { |t| !is_primitive_swift_type?(t) }
221
+ end
222
+
223
+ def potential_object_names(call_string)
224
+ (call_string.scan(/^[A-Z_][A-Za-z0-9_]+/) + call_string.scan(/\s[A-Z_][A-Za-z0-9_]+/))
225
+ .select { |t| !t.empty? }
226
+ .select { |t| !is_primitive_swift_type?(t) }
227
+ end
228
+
229
+ end