1##
2# Abstract class representing either a method or an attribute.
3
4class RDoc::MethodAttr < RDoc::CodeObject
5
6  include Comparable
7
8  ##
9  # Name of this method/attribute.
10
11  attr_accessor :name
12
13  ##
14  # public, protected, private
15
16  attr_accessor :visibility
17
18  ##
19  # Is this a singleton method/attribute?
20
21  attr_accessor :singleton
22
23  ##
24  # Source file token stream
25
26  attr_reader :text
27
28  ##
29  # Array of other names for this method/attribute
30
31  attr_reader :aliases
32
33  ##
34  # The method/attribute we're aliasing
35
36  attr_accessor :is_alias_for
37
38  #--
39  # The attributes below are for AnyMethod only.
40  # They are left here for the time being to
41  # allow ri to operate.
42  # TODO modify ri to avoid calling these on attributes.
43  #++
44
45  ##
46  # Parameters yielded by the called block
47
48  attr_reader :block_params
49
50  ##
51  # Parameters for this method
52
53  attr_accessor :params
54
55  ##
56  # Different ways to call this method
57
58  attr_accessor :call_seq
59
60  ##
61  # The call_seq or the param_seq with method name, if there is no call_seq.
62
63  attr_reader :arglists
64
65  ##
66  # Pretty parameter list for this method
67
68  attr_reader :param_seq
69
70
71  ##
72  # Creates a new MethodAttr from token stream +text+ and method or attribute
73  # name +name+.
74  #
75  # Usually this is called by super from a subclass.
76
77  def initialize text, name
78    super()
79
80    @text = text
81    @name = name
82
83    @aliases      = []
84    @is_alias_for = nil
85    @parent_name  = nil
86    @singleton    = nil
87    @visibility   = :public
88    @see = false
89
90    @arglists     = nil
91    @block_params = nil
92    @call_seq     = nil
93    @param_seq    = nil
94    @params       = nil
95  end
96
97  ##
98  # Order by #singleton then #name
99
100  def <=>(other)
101    [     @singleton ? 0 : 1,       name] <=>
102    [other.singleton ? 0 : 1, other.name]
103  end
104
105  def == other # :nodoc:
106    super or self.class == other.class and full_name == other.full_name
107  end
108
109  ##
110  # A method/attribute is documented if any of the following is true:
111  # - it was marked with :nodoc:;
112  # - it has a comment;
113  # - it is an alias for a documented method;
114  # - it has a +#see+ method that is documented.
115
116  def documented?
117    super or
118      (is_alias_for and is_alias_for.documented?) or
119      (see and see.documented?)
120  end
121
122  ##
123  # A method/attribute to look at,
124  # in particular if this method/attribute has no documentation.
125  #
126  # It can be a method/attribute of the superclass or of an included module,
127  # including the Kernel module, which is always appended to the included
128  # modules.
129  #
130  # Returns +nil+ if there is no such method/attribute.
131  # The +#is_alias_for+ method/attribute, if any, is not included.
132  #
133  # Templates may generate a "see also ..." if this method/attribute
134  # has documentation, and "see ..." if it does not.
135
136  def see
137    @see = find_see if @see == false
138    @see
139  end
140
141  ##
142  # Sets the store for this class or module and its contained code objects.
143
144  def store= store
145    super
146
147    @file = @store.add_file @file.full_name if @file
148  end
149
150  def find_see # :nodoc:
151    return nil if singleton || is_alias_for
152
153    # look for the method
154    other = find_method_or_attribute name
155    return other if other
156
157    # if it is a setter, look for a getter
158    return nil unless name =~ /[a-z_]=$/i   # avoid == or ===
159    return find_method_or_attribute name[0..-2]
160  end
161
162  def find_method_or_attribute name # :nodoc:
163    return nil unless parent.respond_to? :ancestors
164
165    searched = parent.ancestors
166    kernel = @store.modules_hash['Kernel']
167
168    searched << kernel if kernel &&
169      parent != kernel && !searched.include?(kernel)
170
171    searched.each do |ancestor|
172      next if parent == ancestor
173      next if String === ancestor
174
175      other = ancestor.find_method_named('#' << name) ||
176              ancestor.find_attribute_named(name)
177
178      return other if other
179    end
180
181    nil
182  end
183
184  ##
185  # Abstract method. Contexts in their building phase call this
186  # to register a new alias for this known method/attribute.
187  #
188  # - creates a new AnyMethod/Attribute named <tt>an_alias.new_name</tt>;
189  # - adds +self+ as an alias for the new method or attribute
190  # - adds the method or attribute to #aliases
191  # - adds the method or attribute to +context+.
192
193  def add_alias(an_alias, context)
194    raise NotImplementedError
195  end
196
197  ##
198  # HTML fragment reference for this method
199
200  def aref
201    type = singleton ? 'c' : 'i'
202    # % characters are not allowed in html names => dash instead
203    "#{aref_prefix}-#{type}-#{html_name}"
204  end
205
206  ##
207  # Prefix for +aref+, defined by subclasses.
208
209  def aref_prefix
210    raise NotImplementedError
211  end
212
213  ##
214  # Attempts to sanitize the content passed by the ruby parser:
215  # remove outer parentheses, etc.
216
217  def block_params=(value)
218    # 'yield.to_s' or 'assert yield, msg'
219    return @block_params = '' if value =~ /^[\.,]/
220
221    # remove trailing 'if/unless ...'
222    return @block_params = '' if value =~ /^(if|unless)\s/
223
224    value = $1.strip if value =~ /^(.+)\s(if|unless)\s/
225
226    # outer parentheses
227    value = $1 if value =~ /^\s*\((.*)\)\s*$/
228    value = value.strip
229
230    # proc/lambda
231    return @block_params = $1 if value =~ /^(proc|lambda)(\s*\{|\sdo)/
232
233    # surrounding +...+ or [...]
234    value = $1.strip if value =~ /^\+(.*)\+$/
235    value = $1.strip if value =~ /^\[(.*)\]$/
236
237    return @block_params = '' if value.empty?
238
239    # global variable
240    return @block_params = 'str' if value =~ /^\$[&0-9]$/
241
242    # wipe out array/hash indices
243    value.gsub!(/(\w)\[[^\[]+\]/, '\1')
244
245    # remove @ from class/instance variables
246    value.gsub!(/@@?([a-z0-9_]+)/, '\1')
247
248    # method calls => method name
249    value.gsub!(/([A-Z:a-z0-9_]+)\.([a-z0-9_]+)(\s*\(\s*[a-z0-9_.,\s]*\s*\)\s*)?/) do
250      case $2
251      when 'to_s'      then $1
252      when 'const_get' then 'const'
253      when 'new' then
254        $1.split('::').last.  # ClassName => class_name
255          gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
256          gsub(/([a-z\d])([A-Z])/,'\1_\2').
257          downcase
258      else
259        $2
260      end
261    end
262
263    # class prefixes
264    value.gsub!(/[A-Za-z0-9_:]+::/, '')
265
266    # simple expressions
267    value = $1 if value =~ /^([a-z0-9_]+)\s*[-*+\/]/
268
269    @block_params = value.strip
270  end
271
272  ##
273  # HTML id-friendly method/attribute name
274
275  def html_name
276    require 'cgi'
277
278    CGI.escape(@name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '')
279  end
280
281  ##
282  # Full method/attribute name including namespace
283
284  def full_name
285    @full_name ||= "#{parent_name}#{pretty_name}"
286  end
287
288  def inspect # :nodoc:
289    alias_for = @is_alias_for ? " (alias for #{@is_alias_for.name})" : nil
290    visibility = self.visibility
291    visibility = "forced #{visibility}" if force_documentation
292    "#<%s:0x%x %s (%s)%s>" % [
293      self.class, object_id,
294      full_name,
295      visibility,
296      alias_for,
297    ]
298  end
299
300  ##
301  # '::' for a class method/attribute, '#' for an instance method.
302
303  def name_prefix
304    @singleton ? '::' : '#'
305  end
306
307  ##
308  # Name for output to HTML.  For class methods the full name with a "." is
309  # used like +SomeClass.method_name+.  For instance methods the class name is
310  # used if +context+ does not match the parent.
311  #
312  # This is to help prevent people from using :: to call class methods.
313
314  def output_name context
315    return "#{name_prefix}#{@name}" if context == parent
316
317    "#{parent_name}#{@singleton ? '.' : '#'}#{@name}"
318  end
319
320  ##
321  # Method/attribute name with class/instance indicator
322
323  def pretty_name
324    "#{name_prefix}#{@name}"
325  end
326
327  ##
328  # Type of method/attribute (class or instance)
329
330  def type
331    singleton ? 'class' : 'instance'
332  end
333
334  ##
335  # Path to this method for use with HTML generator output.
336
337  def path
338    "#{@parent.path}##{aref}"
339  end
340
341  ##
342  # Name of our parent with special handling for un-marshaled methods
343
344  def parent_name
345    @parent_name || super
346  end
347
348  def pretty_print q # :nodoc:
349    alias_for = @is_alias_for ? "alias for #{@is_alias_for.name}" : nil
350
351    q.group 2, "[#{self.class.name} #{full_name} #{visibility}", "]" do
352      if alias_for then
353        q.breakable
354        q.text alias_for
355      end
356
357      if text then
358        q.breakable
359        q.text "text:"
360        q.breakable
361        q.pp @text
362      end
363
364      unless comment.empty? then
365        q.breakable
366        q.text "comment:"
367        q.breakable
368        q.pp @comment
369      end
370    end
371  end
372
373  ##
374  # Used by RDoc::Generator::JsonIndex to create a record for the search
375  # engine.
376
377  def search_record
378    [
379      @name,
380      full_name,
381      @name,
382      @parent.full_name,
383      path,
384      params,
385      snippet(@comment),
386    ]
387  end
388
389  def to_s # :nodoc:
390    if @is_alias_for
391      "#{self.class.name}: #{full_name} -> #{is_alias_for}"
392    else
393      "#{self.class.name}: #{full_name}"
394    end
395  end
396
397end
398
399