class Docurium::CParser

Constants

DETECTORS

Array of detectors to execute in order

Public Instance Methods

cleanup_comment(comment) click to toggle source

Remove common prefix of all lines in comment. Otherwise tries to preserve formatting in case it is relevant.

# File lib/docurium/cparser.rb, line 6
def cleanup_comment(comment)
  return "" unless comment

  lines = 0
  prefixed = 0
  shortest = nil

  compacted = comment.sub(/^\n+/,"").sub(/\n\s*$/, "\n")

  compacted.split(/\n/).each do |line|
    lines += 1
    if line =~ /^\s*\*\s*$/ || line =~ /^\s*$/
      # don't count length of blank lines or lines with just a " * " on
      # them towards shortest common prefix
      prefixed += 1
      shortest = line if shortest.nil?
    elsif line =~ /^(\s*\*\s*)/
      prefixed += 1
      shortest = $1 if shortest.nil? || shortest.length > $1.length
    end
  end

  if shortest =~ /\s$/
    shortest = Regexp.quote(shortest.chop) + "[ \t]"
  elsif shortest
    shortest = Regexp.quote(shortest)
  end

  if lines == prefixed && !shortest.nil? && shortest.length > 0
    if shortest =~ /\*/
      return comment.gsub(/^#{shortest}/, "").gsub(/^\s*\*\s*\n/, "\n")
    else
      return comment.gsub(/^#{shortest}/, "")
    end
  else
    return comment
  end
end
detect_catchall(d) click to toggle source

Match otherwise unrecognized commented blocks

# File lib/docurium/cparser.rb, line 311
def detect_catchall(d)
  d[:type] = :file
  d[:decl] = ""
end
detect_define(d) click to toggle source

Match define A(B) or define A and convert a series of simple defines into an enum

# File lib/docurium/cparser.rb, line 54
def detect_define(d)
  if d[:body] =~ /\A\#\s*define\s+((\w+)\([^\)]+\))/
    d[:type] = :macro
    d[:decl] = $1.strip
    d[:name] = $2
    d[:tdef] = nil
  elsif d[:body] =~ /\A\#\s*define\s+(\w+)/
    names = []
    d[:body].scan(/\#\s*define\s+(\w+)/) { |m| names << m[0].to_s }
    d[:tdef] = nil
    names.uniq!
    if names.length == 1
      d[:type] = :define
      d[:decl] = names[0]
      d[:name] = names[0]
    elsif names.length > 1
      d[:type] = :enum
      d[:decl] = names
      d[:name] = shortest_common_prefix(names)
      d[:name].sub!(/_*$/, '')
    end
  end
end
detect_enum(d) click to toggle source

Match enum {} (and possibly a typedef thereof)

# File lib/docurium/cparser.rb, line 106
def detect_enum(d)
  if d[:body] =~ /\A(typedef)?\s*enum\s*\{([^\}]+)\}\s*([^;]+)?;/i
    typedef, values, name = $1, $2, $3
    d[:type] = :enum
    d[:decl] = values.strip.split(/\s*,\s*/).map do |v|
      v.split(/\s*=\s*/)[0].strip
    end
    if typedef.nil?
      d[:name] = shortest_common_prefix(d[:decl])
      d[:name].sub!(/_*$/, '')
      # Using the common prefix for grouping enum values is a little
      # overly aggressive in some cases.  If we ended up with too short
      # a prefix or a prefix which is too generic, then skip it.
      d[:name] = nil unless d[:name].scan('_').length > 1
    else
      d[:name] = name
    end
    d[:tdef] = typedef
  end
end
detect_function(d) click to toggle source

Match function prototypes or inline function declarations

# File lib/docurium/cparser.rb, line 191
def detect_function(d)
  if d[:body] =~ /[;\{]/
    d[:type] = :file
    d[:decl] = ""

    proto = d[:body].split(/[;\{]/, 2).first.strip
    if proto[-1] == ?)
      (proto.length - 1).downto(0) do |p|
        tail = proto[p .. -1]
        if tail.count(")") == tail.count("(")
          if proto[0..p] =~ /(\w+)\(\z/
            d[:name] = $1
            d[:type] = :function
            d[:decl] = proto
          end
          break
        end
      end
    end
  end
end
detect_struct(d) click to toggle source

Match struct {} (and typedef thereof) or opaque struct typedef

# File lib/docurium/cparser.rb, line 138
def detect_struct(d)
  if d[:body] =~ /\A(typedef)?\s*struct\s*(\w+)?\s*\{([^\}]+)\}\s*([^;]+)?/i
    typedef, name1, fields, name2 = $1, $2, $3, $4
    d[:type] = :struct
    d[:name] = typedef.nil? ? name1 : name2;
    d[:tdef] = typedef
    d[:decl] = fields.strip.split(/\s*\;\s*/).map do |x|
      x.strip.gsub(/\s+/, " ").gsub(/\(\s+/,"(")
    end
  elsif d[:body] =~ /\A(typedef)\s+struct\s+\w+\s+(\w+)/
    d[:type] = :struct
    d[:decl] = ""
    d[:name] = $2
    d[:tdef] = $1
  end
end
detect_typedef(d) click to toggle source

Match other typedefs, checking explicitly for function pointers but otherwise just trying to extract a name as simply as possible.

# File lib/docurium/cparser.rb, line 167
def detect_typedef(d)
  if d[:body] =~ /\Atypedef\s+([^;]+);/
    d[:decl] = $1.strip
    if d[:decl] =~ /\S+\s+\(\*([^\)]+)\)\(/
      d[:type] = :fnptr
      d[:name] = $1
    else
      d[:type] = :typedef
      d[:name] = d[:decl].split(/\s+/).last
    end
  end
end
join_define(text) click to toggle source

Take a multi-line define and join into a simple definition

# File lib/docurium/cparser.rb, line 79
def join_define(text)
  text = text.split("\n\n", 2).first || ""

  # Ruby 1.8 does not support negative lookbehind regex so let's
  # get the joined macro definition a slightly more awkward way
  text.split(/\s*\n\s*/).inject("\\") do |val, line|
    (val[-1] == ?\\) ? val = val.chop.strip + " " + line : val
  end.strip.gsub(/^\s*\\*\s*/, '')
end
parse_declaration_block(d) click to toggle source

Given a commented chunk of file, try to parse it.

# File lib/docurium/cparser.rb, line 334
def parse_declaration_block(d)
  # skip uncommented declarations
  return unless d[:rawComments].length > 0

  # remove inline comments in declaration
  while comment = d[:body].index("/*") do
    end_comment = d[:body].index("*/", comment)
    d[:body].slice!(comment, end_comment - comment + 2)
  end

  # if there are multiple #ifdef'ed declarations, we'll just
  # strip out the #if/#ifdef and use the first one
  d[:body].sub!(/[^\n]+\n/, '') if d[:body] =~ /\A\#\s*if/

  # try detectors until one assigns a :type to the declaration
  # it's going to be one of:
  # - :define   -> #defines + convert a series of simple ones to :enum
  # - :enum     -> (typedef)? enum { ... };
  # - :struct   -> (typedef)? struct { ... };
  # - :fnptr    -> typedef x (*fn)(...);
  # - :typedef  -> typedef x y; (not enum, struct, fnptr)
  # - :function -> rval something(like this);
  # - :file     -> everything else goes to "file" scope
  DETECTORS.find { |p| method("detect_#{p}").call(d); d.has_key?(:type) }

  # if we detected something, call a parser for that type of thing
  method("parse_#{d[:type]}").call(d) if d[:type]
end
parse_define(d) click to toggle source

Process define A … macros

# File lib/docurium/cparser.rb, line 98
def parse_define(d)
  if d[:body] =~ /define\s+#{Regexp.quote(d[:name])}[ \t]*(.*)/
    d[:value] = join_define($1)
  end
  d[:comments] = d[:rawComments].strip
end
parse_enum(d) click to toggle source

Process enum definitions

# File lib/docurium/cparser.rb, line 128
def parse_enum(d)
  if d[:decl].respond_to? :map
    d[:block] = d[:decl].map { |v| v.strip }.join("\n")
  else
    d[:block] = d[:decl]
  end
  d[:comments] = d[:rawComments].strip
end
parse_file(d) click to toggle source

Process comment blocks that are only associated with the whole file.

# File lib/docurium/cparser.rb, line 317
def parse_file(d)
  m = []
  d[:brief]    = m[1] if m = /@brief (.*?)$/.match(d[:rawComments])
  d[:defgroup] = m[1] if m = /@defgroup (.*?)$/.match(d[:rawComments])
  d[:ingroup]  = m[1] if m = /@ingroup (.*?)$/.match(d[:rawComments])
  comments = d[:rawComments].gsub(/^@.*$/, '').strip + "\n"
  if d[:comments]
    d[:comments] = d[:comments].strip + "\n\n" + comments
  else
    d[:comments] = comments
  end
end
parse_fnptr(d) click to toggle source

Process function pointer typedef definition

# File lib/docurium/cparser.rb, line 186
def parse_fnptr(d)
  d[:comments] = d[:rawComments].strip
end
parse_function(d) click to toggle source

Process function prototype and comments

# File lib/docurium/cparser.rb, line 214
def parse_function(d)
  d[:args] = []

  rval, argline = d[:decl].split(/\s*#{Regexp.quote(d[:name])}\(\s*/, 2)

  # clean up rval if it is like "extern static int" or "GIT_EXTERN(int)"
  while rval =~ /[A-Za-z0-9_]+\(([^\)]+)\)$/i
    rval = $1
  end
  rval.gsub!(/extern|static/, '')
  rval.strip!
  d[:return] = { :type => rval }

  # we removed the opening parenthesis, which this is expecting
  argline = '(' + argline
  # clean up argline
  argline = argline.slice(1..-2) while argline[0] == ?( && argline[-1] == ?)
  d[:argline] = argline.strip
  d[:args] = []
  left = 0

  # parse argline
  (0 .. argline.length).each do |i|
    next unless argline[i] == ?, || argline.length == i

    s = argline.slice(left .. i)
    next unless s.count("(") == s.count(")")

    s.chop! if argline[i] == ?,
    s.strip!

    if s =~ /\(\s*\*\s*(\w+)\s*\)\s*\(/
      argname = $1
      d[:args] << {
        :name => argname,
        :type => s.sub(/\s*#{Regexp.quote(argname)}\s*/, '').strip
      }
    elsif s =~ /\W(\w+)$/
      argname = $1
      d[:args] << {
        :name => argname,
        :type => s[0 ... - argname.length].strip,
      }
    else
      # argline is probably something like "(void)"
    end

    left = i + 1
  end

  # parse comments
  if d[:rawComments] =~ /\@(param|return)/i
    d[:args].each do |arg|
      param_comment = /\@param\s+#{Regexp.quote(arg[:name])}/.match(d[:rawComments])
      if param_comment
        after = param_comment.post_match
        end_comment = after.index(/(?:@param|@return|\Z)/)
        arg[:comment] = after[0 ... end_comment].strip.gsub(/\s+/, ' ')
      end
    end

    return_comment = /\@return\s+/.match(d[:rawComments])
    if return_comment
      after = return_comment.post_match
      d[:return][:comment] = after[0 ... after.index(/(?:@param|\Z)/)].strip.gsub(/\s+/, ' ')
    end
  else
    # support for TomDoc params
  end

  # add in inline parameter comments
  if d[:inlines] # array of [param line]/[comment] pairs
    d[:inlines].each do |inl|
      d[:args].find do |arg|
        if inl[0] =~ /\b#{Regexp.quote(arg[:name])}$/
          arg[:comment] += "\n#{inl[1]}"
        end
      end
    end
  end

  # generate function signature
  d[:sig] = d[:args].map { |a| a[:type].to_s }.join('::')

  # pull off function description
  if d[:rawComments] =~ /^\s*(public|internal|deprecated):/i
    # support for TomDoc description
  else
    desc, comments = d[:rawComments].split("\n\n", 2)
    d[:description] = desc.strip
    d[:comments] = comments || ""
    params_start = d[:comments].index(/\s?\@(?:param|return)/)
    d[:comments] = d[:comments].slice(0, params_start) if params_start
  end
end
parse_macro(d) click to toggle source

Process define A(B) macros

# File lib/docurium/cparser.rb, line 90
def parse_macro(d)
  if d[:body] =~ /define\s+#{Regexp.quote(d[:name])}\([^\)]*\)[ \t]*(.*)/m
    d[:value] = join_define($1)
  end
  d[:comments] = d[:rawComments].strip
end
parse_struct(d) click to toggle source

Process struct definition

# File lib/docurium/cparser.rb, line 156
def parse_struct(d)
  if d[:decl].respond_to? :map
    d[:block] = d[:decl].map { |v| v.strip }.join("\n")
  else
    d[:block] = d[:decl]
  end
  d[:comments] = d[:rawComments].strip
end
parse_text(filename, content) click to toggle source

Parse a chunk of text as a header file

# File lib/docurium/cparser.rb, line 364
def parse_text(filename, content)
  # break into comments and non-comments with line numbers
  content = "/** */" + content if content[0..2] != "/**"
  recs = []
  lineno = 1
  openblock = false

  content.split(/\/\*\*/).each do |chunk|
    c, b = chunk.split(/[ \t]*\*\//, 2)
    next unless c || b

    lineno += c.scan("\n").length if c

    # special handling for /**< ... */ inline comments or
    # for /** ... */ inside an open block
    if openblock || c[0] == ?<
      c = c.sub(/^</, '').strip

      so_far = recs[-1][:body]
      last_line = so_far[ so_far.rindex("\n")+1 .. -1 ].strip.chomp(",").chomp(";")
      if last_line.empty? && b =~ /^([^;]+)\;/ # apply to this line instead
        last_line = $1.strip.chomp(",").chomp(";")
      end

      if !last_line.empty?
        recs[-1][:inlines] ||= []
        recs[-1][:inlines] << [ last_line, c ]
        if b
          recs[-1][:body] += b
          lineno += b.scan("\n").length
          openblock = false if b =~ /\}/
        end
        next
      end
    end

    # make comment have a uniform " *" prefix if needed
    if c !~ /\A[ \t]*\n/ && c =~ /^(\s*\*)/
      c = $1 + c
    end

    # check for unterminated { brace (to handle inline comments later)
    openblock = true if b =~ /\{[^\}]+\Z/

    recs << {
      :file => filename,
      :line => lineno + (b.start_with?("\n") ? 1 : 0),
      :body => b,
      :rawComments => cleanup_comment(c),
    }

    lineno += b.scan("\n").length if b
  end

  # try parsers on each chunk of commented header
  recs.each do |r|
    r[:body].strip!
    r[:rawComments].strip!
    r[:lineto] = r[:line] + r[:body].scan("\n").length
    parse_declaration_block(r)
  end

  recs
end
parse_typedef(d) click to toggle source

Process typedef definition

# File lib/docurium/cparser.rb, line 181
def parse_typedef(d)
  d[:comments] = d[:rawComments].strip
end
shortest_common_prefix(arr) click to toggle source

Find the shortest common prefix of an array of strings

# File lib/docurium/cparser.rb, line 46
def shortest_common_prefix(arr)
  arr.inject do |pfx,str|
    pfx = pfx.chop while pfx != str[0...pfx.length]; pfx
  end
end