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.
316 lines
9.9 KiB
316 lines
9.9 KiB
# -*- coding: utf-8 -*-
|
|
######################################################################################
|
|
#
|
|
# Cybrosys Technologies Pvt. Ltd.
|
|
#
|
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
|
|
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
|
|
#
|
|
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
|
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
|
# or modified copies of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT.
|
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
# DEALINGS IN THE SOFTWARE.
|
|
#
|
|
######################################################################################
|
|
import json
|
|
import re
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
from odoo.modules import get_module_path
|
|
from . import check_odoo_python_guidelines
|
|
|
|
|
|
def violations_report(self, module):
|
|
"""
|
|
Check the violations for selected modules for PDF report.
|
|
|
|
Args:
|
|
self (obj): The odoo ClassDef.
|
|
module (str): The module technical name.
|
|
|
|
Returns:
|
|
dict: Module name and its violations
|
|
"""
|
|
violations = get_violations(module)
|
|
report_dict = {
|
|
'module': self.env.ref(f'base.module_{module}').display_name,
|
|
'violations': {
|
|
"odoo_standards": violations.get('odoo_standards_check'),
|
|
"style_lint": violations.get('style_lint_check'),
|
|
"code_quality": violations.get('code_quality_check'),
|
|
"maintainability_index": violations.get('mi_check'),
|
|
"import_sort": violations.get('import_sort_check'),
|
|
"code_format": violations.get('code_format_check'),
|
|
"code_complexity": violations.get('cc_check'),
|
|
"security_scan": violations.get('security_scan')
|
|
}
|
|
}
|
|
return report_dict
|
|
|
|
def get_violations(module):
|
|
"""
|
|
Check the violations for selected module.
|
|
|
|
Args:
|
|
module (str): The module technical name.
|
|
|
|
Returns:
|
|
dict: Selected module violations
|
|
"""
|
|
return {
|
|
"style_lint_check": check_style_lint(module),
|
|
"code_quality_check": check_code_quality(module),
|
|
"mi_check": check_maintainability_index(module),
|
|
"import_sort_check": check_import_sort(module),
|
|
"code_format_check": check_code_format(module),
|
|
"cc_check": check_code_complexity(module),
|
|
"security_scan": scan_code_security(module),
|
|
"odoo_standards_check": check_odoo_python_guidelines.check_odoo_python_standards(
|
|
name=module, path=get_module_path(module))
|
|
}
|
|
|
|
def check_style_lint(module):
|
|
"""
|
|
Check the style and lint for selected module .
|
|
|
|
Args:
|
|
module (str): The module technical name.
|
|
|
|
Returns:
|
|
list of dictionaries for the violations with the file name and line number.
|
|
"""
|
|
excluded_files = ['__init__.py', '__manifest__.py', '__pycache__', '*.pyc', '.git']
|
|
ignored_violations = ['E501', 'E301', 'E302']
|
|
|
|
venv_python = sys.executable
|
|
exclude_param = f"--exclude={','.join(excluded_files)}"
|
|
ignore_param = f"--ignore={','.join(ignored_violations)}"
|
|
module_path = get_module_path(module)
|
|
|
|
result = subprocess.run(
|
|
[venv_python, '-m', 'flake8', '--select=D', exclude_param, ignore_param, module_path],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
violations = result.stdout.strip().splitlines()
|
|
|
|
violations_list = []
|
|
for violation in violations:
|
|
parts = violation.split(":", 3)
|
|
if len(parts) >= 4:
|
|
file_path = f"{module}/{parts[0].split(f'/{module}/')[1]}"
|
|
violations_list.append({
|
|
'file_name': file_path,
|
|
'line_number': parts[1],
|
|
'violation_message': parts[3]
|
|
})
|
|
return violations_list
|
|
|
|
def check_code_quality(module):
|
|
"""
|
|
Check the code quality for selected module .
|
|
|
|
Args:
|
|
module (str): The module technical name.
|
|
|
|
Returns:
|
|
list of dictionaries for the violations with the file name, code, line and column number.
|
|
"""
|
|
pylint_result = subprocess.run([
|
|
sys.executable, '-m', 'pylint',
|
|
'--load-plugins=pylint_odoo',
|
|
'-d all',
|
|
'-e odoolint',
|
|
'--ignore-patterns=__init__.py,__manifest__.py',
|
|
get_module_path(module)
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
output = pylint_result.stdout.strip().splitlines()
|
|
result = []
|
|
for rec in output:
|
|
if rec.startswith(('*', '-')) or rec == '':
|
|
continue
|
|
else:
|
|
rec = rec.split(':')
|
|
if len(rec) >= 5:
|
|
file_path = f"{module}/{rec[0].split(f'/{module}/')[1]}"
|
|
result.append({
|
|
'file': file_path,
|
|
'line': rec[1],
|
|
'column': rec[2],
|
|
'code': rec[3].strip(),
|
|
'message': rec[4].strip(),
|
|
})
|
|
return result
|
|
|
|
def check_maintainability_index(module):
|
|
"""
|
|
Check the maintainability index for selected module .
|
|
|
|
Args:
|
|
module (str): The module technical name.
|
|
|
|
Returns:
|
|
list of dictionaries for the file name and grade.
|
|
"""
|
|
result = subprocess.run([
|
|
sys.executable, '-m', 'radon',
|
|
'mi',
|
|
get_module_path(module),
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True
|
|
)
|
|
excluded_files = ("__init__.py", "__manifest__.py")
|
|
mi_list = []
|
|
for rec in result.stdout.splitlines():
|
|
if not any(exclude in rec for exclude in excluded_files):
|
|
rec = rec.split(' - ')
|
|
if len(rec) >= 2:
|
|
file_path = f"{module}/{rec[0].split(f'/{module}/')[1]}"
|
|
mi_list.append({'file': file_path, 'grade': rec[1]})
|
|
return mi_list
|
|
|
|
def check_import_sort(module):
|
|
"""
|
|
Check the import sorting for selected module .
|
|
|
|
Args:
|
|
module (str): The module technical name.
|
|
|
|
Returns:
|
|
list of dictionaries for the file name and message.
|
|
"""
|
|
result = subprocess.run([
|
|
sys.executable, '-m', 'isort',
|
|
'--check-only',
|
|
'--skip', '__init__.py',
|
|
'--skip', '__manifest__.py',
|
|
get_module_path(module),
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
import_sort_list = []
|
|
for rec in result.stderr.splitlines():
|
|
rec = rec.replace("ERROR: ", "")
|
|
parts = rec.split(" ", 1)
|
|
if len(parts) >= 2:
|
|
file_path = f"{module}/{parts[0].split(f'/{module}/')[1]}"
|
|
import_sort_list.append({'file': file_path, 'message': parts[1]})
|
|
return import_sort_list
|
|
|
|
def parse_black_output_simple(line):
|
|
"""Simple parsing for all Black output types"""
|
|
actions = ['would reformat ', 'reformatted ', 'formatted ', 'would format ']
|
|
for action in actions:
|
|
if line.startswith(action):
|
|
filename = line[len(action):]
|
|
return action.strip(), filename
|
|
return None, None
|
|
|
|
def is_file_line(line):
|
|
"""Check if line is a file entry (not summary)"""
|
|
skip_patterns = ['💥', '💔', '!', 'files would be', 'file would be', 'All done']
|
|
actions = ['would reformat ', 'reformatted ', 'formatted ', 'would format ']
|
|
if any(pattern in line for pattern in skip_patterns):
|
|
return False
|
|
return any(line.startswith(action) for action in actions)
|
|
|
|
def check_code_format(module):
|
|
"""
|
|
Check the code format for selected module .
|
|
|
|
Args:
|
|
module (str): The module technical name.
|
|
|
|
Returns:
|
|
list of dictionaries for the file name and action.
|
|
"""
|
|
result = subprocess.run([
|
|
sys.executable, '-m', 'black',
|
|
'--check',
|
|
'--exclude', '__init__.py|__manifest__.py',
|
|
get_module_path(module),
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
|
|
code_format_list = []
|
|
for line in result.stderr.strip().split('\n'):
|
|
line = line.strip()
|
|
if not line or not is_file_line(line):
|
|
continue
|
|
parsed = parse_black_output_simple(line)
|
|
if parsed[0] is not None:
|
|
code_format_list.append(parsed)
|
|
|
|
return code_format_list
|
|
|
|
def check_code_complexity(module):
|
|
"""
|
|
Check the code complexity for selected module .
|
|
|
|
Args:
|
|
module (str): The module technical name.
|
|
|
|
Returns:
|
|
list of dictionaries for the file name and grade.
|
|
"""
|
|
result = subprocess.run([
|
|
sys.executable, '-m', 'radon',
|
|
'cc',
|
|
'--json',
|
|
get_module_path(module),
|
|
],
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
if result.returncode == 0 and result.stdout:
|
|
clean_stdout = re.sub(r'\x1b\[[0-9;]*m', '', result.stdout)
|
|
cc_dict = json.loads(clean_stdout.strip())
|
|
return cc_dict
|
|
return {}
|
|
|
|
def scan_code_security(module):
|
|
"""
|
|
Scan the code security for selected module .
|
|
|
|
Args:
|
|
module (str): The module technical name.
|
|
|
|
Returns:
|
|
list of dictionaries of the security scan.
|
|
"""
|
|
result = subprocess.run([
|
|
sys.executable, '-m', 'bandit',
|
|
'-r', get_module_path(module),
|
|
'-x', '__init__.py,__manifest__.py',
|
|
'-f', 'json'
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True)
|
|
|
|
if result.stdout:
|
|
security_list = json.loads(result.stdout).get('results', [])
|
|
return security_list
|
|
return []
|
|
|