Files
CMock/lib/cmock_header_parser.rb
T
alufers 3caf511b8f fix: Don't smush macros which have an escaped empty line at the end
nanopb has a habit of generating macros which end with an escaped empty line (for example for messages which are empty). They look like this:

   #define some_msg_t_FIELDLIST(X, a) \

   #define some_msg_t_CALLBACK NULL

This caused CMock to strip all of the newlines after the backslash instead of only one, which the backslash was escaping.
This in turn caused both the macros to be in one line, causing a compile error in the generated mock.
2023-04-26 14:16:41 +02:00

628 lines
26 KiB
Ruby

# ==========================================
# 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)
@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[\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)
parse_project = {
:module_name => name.gsub(/\W/, ''),
:typedefs => [],
:functions => [],
:normalized_source => nil
}
function_names = []
all_funcs = parse_functions(import_source(source, parse_project)).map { |item| [item] }
all_funcs += parse_cpp_functions(import_source(source, parse_project, true))
all_funcs.map do |decl|
func = parse_declaration(parse_project, *decl)
unless function_names.include? func[:name]
parse_project[:functions] << func
function_names << func[:name]
end
end
parse_project[:normalized_source] = if @treat_inlines == :include
transform_inline_functions(source)
else
''
end
{ :includes => nil,
:functions => parse_project[:functions],
:typedefs => parse_project[:typedefs],
:normalized_source => parse_project[:normalized_source] }
end
private if $ThisIsOnlyATest.nil? ################
# Remove C/C++ comments from a string
# +source+:: String which will have the comments removed
def remove_comments_from_source(source)
# remove comments (block and line, in three steps to ensure correct precedence)
source.gsub!(/(?<!\*)\/\/(?:.+\/\*|\*(?:$|[^\/])).*$/, '') # remove line comments that comment out the start of blocks
source.gsub!(/\/\*.*?\*\//m, '') # remove block comments
source.gsub!(/\/\/.*$/, '') # remove line comments (all that remain)
end
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)
# Comments can contain words that will trigger the parser (static|inline|<user_defined_static_keyword>)
remove_comments_from_source(source)
# 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*\\(\n|\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|
inspected_source = ''
regex_matched = false
loop do
inline_function_match = source.match(/#{format}/) # Search for inline function declaration
if inline_function_match.nil? # No inline functions so nothing to do
# Join pre and post match stripped parts for the next inline function detection regex
source = inspected_source + source if regex_matched == true
break
end
regex_matched = true
# 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]?)/, '')
inspected_source += stripped_pre_match
source = 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
inspected_source += inline_function_match.pre_match
source = 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 declaration.
if /\A#{@function_declaration_parse_base_match}\s*\{/m =~ inline_function_match.post_match
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
inspected_source += inline_function_match.pre_match
source = inline_function_stripped
next
end
# 4. If we get here, it means the regex match, but it is not related to the function (ex. static variable in header)
# Leave this code as it is.
inspected_source += inline_function_match.pre_match + inline_function_match[0]
source = inline_function_match.post_match
end
end
source
end
def import_source(source, parse_project, 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_from_source(source)
# remove assembler pragma sections
source.gsub!(/^\s*#\s*pragma\s+asm\s+.*?#\s*pragma\s+endasm/m, '')
# remove gcc's __attribute__ tags
source.gsub!(/__attribute(?:__)?\s*\(\(+.*\)\)+/, '')
# remove preprocessor statements and extern "C"
source.gsub!(/extern\s+\"C\"\s*\{/, '')
source.gsub!(/^\s*#.*/, '')
# enums, unions, structs, and typedefs can all contain things (e.g. function pointers) that parse like function prototypes, so yank them
# forward declared structs are removed before struct definitions so they don't mess up real thing later. we leave structs keywords in function prototypes
source.gsub!(/^[\w\s]*struct[^;\{\}\(\)]+;/m, '') # remove forward declared structs
source.gsub!(/^[\w\s]*(enum|union|struct|typedef)[\w\s]*\{[^\}]+\}[\w\s\*\,]*;/m, '') # remove struct, union, and enum definitions and typedefs with braces
# remove problem keywords
source.gsub!(/(\W)(?:register|auto|restrict)(\W)/, '\1\2')
source.gsub!(/(\W)(?:static)(\W)/, '\1\2') unless cpp
source.gsub!(/\s*=\s*['"a-zA-Z0-9_\.]+\s*/, '') # remove default value statements from argument lists
source.gsub!(/^(?:[\w\s]*\W)?typedef\W[^;]*/m, '') # remove typedef statements
source.gsub!(/\)(\w)/, ') \1') # add space between parenthese and alphanumeric
source.gsub!(/(^|\W+)(?:#{@c_strippables.join('|')})(?=$|\W+)/, '\1') unless @c_strippables.empty? # remove known attributes slated to be stripped
# scan standalone function pointers and remove them, because they can just be ignored
source.gsub!(/\w+\s*\(\s*\*\s*\w+\s*\)\s*\([^)]*\)\s*;/, ';')
# scan for functions which return function pointers, because they are a pain
source.gsub!(/([\w\s\*]+)\(*\(\s*\*([\w\s\*]+)\s*\(([\w\s\*,]*)\)\)\s*\(([\w\s\*,]*)\)\)*/) do |_m|
functype = "cmock_#{parse_project[:module_name]}_func_ptr#{parse_project[:typedefs].size + 1}"
unless cpp # only collect once
parse_project[:typedefs] << "typedef #{Regexp.last_match(1).strip}(*#{functype})(#{Regexp.last_match(4)});"
"#{functype} #{Regexp.last_match(2).strip}(#{Regexp.last_match(3)});"
end
end
source = remove_nested_pairs_of_braces(source) unless cpp
if @treat_inlines == :include
# Functions having "{ }" at this point are/were inline functions,
# User wants them in so 'disguise' them as normal functions with the ";"
source.gsub!('{ }', ';')
end
# remove function definitions by stripping off the arguments right now
source.gsub!(/\([^\)]*\)\s*\{[^\}]*\}/m, ';')
# drop extra white space to make the rest go faster
source.gsub!(/^\s+/, '') # remove extra white space from beginning of line
source.gsub!(/\s+$/, '') # remove extra white space from end of line
source.gsub!(/\s*\(\s*/, '(') # remove extra white space from before left parens
source.gsub!(/\s*\)\s*/, ')') # remove extra white space from before right parens
source.gsub!(/\s+/, ' ') # remove remaining extra white space
# split lines on semicolons and remove things that are obviously not what we are looking for
src_lines = source.split(/\s*;\s*/)
src_lines = src_lines.uniq unless cpp # must retain closing braces for class/namespace
src_lines.delete_if { |line| line.strip.empty? } # remove blank lines
src_lines.delete_if { |line| !(line =~ /[\w\s\*]+\(+\s*\*[\*\s]*[\w\s]+(?:\[[\w\s]*\]\s*)+\)+\s*\((?:[\w\s\*]*,?)*\s*\)/).nil? } # remove function pointer arrays
unless @treat_externs == :include
src_lines.delete_if { |line| !(line =~ /(?:^|\s+)(?:extern)\s+/).nil? } # remove extern functions
end
unless @treat_inlines == :include
src_lines.delete_if { |line| !(line =~ /(?:^|\s+)(?:inline)\s+/).nil? } # remove inline functions
end
src_lines.delete_if(&:empty?) # drop empty lines
end
# Rudimentary C++ parser - does not handle all situations - e.g.:
# * A namespace function appears after a class with private members (should be parsed)
# * Anonymous namespace (shouldn't parse anything - no matter how nested - within it)
# * A class nested within another class
def parse_cpp_functions(source)
funcs = []
ns = []
pub = false
source.each do |line|
# Search for namespace, class, opening and closing braces
line.scan(/(?:(?:\b(?:namespace|class)\s+(?:\S+)\s*)?{)|}/).each do |item|
if item == '}'
ns.pop
else
token = item.strip.sub(/\s+/, ' ')
ns << token
pub = false if token.start_with? 'class'
pub = true if token.start_with? 'namespace'
end
end
pub = true if line =~ /public:/
pub = false if line =~ /private:/ || line =~ /protected:/
# ignore non-public and non-static
next unless pub
next unless line =~ /\bstatic\b/
line.sub!(/^.*static/, '')
next unless line =~ @declaration_parse_matcher
tmp = ns.reject { |item| item == '{' }
# Identify class name, if any
cls = nil
if tmp[-1].start_with? 'class '
cls = tmp.pop.sub(/class (\S+) {/, '\1')
end
# Assemble list of namespaces
tmp.each { |item| item.sub!(/(?:namespace|class) (\S+) {/, '\1') }
funcs << [line.strip.gsub(/\s+/, ' '), tmp, cls]
end
funcs
end
def parse_functions(source)
funcs = []
source.each { |line| funcs << line.strip.gsub(/\s+/, ' ') if line =~ @declaration_parse_matcher }
if funcs.empty?
case @when_no_prototypes
when :error
raise 'ERROR: No function prototypes found!'
when :warn
puts 'WARNING: No function prototypes found!' unless @verbosity < 1
end
end
funcs
end
def parse_type_and_name(arg)
# Split up words and remove known attributes. For pointer types, make sure
# to remove 'const' only when it applies to the pointer itself, not when it
# applies to the type pointed to. For non-pointer types, remove any
# occurrence of 'const'.
arg.gsub!(/(\w)\*/, '\1 *') # pull asterisks away from preceding word
arg.gsub!(/\*(\w)/, '* \1') # pull asterisks away from following word
arg_array = arg.split
arg_info = divine_ptr_and_const(arg)
arg_info[:name] = arg_array[-1]
attributes = arg.include?('*') ? @c_attr_noconst : @c_attributes
attr_array = []
type_array = []
arg_array[0..-2].each do |word|
if attributes.include?(word)
attr_array << word
elsif @c_calling_conventions.include?(word)
arg_info[:c_calling_convention] = word
else
type_array << word
end
end
if arg_info[:const_ptr?]
attr_array << 'const'
type_array.delete_at(type_array.rindex('const'))
end
arg_info[:modifier] = attr_array.join(' ')
arg_info[:type] = type_array.join(' ').gsub(/\s+\*/, '*') # remove space before asterisks
arg_info
end
def parse_args(arg_list)
args = []
arg_list.split(',').each do |arg|
arg.strip!
return args if arg =~ /^\s*((\.\.\.)|(void))\s*$/ # we're done if we reach void by itself or ...
arg_info = parse_type_and_name(arg)
arg_info.delete(:modifier) # don't care about this
arg_info.delete(:c_calling_convention) # don't care about this
# in C, array arguments implicitly degrade to pointers
# make the translation explicit here to simplify later logic
if @treat_as_array[arg_info[:type]] && !(arg_info[:ptr?])
arg_info[:type] = "#{@treat_as_array[arg_info[:type]]}*"
arg_info[:type] = "const #{arg_info[:type]}" if arg_info[:const?]
arg_info[:ptr?] = true
end
args << arg_info
end
# Try to find array pair in parameters following this pattern : <type> * <name>, <@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, parse_project)
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\*]+)\(+([\w\s]*)\*[\*\s]*([\w\s]*)\s*\)+\s*\(((?:[\w\s\*]*,?)*)\s*\)*/) do |_m|
functype = "cmock_#{parse_project[:module_name]}_func_ptr#{parse_project[:typedefs].size + 1}"
funcret = Regexp.last_match(1).strip
funcdecl = Regexp.last_match(2).strip
funcname = Regexp.last_match(3).strip
funcargs = Regexp.last_match(4).strip
funconst = ''
if funcname.include? 'const'
funcname.gsub!('const', '').strip!
funconst = 'const '
end
if funcdecl != ''
funcdecl += ' '
end
parse_project[:typedefs] << "typedef #{funcret}(#{funcdecl}*#{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_#{parse_project[:module_name]}_func_ptr#{parse_project[: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
parse_project[: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(parse_project, 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, parse_project)
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