xdry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +11 -0
  3. data/Rakefile +76 -0
  4. data/VERSION +1 -0
  5. data/bin/xdry +4 -0
  6. data/lib/xdry.rb +18 -0
  7. data/lib/xdry/boxing.rb +212 -0
  8. data/lib/xdry/generators/ctor_from_field.rb +91 -0
  9. data/lib/xdry/generators/dealloc.rb +53 -0
  10. data/lib/xdry/generators/dictionary_coding.rb +129 -0
  11. data/lib/xdry/generators/field_from_property.rb +20 -0
  12. data/lib/xdry/generators/property-from-field.rb +22 -0
  13. data/lib/xdry/generators/storing_constructor.rb +72 -0
  14. data/lib/xdry/generators/synthesize.rb +25 -0
  15. data/lib/xdry/generators_support.rb +42 -0
  16. data/lib/xdry/parsing/driver.rb +106 -0
  17. data/lib/xdry/parsing/model.rb +272 -0
  18. data/lib/xdry/parsing/nodes.rb +260 -0
  19. data/lib/xdry/parsing/parsers.rb +166 -0
  20. data/lib/xdry/parsing/parts/selectors.rb +95 -0
  21. data/lib/xdry/parsing/parts/var_types.rb +66 -0
  22. data/lib/xdry/parsing/pos.rb +75 -0
  23. data/lib/xdry/parsing/scope_stack.rb +68 -0
  24. data/lib/xdry/parsing/scopes.rb +61 -0
  25. data/lib/xdry/parsing/scopes_support.rb +143 -0
  26. data/lib/xdry/patching/emitter.rb +60 -0
  27. data/lib/xdry/patching/insertion_points.rb +209 -0
  28. data/lib/xdry/patching/item_patchers.rb +74 -0
  29. data/lib/xdry/patching/patcher.rb +139 -0
  30. data/lib/xdry/run.rb +227 -0
  31. data/lib/xdry/support/enumerable_additions.rb +35 -0
  32. data/lib/xdry/support/string_additions.rb +27 -0
  33. data/lib/xdry/support/symbol_additions.rb +14 -0
  34. data/site/_config.yml +3 -0
  35. data/site/_example +9 -0
  36. data/site/_layouts/default.html +30 -0
  37. data/site/_plugins/example.rb +16 -0
  38. data/site/_plugins/highlight_unindent.rb +17 -0
  39. data/site/index.md +417 -0
  40. data/site/master.css +94 -0
  41. data/spec/boxing_spec.rb +80 -0
  42. data/spec/ctor_from_field_spec.rb +251 -0
  43. data/spec/dealloc_spec.rb +103 -0
  44. data/spec/dictionary_coding_spec.rb +132 -0
  45. data/spec/field_from_prop_spec.rb +72 -0
  46. data/spec/prop_from_field_spec.rb +39 -0
  47. data/spec/readme_samples_spec.rb +76 -0
  48. data/spec/spec.opts +3 -0
  49. data/spec/spec_helper.rb +53 -0
  50. data/spec/synthesize_spec.rb +94 -0
  51. data/xdry.gemspec +103 -0
  52. metadata +141 -0
@@ -0,0 +1,209 @@
1
+
2
+ module XDry
3
+
4
+ class InsertionPoint
5
+
6
+ attr_reader :method, :node, :ip
7
+
8
+ def initialize
9
+ find!
10
+ end
11
+
12
+ def insert patcher, lines
13
+ raise StandardError, "#{self.class.name} has not been found but trying to insert" unless found?
14
+ patcher.send(@method, @node.pos, wrap(lines), @indent)
15
+ end
16
+
17
+ def found?
18
+ not @method.nil?
19
+ end
20
+
21
+ protected
22
+
23
+ def wrap lines
24
+ lines
25
+ end
26
+
27
+ def before node
28
+ @method = :insert_before
29
+ @node = node
30
+ @indent = node.indent
31
+ end
32
+
33
+ def after node
34
+ @method = :insert_after
35
+ @node = node
36
+ @indent = node.indent
37
+ end
38
+
39
+ def indented_before node
40
+ before node
41
+ @indent = @indent + INDENT_STEP
42
+ end
43
+
44
+ def indented_after node
45
+ after node
46
+ @indent = @indent + INDENT_STEP
47
+ end
48
+
49
+ def try insertion_point
50
+ if insertion_point.found?
51
+ @method, @node, @ip = insertion_point.method, insertion_point.node, insertion_point
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ def find!
59
+ end
60
+
61
+ end
62
+
63
+ class ImplementationStartIP < InsertionPoint
64
+
65
+ def initialize oclass
66
+ @oclass = oclass
67
+ super()
68
+ end
69
+
70
+ def find!
71
+ after @oclass.main_implementation.start_node
72
+ end
73
+
74
+ end
75
+
76
+ class BeforeImplementationStartIP < InsertionPoint
77
+
78
+ def initialize oclass
79
+ @oclass = oclass
80
+ super()
81
+ end
82
+
83
+ def find!
84
+ before @oclass.main_implementation.start_node
85
+ end
86
+
87
+ end
88
+
89
+ class BeforeInterfaceEndIP < InsertionPoint
90
+
91
+ def initialize oclass
92
+ @oclass = oclass
93
+ super()
94
+ end
95
+
96
+ def find!
97
+ before @oclass.main_interface.end_node
98
+ end
99
+
100
+ end
101
+
102
+ class BeforeSuperCallIP < InsertionPoint
103
+
104
+ def initialize scope
105
+ @scope = scope
106
+ super()
107
+ end
108
+
109
+ def find!
110
+ child_node = @scope.children.find { |child| child.is_a? NSuperCall }
111
+ if child_node.nil?
112
+ indented_before @scope.ending_node
113
+ else
114
+ before child_node
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ class BeforeReturnIP < InsertionPoint
121
+
122
+ def initialize scope
123
+ @scope = scope
124
+ super()
125
+ end
126
+
127
+ def find!
128
+ child_node = @scope.children.find { |child| child.is_a? NReturn }
129
+ if child_node.nil?
130
+ before @scope.ending_node
131
+ else
132
+ before child_node
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ class AfterDefineIP < InsertionPoint
139
+
140
+ def initialize scope
141
+ @scope = scope
142
+ super()
143
+ end
144
+
145
+ def find!
146
+ child_nodes = @scope.children.select { |child| child.is_a? NDefine }
147
+ unless child_nodes.empty?
148
+ after child_nodes.last
149
+ end
150
+ end
151
+
152
+ end
153
+
154
+ class InsideConstructorIfSuperIP < InsertionPoint
155
+
156
+ def initialize scope
157
+ @scope = scope
158
+ super()
159
+ end
160
+
161
+ def find!
162
+ if_start_node = @scope.children.find { |child| child.is_a? NSuperCall }
163
+ if if_start_node.nil?
164
+ indented_before @scope.ending_node
165
+ else
166
+ if_end_node = @scope.children.find { |child| child.is_a?(NClosingBrace) && child.indent == if_start_node.indent }
167
+ if if_end_node.nil?
168
+ indented_after if_start_node
169
+ else
170
+ indented_before if_end_node
171
+ end
172
+ end
173
+ end
174
+
175
+ end
176
+
177
+ class MultiIP < InsertionPoint
178
+
179
+ def initialize *insertion_points
180
+ @insertion_points = insertion_points
181
+ @last_before = []
182
+ @last_after = []
183
+ super()
184
+ end
185
+
186
+ def wrap_if_last! before, after
187
+ @last_before = before
188
+ @last_after = after
189
+ end
190
+
191
+ def wrap_with_empty_lines_if_last!
192
+ wrap_if_last! [""], [""]
193
+ end
194
+
195
+ def find!
196
+ @insertion_points.detect { |ip| try ip }
197
+ end
198
+
199
+ def wrap lines
200
+ if @ip == @insertion_points.last
201
+ @last_before + lines + @last_after
202
+ else
203
+ lines
204
+ end
205
+ end
206
+
207
+ end
208
+
209
+ end
@@ -0,0 +1,74 @@
1
+
2
+ module XDry
3
+
4
+ class ItemPatcher
5
+
6
+ attr_reader :item
7
+ attr_reader :patcher
8
+
9
+ def initialize patcher
10
+ @patcher = patcher
11
+ find!
12
+ yield @item if block_given? && found?
13
+ end
14
+
15
+ def found?
16
+ not item.nil?
17
+ end
18
+
19
+ protected
20
+
21
+ def find
22
+ end
23
+
24
+ def insertion_point
25
+ end
26
+
27
+ def new_code
28
+ end
29
+
30
+ private
31
+
32
+ def find!
33
+ @item = find
34
+ if @item.nil?
35
+ patch!
36
+ @item = find
37
+ raise StandardError, "#{self.class.name} cannot find item even after adding a new one" if @item.nil?
38
+ end
39
+ end
40
+
41
+ def patch!
42
+ insertion_point.insert patcher, new_code
43
+ end
44
+
45
+ end
46
+
47
+ class MethodPatcher < ItemPatcher
48
+
49
+ attr_reader :oclass
50
+ attr_reader :insertion_point
51
+ attr_reader :new_code
52
+
53
+ def initialize patcher, oclass, selector, insertion_point, new_code
54
+ @oclass = oclass
55
+ @selector = selector
56
+ @insertion_point = insertion_point
57
+ @new_code = new_code
58
+ super(patcher)
59
+ end
60
+
61
+ protected
62
+
63
+ def find
64
+ find_method_impl_by_selector(@selector)
65
+ end
66
+
67
+ def find_method_impl_by_selector selector
68
+ m = oclass.find_method(selector)
69
+ m && (m.has_impl? ? m : nil)
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,139 @@
1
+
2
+ module XDry
3
+
4
+ class Patcher
5
+
6
+ attr_accessor :dry_run, :verbose
7
+
8
+ def initialize
9
+ @patched = {}
10
+ @dry_run = true
11
+ end
12
+
13
+ def insert_after pos, new_lines, indent = '', parse = true
14
+ do_insert_after pos.file_ref, pos.scope_after, pos.line_no - 1, new_lines, indent || '', parse
15
+ end
16
+
17
+ def insert_before pos, new_lines, indent = '', parse = true
18
+ do_insert_after pos.file_ref, pos.scope_before, pos.line_no - 2, new_lines, indent || '', parse
19
+ end
20
+
21
+ def delete_line pos
22
+ do_delete_lines pos.file_ref, pos.line_no - 1, 1
23
+ end
24
+
25
+ def replace_line pos
26
+ old_line = patched_lines_of(pos.file_ref)[pos.line_no-1].rstrip
27
+ new_line = yield(old_line)
28
+ delete_line pos
29
+ insert_before pos, [new_line], '', false
30
+ end
31
+
32
+ def do_delete_lines file_ref, line_index, line_count
33
+ lines = patched_lines_of(file_ref)
34
+
35
+ if @verbose
36
+ puts "DELETING #{line_count} LINE(S) FROM LINE NO.#{line_index+1}:"
37
+ lines[line_index .. line_index+line_count-1].each { |line| puts " #{line}" }
38
+ end
39
+
40
+ file_ref.fixup_positions! line_index+line_count, -line_count
41
+ lines[line_index .. line_index+line_count-1] = []
42
+ end
43
+
44
+ def do_insert_after file_ref, start_scope, line_index, new_lines, indent, parse
45
+ new_lines = new_lines.collect { |line| line.blank? ? line : indent + line }
46
+ new_lines = new_lines.collect { |line| line.gsub("\t", INDENT_STEP) }
47
+ lines = patched_lines_of(file_ref)
48
+
49
+ if @verbose
50
+ puts "INSERTING LINES AFTER LINE NO.#{line_index+1}:"
51
+ new_lines.each { |line| puts " #{line}" }
52
+ puts " AFTER LINE:"
53
+ puts " #{lines[line_index]}"
54
+ end
55
+
56
+ # collapse leading/trailing empty lines with the empty lines that already exist
57
+ # in the source code
58
+
59
+ # when line_index == -1 (insert at the beginning of the file), there are no leading lines
60
+ if line_index >= 0
61
+ desired_leading_empty_lines = new_lines.prefix_while(&:blank?).length
62
+ actual_leading_empty_lines = lines[0..line_index].suffix_while(&:blank?).length
63
+ leading_lines_to_remove = [actual_leading_empty_lines, desired_leading_empty_lines].min
64
+ new_lines = new_lines[leading_lines_to_remove .. -1]
65
+ end
66
+
67
+ # if all lines were empty, the number of trailing empty lines might have changed
68
+ # after removal of some leading lines, so we compute this after the removal
69
+ desired_trailing_empty_lines = new_lines.suffix_while(&:blank?).length
70
+ actual_trailing_empty_lines = lines[line_index+1..-1].prefix_while(&:blank?).length
71
+ trailing_lines_to_remove = [actual_trailing_empty_lines, desired_trailing_empty_lines].min
72
+ new_lines = new_lines[0 .. -(trailing_lines_to_remove+1)]
73
+
74
+ file_ref.fixup_positions! line_index+1, new_lines.size
75
+
76
+ lines[line_index+1 .. line_index+1] = new_lines.collect { |line| "#{line}\n" } + [lines[line_index+1]]
77
+
78
+ if parse
79
+ driver = ParsingDriver.new(nil)
80
+ driver.verbose = @verbose
81
+ driver.parse_fragment file_ref, new_lines, line_index+1+1, start_scope
82
+ end
83
+ end
84
+
85
+ def save!
86
+ changed_file_refs = []
87
+ for file_ref, lines in @patched
88
+ original_path = file_ref.path
89
+
90
+ text = lines.join("")
91
+ next if text == file_ref.read
92
+
93
+ changed_file_refs << file_ref
94
+
95
+ new_path = if @dry_run
96
+ ext = File.extname(original_path)
97
+ File.join(File.dirname(original_path), File.basename(original_path, ext) + '.xdry' + ext)
98
+ else
99
+ original_path
100
+ end
101
+
102
+ open(new_path, 'w') { |f| f.write text }
103
+ end
104
+ @patched = {}
105
+ return changed_file_refs
106
+ end
107
+
108
+ def retrieve!
109
+ result = {}
110
+ for file_ref, lines in @patched
111
+ result[file_ref.path] = lines.join("")
112
+ end
113
+ @patched = {}
114
+ return result
115
+ end
116
+
117
+ def remove_marker! marker
118
+ if marker.is_a? NFullLineMarker
119
+ delete_line marker.pos
120
+ else
121
+ replace_line marker.pos do |old_line|
122
+ old_line.gsub(marker.text, '').gsub(/ +$/, '')
123
+ end
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def patched_lines_of file_ref
130
+ @patched[file_ref] ||= load_lines_of(file_ref)
131
+ end
132
+
133
+ def load_lines_of file_ref
134
+ file_ref.read.lines.collect
135
+ end
136
+
137
+ end
138
+
139
+ end
data/lib/xdry/run.rb ADDED
@@ -0,0 +1,227 @@
1
+ require 'optparse'
2
+
3
+ module XDry
4
+
5
+ def self.produce_everything oglobal, patcher, config
6
+ puts "Generating code... " if config.verbose
7
+
8
+ generators = Generators::ALL.select { |klass| config.enabled?(klass.id) }.
9
+ collect { |klass| klass.new(config, patcher) }
10
+
11
+ if config.verbose
12
+ puts "Running generators: " + generators.collect { |gen| gen.class.id }.join(", ")
13
+ end
14
+
15
+ oglobal.classes.each do |oclass|
16
+ puts " - #{oclass.name}" if config.verbose
17
+
18
+ if config.verbose
19
+ oclass.attributes.each do |oattr|
20
+ puts " #{oattr}"
21
+ end
22
+
23
+ oclass.methods.each do |omethod|
24
+ puts " #{omethod}"
25
+ end
26
+
27
+ oclass.implementations.each do |nimpl|
28
+ puts " #{nimpl}"
29
+ nimpl.synthesizes.each do |nsynth|
30
+ puts " #{nsynth}"
31
+ end
32
+ end
33
+ end
34
+
35
+ generators.each { |gen| gen.process_class(oclass) }
36
+ end
37
+ end
38
+
39
+ class Config < Struct.new(:only, :dry_run, :watch, :verbose, :disable, :enable_only)
40
+
41
+ def initialize
42
+ self.only = nil
43
+ self.dry_run = false
44
+ self.watch = false
45
+ self.verbose = false
46
+ self.disable = []
47
+ self.enable_only = nil
48
+ end
49
+
50
+ def enabled? gen_id
51
+ (enable_only.nil? || enable_only.include?(gen_id)) && !disable.include?(gen_id)
52
+ end
53
+
54
+ end
55
+
56
+ def self.parse_command_line_config(args)
57
+ config = Config.new
58
+
59
+ opts = OptionParser.new do |opts|
60
+ opts.banner = "Usage: xdry [options]"
61
+
62
+ opts.separator ""
63
+ opts.separator "General options:"
64
+
65
+ opts.on("-w", "--watch", "Watch for file system changes and rerun each time .h/.m is modified") do
66
+ config.watch = true
67
+ end
68
+
69
+ opts.separator ""
70
+ opts.separator "Filtering options:"
71
+
72
+ opts.on("-o", "--only=MASK", "Only process files matching this mask") do |v|
73
+ config.only = v
74
+ end
75
+
76
+ opts.separator ""
77
+ opts.separator "Choosing which generators to run:"
78
+
79
+ opts.on("-e", "--enable-only=LIST", "Only run the given generators (e.g.: -e dealloc,synth)") do |v|
80
+ config.enable_only = v.split(",").collect { |n| n.strip }
81
+
82
+ all = XDry::Generators::ALL.collect { |kl| kl.id }
83
+ unless (unsup = config.enable_only - all).empty?
84
+ puts "Unknown generator names in -e: #{unsup.join(', ')}."
85
+ puts "Supported names are: #{all.join(', ')}."
86
+ exit 1
87
+ end
88
+ end
89
+
90
+ opts.on("-d", "--disable=LIST", "Disable the given generators (e.g.: -d dealloc,synth)") do |v|
91
+ config.disable = v.split(",").collect { |n| n.strip }
92
+
93
+ all = XDry::Generators::ALL.collect { |kl| kl.id }
94
+ unless (unsup = config.disable - all).empty?
95
+ puts "Unknown generator names in -d: #{unsup.join(', ')}."
96
+ puts "Supported names are: #{all.join(', ')}."
97
+ exit 1
98
+ end
99
+ end
100
+
101
+ opts.on("--list", "List all supported generators and exit") do |v|
102
+ XDry::Generators::ALL.each { |kl| puts "#{kl.id}" }
103
+ exit
104
+ end
105
+
106
+ opts.separator ""
107
+ opts.separator "Patching options:"
108
+
109
+ opts.on("-n", "--dry-run", "Save changed files as .xdry.{h/m}") do |v|
110
+ config.dry_run = true
111
+ end
112
+
113
+ opts.separator ""
114
+ opts.separator "Common options:"
115
+
116
+ opts.on("-v", "--verbose", "Print TONS of progress information") do
117
+ config.verbose = true
118
+ end
119
+
120
+ opts.on_tail("-h", "--help", "Show this message") do
121
+ puts opts
122
+ exit
123
+ end
124
+ end
125
+
126
+ opts.parse!(args)
127
+ return config
128
+ end
129
+
130
+ def self.run_once config
131
+ oglobal = OGlobal.new
132
+
133
+ parser = ParsingDriver.new(oglobal)
134
+ parser.verbose = config.verbose
135
+
136
+ Dir["**/*.m"].each do |m_file|
137
+ next if config.only and not File.fnmatch(config.only, m_file)
138
+ next if m_file =~ /\.xdry\./
139
+ h_file = m_file.sub /\.m$/, '.h'
140
+ if File.file? h_file
141
+ puts h_file if config.verbose
142
+
143
+ parser.parse_file(h_file)
144
+ parser.parse_file(m_file)
145
+ end
146
+ end
147
+
148
+ patcher = Patcher.new
149
+ patcher.dry_run = config.dry_run
150
+ patcher.verbose = config.verbose
151
+
152
+ parser.markers.each { |marker| patcher.remove_marker! marker }
153
+
154
+ self.produce_everything(oglobal, patcher, config)
155
+
156
+ return patcher.save!
157
+ end
158
+
159
+ def self.test_run sources, config
160
+ oglobal = OGlobal.new
161
+
162
+ parser = ParsingDriver.new(oglobal)
163
+ parser.verbose = config.verbose
164
+ sources.each do |file_path, content|
165
+ parser.parse_string file_path, content
166
+ end
167
+
168
+ patcher = Patcher.new
169
+ patcher.verbose = config.verbose
170
+
171
+ parser.markers.each { |marker| patcher.remove_marker! marker }
172
+
173
+ self.produce_everything(oglobal, patcher, config)
174
+
175
+ return patcher.retrieve!
176
+ end
177
+
178
+ def self.run args
179
+ config = parse_command_line_config(args)
180
+
181
+ while Dir.pwd != '/' && Dir['*.xcodeproj'] == []
182
+ Dir.chdir('..')
183
+ end
184
+ if Dir['*.xcodeproj'] == []
185
+ puts "Cannot find *.xcodeproj in any of the parent directories. Stop."
186
+ exit 1
187
+ end
188
+
189
+ changed_file_refs = run_once config
190
+
191
+ if config.watch
192
+ require 'rubygems'
193
+ require 'fssm'
194
+ rebuild = lambda do |base, relative|
195
+ unless File.basename(relative) == 'xdry.m'
196
+ changed_file_refs = run_once(config)
197
+ unless changed_file_refs.empty?
198
+ system "growlnotify", "-a", "Xcode", "-t", "XD.R.Y.", "-m", "Updating..."
199
+ system "osascript", "-e", '
200
+ tell application "Finder" to activate
201
+ delay 0.3
202
+ tell application "Xcode" to activate
203
+ delay 0.5
204
+ tell application "System Events" to keystroke "u" using {command down}
205
+ '
206
+ system "growlnotify", "-a", "Xcode", "-t", "XD.R.Y.", "-m", "Updated!"
207
+ end
208
+ end
209
+ end
210
+ puts
211
+ puts "Monitoring for file system changes..."
212
+ FSSM.monitor '.', ['**/*.{h,m}'] do |monitor|
213
+ monitor.create &rebuild
214
+ monitor.update &rebuild
215
+ monitor.delete &rebuild
216
+ end
217
+ else
218
+ if changed_file_refs.empty?
219
+ puts "No changes."
220
+ else
221
+ puts "Modified:"
222
+ changed_file_refs.each { |ref| puts "-> #{ref.path}" }
223
+ end
224
+ end
225
+ end
226
+
227
+ end