java.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. #
  2. # Metrix++, Copyright 2009-2013, Metrix++ Project
  3. # Link: http://metrixplusplus.sourceforge.net
  4. #
  5. # This file is a part of Metrix++ Tool.
  6. #
  7. # Metrix++ is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, version 3 of the License.
  10. #
  11. # Metrix++ is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with Metrix++. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. import re
  20. import binascii
  21. import logging
  22. import core.api
  23. class Plugin(core.api.Plugin, core.api.Parent, core.api.IParser, core.api.IConfigurable, core.api.ICode):
  24. def declare_configuration(self, parser):
  25. parser.add_option("--std.code.java.files", default="*.java",
  26. help="Enumerates filename extensions to match Java files [default: %default]")
  27. def configure(self, options):
  28. self.files = options.__dict__['std.code.java.files'].split(',')
  29. self.files.sort() # sorted list goes to properties
  30. def initialize(self):
  31. # trigger version property set
  32. core.api.Plugin.initialize(self)
  33. db_loader = self.get_plugin_loader().get_database_loader()
  34. prev_ext = db_loader.set_property(self.get_name() + ":files", ','.join(self.files))
  35. if prev_ext != ','.join(self.files):
  36. self.is_updated = True
  37. self.get_plugin_loader().register_parser(self.files, self)
  38. def process(self, parent, data, is_updated):
  39. is_updated = is_updated or self.is_updated
  40. count_mismatched_brackets = 0
  41. if is_updated == True:
  42. count_mismatched_brackets = JavaCodeParser().run(data)
  43. self.notify_children(data, is_updated)
  44. return count_mismatched_brackets
  45. class JavaCodeParser(object):
  46. regex_cpp = re.compile(r'''
  47. //(?=\n|\r|\r\n) # Match Java style comments (empty comment line)
  48. | //.*?(?=\n|\r|\r\n) # Match Java style comments
  49. # NOTE: end of line is NOT consumed
  50. # NOTE: it is slightly different in C++
  51. | /\*\*/ # Match C style comments (empty comment line)
  52. # NOTE: it is slightly different in C++
  53. | /\*.*?\*/ # Match C style comments
  54. # NOTE: it is slightly different in C++
  55. | \'(?:\\.|[^\\\'])*\' # Match quoted strings
  56. | "(?:\\.|[^\\"])*" # Match double quoted strings
  57. | (?P<fn_name>([@]?[_$a-zA-Z][_$a-zA-Z0-9]*))\s*[(] # Match function
  58. # NOTE: Matches attributes which are excluded later
  59. # NOTE: Java may include $ in the name
  60. # LIMITATION: if there are comments after function name
  61. # and before '(', it is not detected
  62. | ((?P<block_type>class|interface) # Match class or namespace
  63. (?P<block_name>(\s+[_$a-zA-Z][_$a-zA-Z0-9]*)))
  64. # NOTE: noname instances are impossible in Java
  65. # LIMITATION: if there are comments between keyword and name,
  66. # it is not detected
  67. | [{};] # Match block start/end and statement separator
  68. # NOTE: C++ parser includes processing of <> and :
  69. # to handle template definitions, it is easier in Java
  70. | ((?:\n|\r|\r\n)\s*(?:\n|\r|\r\n)) # Match double empty line
  71. ''',
  72. re.DOTALL | re.MULTILINE | re.VERBOSE
  73. )
  74. regex_ln = re.compile(r'(\n)|(\r)|(\r\n)')
  75. def run(self, data):
  76. self.__init__() # Go to initial state if it is called twice
  77. return self.parse(data)
  78. def finalize_block(self, text, block, block_end):
  79. space_match = re.match('^\s*', text[block['start']:block_end], re.MULTILINE)
  80. block['start'] += space_match.end() # trim spaces at the beginning
  81. block['end'] = block_end
  82. start_pos = block['start']
  83. crc32 = 0
  84. for child in block['children']:
  85. # exclude children
  86. crc32 = binascii.crc32(text[start_pos:child['start']], crc32)
  87. start_pos = child['end']
  88. block['checksum'] = binascii.crc32(text[start_pos:block['end']], crc32) & 0xffffffff # to match python 3
  89. def add_lines_data(self, text, blocks):
  90. def add_lines_data_rec(self, text, blocks):
  91. for each in blocks:
  92. # add line begin
  93. self.total_current += len(self.regex_ln.findall(text, self.total_last_pos, each['start']))
  94. each['line_begin'] = self.total_current
  95. self.total_last_pos = each['start']
  96. # process enclosed
  97. add_lines_data_rec(self, text, each['children'])
  98. # add line end
  99. self.total_current += len(self.regex_ln.findall(text, self.total_last_pos, each['end']))
  100. each['line_end'] = self.total_current
  101. self.total_last_pos = each['end']
  102. self.total_last_pos = 0
  103. self.total_current = 1
  104. add_lines_data_rec(self, text, blocks)
  105. def add_regions(self, data, blocks):
  106. # Note: data.add_region() internals depend on special ordering of regions
  107. # in order to identify enclosed regions efficiently
  108. def add_regions_rec(self, data, blocks):
  109. def get_type_id(data, named_type):
  110. if named_type == "function":
  111. return data.get_region_types().FUNCTION
  112. elif named_type == "class":
  113. return data.get_region_types().CLASS
  114. elif named_type == "interface":
  115. return data.get_region_types().INTERFACE
  116. elif named_type == "__global__":
  117. return data.get_region_types().GLOBAL
  118. else:
  119. assert(False)
  120. for each in blocks:
  121. data.add_region(each['name'], each['start'], each['end'],
  122. each['line_begin'], each['line_end'], each['cursor'],
  123. get_type_id(data, each['type']), each['checksum'])
  124. add_regions_rec(self, data, each['children'])
  125. add_regions_rec(self, data, blocks)
  126. def parse(self, data):
  127. def reset_next_block(start):
  128. return {'name':'', 'start':start, 'cursor':0, 'type':''}
  129. count_mismatched_brackets = 0
  130. text = data.get_content()
  131. indent_current = 0;
  132. blocks = [{'name':'__global__', 'start':0, 'cursor':0, 'type':'__global__', 'indent_start':indent_current, 'children':[]}]
  133. curblk = 0
  134. next_block = reset_next_block(0)
  135. cursor_last_pos = 0
  136. cursor_current = 1
  137. for m in re.finditer(self.regex_cpp, text):
  138. # Comment
  139. if text[m.start()] == '/':
  140. data.add_marker(m.start(), m.end(), data.get_marker_types().COMMENT)
  141. if text[m.start():m.end()].startswith("//\n"):
  142. print text[m.start():m.end()]
  143. # String
  144. elif text[m.start()] == '"' or text[m.start()] == '\'':
  145. data.add_marker(m.start() + 1, m.end() - 1, data.get_marker_types().STRING)
  146. # Statement end
  147. elif text[m.start()] == ';':
  148. # Reset next block name and start
  149. next_block['name'] = ""
  150. next_block['start'] = m.end() # potential region start
  151. # Double end line
  152. elif text[m.start()] == '\n' or text[m.start()] == '\r':
  153. # Reset next block start, if has not been named yet
  154. if next_block['name'] == "":
  155. next_block['start'] = m.end() # potential region start
  156. # Block start...
  157. elif text[m.start()] == '{':
  158. # shift indent right
  159. indent_current += 1
  160. # ... if name detected previously
  161. if next_block['name'] != '': # - Start of enclosed block
  162. blocks.append({'name':next_block['name'],
  163. 'start':next_block['start'],
  164. 'cursor':next_block['cursor'],
  165. 'type':next_block['type'],
  166. 'indent_start':indent_current,
  167. 'children':[]})
  168. next_block = reset_next_block(m.end())
  169. curblk += 1
  170. # ... reset next block start, otherwise
  171. else: # - unknown type of block start
  172. next_block['start'] = m.end() # potential region start
  173. # Block end...
  174. elif text[m.start()] == '}':
  175. # ... if indent level matches the start
  176. if blocks[curblk]['indent_start'] == indent_current:
  177. next_block = reset_next_block(m.end())
  178. if curblk == 0:
  179. logging.warning("Non-matching closing bracket '}' detected: " + data.get_path() + ":" +
  180. str(cursor_current + len(self.regex_ln.findall(text, cursor_last_pos, m.start()))))
  181. count_mismatched_brackets += 1
  182. continue
  183. self.finalize_block(text, blocks[curblk], m.end())
  184. assert(blocks[curblk]['type'] != '__global__')
  185. curblk -= 1
  186. assert(curblk >= 0)
  187. blocks[curblk]['children'].append(blocks.pop())
  188. # shift indent left
  189. indent_current -= 1
  190. if indent_current < 0:
  191. logging.warning("Non-matching closing bracket '}' detected")
  192. count_mismatched_brackets += 1
  193. indent_current = 0
  194. # Potential class, interface
  195. elif m.group('block_type') != None:
  196. if next_block['name'] == "":
  197. # - 'name'
  198. next_block['name'] = m.group('block_name').strip()
  199. # - 'cursor'
  200. cursor_current += len(self.regex_ln.findall(text, cursor_last_pos, m.start('block_name')))
  201. cursor_last_pos = m.start('block_name')
  202. next_block['cursor'] = cursor_current
  203. # - 'type'
  204. next_block['type'] = m.group('block_type').strip()
  205. # - 'start' detected earlier
  206. # Potential function name detected...
  207. elif m.group('fn_name') != None:
  208. # ... if outside of a function
  209. # (do not detect functions enclosed directly in a function, i.e. without classes)
  210. # ... and other name before has not been matched
  211. if blocks[curblk]['type'] != 'function' and (next_block['name'] == "") and m.group('fn_name')[0] != '@':
  212. # - 'name'
  213. next_block['name'] = m.group('fn_name').strip()
  214. # - 'cursor'
  215. cursor_current += len(self.regex_ln.findall(text, cursor_last_pos, m.start('fn_name')))
  216. cursor_last_pos = m.start('fn_name')
  217. # NOTE: cursor could be collected together with line_begin, line_end,
  218. # but we keep it here separately for easier debugging of file parsing problems
  219. next_block['cursor'] = cursor_current
  220. # - 'type'
  221. next_block['type'] = 'function'
  222. # - 'start' detected earlier
  223. else:
  224. assert(len("Unknown match by regular expression") == 0)
  225. while indent_current > 0:
  226. # log all
  227. logging.warning("Non-matching opening bracket '{' detected")
  228. count_mismatched_brackets += 1
  229. indent_current -= 1
  230. for (ind, each) in enumerate(blocks):
  231. each = each # used
  232. block = blocks[len(blocks) - 1 - ind]
  233. self.finalize_block(text, block, len(text))
  234. self.add_lines_data(text, blocks)
  235. self.add_regions(data, blocks)
  236. return count_mismatched_brackets