xdry 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.
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