public
Description: The official book for the Ramaze web framework
Homepage: http://book.ramaze.net
Clone URL: git://github.com/manveru/ramaze-book.git
Click here to lend your support to: ramaze-book and make a donation at www.pledgie.com !
Vincent Roy (author)
Tue Sep 29 18:28:34 -0700 2009
manveru (committer)
Mon Oct 05 11:52:12 -0700 2009
commit  111d7cb5c0614d6aed943f096dca69de36e08330
tree    540e03faa6c26f932c1eecc0058b4c87922040b1
parent  11ed18f48f5fee362308f3c52c9fd83e1eaeaaef
ramaze-book / xmpfilter.rb
27be08b6 » manveru 2009-04-13 First go at automatically a... 1 #!/usr/bin/env ruby
2 # Copyright (c) 2005-2006 Mauricio Fernandez <mfp@acm.org> http://eigenclass.org
3 # rubikitch <rubikitch@ruby-lang.org>
4 # Use and distribution subject to the terms of the Ruby license.
5
6 class XMPFilter
7 VERSION = "0.4.0"
8
9 MARKER = "!XMP#{Time.new.to_i}_#{Process.pid}_#{rand(1000000)}!"
10 XMP_RE = Regexp.new("^" + Regexp.escape(MARKER) + '\[([0-9]+)\] (=>|~>|==>) (.*)')
11 VAR = "_xmp_#{Time.new.to_i}_#{Process.pid}_#{rand(1000000)}"
12 WARNING_RE = /.*:([0-9]+): warning: (.*)/
13
14 RuntimeData = Struct.new(:results, :exceptions, :bindings)
15
16 INITIALIZE_OPTS = {:interpreter => "ruby", :options => [], :libs => [],
17 :include_paths => [], :warnings => true,
18 :use_parentheses => true}
19 def initialize(opts = {})
20 options = INITIALIZE_OPTS.merge opts
21 @interpreter = options[:interpreter]
22 @options = options[:options]
23 @libs = options[:libs]
24 @evals = options[:evals] || []
25 @include_paths = options[:include_paths]
26 @output_stdout = options[:output_stdout]
27 @dump = options[:dump]
28 @warnings = options[:warnings]
29 @parentheses = options[:use_parentheses]
30
31 @postfix = ""
32 end
33
34 def add_markers(code, min_codeline_size = 50)
35 maxlen = code.map{|x| x.size}.max
36 maxlen = [min_codeline_size, maxlen + 2].max
37 ret = ""
38 code.each do |l|
39 l = l.chomp.gsub(/ # (=>|!>).*/, "").gsub(/\s*$/, "")
40 ret << (l + " " * (maxlen - l.size) + " # =>\n")
41 end
42 ret
43 end
44
45 def annotate(code)
46 idx = 0
47 newcode = code.gsub(/^(.*) # =>.*/){|l| prepare_line($1, idx += 1) }
48 if @dump
49 File.open(@dump, "w"){|f| f.puts newcode}
50 end
51 stdout, stderr = execute(newcode)
52 output = stderr.readlines
53 runtime_data = extract_data(output)
54 idx = 0
55 annotated = code.gsub(/^(.*) # =>.*/) do |l|
56 expr = $1
57 if /^\s*#/ =~ l
58 l
59 else
60 annotated_line(l, expr, runtime_data, idx += 1)
61 end
62 end.gsub(/ # !>.*/, '').gsub(/# (>>|~>)[^\n]*\n/m, "");
63 ret = final_decoration(annotated, output)
64 if @output_stdout and (s = stdout.read) != ""
65 ret << s.inject(""){|s,line| s + "# >> #{line}".chomp + "\n" }
66 end
67 ret
68 end
69
70 def annotated_line(line, expression, runtime_data, idx)
71 "#{expression} # => " + (runtime_data.results[idx].map{|x| x[1]} || []).join(", ")
72 end
73
74 def prepare_line_annotation(expr, idx)
75 v = "#{VAR}"
76 blocal = "__#{VAR}"
77 blocal2 = "___#{VAR}"
78 # rubikitch: oneline-ized
79 # <<EOF.chomp
80 # ((#{v} = (#{expr}); $stderr.puts("#{MARKER}[#{idx}] => " + #{v}.class.to_s + " " + #{v}.inspect) || begin; $stderr.puts local_variables; local_variables.each{|#{blocal}| #{blocal2} = eval(#{blocal}); if #{v} == #{blocal2} && #{blocal} != %#{expr}.strip; $stderr.puts("#{MARKER}[#{idx}] ==> " + #{blocal}); elsif [#{blocal2}] == #{v}; $stderr.puts("#{MARKER}[#{idx}] ==> [" + #{blocal} + "]") end }; nil rescue Exception; nil end || #{v}))
81 # EOF
82 oneline_ize(<<-EOF).chomp
83 #{v} = (#{expr})
84 $stderr.puts("#{MARKER}[#{idx}] => " + #{v}.class.to_s + " " + #{v}.inspect) || begin
85 $stderr.puts local_variables
86 local_variables.each{|#{blocal}|
87 #{blocal2} = eval(#{blocal})
88 if #{v} == #{blocal2} && #{blocal} != %#{expr}.strip
89 $stderr.puts("#{MARKER}[#{idx}] ==> " + #{blocal})
90 elsif [#{blocal2}] == #{v}
91 $stderr.puts("#{MARKER}[#{idx}] ==> [" + #{blocal} + "]")
92 end
93 }
94 nil
95 rescue Exception
96 nil
97 end || #{v}
98 EOF
99
100 end
101 alias_method :prepare_line, :prepare_line_annotation
102
103 def execute_tmpfile(code)
104 stdin, stdout, stderr = (1..3).map do |i|
105 fname = "xmpfilter.tmpfile_#{Process.pid}-#{i}.rb"
106 at_exit { File.unlink fname }
107 File.open(fname, "w+")
108 end
109 stdin.puts code
110 stdin.close
111 exe_line = <<-EOF.map{|l| l.strip}.join(";")
112 $stdout.reopen('#{stdout.path}', 'w')
113 $stderr.reopen('#{stderr.path}', 'w')
114 $0.replace '#{stdin.path}'
115 ARGV.replace(#{@options.inspect})
116 load #{stdin.path.inspect}
117 #{@evals.join(";")}
118 EOF
119 system(*(interpreter_command << "-e" << exe_line))
120 [stdout, stderr]
121 end
122
123 def execute_popen(code)
124 require 'open3'
125 stdin, stdout, stderr = Open3::popen3(*interpreter_command)
126 stdin.puts code
127 @evals.each{|x| stdin.puts x } unless @evals.empty?
128 stdin.close
129 [stdout, stderr]
130 end
131
132 if /win|mingw/ =~ RUBY_PLATFORM && /darwin/ !~ RUBY_PLATFORM
133 alias_method :execute, :execute_tmpfile
134 else
135 alias_method :execute, :execute_popen
136 end
137
138 def interpreter_command
139 r = [@interpreter, "-w"]
140 r << "-d" if $DEBUG
141 r << "-I#{@include_paths.join(":")}" unless @include_paths.empty?
142 @libs.each{|x| r << "-r#{x}" } unless @libs.empty?
143 r
144 end
145
146 def extract_data(output)
147 results = Hash.new{|h,k| h[k] = []}
148 exceptions = Hash.new{|h,k| h[k] = []}
149 bindings = Hash.new{|h,k| h[k] = []}
150 output.grep(XMP_RE).each do |line|
151 result_id, op, result = XMP_RE.match(line).captures
152 case op
153 when "=>"
154 klass, value = /(\S+)\s+(.*)/.match(result).captures
155 results[result_id.to_i] << [klass, value]
156 when "~>"
157 exceptions[result_id.to_i] << result
158 when "==>"
159 bindings[result_id.to_i] << result unless result.index(VAR)
160 end
161 end
162 RuntimeData.new(results, exceptions, bindings)
163 end
164
165 def final_decoration(code, output)
166 warnings = {}
07ab3088 » manveru 2009-06-16 Fix xmpfilter for 1.9 167 output.join.lines.grep(WARNING_RE).map do |x|
27be08b6 » manveru 2009-04-13 First go at automatically a... 168 md = WARNING_RE.match(x)
169 warnings[md[1].to_i] = md[2]
170 end
171 idx = 0
07ab3088 » manveru 2009-06-16 Fix xmpfilter for 1.9 172 ret = code.lines.map do |line|
27be08b6 » manveru 2009-04-13 First go at automatically a... 173 w = warnings[idx+=1]
174 if @warnings
175 w ? (line.chomp + " # !> #{w}") : line
176 else
177 line
178 end
179 end
180 output = output.reject{|x| /^-:[0-9]+: warning/.match(x)}
181 if exception = /^-:[0-9]+:.*/m.match(output.join)
182 ret << exception[0].map{|line| "# ~> " + line }
183 end
184 ret
185 end
186
187 def oneline_ize(code)
188 "((" + code.gsub(/\r?\n|\r/, ';') + "))#{@postfix}\n"
189 end
190
191 def debugprint(*args)
192 $stderr.puts(*args) if $DEBUG
193 end
194 end # clas XMPFilter
195
196 class XMPTestUnitFilter < XMPFilter
197 def initialize(opts = {})
198 super
199 @output_stdout = false
200 end
201
202 private
203 def annotated_line(line, expression, runtime_data, idx)
204 indent = /^\s*/.match(line)[0]
205 assertions(expression.strip, runtime_data, idx).map{|x| indent + x}.join("\n")
206 end
207
208 def prepare_line(expr, idx)
209 basic_eval = prepare_line_annotation(expr, idx)
210 %|begin; #{basic_eval}; rescue Exception; $stderr.puts("#{MARKER}[#{idx}] ~> " + $!.class.to_s); end|
211 end
212
213 def assertions(expression, runtime_data, index)
214 exceptions = runtime_data.exceptions
215 ret = []
216
217 unless (vars = runtime_data.bindings[index]).empty?
218 vars.each{|var| ret << equal_assertion(var, expression) }
219 end
220 if !(wanted = runtime_data.results[index]).empty? || !exceptions[index]
221 case (wanted[0][1] rescue 1)
222 when "nil"
223 ret.concat nil_assertion(expression)
224 else
225 case wanted.size
226 when 1
227 ret.concat value_assertions(wanted[0], expression)
228 else
229 # discard values from multiple runs
230 ret.concat(["#xmpfilter: WARNING!! extra values ignored"] +
231 value_assertions(wanted[0], expression))
232 end
233 end
234 else
235 ret.concat raise_assertion(expression, exceptions, index)
236 end
237
238 ret
239 end
240
241 def nil_assertion(expression)
242 if @parentheses
243 ["assert_nil(#{expression})"]
244 else
245 ["assert_nil #{expression}"]
246 end
247 end
248
249 def raise_assertion(expression, exceptions, index)
250 ["assert_raise(#{exceptions[index][0]}){#{expression}}"]
251 end
252
253 OTHER = Class.new
254 def value_assertions(klass_value_txt_pair, expression)
255 klass_txt, value_txt = klass_value_txt_pair
256 value = eval(value_txt) || OTHER.new
257 # special cases
258 value = nil if value_txt.strip == "nil"
259 value = false if value_txt.strip == "false"
260 case value
261 when Float
262 @parentheses ? ["assert_in_delta(#{value.inspect}, #{expression}, 0.0001)"] :
263 ["assert_in_delta #{value.inspect}, #{expression}, 0.0001"]
264 when Numeric, String, Hash, Array, Regexp, TrueClass, FalseClass, Symbol, NilClass
265 @parentheses ? ["assert_equal(#{value_txt}, #{expression})"] :
266 ["assert_equal #{value_txt}, #{expression}"]
267 else
268 @parentheses ? [ "assert_kind_of(#{klass_txt}, #{expression})",
269 "assert_equal(#{value_txt.inspect}, #{expression}.inspect)" ] :
270 [ "assert_kind_of #{klass_txt}, #{expression} ",
271 "assert_equal #{value_txt.inspect}, #{expression}.inspect" ]
272 end
273 rescue Exception
274 return @parentheses ? [ "assert_kind_of(#{klass_txt}, #{expression})",
275 "assert_equal(#{value_txt.inspect}, #{expression}.inspect)" ] :
276 [ "assert_kind_of #{klass_txt}, #{expression}",
277 "assert_equal #{value_txt.inspect}, #{expression}.inspect" ]
278 end
279
280 def equal_assertion(expected, actual)
281 @parentheses ? "assert_equal(#{expected}, #{actual})" : "assert_equal #{expected}, #{actual}"
282 end
283 end
284
285 class XMPRSpecFilter < XMPTestUnitFilter
286 private
287 def execute(code)
288 codefile = "xmpfilter.rspec_tmpfile_#{Process.pid}.rb"
289 File.open(codefile, "w"){|f| f.puts code}
290 path = File.expand_path(codefile)
291 at_exit { File.unlink path }
292 stdout, stderr = (1..2).map do |i|
293 fname = "xmpfilter.rspec_tmpfile_#{Process.pid}-#{i}.rb"
294 fullname = File.expand_path(fname)
295 at_exit { File.unlink fullname }
296 File.open(fname, "w+")
297 end
298 args = *(interpreter_command << %["#{codefile}"] << "2>" <<
299 %["#{stderr.path}"] << ">" << %["#{stdout.path}"])
300 system(args.join(" "))
301 [stdout, stderr]
302 end
303
304 def interpreter_command
305 [@interpreter] + @libs.map{|x| "-r#{x}"}
306 end
307
308 def nil_assertion(expression)
309 @parentheses ? ["(#{expression}).should_be_nil"] : ["#{expression}.should_be_nil"]
310 end
311
312 def raise_assertion(expression, exceptions, index)
313 ["lambda{#{expression}}.should_raise #{exceptions[index][0]}"]
314 end
315
316 def value_assertions(klass_value_txt_pair, expression)
317 klass_txt, value_txt = klass_value_txt_pair
318 value = eval(value_txt) || OTHER.new
319 # special cases
320 value = nil if value_txt.strip == "nil"
321 value = false if value_txt.strip == "false"
322 case value
323 when Float
324 @parentheses ?
325 ["(#{expression}).should_be_close #{value.inspect}, 0.0001"] :
326 ["#{expression}.should_be_close #{value.inspect}, 0.0001"]
327 when Numeric, String, Hash, Array, Regexp, TrueClass, FalseClass, Symbol, NilClass
328 @parentheses ?
329 ["(#{expression}).should_equal #{value_txt}"] :
330 ["#{expression}.should_equal #{value_txt}"]
331 else
332 @parentheses ?
333 [ "(#{expression}).should_be_a_kind_of #{klass_txt}",
334 "(#{expression}.inspect).should_equal #{value_txt.inspect}" ] :
335 [ "#{expression}.should_be_a_kind_of #{klass_txt}",
336 "#{expression}.inspect.should_equal #{value_txt.inspect}" ]
337 end
338 rescue
339 return @parentheses ?
340 [ "(#{expression}).should_be_a_kind_of #{klass_txt}",
341 "(#{expression}.inspect).should_equal #{value_txt.inspect}" ] :
342 [ "#{expression}.should_be_a_kind_of #{klass_txt}",
343 "#{expression}.inspect.should_equal #{value_txt.inspect}" ]
344 end
345
346 def equal_assertion(expected, actual)
347 @parentheses ?
348 "(#{actual}).should_equal #{expected}" : "#{actual}.should_equal #{expected}"
349 end
350 end
351
352 require 'enumerator'
353 # Common routines for XMPCompletionFilter/XMPDocFilter
354 module ProcessParticularLine
355 def fill_literal!(expr)
356 [ "\"", "'", "`" ].each do |q|
357 expr.gsub!(/#{q}(.+)#{q}/){ '"' + "x"*$1.length + '"' }
358 end
359 expr.gsub!(/(%([wWqQxrs])?(\W))(.+?)\3/){
360 percent = $2 == 'x' ? '%'+$3 : $1 # avoid executing shell command
361 percent + "x"*$4.length + $3
362 }
363 [ %w[( )], %w[{ }], %w![ ]!, %w[< >] ].each do |b,e|
364 rb, re = [b,e].map{ |x| Regexp.quote(x)}
365 expr.gsub!(/(%([wWqQxrs])?(#{rb}))(.+)#{re}/){
366 percent = $2 == 'x' ? '%'+$3 : $1 # avoid executing shell command
367 percent + "x"*$4.length + e
368 }
369 end
370 end
371
372 module ExpressionExtension
373 attr_accessor :eval_string
374 attr_accessor :meth
375 end
376 OPERATOR_CHARS = '\|^&<>=~\+\-\*\/%\['
377 def set_expr_and_postfix!(expr, column, &regexp)
378 expr.extend ExpressionExtension
379
380 @postfix = ""
381 expr_orig = expr.clone
382 column ||= expr.length
383 last_char = expr[column-1]
384 expr.replace expr[ regexp[column] ]
385 debugprint "expr_orig=#{expr_orig}", "expr(sliced)=#{expr}"
386 right_stripped = Regexp.last_match.post_match
387 _handle_do_end right_stripped
388 aref_or_aset = aref_or_aset? right_stripped, last_char
389 debugprint "aref_or_aset=#{aref_or_aset.inspect}"
390 set_last_word! expr, aref_or_aset
391 fill_literal! expr_orig
392 _handle_brackets expr_orig, expr
393 expr << aref_or_aset if aref_or_aset
394 _handle_keywords expr_orig, column
395 debugprint "expr(processed)=#{expr}"
396 expr
397 end
398
399 def _handle_do_end(right_stripped)
400 right_stripped << "\n"
401 n_do = right_stripped.scan(/[\s\)]do\s/).length
402 n_end = right_stripped.scan(/\bend\b/).length
403 @postfix = ";begin" * (n_do - n_end)
404 end
405
406 def _handle_brackets(expr_orig, expr)
407 [ %w[{ }], %w[( )], %w![ ]! ].each do |left, right|
408 n_left = expr_orig.count(left) - expr.count(left)
409 n_right = expr_orig.count(right) - expr.count(right)
410 n = n_left - n_right
411 @postfix << ";#{left}" * n if n >= 0
412 end
413 end
414
415 def _handle_keywords(expr_orig, column)
416 %w[if unless while until for].each do |keyw|
417 pos = expr_orig.index(/\b#{keyw}\b/)
418 @postfix << ";begin" if pos and pos < column # if * xxx
419
420 pos = expr_orig.index(/;\s*#{keyw}\b/)
421 @postfix << ";begin" if pos and column < pos # * ; if xxx
422 end
423 end
424
425 def aref_or_aset?(right_stripped, last_char)
426 if last_char == ?[
427 case right_stripped
07ab3088 » manveru 2009-06-16 Fix xmpfilter for 1.9 428 when /\]\s*=/; "[]="
429 when /\]/; "[]"
27be08b6 » manveru 2009-04-13 First go at automatically a... 430 end
431 end
432 end
433
434 def set_last_word!(expr, aref_or_aset=nil)
435 debugprint "expr(before set_last_word)=#{expr}"
436 if aref_or_aset
437 opchars = ""
438 else
439 opchars = expr.slice!(/\s*[#{OPERATOR_CHARS}]+$/)
440 debugprint "expr(strip opchars)=#{expr}"
441 end
442
443 expr.replace(if expr =~ /[\"\'\`]$/ # String operations
444 "''"
445 else
446 fill_literal! expr
447 phrase = current_phrase(expr)
448 if aref_or_aset
449 expr.eval_string = expr[0..-2]
450 expr.meth = aref_or_aset
451 elsif phrase.match( /^(.+)\.(.*)$/ )
452 expr.eval_string, expr.meth = $1, $2
453 elsif opchars != ''
454 expr
455 end
456 debugprint "expr.eval_string=#{expr.eval_string}", "expr.meth=#{expr.meth}"
457 phrase
458 end << (opchars || '')) # ` font-lock hack
459 debugprint "expr(after set_last_word)=#{expr}"
460 end
461
462 def current_phrase(expr)
463 paren_level = 0
464 start = 0
465 (expr.length-1).downto(0) do |i|
466 c = expr[i,1]
467 if c =~ /[\)\}\]]/
468 paren_level += 1
469 next
470 end
471 if paren_level > 0
472 next if c =~ /[, ]/
473 else
474 break (start = i+1) if c =~ /[ ,\(\{\[]/
475 end
476 if c =~ /[\(\{\[]/
477 paren_level -= 1
478 break (start = i+1) if paren_level < 0
479 end
480 end
481 expr[start..-1]
482 end
483
484 class RuntimeDataError < RuntimeError; end
485 def runtime_data(code, lineno, column=nil)
486 newcode = code.to_a.enum_with_index.map{|line, i|
487 i+1==lineno ? prepare_line(line.chomp, column) : line
488 }.join
489 debugprint "newcode", newcode, "-"*80
490 stdout, stderr = execute(newcode)
491 output = stderr.readlines
492 debugprint "stdout", output, "-"*80
493 runtime_data = extract_data(output)
494 begin
495 runtime_data.results[1][0][1..-1].to_s
496 rescue
497 raise RuntimeDataError, runtime_data.inspect
498 end
499
500 end
501
502 end
503
504 # Nearly 100% accurate completion for any editors!!
505 # by rubikitch <rubikitch@ruby-lang.org>
506 class XMPCompletionFilter < XMPFilter
507 include ProcessParticularLine
508
509 # String completion begins with this.
510 attr :prefix
511
512 def prepare_line(expr, column)
513 set_expr_and_postfix!(expr, column){|c| /^.{#{c}}/ }
514 @prefix = expr
515 case expr
516 when /^\$\w+$/ # global variable
517 __prepare_line 'global_variables'
518 when /^@@\w+$/ # class variable
519 __prepare_line 'Module === self ? class_variables : self.class.class_variables'
520 when /^@\w+$/ # instance variable
521 __prepare_line 'instance_variables'
522 when /^([A-Z].*)::(.*)$/ # nested constants / class methods
523 @prefix = $2
524 __prepare_line "#$1.constants | #$1.methods(true)"
525 when /^[A-Z]\w*$/ # normal constants
526 __prepare_line 'Module.constants'
527 when /^::(.+)::(.*)$/ # toplevel nested constants
528 @prefix = $2
529 __prepare_line "::#$1.constants | ::#$1.methods"
530 when /^::(.*)/ # toplevel constant
531 @prefix = $1
532 __prepare_line 'Object.constants'
533 when /^(:[^:.]*)$/ # symbol
534 __prepare_line 'Symbol.all_symbols.map{|s| ":" + s.id2name}'
535 when /\.([^.]*)$/ # method call
536 @prefix = $1
537 __prepare_line "(#{Regexp.last_match.pre_match}).methods(true)"
538 else # bare words
539 __prepare_line "methods | private_methods | local_variables | self.class.constants"
540 end
541 end
542
543 def __prepare_line(all_completion_expr)
544 v = "#{VAR}"
545 idx = 1
546 oneline_ize(<<EOC)
547 #{v} = (#{all_completion_expr}).grep(/^#{Regexp.quote(@prefix)}/)
548 $stderr.puts("#{MARKER}[#{idx}] => " + #{v}.class.to_s + " " + #{v}.join(" ")) || #{v}
549 exit
550 EOC
551 end
552
553 # Array of completion candidates.
554 def candidates(code, lineno, column=nil)
555 methods = runtime_data(code, lineno, column) rescue ""
556 methods.split
557 end
558
559 # Completion code for editors.
560 def completion_code(code, lineno, column=nil)
561 candidates(code, lineno, column).join("\n")
562 end
563 end
564
565 class XMPCompletionEmacsFilter < XMPCompletionFilter
566 def completion_code(code, lineno, column=nil)
567 elisp = "(progn\n"
568 elisp << "(setq xmpfilter-method-completion-table '("
569 begin
570 candidates(code, lineno, column).each do |meth|
571 elisp << format('("%s") ', meth)
572 end
573 rescue => err
574 return %Q[(error "#{err.message}")]
575 end
576 elisp << "))\n"
577 elisp << %Q[(setq pattern "#{prefix}")\n]
578 elisp << %Q[(try-completion pattern xmpfilter-method-completion-table nil)\n]
579 elisp << ")" # /progn
580 end
581 end
582
583 # FIXME rubikitch: I do not use vim, so I cannot implement XMPCompletionVimFilter class.
584 class XMPCompletionVimFilter < XMPCompletionFilter
585 def completion_code(code, lineno, column=nil)
586 raise NotImplementedError
587 end
588 end
589
590
591 # Call Ri for any editors!!
592 # by rubikitch <rubikitch@ruby-lang.org>
593 class XMPDocFilter < XMPFilter
594 include ProcessParticularLine
595
596 def initialize(opts = {})
597 super
598 @filename = opts[:filename]
599 extend UseMethodAnalyzer if opts[:use_method_analyzer]
600 end
601
602 def prepare_line(expr, column)
603 set_expr_and_postfix!(expr, column){|c|
604 withop_re = /^.{#{c-1}}[#{OPERATOR_CHARS}]+/
605 if expr =~ withop_re
606 withop_re
607 else
608 /^.{#{c}}[\w#{OPERATOR_CHARS}]*/
609 end
610 }
611 recv = expr
612
613 # When expr already knows receiver and method,
614 return(__prepare_line :recv => expr.eval_string, :meth => expr.meth) if expr.eval_string
615
616 case expr
617 when /^(?:::)?([A-Z].*)(?:::|\.)(.*)$/ # nested constants / class methods
618 __prepare_line :klass => $1, :meth_or_constant => $2
619 when /^(?:::)?[A-Z]/ # normal constants
620 __prepare_line :klass => expr
621 when /\.([^.]*)$/ # method call
622 __prepare_line :recv => Regexp.last_match.pre_match, :meth => $1
623 when /^(.+)(\[\]=?)$/ # [], []=
624 __prepare_line :recv => $1, :meth => $2
625 when /[#{OPERATOR_CHARS}]+$/ # operator
626 __prepare_line :recv => Regexp.last_match.pre_match, :meth => $&
627 else # bare words
628 __prepare_line :recv => "self", :meth => expr
629 end
630 end
631
632 def __prepare_line(x)
633 v = "#{VAR}"
634 klass = "#{VAR}_klass"
635 flag = "#{VAR}_flag"
636 which_methods = "#{VAR}_methods"
637 ancestor_class = "#{VAR}_ancestor_class"
638 idx = 1
639 recv = x[:recv] || x[:klass] || raise(ArgumentError, "need :recv or :klass")
640 meth = x[:meth_or_constant] || x[:meth]
641 debugprint "recv=#{recv}", "meth=#{meth}"
642 if meth
643 code = <<-EOC
644 #{v} = (#{recv})
645 if Class === #{v}
646 #{flag} = #{v}.respond_to?('#{meth}') ? "." : "::"
647 #{klass} = #{v}
648 #{which_methods} = :methods
649 else
650 #{flag} = "#"
651 #{klass} = #{v}.class
652 #{which_methods} = :instance_methods
653 end
654 #{ancestor_class} = #{klass}.ancestors.delete_if{|c| c==Kernel }.find{|c| c.__send__(#{which_methods}, false).include? '#{meth}' }
655 $stderr.print("#{MARKER}[#{idx}] => " + #{v}.class.to_s + " ")
656
657 if #{ancestor_class}
658 $stderr.puts(#{ancestor_class}.to_s + #{flag} + '#{meth}')
659 else
660 [Kernel, Module, Class].each do |k|
661 if (k.instance_methods(false) + k.private_instance_methods(false)).include? '#{meth}'
662 $stderr.printf("%s#%s\\n", k, '#{meth}'); exit
663 end
664 end
665 $stderr.puts(#{v}.to_s + '::' + '#{meth}')
666 end
667 exit
668 EOC
669 else
670 code = <<-EOC
671 #{v} = (#{recv})
672 $stderr.print("#{MARKER}[#{idx}] => " + #{v}.class.to_s + " ")
673 $stderr.puts(#{v}.to_s)
674 exit
675 EOC
676 end
677 oneline_ize(code)
678 end
679
680
681 # Completion code for editors.
682 def completion_code(code, lineno, column=nil)
683 candidates(code, lineno, column).join("\n")
684 end
685
686 # overridable by module
687 def _doc(code, lineno, column)
688 end
689
690 def doc(code, lineno, column=nil)
691 _doc(code, lineno, column) or runtime_data(code, lineno, column).to_s
692 end
693
694 module UseMethodAnalyzer
695 METHOD_ANALYSIS = "method_analysis"
696 def have_method_analysis
697 File.file? METHOD_ANALYSIS
698 end
699
700 def find_method_analysis
701 here = Dir.pwd
702 oldpwd = here
703 begin
704 while ! have_method_analysis
705 Dir.chdir("..")
706 if Dir.pwd == here
707 return nil # not found
708 end
709 here = Dir.pwd
710 end
711 ensure
712 Dir.chdir oldpwd
713 end
714 yield(File.join(here, METHOD_ANALYSIS))
715 end
716
717 def _doc(code, lineno, column=nil)
718 find_method_analysis do |ma_file|
719 methods = open(ma_file, "rb"){ |f| Marshal.load(f)}
720 line = File.readlines(@filename)[lineno-1]
721 current_method = line[ /^.{#{column}}\w*/][ /\w+[\?!]?$/ ].sub(/:+/,'')
722 filename = @filename # FIXME
723 begin
724 methods[filename][lineno].grep(Regexp.new(Regexp.quote(current_method)))[0]
725 rescue NoMethodError
726 raise "doc/method_analyzer:cannot find #{current_method}"
727 end
728
729 end
730 end
731 end
732
733 end
734
735 # ReFe is so-called `Japanese Ri'.
736 class XMPReFeFilter < XMPDocFilter
737 def doc(code, lineno, column=nil)
738 "refe '#{super}'"
739 end
740 end
741
742 class XMPRiFilter < XMPDocFilter
743 def doc(code, lineno, column=nil)
744 "ri '#{super}'"
745 end
746 end
747
748 class XMPRiEmacsFilter < XMPDocFilter
749 def doc(code, lineno, column=nil)
750 begin
751 %!(xmpfilter-find-tag-or-ri "#{super}")!
752 rescue => err
753 return %Q[(error "#{err.message}")]
754 end
755 end
756 end
757
758 # FIXME rubikitch: I do not use vim, so I cannot implement XMPRiVimFilter class.
759 class XMPRiVimFilter < XMPRiFilter
760 def doc(code, lineno, column=nil)
761 end
762 end
763
764 #{{{ Main code
765 if __FILE__ == $0
766 require 'optparse'
767 require 'ostruct'
768
769 options = OpenStruct.new
770 options.interpreter = "ruby"
771 options.options = ""
772 options.mode = :annotation
773 options.min_codeline_size = 50
774 options.libs = []
775 options.evals = []
776 options.include_paths = []
777 options.debug = nil
778 options.wd = nil
779 options.warnings = true
780 options.poetry = false
781 options.column = nil
782 options.output_stdout = true
783
784 rails_settings = false
785
786 opts = OptionParser.new do |opts|
787 opts.banner = "Usage: xmpfilter.rb [options] [inputfile] [-- cmdline args]"
788
789 opts.separator ""
790 opts.separator "Modes:"
791 opts.on("-a", "--annotations", "Annotate code (default)") do
792 options.mode = :annotation
793 end
794 opts.on("-u", "--unittest", "Complete Test::Unit assertions.") do
795 options.mode = :unittest
796 end
797 opts.on("-s", "--spec", "Complete RSpec expectations.") do
798 options.mode = :rspec
799 options.interpreter = "spec"
800 end
801 opts.on("-m", "--markers", "Add # => markers.") do
802 options.mode = :marker
803 end
804 opts.on("-C", "--completion", "List completion candidates.") do
805 options.mode = :completion
806 end
807
808 opts.on("--completion-emacs", "Generate completion code for Emacs.") do
809 options.mode = :completion_emacs
810 end
811 opts.on("--completion-vim", "Generate completion code for Vim.") do
812 options.mode = :completion_vim
813 end
814
815 opts.on("-D", "--doc", "Print callee method with class.") do
816 options.mode = :doc
817 end
818 opts.on("--refe", "Refe callee method.") do
819 options.mode = :refe
820 end
821 opts.on("--ri", "Ri callee method.") do
822 options.mode = :ri
823 end
824 opts.on("--ri-emacs", "Generate ri code for emacs.") do
825 options.mode = :ri_emacs
826 end
827 opts.on("--ri-vim", "Generate ri code for vim.") do
828 options.mode = :ri_vim
829 end
830
831
832 opts.separator ""
833 opts.separator "Completion and documentation options:"
834 opts.on("--line=LINE", "Current line number.") do |n|
835 options.lineno = n.to_i
836 end
837 opts.on("--column=COLUMN", "Current column number.") do |n|
838 options.column = n.to_i
839 end
840 opts.on("--use-method-analyzer", "") do |n|
841 options.use_method_analyzer = true
842 end
843 opts.on("--filename=FILENAME") do |n|
844 options.filename = File.expand_path n
845 end
846
847 opts.separator ""
848 opts.separator "Interpreter options:"
849 opts.on("-I PATH", "Add PATH to $LOAD_PATH") do |path|
850 options.include_paths << path
851 end
852 opts.on("-r LIB", "Require LIB before execution.") do |lib|
853 options.libs << lib
854 end
855 opts.on("-e EXPR", "--eval=EXPR", "--stub=EXPR", "Evaluate EXPR after execution.") do |expr|
856 options.evals << expr
857 end
858
859 opts.separator ""
860 opts.on("--cd DIR", "Change working directory to DIR.") do |dir|
861 options.wd = dir
862 end
863 opts.on("--debug", "Write transformed source code to xmp-tmp.PID.rb.") do
864 options.debug = "xmp-tmp.#{Process.pid}.rb"
865 end
866 opts.on("-S FILE", "--interpreter FILE", "Use interpreter FILE.") do |interpreter|
867 options.interpreter = interpreter
868 end
869 opts.on("-l N", "--min-line-length N", Integer, "Align markers to N spaces.") do |min_codeline_size|
870 options.min_codeline_size = min_codeline_size
871 end
872 opts.on("--rails", "Setting appropriate for Rails.",
873 "(no warnings, find working directory,",
874 " Test::Unit assertions)") do
875 options.warnings = false
876 options.mode = :unittest
877 rails_settings = true
878 end
879 opts.on("--[no-]poetry", "Whether to use extra parentheses.",
880 "(default: use them)") do |poetry_p|
881 options.poetry = poetry_p
882 end
883 opts.on("--[no-]warnings", "Whether to add warnings (# !>).",
884 "(default: enabled)") {|warnings_p| options.warnings = warnings_p }
885 opts.on("-q", "--quiet", "Supress standard output.") do
886 options.output_stdout = false
887 end
888 opts.separator ""
889 opts.on("-h", "--help", "Show this message") do
890 puts opts
891 exit
892 end
893 opts.on("-v", "--version", "Show version information") do
894 puts "xmpfilter.rb #{XMPFilter::VERSION}"
895 exit
896 end
897 end
898
899 if idx = ARGV.index("--")
900 extra_opts = ARGV[idx+1..-1]
901 ARGV.replace ARGV[0...idx]
902 else
903 extra_opts = []
904 end
905 opts.parse!(ARGV)
906
907 if rails_settings && !options.wd
908 if File.exist? ARGF.path
909 options.wd = File.dirname(ARGF.path)
910 elsif File.exist? "test/unit"
911 options.wd = "test/unit"
912 elsif File.exist? "unit"
913 options.wd = "unit"
914 end
915 end
916 targetcode = ARGF.read
917 Dir.chdir options.wd if options.wd
918
919 klass = case options.mode
920 when :annotation; XMPFilter
921 when :unittest; XMPTestUnitFilter
922 when :rspec; XMPRSpecFilter
923 when :completion; XMPCompletionFilter
924 when :completion_emacs; XMPCompletionEmacsFilter
925 when :completion_vim; XMPCompletionVimFilter
926 when :doc; XMPDocFilter
927 when :refe; XMPReFeFilter
928 when :ri; XMPRiFilter
929 when :ri_emacs; XMPRiEmacsFilter
930 when :ri_vim; XMPRiVimFilter
931 else XMPFilter
932 end
933
934 xmp = klass.new(:interpreter => options.interpreter, :options => extra_opts,
935 :output_stdout => options.output_stdout,
936 :include_paths => options.include_paths, :libs => options.libs,
937 :evals => options.evals,
938 :use_method_analyzer => options.use_method_analyzer,
939 :filename => options.filename,
940 :dump => options.debug, :warnings => options.warnings,
941 :use_parentheses => !options.poetry)
942
943 case options.mode
07ab3088 » manveru 2009-06-16 Fix xmpfilter for 1.9 944 when :marker ; puts xmp.add_markers(targetcode, options.min_codeline_size)
27be08b6 » manveru 2009-04-13 First go at automatically a... 945 when :annotation, :unittest, :rspec
946 puts xmp.annotate(targetcode)
947 when :completion, :completion_emacs, :completion_vim
948 puts xmp.completion_code(targetcode, options.lineno, options.column)
949 when :doc, :refe, :ri, :ri_emacs, :ri_vim
950 puts xmp.doc(targetcode, options.lineno, options.column)
951 else
952 puts opts
953 exit
954 end
955 end