objc-dependency-tree-generator 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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