gooderp18绿色标准版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

339 line
14KB

  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import ast
  4. import pathlib
  5. import os
  6. import re
  7. import shutil
  8. import odoo
  9. from odoo.tools.config import config
  10. VERSION = 1
  11. DEFAULT_EXCLUDE = [
  12. "__manifest__.py",
  13. "__openerp__.py",
  14. "tests/**/*",
  15. "static/lib/**/*",
  16. "static/tests/**/*",
  17. "migrations/**/*",
  18. "upgrades/**/*",
  19. ]
  20. STANDARD_MODULES = ['web', 'web_enterprise', 'theme_common', 'base']
  21. MAX_FILE_SIZE = 25 * 2**20 # 25 MB
  22. MAX_LINE_SIZE = 100000
  23. VALID_EXTENSION = ['.py', '.js', '.xml', '.css', '.scss']
  24. class Cloc(object):
  25. def __init__(self):
  26. self.modules = {}
  27. self.code = {}
  28. self.total = {}
  29. self.errors = {}
  30. self.excluded = {}
  31. self.max_width = 70
  32. #------------------------------------------------------
  33. # Parse
  34. #------------------------------------------------------
  35. def parse_xml(self, s):
  36. s = s.strip() + "\n"
  37. # Unbalanced xml comments inside a CDATA are not supported, and xml
  38. # comments inside a CDATA will (wrongly) be considered as comment
  39. total = s.count("\n")
  40. s = re.sub("(<!--.*?-->)", "", s, flags=re.DOTALL)
  41. s = re.sub(r"\s*\n\s*", r"\n", s).lstrip()
  42. return s.count("\n"), total
  43. def parse_py(self, s):
  44. try:
  45. s = s.strip() + "\n"
  46. total = s.count("\n")
  47. lines = set()
  48. for i in ast.walk(ast.parse(s)):
  49. # we only count 1 for a long string or a docstring
  50. if hasattr(i, 'lineno'):
  51. lines.add(i.lineno)
  52. return len(lines), total
  53. except Exception:
  54. return (-1, "Syntax Error")
  55. def parse_c_like(self, s, regex):
  56. # Based on https://stackoverflow.com/questions/241327
  57. s = s.strip() + "\n"
  58. total = s.count("\n")
  59. # To avoid to use too much memory we don't try to count file
  60. # with very large line, usually minified file
  61. if max(len(l) for l in s.split('\n')) > MAX_LINE_SIZE:
  62. return -1, "Max line size exceeded"
  63. def replacer(match):
  64. s = match.group(0)
  65. return " " if s.startswith('/') else s
  66. comments_re = re.compile(regex, re.DOTALL | re.MULTILINE)
  67. s = re.sub(comments_re, replacer, s)
  68. s = re.sub(r"\s*\n\s*", r"\n", s).lstrip()
  69. return s.count("\n"), total
  70. def parse_js(self, s):
  71. return self.parse_c_like(s, r'//.*?$|(?<!\\)/\*.*?\*/|\'(\\.|[^\\\'])*\'|"(\\.|[^\\"])*"')
  72. def parse_scss(self, s):
  73. return self.parse_c_like(s, r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"')
  74. def parse_css(self, s):
  75. return self.parse_c_like(s, r'/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"')
  76. def parse(self, s, ext):
  77. if ext == '.py':
  78. return self.parse_py(s)
  79. elif ext == '.js':
  80. return self.parse_js(s)
  81. elif ext == '.xml':
  82. return self.parse_xml(s)
  83. elif ext == '.css':
  84. return self.parse_css(s)
  85. elif ext == '.scss':
  86. return self.parse_scss(s)
  87. #------------------------------------------------------
  88. # Enumeration
  89. #------------------------------------------------------
  90. def book(self, module, item='', count=(0, 0), exclude=False):
  91. if count[0] == -1:
  92. self.errors.setdefault(module, {})
  93. self.errors[module][item] = count[1]
  94. elif exclude and item:
  95. self.excluded.setdefault(module, {})
  96. self.excluded[module][item] = count
  97. else:
  98. self.modules.setdefault(module, {})
  99. if item:
  100. self.modules[module][item] = count
  101. self.code[module] = self.code.get(module, 0) + count[0]
  102. self.total[module] = self.total.get(module, 0) + count[1]
  103. self.max_width = max(self.max_width, len(module), len(item) + 4)
  104. def count_path(self, path, exclude=None):
  105. path = path.rstrip('/')
  106. exclude_list = []
  107. for i in odoo.modules.module.MANIFEST_NAMES:
  108. manifest_path = os.path.join(path, i)
  109. try:
  110. with open(manifest_path, 'rb') as manifest:
  111. exclude_list.extend(DEFAULT_EXCLUDE)
  112. d = ast.literal_eval(manifest.read().decode('latin1'))
  113. for j in ['cloc_exclude', 'demo', 'demo_xml']:
  114. exclude_list.extend(d.get(j, []))
  115. break
  116. except Exception:
  117. pass
  118. if not exclude:
  119. exclude = set()
  120. for i in filter(None, exclude_list):
  121. exclude.update(str(p) for p in pathlib.Path(path).glob(i))
  122. module_name = os.path.basename(path)
  123. self.book(module_name)
  124. for root, dirs, files in os.walk(path):
  125. for file_name in files:
  126. file_path = os.path.join(root, file_name)
  127. if file_path in exclude:
  128. continue
  129. ext = os.path.splitext(file_path)[1].lower()
  130. if ext not in VALID_EXTENSION:
  131. continue
  132. if os.path.getsize(file_path) > MAX_FILE_SIZE:
  133. self.book(module_name, file_path, (-1, "Max file size exceeded"))
  134. continue
  135. with open(file_path, 'rb') as f:
  136. # Decode using latin1 to avoid error that may raise by decoding with utf8
  137. # The chars not correctly decoded in latin1 have no impact on how many lines will be counted
  138. content = f.read().decode('latin1')
  139. self.book(module_name, file_path, self.parse(content, ext))
  140. def count_modules(self, env):
  141. # Exclude standard addons paths
  142. exclude_heuristic = [odoo.modules.get_module_path(m, display_warning=False) for m in STANDARD_MODULES]
  143. exclude_path = set([os.path.dirname(os.path.realpath(m)) for m in exclude_heuristic if m])
  144. domain = [('state', '=', 'installed')]
  145. # if base_import_module is present
  146. if env['ir.module.module']._fields.get('imported'):
  147. domain.append(('imported', '=', False))
  148. module_list = env['ir.module.module'].search(domain).mapped('name')
  149. for module_name in module_list:
  150. module_path = os.path.realpath(odoo.modules.get_module_path(module_name))
  151. if module_path:
  152. if any(module_path.startswith(i) for i in exclude_path):
  153. continue
  154. self.count_path(module_path)
  155. def count_customization(self, env):
  156. imported_module_sa = ""
  157. if env['ir.module.module']._fields.get('imported'):
  158. imported_module_sa = "OR (m.imported = TRUE AND m.state = 'installed')"
  159. query = """
  160. SELECT s.id, min(m.name), array_agg(d.module)
  161. FROM ir_act_server AS s
  162. LEFT JOIN ir_model_data AS d
  163. ON (d.res_id = s.id AND d.model = 'ir.actions.server')
  164. LEFT JOIN ir_module_module AS m
  165. ON m.name = d.module
  166. WHERE s.state = 'code' AND (m.name IS null {})
  167. GROUP BY s.id
  168. """.format(imported_module_sa)
  169. env.cr.execute(query)
  170. data = {r[0]: (r[1], r[2]) for r in env.cr.fetchall()}
  171. for a in env['ir.actions.server'].browse(data.keys()):
  172. self.book(
  173. data[a.id][0] or "odoo/studio",
  174. "ir.actions.server/%s: %s" % (a.id, a.name),
  175. self.parse_py(a.code),
  176. '__cloc_exclude__' in data[a.id][1]
  177. )
  178. imported_module_field = ("'odoo/studio'", "")
  179. if env['ir.module.module']._fields.get('imported'):
  180. imported_module_field = ("min(m.name)", "AND m.imported = TRUE AND m.state = 'installed'")
  181. # We always want to count manual compute field unless they are generated by studio
  182. # the module should be odoo/studio unless it comes from an imported module install
  183. # because manual field get an external id from the original module of the model
  184. query = r"""
  185. SELECT f.id, f.name, {}, array_agg(d.module)
  186. FROM ir_model_fields AS f
  187. LEFT JOIN ir_model_data AS d ON (d.res_id = f.id AND d.model = 'ir.model.fields')
  188. LEFT JOIN ir_module_module AS m ON m.name = d.module {}
  189. WHERE f.compute IS NOT null AND f.state = 'manual'
  190. GROUP BY f.id, f.name
  191. """.format(*imported_module_field)
  192. env.cr.execute(query)
  193. # Do not count field generated by studio
  194. all_data = env.cr.fetchall()
  195. data = {r[0]: (r[2], r[3]) for r in all_data if not ("studio_customization" in r[3] and not r[1].startswith('x_studio'))}
  196. for f in env['ir.model.fields'].browse(data.keys()):
  197. self.book(
  198. data[f.id][0] or "odoo/studio",
  199. "ir.model.fields/%s: %s" % (f.id, f.name),
  200. self.parse_py(f.compute),
  201. '__cloc_exclude__' in data[f.id][1]
  202. )
  203. if not env['ir.module.module']._fields.get('imported'):
  204. return
  205. # Count qweb view only from imported module and not studio
  206. query = """
  207. SELECT view.id, min(mod.name), array_agg(data.module)
  208. FROM ir_ui_view view
  209. INNER JOIN ir_model_data data ON view.id = data.res_id AND data.model = 'ir.ui.view'
  210. LEFT JOIN ir_module_module mod ON mod.name = data.module AND mod.imported = True
  211. WHERE view.type = 'qweb' AND data.module != 'studio_customization'
  212. GROUP BY view.id
  213. HAVING count(mod.name) > 0
  214. """
  215. env.cr.execute(query)
  216. custom_views = {r[0]: (r[1], r[2]) for r in env.cr.fetchall()}
  217. for view in env['ir.ui.view'].browse(custom_views.keys()):
  218. module_name = custom_views[view.id][0]
  219. self.book(
  220. module_name,
  221. "/%s/views/%s.xml" % (module_name, view.name),
  222. self.parse_xml(view.arch_base),
  223. '__cloc_exclude__' in custom_views[view.id][1]
  224. )
  225. # Count js, xml, css/scss file from imported module
  226. query = r"""
  227. SELECT attach.id, min(mod.name), array_agg(data.module)
  228. FROM ir_attachment attach
  229. INNER JOIN ir_model_data data ON attach.id = data.res_id AND data.model = 'ir.attachment'
  230. LEFT JOIN ir_module_module mod ON mod.name = data.module AND mod.imported = True
  231. WHERE attach.name ~ '.*\.(js|xml|css|scss)$'
  232. GROUP BY attach.id
  233. HAVING count(mod.name) > 0
  234. """
  235. env.cr.execute(query)
  236. uploaded_file = {r[0]: (r[1], r[2]) for r in env.cr.fetchall()}
  237. for attach in env['ir.attachment'].browse(uploaded_file.keys()):
  238. module_name = uploaded_file[attach.id][0]
  239. ext = os.path.splitext(attach.url)[1].lower()
  240. if ext not in VALID_EXTENSION:
  241. continue
  242. if len(attach.datas) > MAX_FILE_SIZE:
  243. self.book(module_name, attach.url, (-1, "Max file size exceeded"))
  244. continue
  245. # Decode using latin1 to avoid error that may raise by decoding with utf8
  246. # The chars not correctly decoded in latin1 have no impact on how many lines will be counted
  247. content = attach.raw.decode('latin1')
  248. self.book(
  249. module_name,
  250. attach.url,
  251. self.parse(content, ext),
  252. '__cloc_exclude__' in uploaded_file[attach.id][1],
  253. )
  254. def count_env(self, env):
  255. self.count_modules(env)
  256. self.count_customization(env)
  257. def count_database(self, database):
  258. registry = odoo.modules.registry.Registry(config['db_name'])
  259. with registry.cursor() as cr:
  260. uid = odoo.SUPERUSER_ID
  261. env = odoo.api.Environment(cr, uid, {})
  262. self.count_env(env)
  263. #------------------------------------------------------
  264. # Report
  265. #------------------------------------------------------
  266. # pylint: disable=W0141
  267. def report(self, verbose=False, width=None):
  268. # Prepare format
  269. if not width:
  270. width = min(self.max_width, shutil.get_terminal_size()[0] - 24)
  271. hr = "-" * (width + 24) + "\n"
  272. fmt = '{k:%d}{lines:>8}{other:>8}{code:>8}\n' % (width,)
  273. # Render
  274. s = fmt.format(k="Odoo cloc", lines="Line", other="Other", code="Code")
  275. s += hr
  276. for m in sorted(self.modules):
  277. s += fmt.format(k=m, lines=self.total[m], other=self.total[m]-self.code[m], code=self.code[m])
  278. if verbose:
  279. for i in sorted(self.modules[m], key=lambda i: self.modules[m][i][0], reverse=True):
  280. code, total = self.modules[m][i]
  281. s += fmt.format(k=' ' + i, lines=total, other=total - code, code=code)
  282. s += hr
  283. total = sum(self.total.values())
  284. code = sum(self.code.values())
  285. s += fmt.format(k='', lines=total, other=total - code, code=code)
  286. print(s)
  287. if self.excluded and verbose:
  288. ex = fmt.format(k="Excluded", lines="Line", other="Other", code="Code")
  289. ex += hr
  290. for m in sorted(self.excluded):
  291. for i in sorted(self.excluded[m], key=lambda i: self.excluded[m][i][0], reverse=True):
  292. code, total = self.excluded[m][i]
  293. ex += fmt.format(k=' ' + i, lines=total, other=total - code, code=code)
  294. ex += hr
  295. print(ex)
  296. if self.errors:
  297. e = "\nErrors\n\n"
  298. for m in sorted(self.errors):
  299. e += "{}\n".format(m)
  300. for i in sorted(self.errors[m]):
  301. e += fmt.format(k=' ' + i, lines=self.errors[m][i], other='', code='')
  302. print(e)
上海开阖软件有限公司 沪ICP备12045867号-1