# ========================================== # CMock Project - Automatic Mock Generation for C # Copyright (c) 2007 Mike Karlesky, Mark VanderVoord, Greg Williams # [Released under MIT License. Please refer to license.txt for details] # ========================================== class CMockHeaderParser attr_accessor :funcs, :c_attr_noconst, :c_attributes, :treat_as_void, :treat_externs, :treat_inlines, :inline_function_patterns def initialize(cfg) @funcs = [] @c_strippables = cfg.strippables @c_attr_noconst = cfg.attributes.uniq - ['const'] @c_attributes = ['const'] + c_attr_noconst @c_calling_conventions = cfg.c_calling_conventions.uniq @treat_as_array = cfg.treat_as_array @treat_as_void = (['void'] + cfg.treat_as_void).uniq @function_declaration_parse_base_match = '([\w\s\*\(\),\[\]]+??)\(([\w\s\*\(\),\.\[\]+\-\/]*)\)' @declaration_parse_matcher = /#{@function_declaration_parse_base_match}$/m @standards = (%w[int short char long unsigned signed] + cfg.treat_as.keys).uniq @array_size_name = cfg.array_size_name @array_size_type = (%w[int size_t] + cfg.array_size_type).uniq @when_no_prototypes = cfg.when_no_prototypes @local_as_void = @treat_as_void @verbosity = cfg.verbosity @treat_externs = cfg.treat_externs @treat_inlines = cfg.treat_inlines @inline_function_patterns = cfg.inline_function_patterns @c_strippables += ['extern'] if @treat_externs == :include # we'll need to remove the attribute if we're allowing externs @c_strippables += ['inline'] if @treat_inlines == :include # we'll need to remove the attribute if we're allowing inlines end def parse(name, source) @module_name = name.gsub(/\W/, '') @typedefs = [] @funcs = [] @normalized_source = nil function_names = [] all_funcs = parse_functions(import_source(source)).map { |item| [item] } all_funcs += parse_cpp_functions(import_source(source, true)) all_funcs.map do |decl| func = parse_declaration(*decl) unless function_names.include? func[:name] @funcs << func function_names << func[:name] end end @normalized_source = if @treat_inlines == :include transform_inline_functions(source) else '' end { :includes => nil, :functions => @funcs, :typedefs => @typedefs, :normalized_source => @normalized_source } end private if $ThisIsOnlyATest.nil? ################ def remove_nested_pairs_of_braces(source) # remove nested pairs of braces because no function declarations will be inside of them (leave outer pair for function definition detection) if RUBY_VERSION.split('.')[0].to_i > 1 # we assign a string first because (no joke) if Ruby 1.9.3 sees this line as a regex, it will crash. r = '\\{([^\\{\\}]*|\\g<0>)*\\}' source.gsub!(/#{r}/m, '{ }') else while source.gsub!(/\{[^\{\}]*\{[^\{\}]*\}[^\{\}]*\}/m, '{ }') end end source end # Return the number of pairs of braces/square brackets in the function provided by the user # +source+:: String containing the function to be processed def count_number_of_pairs_of_braces_in_function(source) is_function_start_found = false curr_level = 0 total_pairs = 0 source.each_char do |c| if c == '{' curr_level += 1 total_pairs += 1 is_function_start_found = true elsif c == '}' curr_level -= 1 end break if is_function_start_found && curr_level == 0 # We reached the end of the inline function body end if curr_level != 0 total_pairs = 0 # Something is fishy about this source, not enough closing braces? end total_pairs end # Transform inline functions to regular functions in the source by the user # +source+:: String containing the source to be processed def transform_inline_functions(source) inline_function_regex_formats = [] square_bracket_pair_regex_format = /\{[^\{\}]*\}/ # Regex to match one whole block enclosed by two square brackets # Convert user provided string patterns to regex # Use word bounderies before and after the user regex to limit matching to actual word iso part of a word @inline_function_patterns.each do |user_format_string| user_regex = Regexp.new(user_format_string) word_boundary_before_user_regex = /\b/ cleanup_spaces_after_user_regex = /[ ]*\b/ inline_function_regex_formats << Regexp.new(word_boundary_before_user_regex.source + user_regex.source + cleanup_spaces_after_user_regex.source) end # let's clean up the encoding in case they've done anything weird with the characters we might find source = source.force_encoding('ISO-8859-1').encode('utf-8', :replace => nil) # smush multiline macros into single line (checking for continuation character at end of line '\') # If the user uses a macro to declare an inline function, # smushing the macros makes it easier to recognize them as a macro and if required, # remove them later on in this function source.gsub!(/\s*\\\s*/m, ' ') # Just looking for static|inline in the gsub is a bit too aggressive (functions that are named like this, ...), so we try to be a bit smarter # Instead, look for an inline pattern (f.e. "static inline") and parse it. # Below is a small explanation on how the general mechanism works: # - Everything before the match should just be copied, we don't want # to touch anything but the inline functions. # - Remove the implementation of the inline function (this is enclosed # in square brackets) and replace it with ";" to complete the # transformation to normal/non-inline function. # To ensure proper removal of the function body, we count the number of square-bracket pairs # and remove the pairs one-by-one. # - Copy everything after the inline function implementation and start the parsing of the next inline function # There are ofcourse some special cases (inline macro declarations, inline function declarations, ...) which are handled and explained below inline_function_regex_formats.each do |format| loop do inline_function_match = source.match(/#{format}/) # Search for inline function declaration break if inline_function_match.nil? # No inline functions so nothing to do # 1. Determine if we are dealing with a user defined macro to declare inline functions # If the end of the pre-match string is a macro-declaration-like string, # we are dealing with a user defined macro to declare inline functions if /(#define\s*)\z/ =~ inline_function_match.pre_match # Remove the macro from the source stripped_pre_match = inline_function_match.pre_match.sub(/(#define\s*)\z/, '') stripped_post_match = inline_function_match.post_match.sub(/\A(.*[\n]?)/, '') source = stripped_pre_match + stripped_post_match next end # 2. Determine if we are dealing with an inline function declaration iso function definition # If the start of the post-match string is a function-declaration-like string (something ending with semicolon after the function arguments), # we are dealing with a inline function declaration if /\A#{@function_declaration_parse_base_match}\s*;/m =~ inline_function_match.post_match # Only remove the inline part from the function declaration, leaving the function declaration won't do any harm source = inline_function_match.pre_match + inline_function_match.post_match next end # 3. If we get here, we found an inline function declaration AND inline function body. # Remove the function body to transform it into a 'normal' function. total_pairs_to_remove = count_number_of_pairs_of_braces_in_function(inline_function_match.post_match) break if total_pairs_to_remove == 0 # Bad source? inline_function_stripped = inline_function_match.post_match total_pairs_to_remove.times do inline_function_stripped.sub!(/\s*#{square_bracket_pair_regex_format}/, ';') # Remove inline implementation (+ some whitespace because it's prettier) end source = inline_function_match.pre_match + inline_function_stripped # Make new source with the inline function removed and move on to the next end end source end def import_source(source, cpp = false) # let's clean up the encoding in case they've done anything weird with the characters we might find source = source.force_encoding('ISO-8859-1').encode('utf-8', :replace => nil) # void must be void for cmock _ExpectAndReturn calls to process properly, not some weird typedef which equates to void # to a certain extent, this action assumes we're chewing on pre-processed header files, otherwise we'll most likely just get stuff from @treat_as_void @local_as_void = @treat_as_void void_types = source.scan(/typedef\s+(?:\(\s*)?void(?:\s*\))?\s+([\w]+)\s*;/) if void_types @local_as_void += void_types.flatten.uniq.compact end # If user wants to mock inline functions, # remove the (user specific) inline keywords before removing anything else to avoid missing an inline function if @treat_inlines == :include @inline_function_patterns.each do |user_format_string| source.gsub!(/#{user_format_string}/, '') # remove user defined inline function patterns end end # smush multiline macros into single line (checking for continuation character at end of line '\') source.gsub!(/\s*\\\s*/m, ' ') # remove comments (block and line, in three steps to ensure correct precedence) source.gsub!(/(? * , <@array_size_type> <@array_size_name> args.each_with_index do |val, index| next_index = index + 1 next unless args.length > next_index if (val[:ptr?] == true) && args[next_index][:name].match(@array_size_name) && @array_size_type.include?(args[next_index][:type]) val[:array_data?] = true args[next_index][:array_size?] = true end end args end def divine_ptr(arg) return false unless arg.include? '*' # treat "const char *" and similar as a string, not a pointer return false if /(^|\s)(const\s+)?char(\s+const)?\s*\*(?!.*\*)/ =~ arg true end def divine_const(arg) # a non-pointer arg containing "const" is a constant # an arg containing "const" before the last * is a pointer to a constant if arg.include?('*') ? (/(^|\s|\*)const(\s(\w|\s)*)?\*(?!.*\*)/ =~ arg) : (/(^|\s)const(\s|$)/ =~ arg) true else false end end def divine_ptr_and_const(arg) divination = {} divination[:ptr?] = divine_ptr(arg) divination[:const?] = divine_const(arg) # an arg containing "const" after the last * is a constant pointer divination[:const_ptr?] = /\*(?!.*\*)\s*const(\s|$)/ =~ arg ? true : false divination end def clean_args(arg_list) if @local_as_void.include?(arg_list.strip) || arg_list.empty? 'void' else c = 0 # magically turn brackets into asterisks, also match for parentheses that come from macros arg_list.gsub!(/(\w+)(?:\s*\[[^\[\]]*\])+/, '*\1') # remove space to place asterisks with type (where they belong) arg_list.gsub!(/\s+\*/, '*') # pull asterisks away from arg to place asterisks with type (where they belong) arg_list.gsub!(/\*(\w)/, '* \1') # scan argument list for function pointers and replace them with custom types arg_list.gsub!(/([\w\s\*]+)\(+\s*\*[\*\s]*([\w\s]*)\s*\)+\s*\(((?:[\w\s\*]*,?)*)\s*\)*/) do |_m| functype = "cmock_#{@module_name}_func_ptr#{@typedefs.size + 1}" funcret = Regexp.last_match(1).strip funcname = Regexp.last_match(2).strip funcargs = Regexp.last_match(3).strip funconst = '' if funcname.include? 'const' funcname.gsub!('const', '').strip! funconst = 'const ' end @typedefs << "typedef #{funcret}(*#{functype})(#{funcargs});" funcname = "cmock_arg#{c += 1}" if funcname.empty? "#{functype} #{funconst}#{funcname}" end # scan argument list for function pointers with shorthand notation and replace them with custom types arg_list.gsub!(/([\w\s\*]+)+\s+(\w+)\s*\(((?:[\w\s\*]*,?)*)\s*\)*/) do |_m| functype = "cmock_#{@module_name}_func_ptr#{@typedefs.size + 1}" funcret = Regexp.last_match(1).strip funcname = Regexp.last_match(2).strip funcargs = Regexp.last_match(3).strip funconst = '' if funcname.include? 'const' funcname.gsub!('const', '').strip! funconst = 'const ' end @typedefs << "typedef #{funcret}(*#{functype})(#{funcargs});" funcname = "cmock_arg#{c += 1}" if funcname.empty? "#{functype} #{funconst}#{funcname}" end # automatically name unnamed arguments (those that only had a type) arg_list.split(/\s*,\s*/).map do |arg| parts = (arg.split - ['struct', 'union', 'enum', 'const', 'const*']) if (parts.size < 2) || (parts[-1][-1].chr == '*') || @standards.include?(parts[-1]) "#{arg} cmock_arg#{c += 1}" else arg end end.join(', ') end end def parse_declaration(declaration, namespace = [], classname = nil) decl = {} decl[:namespace] = namespace decl[:class] = classname regex_match = @declaration_parse_matcher.match(declaration) raise "Failed parsing function declaration: '#{declaration}'" if regex_match.nil? # grab argument list args = regex_match[2].strip # process function attributes, return type, and name parsed = parse_type_and_name(regex_match[1]) # Record original name without scope prefix decl[:unscoped_name] = parsed[:name] # Prefix name with namespace scope (if any) and then class decl[:name] = namespace.join('_') unless classname.nil? decl[:name] << '_' unless decl[:name].empty? decl[:name] << classname end # Add original name to complete fully scoped name decl[:name] << '_' unless decl[:name].empty? decl[:name] << decl[:unscoped_name] decl[:modifier] = parsed[:modifier] unless parsed[:c_calling_convention].nil? decl[:c_calling_convention] = parsed[:c_calling_convention] end rettype = parsed[:type] rettype = 'void' if @local_as_void.include?(rettype.strip) decl[:return] = { :type => rettype, :name => 'cmock_to_return', :str => "#{rettype} cmock_to_return", :void? => (rettype == 'void'), :ptr? => parsed[:ptr?] || false, :const? => parsed[:const?] || false, :const_ptr? => parsed[:const_ptr?] || false } # remove default argument statements from mock definitions args.gsub!(/=\s*[a-zA-Z0-9_\.]+\s*/, ' ') # check for var args if args =~ /\.\.\./ decl[:var_arg] = args.match(/[\w\s]*\.\.\./).to_s.strip args = if args =~ /\,[\w\s]*\.\.\./ args.gsub!(/\,[\w\s]*\.\.\./, '') else 'void' end else decl[:var_arg] = nil end args = clean_args(args) decl[:args_string] = args decl[:args] = parse_args(args) decl[:args_call] = decl[:args].map { |a| a[:name] }.join(', ') decl[:contains_ptr?] = decl[:args].inject(false) { |ptr, arg| arg[:ptr?] ? true : ptr } if decl[:return][:type].nil? || decl[:name].nil? || decl[:args].nil? || decl[:return][:type].empty? || decl[:name].empty? raise "Failed Parsing Declaration Prototype!\n" \ " declaration: '#{declaration}'\n" \ " modifier: '#{decl[:modifier]}'\n" \ " return: #{prototype_inspect_hash(decl[:return])}\n" \ " function: '#{decl[:name]}'\n" \ " args: #{prototype_inspect_array_of_hashes(decl[:args])}\n" end decl end def prototype_inspect_hash(hash) pairs = [] hash.each_pair { |name, value| pairs << ":#{name} => #{"'" if value.class == String}#{value}#{"'" if value.class == String}" } "{#{pairs.join(', ')}}" end def prototype_inspect_array_of_hashes(array) hashes = [] array.each { |hash| hashes << prototype_inspect_hash(hash) } case array.size when 0 return '[]' when 1 return "[#{hashes[0]}]" else return "[\n #{hashes.join("\n ")}\n ]\n" end end end