class Docurium::CParser
Constants
- DETECTORS
Array of detectors to execute in order
Public Instance Methods
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
Match otherwise unrecognized commented blocks
# File lib/docurium/cparser.rb, line 311 def detect_catchall(d) d[:type] = :file d[:decl] = "" end
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
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
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
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
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
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
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
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
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
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
Process function pointer typedef definition
# File lib/docurium/cparser.rb, line 186 def parse_fnptr(d) d[:comments] = d[:rawComments].strip end
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
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
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 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
Process typedef definition
# File lib/docurium/cparser.rb, line 181 def parse_typedef(d) d[:comments] = d[:rawComments].strip end
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