From 079f6e78a1205c46fe79289edd5fc41aadc03cc4 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 5 Nov 2025 18:14:44 +0100 Subject: [PATCH 01/37] Move stuff in platform_helper --- sonar/platform.py | 42 ++--------------------- sonar/util/platform_helper.py | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 40 deletions(-) create mode 100644 sonar/util/platform_helper.py diff --git a/sonar/platform.py b/sonar/platform.py index fe0086a2..419ea789 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -40,6 +40,7 @@ import sonar.utilities as util from sonar.util import types, update_center import sonar.util.constants as c +import sonar.util.platform_helper as pfhelp from sonar import errcodes, settings, devops, version, sif, exceptions, organizations from sonar.permissions import permissions, global_permissions, permission_templates @@ -262,7 +263,7 @@ def delete(self, api: str, params: types.ApiParams = None, **kwargs) -> requests def __run_request(self, request: callable, api: str, params: types.ApiParams = None, **kwargs) -> requests.Response: """Makes an HTTP request to SonarQube""" mute = kwargs.pop("mute", ()) - api = _normalize_api(api) + api = pfhelp.normalize_api(api) headers = {"user-agent": self._user_agent, "accept": _APP_JSON} | kwargs.get("headers", {}) params = params or {} with_org = kwargs.pop("with_organization", True) @@ -791,19 +792,6 @@ def set_standard_experience(self) -> bool: this.context = Platform(os.getenv("SONAR_HOST_URL", "http://localhost:9000"), os.getenv("SONAR_TOKEN", "")) -def _normalize_api(api: str) -> str: - """Normalizes an API based on its multiple original forms""" - if api.startswith("/api/"): - pass - elif api.startswith("api/"): - api = "/" + api - elif api.startswith("/"): - api = "/api" + api - else: - api = "/api/" + api - return api - - def _audit_setting_value(key: str, platform_settings: dict[str, Any], audit_settings: types.ConfigSettings, url: str) -> list[Problem]: """Audits a particular platform setting is set to expected value""" if (v := _get_multiple_values(4, audit_settings[key], "MEDIUM", "CONFIGURATION")) is None: @@ -963,29 +951,3 @@ def audit(endpoint: Platform, audit_settings: types.ConfigSettings, **kwargs) -> pbs = endpoint.audit(audit_settings) "write_q" in kwargs and kwargs["write_q"].put(pbs) return pbs - - -def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: - """Converts sonar-config "plaform" section old JSON report format to new format""" - if "plugins" in old_json: - old_json["plugins"] = util.dict_to_list(old_json["plugins"], "key") - return old_json - - -def global_settings_old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: - """Converts sonar-config "globalSettings" section old JSON report format to new format""" - new_json = {} - special_categories = (settings.LANGUAGES_SETTINGS, settings.DEVOPS_INTEGRATION, "permissions", "permissionTemplates") - for categ in [cat for cat in settings.CATEGORIES if cat not in special_categories]: - new_json[categ] = util.sort_list_by_key(util.dict_to_list(old_json[categ], "key"), "key") - for k, v in old_json[settings.LANGUAGES_SETTINGS].items(): - new_json[settings.LANGUAGES_SETTINGS] = new_json.get(settings.LANGUAGES_SETTINGS, None) or {} - new_json[settings.LANGUAGES_SETTINGS][k] = util.sort_list_by_key(util.dict_to_list(v, "key"), "key") - new_json[settings.LANGUAGES_SETTINGS] = util.dict_to_list(new_json[settings.LANGUAGES_SETTINGS], "language", "settings") - new_json[settings.DEVOPS_INTEGRATION] = util.dict_to_list(old_json[settings.DEVOPS_INTEGRATION], "key") - new_json["permissions"] = util.perms_to_list(old_json["permissions"]) - for v in old_json["permissionTemplates"].values(): - if "permissions" in v: - v["permissions"] = util.perms_to_list(v["permissions"]) - new_json["permissionTemplates"] = util.dict_to_list(old_json["permissionTemplates"], "key") - return new_json diff --git a/sonar/util/platform_helper.py b/sonar/util/platform_helper.py new file mode 100644 index 00000000..0ec6f56a --- /dev/null +++ b/sonar/util/platform_helper.py @@ -0,0 +1,63 @@ +# +# sonar-tools +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +"""Helper tools for the Platform object""" + +from typing import Any +import settings +import utilities as util + + +def normalize_api(api: str) -> str: + """Normalizes an API based on its multiple original forms""" + if api.startswith("/api/"): + pass + elif api.startswith("api/"): + api = "/" + api + elif api.startswith("/"): + api = "/api" + api + else: + api = "/api/" + api + return api + + +def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: + """Converts sonar-config "plaform" section old JSON report format to new format""" + if "plugins" in old_json: + old_json["plugins"] = util.dict_to_list(old_json["plugins"], "key") + return old_json + + +def global_settings_old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: + """Converts sonar-config "globalSettings" section old JSON report format to new format""" + new_json = {} + special_categories = (settings.LANGUAGES_SETTINGS, settings.DEVOPS_INTEGRATION, "permissions", "permissionTemplates") + for categ in [cat for cat in settings.CATEGORIES if cat not in special_categories]: + new_json[categ] = util.sort_list_by_key(util.dict_to_list(old_json[categ], "key"), "key") + for k, v in old_json[settings.LANGUAGES_SETTINGS].items(): + new_json[settings.LANGUAGES_SETTINGS] = new_json.get(settings.LANGUAGES_SETTINGS, None) or {} + new_json[settings.LANGUAGES_SETTINGS][k] = util.sort_list_by_key(util.dict_to_list(v, "key"), "key") + new_json[settings.LANGUAGES_SETTINGS] = util.dict_to_list(new_json[settings.LANGUAGES_SETTINGS], "language", "settings") + new_json[settings.DEVOPS_INTEGRATION] = util.dict_to_list(old_json[settings.DEVOPS_INTEGRATION], "key") + new_json["permissions"] = util.perms_to_list(old_json["permissions"]) + for v in old_json["permissionTemplates"].values(): + if "permissions" in v: + v["permissions"] = util.perms_to_list(v["permissions"]) + new_json["permissionTemplates"] = util.dict_to_list(old_json["permissionTemplates"], "key") + return new_json From 7b778f8a1cf3943f853002841a6bbed2d26e876c Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 5 Nov 2025 19:07:32 +0100 Subject: [PATCH 02/37] Use helpers and change conversion function names --- cli/config.py | 23 ++++---- sonar/applications.py | 4 +- sonar/groups.py | 2 +- sonar/portfolios.py | 60 +++------------------ sonar/projects.py | 67 ++--------------------- sonar/qualitygates.py | 2 +- sonar/qualityprofiles.py | 67 +++-------------------- sonar/rules.py | 4 +- sonar/users.py | 2 +- sonar/util/platform_helper.py | 8 +-- sonar/util/portfolio_helper.py | 44 +++++++++++++++ sonar/util/project_helper.py | 84 +++++++++++++++++++++++++++++ sonar/util/qualityprofile_helper.py | 65 ++++++++++++++++++++++ 13 files changed, 235 insertions(+), 197 deletions(-) create mode 100644 sonar/util/portfolio_helper.py create mode 100644 sonar/util/project_helper.py create mode 100644 sonar/util/qualityprofile_helper.py diff --git a/cli/config.py b/cli/config.py index 56230a6f..42694b52 100644 --- a/cli/config.py +++ b/cli/config.py @@ -32,6 +32,11 @@ from cli import options from sonar import exceptions, errcodes, utilities, version from sonar.util import types, constants as c +from sonar.util import platform_helper as pfhelp +from sonar.util import project_helper as pjhelp +from sonar.util import portfolio_helper as foliohelp +from sonar.util import qualityprofile_helper as qphelp + import sonar.logging as log from sonar import platform, rules, qualityprofiles, qualitygates, users, groups from sonar import projects, portfolios, applications @@ -309,16 +314,16 @@ def convert_json(**kwargs) -> dict[str, Any]: with open(kwargs["convertFrom"], encoding="utf-8") as fd: old_json = json.loads(fd.read()) mapping = { - "platform": platform.old_to_new_json, - "globalSettings": platform.global_settings_old_to_new_json, - "qualityProfiles": qualityprofiles.old_to_new_json, - "qualityGates": qualitygates.old_to_new_json, - "projects": projects.old_to_new_json, - "portfolios": portfolios.old_to_new_json, + "platform": pfhelp.convert_basics_json, + "globalSettings": pfhelp.convert_global_settings_json, + "qualityProfiles": qphelp.convert_qps_json, + "qualityGates": qualitygates.convert_qgs_json, + "projects": pjhelp.convert_projects_json, + "portfolios": foliohelp.convert_portfolios_json, "applications": applications.old_to_new_json, - "users": users.old_to_new_json, - "groups": groups.old_to_new_json, - "rules": rules.old_to_new_json, + "users": users.convert_users_json, + "groups": groups.convert_groups_json, + "rules": rules.convert_rules_json, } new_json = {} for k, func in mapping.items(): diff --git a/sonar/applications.py b/sonar/applications.py index 42745e5c..099625c2 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -518,7 +518,7 @@ def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwarg """ check_supported(endpoint) write_q = kwargs.get("write_q", None) - key_regexp = kwargs.get("key_list", ".*") + key_regexp = kwargs.get("key_list", ".+") app_list = {k: v for k, v in get_list(endpoint).items() if not key_regexp or re.match(key_regexp, k)} apps_settings = [] @@ -547,7 +547,7 @@ def audit(endpoint: pf.Platform, audit_settings: types.ConfigSettings, **kwargs) return [] log.info("--- Auditing applications ---") problems = [] - key_regexp = kwargs.get("key_list", None) or ".*" + key_regexp = kwargs.get("key_list", ".+") for obj in [o for o in get_list(endpoint).values() if not key_regexp or re.match(key_regexp, o.key)]: problems += obj.audit(audit_settings, **kwargs) return problems diff --git a/sonar/groups.py b/sonar/groups.py index 096908a6..c57f89c3 100644 --- a/sonar/groups.py +++ b/sonar/groups.py @@ -434,6 +434,6 @@ def exists(endpoint: pf.Platform, name: str) -> bool: return Group.get_object(endpoint=endpoint, name=name) is not None -def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: +def convert_groups_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts sonar-config old groups JSON report format to new format""" return util.dict_to_list(old_json, "name", "description") diff --git a/sonar/portfolios.py b/sonar/portfolios.py index 1b8f4a9b..39311205 100644 --- a/sonar/portfolios.py +++ b/sonar/portfolios.py @@ -455,26 +455,20 @@ def set_manual_mode(self) -> Portfolio: def set_tags_mode(self, tags: list[str], branch: Optional[str] = None) -> Portfolio: """Sets a portfolio to tags mode""" - if branch is None: - branch = c.DEFAULT_BRANCH - self.post("views/set_tags_mode", params={"portfolio": self.key, "tags": util.list_to_csv(tags), "branch": get_api_branch(branch)}) - self._selection_mode = {_SELECTION_MODE_TAGS: tags, "branch": branch} + self.post("views/set_tags_mode", params={"portfolio": self.key, "tags": util.list_to_csv(tags), "branch": branch}) + self._selection_mode = {_SELECTION_MODE_TAGS: tags, "branch": branch or c.DEFAULT_BRANCH} return self def set_regexp_mode(self, regexp: str, branch: Optional[str] = None) -> Portfolio: """Sets a portfolio to regexp mode""" - if branch is None: - branch = c.DEFAULT_BRANCH - self.post("views/set_regexp_mode", params={"portfolio": self.key, "regexp": regexp, "branch": get_api_branch(branch)}) - self._selection_mode = {_SELECTION_MODE_REGEXP: regexp, "branch": branch} + self.post("views/set_regexp_mode", params={"portfolio": self.key, "regexp": regexp, "branch": branch}) + self._selection_mode = {_SELECTION_MODE_REGEXP: regexp, "branch": branch or c.DEFAULT_BRANCH} return self def set_remaining_projects_mode(self, branch: Optional[str] = None) -> Portfolio: """Sets a portfolio to remaining projects mode""" - if branch is None: - branch = c.DEFAULT_BRANCH - self.post("views/set_remaining_projects_mode", params={"portfolio": self.key, "branch": get_api_branch(branch)}) - self._selection_mode = {"rest": True, "branch": branch} + self.post("views/set_remaining_projects_mode", params={"portfolio": self.key, "branch": branch}) + self._selection_mode = {"rest": True, "branch": branch or c.DEFAULT_BRANCH} return self def set_none_mode(self) -> Portfolio: @@ -808,17 +802,6 @@ def recompute(endpoint: pf.Platform) -> None: endpoint.post(Portfolio.API["REFRESH"]) -def _find_sub_portfolio(key: str, data: types.ApiPayload) -> types.ApiPayload: - """Finds a subportfolio in a JSON hierarchy""" - for subp in data.get("subViews", []): - if subp["key"] == key: - return subp - child = _find_sub_portfolio(key, subp) - if child is not None: - return child - return {} - - def __create_portfolio_hierarchy(endpoint: pf.Platform, data: types.ApiPayload, parent_key: str) -> int: """Creates the hierarchy of portfolios that are new defined by reference""" nbr_creations = 0 @@ -840,34 +823,3 @@ def __create_portfolio_hierarchy(endpoint: pf.Platform, data: types.ApiPayload, o.root_portfolio = o_parent.root_portfolio nbr_creations += __create_portfolio_hierarchy(endpoint, subp, parent_key=key) return nbr_creations - - -def get_api_branch(branch: str) -> str: - """Returns the value to pass to the API for the branch parameter""" - return branch if branch != c.DEFAULT_BRANCH else None - - -def clear_cache(endpoint: pf.Platform) -> None: - """Clears the cache of an endpoint""" - Portfolio.clear_cache(endpoint) - - -def old_to_new_json_one(old_json: dict[str, Any]) -> dict[str, Any]: - """Converts the sonar-config old JSON report format for a single portfolio to the new one""" - new_json = old_json.copy() - for key in "children", "portfolios": - if key in new_json: - new_json[key] = old_to_new_json(new_json[key]) - if "permissions" in old_json: - new_json["permissions"] = util.perms_to_list(old_json["permissions"]) - if "branches" in old_json: - new_json["branches"] = util.dict_to_list(old_json["branches"], "name") - return new_json - - -def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: - """Converts the sonar-config portfolios old JSON report format to the new one""" - new_json = old_json.copy() - for k, v in new_json.items(): - new_json[k] = old_to_new_json_one(v) - return util.dict_to_list(new_json, "key") diff --git a/sonar/projects.py b/sonar/projects.py index 907bd99f..7485f3f9 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -52,6 +52,7 @@ from sonar.audit.rules import get_rule, RuleId from sonar.audit.problem import Problem import sonar.util.constants as c +import sonar.util.project_helper as phelp _CLASS_LOCK = Lock() @@ -87,45 +88,6 @@ _PROJECT_QUALIFIER = "qualifier=TRK" -_UNNEEDED_CONTEXT_DATA = ( - "sonar.announcement.message", - "sonar.auth.github.allowUsersToSignUp", - "sonar.auth.github.apiUrl", - "sonar.auth.github.appId", - "sonar.auth.github.enabled", - "sonar.auth.github.groupsSync", - "sonar.auth.github.organizations", - "sonar.auth.github.webUrl", - "sonar.builtInQualityProfiles.disableNotificationOnUpdate", - "sonar.core.id", - "sonar.core.serverBaseURL", - "sonar.core.startTime", - "sonar.dbcleaner.branchesToKeepWhenInactive", - "sonar.forceAuthentication", - "sonar.host.url", - "sonar.java.jdkHome", - "sonar.links.ci", - "sonar.links.homepage", - "sonar.links.issue", - "sonar.links.scm", - "sonar.links.scm_dev", - "sonar.plugins.risk.consent", -) - -_UNNEEDED_TASK_DATA = ( - "analysisId", - "componentId", - "hasScannerContext", - "id", - "warningCount", - "componentQualifier", - "nodeName", - "componentName", - "componentKey", - "submittedAt", - "executedAt", - "type", -) # Keys to exclude when applying settings in update() _SETTINGS_WITH_SPECIFIC_IMPORT = ( @@ -980,10 +942,10 @@ def migration_export(self, export_settings: types.ConfigSettings) -> types.Objec if last_task: ctxt = last_task.scanner_context() if ctxt: - ctxt = {k: v for k, v in ctxt.items() if k not in _UNNEEDED_CONTEXT_DATA} + ctxt = {k: v for k, v in ctxt.items() if k not in phelp.UNNEEDED_CONTEXT_DATA} t_hist = [] for t in self.task_history(): - t_hist.append({k: v for k, v in t.sq_json.items() if k not in _UNNEEDED_TASK_DATA}) + t_hist.append({k: v for k, v in t.sq_json.items() if k not in phelp.UNNEEDED_TASK_DATA}) json_data["backgroundTasks"] = { "lastTaskScannerContext": ctxt, # "lastTaskWarnings": last_task.warnings(), @@ -1343,9 +1305,6 @@ def count(endpoint: pf.Platform, params: types.ApiParams = None) -> int: """Counts projects :param params: list of parameters to filter projects to search - :type params: dict - :return: Count of projects - :rtype: int """ new_params = {} if params is None else params.copy() new_params.update({"ps": 1, "p": 1}) @@ -1696,23 +1655,3 @@ def import_zips(endpoint: pf.Platform, project_list: list[str], threads: int = 2 log.info("%d/%d imports (%d%%) - Latest: %s - %s", i, nb_projects, int(i * 100 / nb_projects), proj_key, status) log.info("%s", ", ".join([f"{k}:{v}" for k, v in statuses_count.items()])) return statuses - - -def old_to_new_json_one(old_json: dict[str, Any]) -> dict[str, Any]: - """Converts the sonar-config projects old JSON report format for a single project to the new one""" - new_json = old_json.copy() - if "permissions" in old_json: - new_json["permissions"] = util.perms_to_list(old_json["permissions"]) - if "branches" in old_json: - new_json["branches"] = util.dict_to_list(old_json["branches"], "name") - if "settings" in old_json: - new_json["settings"] = util.dict_to_list(old_json["settings"], "key") - return new_json - - -def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: - """Converts the sonar-config projects old JSON report format to the new one""" - new_json = old_json.copy() - for k, v in new_json.items(): - new_json[k] = old_to_new_json_one(v) - return util.dict_to_list(new_json, "key") diff --git a/sonar/qualitygates.py b/sonar/qualitygates.py index 89f0a921..1051595d 100644 --- a/sonar/qualitygates.py +++ b/sonar/qualitygates.py @@ -563,6 +563,6 @@ def search_by_name(endpoint: pf.Platform, name: str) -> dict[str, QualityGate]: return util.search_by_name(endpoint, name, QualityGate.API[c.LIST], "qualitygates") -def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: +def convert_qgs_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config quality gates old JSON report format to the new one""" return util.dict_to_list(old_json, "name") diff --git a/sonar/qualityprofiles.py b/sonar/qualityprofiles.py index 1ef59f60..0cc11908 100644 --- a/sonar/qualityprofiles.py +++ b/sonar/qualityprofiles.py @@ -32,6 +32,7 @@ import sonar.logging as log import sonar.platform as pf from sonar.util import types, cache, constants as c +from sonar.util import qualityprofile_helper as qphelp from sonar import exceptions from sonar import rules, languages import sonar.permissions.qualityprofile_permissions as permissions @@ -41,9 +42,6 @@ from sonar.audit.rules import get_rule, RuleId from sonar.audit.problem import Problem -_KEY_PARENT = "parent" -_CHILDREN_KEY = "children" - _IMPORTABLE_PROPERTIES = ("name", "language", "parentName", "isBuiltIn", "isDefault", "rules", "permissions", "prioritizedRules") _CLASS_LOCK = Lock() @@ -363,7 +361,7 @@ def update(self, data: types.ObjectJsonRepr) -> QualityProfile: QualityProfile.CACHE.pop(self) self.name = data["name"] QualityProfile.CACHE.put(self) - self.set_parent(data.pop(_KEY_PARENT, None)) + self.set_parent(data.pop(qphelp.KEY_PARENT, None)) self.set_rules(data.get("rules", []) + data.get("addedRules", [])) self.activate_rules(data.get("modifiedRules", [])) self.set_permissions(data.get("permissions", [])) @@ -371,12 +369,12 @@ def update(self, data: types.ObjectJsonRepr) -> QualityProfile: if data.get("isDefault", False): self.set_as_default() - for child_name, child_data in data.get(_CHILDREN_KEY, {}).items(): + for child_name, child_data in data.get(qphelp.KEY_CHILDREN, {}).items(): try: child_qp = get_object(self.endpoint, child_name, self.language) except exceptions.ObjectNotFound: child_qp = QualityProfile.create(self.endpoint, child_name, self.language) - child_qp.update(child_data | {_KEY_PARENT: self.name}) + child_qp.update(child_data | {qphelp.KEY_PARENT: self.name}) return self def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: @@ -430,7 +428,6 @@ def rule_impacts(self, rule_key: str, substitute_with_default: bool = True) -> d :param str rule_key: The rule key to get severities for :return: The impacts of the rule in the quality profile - :rtype: dict[str, str] """ return rules.Rule.get_object(self.endpoint, rule_key).impacts(self.key, substitute_with_default=substitute_with_default) @@ -439,7 +436,6 @@ def rule_severity(self, rule_key: str, substitute_with_default: bool = True) -> :param str rule_key: The rule key to get severities for :return: The severity - :rtype: str """ return rules.Rule.get_object(self.endpoint, rule_key).rule_severity(self.key, substitute_with_default=substitute_with_default) @@ -742,12 +738,12 @@ def hierarchize_language(qp_list: dict[str, str], endpoint: pf.Platform, languag continue parent_qp_name = qp_json_data.pop("parentName") parent_qp = hierarchy[parent_qp_name] - if _CHILDREN_KEY not in parent_qp: - parent_qp[_CHILDREN_KEY] = {} + if qphelp.KEY_CHILDREN not in parent_qp: + parent_qp[qphelp.KEY_CHILDREN] = {} this_qp = get_object(endpoint=endpoint, name=qp_name, language=language) qp_json_data |= this_qp.diff(get_object(endpoint=endpoint, name=parent_qp_name, language=language)) qp_json_data.pop("rules", None) - parent_qp[_CHILDREN_KEY][qp_name] = qp_json_data + parent_qp[qphelp.KEY_CHILDREN][qp_name] = qp_json_data to_remove.append(qp_name) for qp_name in to_remove: hierarchy.pop(qp_name) @@ -766,29 +762,6 @@ def hierarchize(qp_list: types.ObjectJsonRepr, endpoint: pf.Platform) -> types.O return {lang: hierarchize_language(lang_qp_list, endpoint=endpoint, language=lang) for lang, lang_qp_list in qp_list.items()} -def flatten_language(language: str, qp_list: types.ObjectJsonRepr) -> types.ObjectJsonRepr: - """Converts a hierarchical list of QP of a given language into a flat list""" - flat_list = {} - for qp_name, qp_data in qp_list.copy().items(): - if _CHILDREN_KEY in qp_data: - children = flatten_language(language, qp_data[_CHILDREN_KEY]) - for child in children.values(): - if "parent" not in child: - child["parent"] = f"{language}:{qp_name}" - qp_data.pop(_CHILDREN_KEY) - flat_list.update(children) - flat_list[f"{language}:{qp_name}"] = qp_data - return flat_list - - -def flatten(qp_list: types.ObjectJsonRepr) -> types.ObjectJsonRepr: - """Organize a hierarchical list of QP in a flat list""" - flat_list = {} - for lang, lang_qp_list in qp_list.items(): - flat_list.update(flatten_language(lang, lang_qp_list)) - return flat_list - - def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwargs) -> types.ObjectJsonRepr: """Exports all or a list of quality profiles configuration as dict @@ -807,7 +780,7 @@ def export(endpoint: pf.Platform, export_settings: types.ConfigSettings, **kwarg qp_list[lang] = {} qp_list[lang][name] = json_data qp_list = hierarchize(qp_list, endpoint=endpoint) - qp_list = __convert_profiles_to_list(qp_list) + qp_list = qphelp.convert_qps_json(qp_list) if write_q := kwargs.get("write_q", None): write_q.put(qp_list) write_q.put(util.WRITE_END) @@ -893,27 +866,3 @@ def exists(endpoint: pf.Platform, name: str, language: str) -> bool: return True except exceptions.ObjectNotFound: return False - - -def convert_one_qp_yaml(qp: types.ObjectJsonRepr) -> types.ObjectJsonRepr: - """Converts a QP in a modified version more suitable for YAML export""" - - if _CHILDREN_KEY in qp: - qp[_CHILDREN_KEY] = {k: convert_one_qp_yaml(q) for k, q in qp[_CHILDREN_KEY].items()} - qp[_CHILDREN_KEY] = util.dict_to_list(qp[_CHILDREN_KEY], "name") - return qp - - -def __convert_children_to_list(qp_json: dict[str, Any]) -> list[dict[str, Any]]: - """Converts a profile's children profiles to list""" - for v in qp_json.values(): - if "children" in v: - v["children"] = __convert_children_to_list(v["children"]) - return util.dict_to_list(qp_json, "name") - - -def old_to_new_json(qp_json: dict[str, Any]) -> list[dict[str, Any]]: - """Converts a language top level list of profiles to list""" - for k, v in qp_json.items(): - qp_json[k] = __convert_children_to_list(v) - return util.dict_to_list(qp_json, "language", "profiles") diff --git a/sonar/rules.py b/sonar/rules.py index f4e6e3c8..8f908024 100644 --- a/sonar/rules.py +++ b/sonar/rules.py @@ -486,7 +486,7 @@ def export(endpoint: platform.Platform, export_settings: types.ConfigSettings, * for k in ("instantiated", "extended", "standard", "thirdParty"): if len(rule_list.get(k, {})) == 0: rule_list.pop(k, None) - rule_list = old_to_new_json(rule_list) + rule_list = convert_rules_json(rule_list) if write_q := kwargs.get("write_q", None): write_q.put(rule_list) write_q.put(utilities.WRITE_END) @@ -581,7 +581,7 @@ def severities(endpoint: platform.Platform, json_data: dict[str, any]) -> Option return json_data.get("severity", None) -def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: +def convert_rules_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config rules old JSON report format to the new one""" new_json = {} for k in ("instantiated", "extended", "standard", "thirdParty"): diff --git a/sonar/users.py b/sonar/users.py index cdd3bede..d723645e 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -570,6 +570,6 @@ def exists(endpoint: pf.Platform, login: str) -> bool: return User.get_object(endpoint=endpoint, login=login) is not None -def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: +def convert_users_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config users old JSON report format to the new one""" return util.dict_to_list(old_json, "login") diff --git a/sonar/util/platform_helper.py b/sonar/util/platform_helper.py index 0ec6f56a..3ee3f852 100644 --- a/sonar/util/platform_helper.py +++ b/sonar/util/platform_helper.py @@ -20,8 +20,8 @@ """Helper tools for the Platform object""" from typing import Any -import settings -import utilities as util +from sonar import settings +from sonar import utilities as util def normalize_api(api: str) -> str: @@ -37,14 +37,14 @@ def normalize_api(api: str) -> str: return api -def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: +def convert_basics_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts sonar-config "plaform" section old JSON report format to new format""" if "plugins" in old_json: old_json["plugins"] = util.dict_to_list(old_json["plugins"], "key") return old_json -def global_settings_old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: +def convert_global_settings_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts sonar-config "globalSettings" section old JSON report format to new format""" new_json = {} special_categories = (settings.LANGUAGES_SETTINGS, settings.DEVOPS_INTEGRATION, "permissions", "permissionTemplates") diff --git a/sonar/util/portfolio_helper.py b/sonar/util/portfolio_helper.py new file mode 100644 index 00000000..aba7455c --- /dev/null +++ b/sonar/util/portfolio_helper.py @@ -0,0 +1,44 @@ +# +# sonar-tools +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +"""Helper tools for the Portfolio object""" + +from typing import Any +from sonar import utilities as util + + +def convert_portfolio_json(old_json: dict[str, Any]) -> dict[str, Any]: + """Converts the sonar-config old JSON report format for a single portfolio to the new one""" + new_json = old_json.copy() + for key in "children", "portfolios": + if key in new_json: + new_json[key] = convert_portfolios_json(new_json[key]) + if "permissions" in old_json: + new_json["permissions"] = util.perms_to_list(old_json["permissions"]) + if "branches" in old_json: + new_json["branches"] = util.dict_to_list(old_json["branches"], "name") + return new_json + + +def convert_portfolios_json(old_json: dict[str, Any]) -> dict[str, Any]: + """Converts the sonar-config portfolios old JSON report format to the new one""" + new_json = old_json.copy() + for k, v in new_json.items(): + new_json[k] = convert_portfolio_json(v) + return util.dict_to_list(new_json, "key") diff --git a/sonar/util/project_helper.py b/sonar/util/project_helper.py new file mode 100644 index 00000000..ac224dad --- /dev/null +++ b/sonar/util/project_helper.py @@ -0,0 +1,84 @@ +# +# sonar-tools +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +"""Helper tools for the Project object""" + +from typing import Any +from sonar import utilities as util + + +UNNEEDED_TASK_DATA = ( + "analysisId", + "componentId", + "hasScannerContext", + "id", + "warningCount", + "componentQualifier", + "nodeName", + "componentName", + "componentKey", + "submittedAt", + "executedAt", + "type", +) + +UNNEEDED_CONTEXT_DATA = ( + "sonar.announcement.message", + "sonar.auth.github.allowUsersToSignUp", + "sonar.auth.github.apiUrl", + "sonar.auth.github.appId", + "sonar.auth.github.enabled", + "sonar.auth.github.groupsSync", + "sonar.auth.github.organizations", + "sonar.auth.github.webUrl", + "sonar.builtInQualityProfiles.disableNotificationOnUpdate", + "sonar.core.id", + "sonar.core.serverBaseURL", + "sonar.core.startTime", + "sonar.dbcleaner.branchesToKeepWhenInactive", + "sonar.forceAuthentication", + "sonar.host.url", + "sonar.java.jdkHome", + "sonar.links.ci", + "sonar.links.homepage", + "sonar.links.issue", + "sonar.links.scm", + "sonar.links.scm_dev", + "sonar.plugins.risk.consent", +) + + +def old_to_new_json_one(old_json: dict[str, Any]) -> dict[str, Any]: + """Converts the sonar-config projects old JSON report format for a single project to the new one""" + new_json = old_json.copy() + if "permissions" in old_json: + new_json["permissions"] = util.perms_to_list(old_json["permissions"]) + if "branches" in old_json: + new_json["branches"] = util.dict_to_list(old_json["branches"], "name") + if "settings" in old_json: + new_json["settings"] = util.dict_to_list(old_json["settings"], "key") + return new_json + + +def convert_projects_json(old_json: dict[str, Any]) -> dict[str, Any]: + """Converts the sonar-config projects old JSON report format to the new one""" + new_json = old_json.copy() + for k, v in new_json.items(): + new_json[k] = old_to_new_json_one(v) + return util.dict_to_list(new_json, "key") diff --git a/sonar/util/qualityprofile_helper.py b/sonar/util/qualityprofile_helper.py new file mode 100644 index 00000000..5a5906a2 --- /dev/null +++ b/sonar/util/qualityprofile_helper.py @@ -0,0 +1,65 @@ +# +# sonar-tools +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +"""Helper tools for the Project object""" + +from typing import Any +from sonar import utilities as util +from sonar.util import types + +KEY_PARENT = "parent" +KEY_CHILDREN = "children" + + +def flatten_language(language: str, qp_list: types.ObjectJsonRepr) -> types.ObjectJsonRepr: + """Converts a hierarchical list of QP of a given language into a flat list""" + flat_list = {} + for qp_name, qp_data in qp_list.copy().items(): + if KEY_CHILDREN in qp_data: + children = flatten_language(language, qp_data[KEY_CHILDREN]) + for child in children.values(): + if "parent" not in child: + child["parent"] = f"{language}:{qp_name}" + qp_data.pop(KEY_CHILDREN) + flat_list.update(children) + flat_list[f"{language}:{qp_name}"] = qp_data + return flat_list + + +def flatten(qp_list: types.ObjectJsonRepr) -> types.ObjectJsonRepr: + """Organize a hierarchical list of QP in a flat list""" + flat_list = {} + for lang, lang_qp_list in qp_list.items(): + flat_list.update(flatten_language(lang, lang_qp_list)) + return flat_list + + +def __convert_children_to_list(qp_json: dict[str, Any]) -> list[dict[str, Any]]: + """Converts a profile's children profiles to list""" + for v in qp_json.values(): + if "children" in v: + v["children"] = __convert_children_to_list(v["children"]) + return util.dict_to_list(qp_json, "name") + + +def convert_qps_json(qp_json: dict[str, Any]) -> list[dict[str, Any]]: + """Converts a language top level list of profiles to list""" + for k, v in qp_json.items(): + qp_json[k] = __convert_children_to_list(v) + return util.dict_to_list(qp_json, "language", "profiles") From 9c80c24e12f188b196f463a505f3f3eb67da635f Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 5 Nov 2025 19:36:27 +0100 Subject: [PATCH 03/37] Change conversion function name --- cli/config.py | 2 +- sonar/applications.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/config.py b/cli/config.py index 42694b52..a3bdba0e 100644 --- a/cli/config.py +++ b/cli/config.py @@ -320,7 +320,7 @@ def convert_json(**kwargs) -> dict[str, Any]: "qualityGates": qualitygates.convert_qgs_json, "projects": pjhelp.convert_projects_json, "portfolios": foliohelp.convert_portfolios_json, - "applications": applications.old_to_new_json, + "applications": applications.convert_apps_json, "users": users.convert_users_json, "groups": groups.convert_groups_json, "rules": rules.convert_rules_json, diff --git a/sonar/applications.py b/sonar/applications.py index 099625c2..61cd9e0f 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -344,7 +344,7 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: "tags": self.get_tags(), } ) - json_data = old_to_new_json_one(json_data) + json_data = convert_app_json(json_data) return util.filter_export(json_data, _IMPORTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False)) def set_permissions(self, data: types.JsonPermissions) -> application_permissions.ApplicationPermissions: @@ -600,7 +600,7 @@ def search_by_name(endpoint: pf.Platform, name: str) -> dict[str, Application]: return data -def old_to_new_json_one(old_app_json: dict[str, Any]) -> dict[str, Any]: +def convert_app_json(old_app_json: dict[str, Any]) -> dict[str, Any]: """Converts sonar-config old JSON report format to new format for a single application""" new_json = old_app_json.copy() if "permissions" in old_app_json: @@ -610,9 +610,9 @@ def old_to_new_json_one(old_app_json: dict[str, Any]) -> dict[str, Any]: return new_json -def old_to_new_json(old_json: dict[str, Any]) -> dict[str, Any]: +def convert_apps_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts sonar-config old JSON report format to new format""" new_json = old_json.copy() for k, v in new_json.items(): - new_json[k] = old_to_new_json_one(v) + new_json[k] = convert_app_json(v) return util.dict_to_list(new_json, "key") From b5d37face3a64c4d86bdeed543f01b5e22003276 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 5 Nov 2025 19:37:03 +0100 Subject: [PATCH 04/37] Use the project conversion function --- sonar/projects.py | 9 +++++---- sonar/util/project_helper.py | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sonar/projects.py b/sonar/projects.py index 7485f3f9..2b4d60ad 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -968,12 +968,12 @@ def export(self, export_settings: types.ConfigSettings, settings_list: Optional[ nc = self.new_code() if nc != "": json_data[settings.NEW_CODE_PERIOD] = nc - json_data["qualityProfiles"] = util.dict_to_list(self.__export_get_qp(), "language", "name") + json_data["qualityProfiles"] = self.__export_get_qp() json_data["links"] = self.links() - json_data["permissions"] = util.perms_to_list(self.permissions().to_json(csv=export_settings.get("INLINE_LISTS", True))) + json_data["permissions"] = self.permissions().to_json(csv=export_settings.get("INLINE_LISTS", True)) if self.endpoint.version() >= (10, 7, 0): json_data["aiCodeFix"] = self.ai_code_fix() - json_data["branches"] = util.dict_to_list(self.__get_branch_export(export_settings), "name") + json_data["branches"] = self.__get_branch_export(export_settings) json_data["tags"] = self.get_tags() json_data["visibility"] = self.visibility() (json_data["qualityGate"], qg_is_default) = self.quality_gate() @@ -1002,7 +1002,8 @@ def export(self, export_settings: types.ConfigSettings, settings_list: Optional[ if contains_ai: json_data[_CONTAINS_AI_CODE] = contains_ai with_inherited = export_settings.get("INCLUDE_INHERITED", False) - json_data["settings"] = [s.to_json() for s in settings_dict.values() if with_inherited or not s.inherited and s.key != "visibility"] + json_data["settings"] = {k: s.to_json() for k, s in settings_dict.values() if with_inherited or not s.inherited and s.key != "visibility"} + return phelp.convert_projects_json(json_data) except Exception as e: traceback.print_exc() diff --git a/sonar/util/project_helper.py b/sonar/util/project_helper.py index ac224dad..75127067 100644 --- a/sonar/util/project_helper.py +++ b/sonar/util/project_helper.py @@ -69,6 +69,8 @@ def old_to_new_json_one(old_json: dict[str, Any]) -> dict[str, Any]: new_json = old_json.copy() if "permissions" in old_json: new_json["permissions"] = util.perms_to_list(old_json["permissions"]) + if "qualityProfiles" in old_json: + new_json["qualityProfiles"] = util.dict_to_list(old_json["qualityProfiles"], "language", "name") if "branches" in old_json: new_json["branches"] = util.dict_to_list(old_json["branches"], "name") if "settings" in old_json: From a632de70089bccb4a74c54929c06bcb53c8b536b Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 5 Nov 2025 19:53:54 +0100 Subject: [PATCH 05/37] Fix --- sonar/projects.py | 4 ++-- sonar/util/project_helper.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sonar/projects.py b/sonar/projects.py index 2b4d60ad..3bdfefe7 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -1002,8 +1002,8 @@ def export(self, export_settings: types.ConfigSettings, settings_list: Optional[ if contains_ai: json_data[_CONTAINS_AI_CODE] = contains_ai with_inherited = export_settings.get("INCLUDE_INHERITED", False) - json_data["settings"] = {k: s.to_json() for k, s in settings_dict.values() if with_inherited or not s.inherited and s.key != "visibility"} - return phelp.convert_projects_json(json_data) + json_data["settings"] = {k: s.to_json() for k, s in settings_dict.items() if with_inherited or not s.inherited and s.key != "visibility"} + return phelp.convert_project_json(json_data) except Exception as e: traceback.print_exc() diff --git a/sonar/util/project_helper.py b/sonar/util/project_helper.py index 75127067..9a112ac8 100644 --- a/sonar/util/project_helper.py +++ b/sonar/util/project_helper.py @@ -64,7 +64,7 @@ ) -def old_to_new_json_one(old_json: dict[str, Any]) -> dict[str, Any]: +def convert_project_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config projects old JSON report format for a single project to the new one""" new_json = old_json.copy() if "permissions" in old_json: @@ -82,5 +82,5 @@ def convert_projects_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config projects old JSON report format to the new one""" new_json = old_json.copy() for k, v in new_json.items(): - new_json[k] = old_to_new_json_one(v) + new_json[k] = convert_project_json(v) return util.dict_to_list(new_json, "key") From 8e8c0326de1ec17bce4862c3d50cf53ba483cc40 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Wed, 5 Nov 2025 20:27:45 +0100 Subject: [PATCH 06/37] Swap order between QP and QG --- cli/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/config.py b/cli/config.py index a3bdba0e..7e848c48 100644 --- a/cli/config.py +++ b/cli/config.py @@ -316,8 +316,8 @@ def convert_json(**kwargs) -> dict[str, Any]: mapping = { "platform": pfhelp.convert_basics_json, "globalSettings": pfhelp.convert_global_settings_json, - "qualityProfiles": qphelp.convert_qps_json, "qualityGates": qualitygates.convert_qgs_json, + "qualityProfiles": qphelp.convert_qps_json, "projects": pjhelp.convert_projects_json, "portfolios": foliohelp.convert_portfolios_json, "applications": applications.convert_apps_json, From 9ea35f86b5e936835a1f18efd859791073d08f93 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:44:02 +0100 Subject: [PATCH 07/37] Add rule format conversion --- cli/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/config.py b/cli/config.py index 7e848c48..d815dfa9 100644 --- a/cli/config.py +++ b/cli/config.py @@ -36,6 +36,7 @@ from sonar.util import project_helper as pjhelp from sonar.util import portfolio_helper as foliohelp from sonar.util import qualityprofile_helper as qphelp +from sonar.util import rule_helper as rhelp import sonar.logging as log from sonar import platform, rules, qualityprofiles, qualitygates, users, groups @@ -323,13 +324,14 @@ def convert_json(**kwargs) -> dict[str, Any]: "applications": applications.convert_apps_json, "users": users.convert_users_json, "groups": groups.convert_groups_json, - "rules": rules.convert_rules_json, + "rules": rhelp.convert_rules_json, } new_json = {} for k, func in mapping.items(): if k in old_json: log.info("Converting %s", k) new_json[k] = func(old_json[k]) + new_json = __normalize_json(new_json, remove_empty=False, remove_none=True) with open(kwargs["convertTo"], mode="w", encoding="utf-8") as fd: print(utilities.json_dump(new_json), file=fd) return new_json From 91614c806428f89a4ee3d4bcb94a53c7f29843c8 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:44:44 +0100 Subject: [PATCH 08/37] Add language sorting --- sonar/platform.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonar/platform.py b/sonar/platform.py index 419ea789..af59e441 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -503,9 +503,10 @@ def export(self, export_settings: types.ConfigSettings, full: bool = False) -> t special_categories = (settings.LANGUAGES_SETTINGS, settings.DEVOPS_INTEGRATION, "permissions", "permissionTemplates") for categ in [cat for cat in settings.CATEGORIES if cat not in special_categories]: json_data[categ] = util.sort_list_by_key(util.dict_to_list(json_data[categ], "key"), "key") - for k, v in json_data[settings.LANGUAGES_SETTINGS].items(): + for k, v in sorted(json_data[settings.LANGUAGES_SETTINGS].items()): json_data[settings.LANGUAGES_SETTINGS][k] = util.sort_list_by_key(util.dict_to_list(v, "key"), "key") json_data[settings.LANGUAGES_SETTINGS] = util.dict_to_list(json_data[settings.LANGUAGES_SETTINGS], "language", "settings") + json_data[settings.LANGUAGES_SETTINGS] = util.sort_list_by_key(json_data[settings.LANGUAGES_SETTINGS], "language") json_data[settings.DEVOPS_INTEGRATION] = util.dict_to_list(json_data[settings.DEVOPS_INTEGRATION], "key") json_data["permissions"] = util.perms_to_list(json_data["permissions"]) for v in json_data["permissionTemplates"].values(): From 62fc9e41399a3cc64a6643c5a83d7a7dd9442869 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:45:27 +0100 Subject: [PATCH 09/37] Add portfolio JSON format conversion --- sonar/portfolios.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sonar/portfolios.py b/sonar/portfolios.py index 39311205..5c2c4995 100644 --- a/sonar/portfolios.py +++ b/sonar/portfolios.py @@ -45,6 +45,7 @@ import sonar.utilities as util from sonar.audit import rules, problem from sonar.portfolio_reference import PortfolioReference +from sonar.util import portfolio_helper as phelp if TYPE_CHECKING: from sonar.util import types @@ -368,7 +369,9 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr subportfolios = self.sub_portfolios() if not self.is_sub_portfolio(): json_data["visibility"] = self._visibility - json_data["permissions"] = util.perms_to_list(self.permissions().export(export_settings=export_settings)) + if perms := self.permissions().export(export_settings=export_settings): + log.info("%s PERMS = %s", self, str(perms)) + json_data["permissions"] = util.perms_to_list(perms) json_data["tags"] = self._tags if subportfolios: json_data["portfolios"] = {} @@ -388,7 +391,10 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: """Exports a portfolio (for sonar-config)""" log.info("Exporting %s", str(self)) - return util.remove_nones(util.filter_export(self.to_json(export_settings), _IMPORTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False))) + json_data = util.remove_nones( + util.filter_export(self.to_json(export_settings), _IMPORTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False)) + ) + return phelp.convert_portfolio_json(json_data) def permissions(self) -> pperms.PortfolioPermissions: """Returns a portfolio permissions (if toplevel) or None if sub-portfolio""" From e801802d5088eb64e59cdf7ab34e60c93ff50951 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:46:04 +0100 Subject: [PATCH 10/37] AICode fix is a setting like others --- sonar/projects.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sonar/projects.py b/sonar/projects.py index 3bdfefe7..bea11419 100644 --- a/sonar/projects.py +++ b/sonar/projects.py @@ -83,7 +83,7 @@ "visibility", "qualityGate", "webhooks", - "aiCodeFix", + phelp.AI_CODE_FIX, ) _PROJECT_QUALIFIER = "qualifier=TRK" @@ -965,14 +965,11 @@ def export(self, export_settings: types.ConfigSettings, settings_list: Optional[ json_data.update({"key": self.key, "name": self.name}) try: json_data["binding"] = self.__export_get_binding() - nc = self.new_code() - if nc != "": - json_data[settings.NEW_CODE_PERIOD] = nc json_data["qualityProfiles"] = self.__export_get_qp() json_data["links"] = self.links() json_data["permissions"] = self.permissions().to_json(csv=export_settings.get("INLINE_LISTS", True)) if self.endpoint.version() >= (10, 7, 0): - json_data["aiCodeFix"] = self.ai_code_fix() + json_data[phelp.AI_CODE_FIX] = self.ai_code_fix() json_data["branches"] = self.__get_branch_export(export_settings) json_data["tags"] = self.get_tags() json_data["visibility"] = self.visibility() @@ -1002,7 +999,10 @@ def export(self, export_settings: types.ConfigSettings, settings_list: Optional[ if contains_ai: json_data[_CONTAINS_AI_CODE] = contains_ai with_inherited = export_settings.get("INCLUDE_INHERITED", False) - json_data["settings"] = {k: s.to_json() for k, s in settings_dict.items() if with_inherited or not s.inherited and s.key != "visibility"} + json_data["settings"] = {} + settings_to_export = {k: s for k, s in settings_dict.items() if with_inherited or not s.inherited and s.key != "visibility"} + for k, s in settings_to_export.items(): + json_data["settings"] |= s.to_json() return phelp.convert_project_json(json_data) except Exception as e: From 35d64807656da2c98aaf121017982d2c6028388c Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:46:52 +0100 Subject: [PATCH 11/37] Handle "severity" like "severities" --- sonar/qualityprofiles.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sonar/qualityprofiles.py b/sonar/qualityprofiles.py index 0cc11908..26ea2641 100644 --- a/sonar/qualityprofiles.py +++ b/sonar/qualityprofiles.py @@ -392,7 +392,9 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr json_data["rules"] = [] for rule in self.rules().values(): data = { - k: v for k, v in rule.export(full).items() if k not in ("isTemplate", "templateKey", "language", "tags", "severities", "impacts") + k: v + for k, v in rule.export(full).items() + if k not in ("isTemplate", "templateKey", "language", "tags", "severity", "severities", "impacts") } if self.rule_is_prioritized(rule.key): data["prioritized"] = True From 24be25012435d64f0a194a7de601d2e9589162a9 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:47:12 +0100 Subject: [PATCH 12/37] Use rule_helper --- sonar/rules.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/sonar/rules.py b/sonar/rules.py index 8f908024..92183fac 100644 --- a/sonar/rules.py +++ b/sonar/rules.py @@ -34,6 +34,7 @@ import sonar.sqobject as sq from sonar.util import types, cache, constants as c, issue_defs as idefs from sonar import platform, utilities, exceptions, languages +from sonar.util import rule_helper as rhelp TYPE_TO_QUALITY = { idefs.TYPE_BUG: idefs.QUALITY_RELIABILITY, @@ -486,7 +487,7 @@ def export(endpoint: platform.Platform, export_settings: types.ConfigSettings, * for k in ("instantiated", "extended", "standard", "thirdParty"): if len(rule_list.get(k, {})) == 0: rule_list.pop(k, None) - rule_list = convert_rules_json(rule_list) + rule_list = rhelp.convert_rules_json(rule_list) if write_q := kwargs.get("write_q", None): write_q.put(rule_list) write_q.put(utilities.WRITE_END) @@ -579,12 +580,3 @@ def severities(endpoint: platform.Platform, json_data: dict[str, any]) -> Option return {impact["softwareQuality"]: impact["severity"] for impact in json_data.get("impacts", [])} else: return json_data.get("severity", None) - - -def convert_rules_json(old_json: dict[str, Any]) -> dict[str, Any]: - """Converts the sonar-config rules old JSON report format to the new one""" - new_json = {} - for k in ("instantiated", "extended", "standard", "thirdParty"): - if k in old_json: - new_json[k] = utilities.dict_to_list(old_json[k], "key") - return new_json From 1bc82c62a1eb5d475e9ce47b0d609a58cad6fcd7 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:47:24 +0100 Subject: [PATCH 13/37] Add user fields sorting --- sonar/users.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sonar/users.py b/sonar/users.py index d723645e..63897587 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -570,6 +570,12 @@ def exists(endpoint: pf.Platform, login: str) -> bool: return User.get_object(endpoint=endpoint, login=login) is not None +def convert_user_json(old_json: dict[str, Any]) -> dict[str, Any]: + return util.order_dict(old_json, ["name", "email", "groups", "scmAccounts", "local"]) + + def convert_users_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config users old JSON report format to the new one""" + for k, u in old_json.items(): + old_json[k] = convert_user_json(u) return util.dict_to_list(old_json, "login") From 93effc6c311b6fad0e36a80f44354d1ea5122d92 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:47:53 +0100 Subject: [PATCH 14/37] Add conversion of strin into float or int or bool --- sonar/utilities.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sonar/utilities.py b/sonar/utilities.py index f7befb7f..4c552030 100644 --- a/sonar/utilities.py +++ b/sonar/utilities.py @@ -200,6 +200,8 @@ def remove_nones(d: Any) -> Any: def clean_data(d: Any, remove_empty: bool = True, remove_none: bool = True) -> Any: """Recursively removes empty lists and dicts and none from a dict""" # log.debug("Cleaning up %s", json_dump(d)) + if isinstance(d, str): + return convert_string(d) if not isinstance(d, (list, dict)): return d @@ -857,6 +859,6 @@ def order_list(list_to_order: list[str], *key_order: str) -> list[str]: def perms_to_list(perms: dict[str, Any]) -> list[str, Any]: """Converts permissions in dict format to list format""" - if not perms: + if not perms or not isinstance(perms, dict): return perms return dict_to_list(perms.get("groups", {}), "group", "permissions") + dict_to_list(perms.get("users", {}), "user", "permissions") From 585e7f7e1ba23cddca18ddaf31954557663357ca Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:48:20 +0100 Subject: [PATCH 15/37] Add template JSON format conversion --- sonar/permissions/permission_templates.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sonar/permissions/permission_templates.py b/sonar/permissions/permission_templates.py index c8259bf2..0bded1fc 100644 --- a/sonar/permissions/permission_templates.py +++ b/sonar/permissions/permission_templates.py @@ -21,13 +21,14 @@ """Abstraction of the SonarQube permission template concept""" from __future__ import annotations -from typing import Optional +from typing import Optional, Any import json import re import sonar.logging as log from sonar.util import types, cache +from sonar.util import platform_helper as phelp from sonar import sqobject, utilities, exceptions from sonar.permissions import template_permissions import sonar.platform as pf @@ -42,8 +43,6 @@ _CREATE_API = "permissions/create_template" _UPDATE_API = "permissions/update_template" -_IMPORTABLE_PROPERTIES = ("name", "description", "pattern", "defaultFor", "permissions") - class PermissionTemplate(sqobject.SqObject): """Abstraction of the Sonar permission template concept""" @@ -183,7 +182,7 @@ def to_json(self, export_settings: types.ConfigSettings = None) -> types.ObjectJ json_data.pop("pattern") json_data["creationDate"] = utilities.date_to_string(self.creation_date) json_data["lastUpdate"] = utilities.date_to_string(self.last_update) - return utilities.remove_nones(utilities.filter_export(json_data, _IMPORTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False))) + return phelp.convert_template_json(json_data, export_settings.get("FULL_EXPORT", False)) def _audit_pattern(self, audit_settings: types.ConfigSettings) -> list[pb.Problem]: log.debug("Auditing %s projectKeyPattern ('%s')", str(self), str(self.project_key_pattern)) From 010f9f31603376e7d29490b33d169f454f568019 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:48:41 +0100 Subject: [PATCH 16/37] Remove groups or users with no permissions in export --- sonar/permissions/permissions.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sonar/permissions/permissions.py b/sonar/permissions/permissions.py index e10bcf7e..0f5799eb 100644 --- a/sonar/permissions/permissions.py +++ b/sonar/permissions/permissions.py @@ -86,10 +86,14 @@ def to_json(self, perm_type: str | None = None, csv: bool = False) -> types.Json if not csv: return self.permissions.get(perm_type, {}) if is_valid(perm_type) else self.permissions perms = {} - for p in normalize(perm_type): - if p not in self.permissions or len(self.permissions[p]) == 0: + for ptype in normalize(perm_type): + for k, v in self.permissions.get(ptype, {}).copy().items(): + if len(v) == 0: + self.permissions[ptype].pop(k) + for ptype in normalize(perm_type): + if ptype not in self.permissions or len(self.permissions[ptype]) == 0: continue - perms[p] = {k: encode(v) for k, v in self.permissions.get(p, {}).items()} + perms[ptype] = {k: encode(v) for k, v in self.permissions.get(ptype, {}).items()} return perms if len(perms) > 0 else None def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr: From 50b6cf84e6827c914f0605115ed245df8be9e1b8 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:49:07 +0100 Subject: [PATCH 17/37] Add perm templates conversion --- sonar/util/platform_helper.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/sonar/util/platform_helper.py b/sonar/util/platform_helper.py index 3ee3f852..555b2207 100644 --- a/sonar/util/platform_helper.py +++ b/sonar/util/platform_helper.py @@ -23,6 +23,8 @@ from sonar import settings from sonar import utilities as util +_PERM_TPL_IMPORTABLE_PROPERTIES = ("name", "description", "pattern", "defaultFor", "permissions") + def normalize_api(api: str) -> str: """Normalizes an API based on its multiple original forms""" @@ -44,20 +46,25 @@ def convert_basics_json(old_json: dict[str, Any]) -> dict[str, Any]: return old_json -def convert_global_settings_json(old_json: dict[str, Any]) -> dict[str, Any]: +def convert_template_json(json_data: dict[str, Any], full: bool = False) -> dict[str, Any]: + if "permissions" in json_data: + json_data["permissions"] = util.perms_to_list(json_data["permissions"]) + return util.remove_nones(util.filter_export(json_data, _PERM_TPL_IMPORTABLE_PROPERTIES, full)) + + +def convert_global_settings_json(old_json: dict[str, Any], full: bool = False) -> dict[str, Any]: """Converts sonar-config "globalSettings" section old JSON report format to new format""" - new_json = {} + new_json = old_json.copy() special_categories = (settings.LANGUAGES_SETTINGS, settings.DEVOPS_INTEGRATION, "permissions", "permissionTemplates") for categ in [cat for cat in settings.CATEGORIES if cat not in special_categories]: - new_json[categ] = util.sort_list_by_key(util.dict_to_list(old_json[categ], "key"), "key") + new_json[categ] = util.sort_list_by_key(util.dict_to_list(dict(sorted(old_json[categ].items())), "key"), "key") for k, v in old_json[settings.LANGUAGES_SETTINGS].items(): new_json[settings.LANGUAGES_SETTINGS] = new_json.get(settings.LANGUAGES_SETTINGS, None) or {} new_json[settings.LANGUAGES_SETTINGS][k] = util.sort_list_by_key(util.dict_to_list(v, "key"), "key") - new_json[settings.LANGUAGES_SETTINGS] = util.dict_to_list(new_json[settings.LANGUAGES_SETTINGS], "language", "settings") - new_json[settings.DEVOPS_INTEGRATION] = util.dict_to_list(old_json[settings.DEVOPS_INTEGRATION], "key") + new_json[settings.LANGUAGES_SETTINGS] = util.dict_to_list(dict(sorted(new_json[settings.LANGUAGES_SETTINGS].items())), "language", "settings") + new_json[settings.DEVOPS_INTEGRATION] = util.dict_to_list(dict(sorted(old_json[settings.DEVOPS_INTEGRATION].items())), "key") new_json["permissions"] = util.perms_to_list(old_json["permissions"]) - for v in old_json["permissionTemplates"].values(): - if "permissions" in v: - v["permissions"] = util.perms_to_list(v["permissions"]) - new_json["permissionTemplates"] = util.dict_to_list(old_json["permissionTemplates"], "key") + for k, v in new_json["permissionTemplates"].items(): + new_json["permissionTemplates"][k] = convert_template_json(new_json["permissionTemplates"][k], full) + new_json["permissionTemplates"] = util.dict_to_list(new_json["permissionTemplates"], "key") return new_json From f75511a4b2c7b5eaad61bdd511083893f24df58b Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:50:27 +0100 Subject: [PATCH 18/37] Order keys and add AI Code fix in settings --- sonar/util/project_helper.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/sonar/util/project_helper.py b/sonar/util/project_helper.py index 9a112ac8..04c27127 100644 --- a/sonar/util/project_helper.py +++ b/sonar/util/project_helper.py @@ -63,6 +63,23 @@ "sonar.plugins.risk.consent", ) +AI_CODE_FIX = "aiCodeFix" + +_JSON_KEY_ORDER = ( + "key", + "name", + "tags", + "visibility", + "settings", + "binding", + "branches", + "permissions", + "qualityGate", + "qualityProfiles", + "links", + "webhooks", +) + def convert_project_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config projects old JSON report format for a single project to the new one""" @@ -73,9 +90,16 @@ def convert_project_json(old_json: dict[str, Any]) -> dict[str, Any]: new_json["qualityProfiles"] = util.dict_to_list(old_json["qualityProfiles"], "language", "name") if "branches" in old_json: new_json["branches"] = util.dict_to_list(old_json["branches"], "name") - if "settings" in old_json: - new_json["settings"] = util.dict_to_list(old_json["settings"], "key") - return new_json + for k, v in old_json.items(): + if k not in _JSON_KEY_ORDER: + new_json.pop(k, None) + new_json["settings"] = new_json.get("settings", None) or {} + new_json["settings"][k] = v + if "settings" in new_json: + if AI_CODE_FIX in new_json["settings"] and not new_json["settings"][AI_CODE_FIX]: + new_json["settings"].pop(AI_CODE_FIX) + new_json["settings"] = util.dict_to_list(dict(sorted(new_json["settings"].items())), "key", "value") + return util.order_dict(new_json, _JSON_KEY_ORDER) def convert_projects_json(old_json: dict[str, Any]) -> dict[str, Any]: From 24e240e1753315b36d504f72768b00bad336ff02 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:50:52 +0100 Subject: [PATCH 19/37] Improve rules export (order etc..) --- sonar/util/qualityprofile_helper.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/sonar/util/qualityprofile_helper.py b/sonar/util/qualityprofile_helper.py index 5a5906a2..10d98c40 100644 --- a/sonar/util/qualityprofile_helper.py +++ b/sonar/util/qualityprofile_helper.py @@ -25,6 +25,7 @@ KEY_PARENT = "parent" KEY_CHILDREN = "children" +KEY_ORDER = ("name", "isBuiltIn", "isDefault", "children", "addedRules", "modifiedRules", "permissions") def flatten_language(language: str, qp_list: types.ObjectJsonRepr) -> types.ObjectJsonRepr: @@ -50,16 +51,23 @@ def flatten(qp_list: types.ObjectJsonRepr) -> types.ObjectJsonRepr: return flat_list -def __convert_children_to_list(qp_json: dict[str, Any]) -> list[dict[str, Any]]: +def __convert_qp_json(qp_json: dict[str, Any]) -> list[dict[str, Any]]: """Converts a profile's children profiles to list""" - for v in qp_json.values(): + + for k, v in sorted(qp_json.items()): + for rtype in "addedRules", "modifiedRules": + for r in v.get(rtype, {}): + if "severities" in r: + r["impacts"] = r["severities"] + r.pop("severities") if "children" in v: - v["children"] = __convert_children_to_list(v["children"]) + v["children"] = __convert_qp_json(v["children"]) + qp_json[k] = util.order_keys(v, *KEY_ORDER) return util.dict_to_list(qp_json, "name") def convert_qps_json(qp_json: dict[str, Any]) -> list[dict[str, Any]]: """Converts a language top level list of profiles to list""" - for k, v in qp_json.items(): - qp_json[k] = __convert_children_to_list(v) + for k, v in sorted(qp_json.items()): + qp_json[k] = __convert_qp_json(v) return util.dict_to_list(qp_json, "language", "profiles") From 1e2683261250e2c7f08fdb9948b98bf426131bbc Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 13:51:03 +0100 Subject: [PATCH 20/37] Add helper --- sonar/util/rule_helper.py | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 sonar/util/rule_helper.py diff --git a/sonar/util/rule_helper.py b/sonar/util/rule_helper.py new file mode 100644 index 00000000..a409b2ce --- /dev/null +++ b/sonar/util/rule_helper.py @@ -0,0 +1,40 @@ +# +# sonar-tools +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +"""Helper tools for the Rule object""" + +from typing import Any +from sonar import utilities + + +def convert_rule_json(old_json: dict[str, Any]) -> dict[str, Any]: + if "tags" in old_json: + old_json["tags"] = utilities.list_to_csv(old_json["tags"]) + return old_json + + +def convert_rules_json(old_json: dict[str, Any]) -> dict[str, Any]: + """Converts the sonar-config rules old JSON report format to the new one""" + new_json = {} + for k in ("instantiated", "extended", "standard", "thirdParty"): + if k in old_json: + for r in old_json[k].values(): + r = convert_rule_json(r) + new_json[k] = utilities.dict_to_list(dict(sorted(old_json[k].items())), "key") + return new_json From e3793211c7451ce0b07641cb367a0e98b1d9a4ba Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 14:29:24 +0100 Subject: [PATCH 21/37] Create common json conversion function --- sonar/util/common_json_helper.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 sonar/util/common_json_helper.py diff --git a/sonar/util/common_json_helper.py b/sonar/util/common_json_helper.py new file mode 100644 index 00000000..022d0e4d --- /dev/null +++ b/sonar/util/common_json_helper.py @@ -0,0 +1,35 @@ +# +# sonar-tools +# Copyright (C) 2025 Olivier Korach +# mailto:olivier.korach AT gmail DOT com +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +"""Helper tools for the Rule object""" + +from typing import Any +from sonar import utilities +from sonar import logging as log + + +def convert_common_fields(json_data: dict[str, Any], with_permissions: bool = True) -> dict[str, Any]: + if with_permissions and "permissions" in json_data: + json_data["permissions"] = utilities.perms_to_list(json_data["permissions"]) + for perm in json_data["permissions"]: + perm["permissions"] = utilities.csv_to_list(perm["permissions"]) + if "tags" in json_data: + log.info("CONVERTING TAGS %s", json_data["tags"]) + json_data["tags"] = utilities.csv_to_list(json_data["tags"]) + return json_data From 4dc61030e764b84c76bbf1565c3f8ad1f8314d22 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 14:29:46 +0100 Subject: [PATCH 22/37] Convert common attributes --- sonar/applications.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonar/applications.py b/sonar/applications.py index 61cd9e0f..339799b5 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -42,6 +42,7 @@ import sonar.utilities as util from sonar.audit import rules, problem import sonar.util.constants as c +from sonar.util import common_json_helper _CLASS_LOCK = Lock() _IMPORTABLE_PROPERTIES = ("key", "name", "description", "visibility", "branches", "permissions", "tags") @@ -602,7 +603,7 @@ def search_by_name(endpoint: pf.Platform, name: str) -> dict[str, Application]: def convert_app_json(old_app_json: dict[str, Any]) -> dict[str, Any]: """Converts sonar-config old JSON report format to new format for a single application""" - new_json = old_app_json.copy() + new_json = common_json_helper.convert_common_fields(old_app_json.copy()) if "permissions" in old_app_json: new_json["permissions"] = util.perms_to_list(old_app_json["permissions"]) if "branches" in old_app_json: From 1a0783dbe2bbef742b33399206dbe4a349a06420 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 14:30:06 +0100 Subject: [PATCH 23/37] Convert common attributes --- sonar/util/portfolio_helper.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sonar/util/portfolio_helper.py b/sonar/util/portfolio_helper.py index aba7455c..891a0ea7 100644 --- a/sonar/util/portfolio_helper.py +++ b/sonar/util/portfolio_helper.py @@ -21,11 +21,14 @@ from typing import Any from sonar import utilities as util +from sonar.util import common_json_helper def convert_portfolio_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config old JSON report format for a single portfolio to the new one""" - new_json = old_json.copy() + new_json = common_json_helper.convert_common_fields(old_json.copy()) + if "projects" in new_json: + new_json["projects"] = common_json_helper.convert_common_fields(new_json["projects"]) for key in "children", "portfolios": if key in new_json: new_json[key] = convert_portfolios_json(new_json[key]) @@ -38,7 +41,6 @@ def convert_portfolio_json(old_json: dict[str, Any]) -> dict[str, Any]: def convert_portfolios_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config portfolios old JSON report format to the new one""" - new_json = old_json.copy() - for k, v in new_json.items(): - new_json[k] = convert_portfolio_json(v) - return util.dict_to_list(new_json, "key") + for k, v in old_json.items(): + old_json[k] = convert_portfolio_json(v) + return util.dict_to_list(old_json, "key") From a904a4123df7530f617b1d8f6ebdba784b576c4a Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 14:30:25 +0100 Subject: [PATCH 24/37] convert common attributes --- sonar/util/project_helper.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sonar/util/project_helper.py b/sonar/util/project_helper.py index 04c27127..0c929e07 100644 --- a/sonar/util/project_helper.py +++ b/sonar/util/project_helper.py @@ -21,7 +21,7 @@ from typing import Any from sonar import utilities as util - +from sonar.util import common_json_helper UNNEEDED_TASK_DATA = ( "analysisId", @@ -84,8 +84,6 @@ def convert_project_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config projects old JSON report format for a single project to the new one""" new_json = old_json.copy() - if "permissions" in old_json: - new_json["permissions"] = util.perms_to_list(old_json["permissions"]) if "qualityProfiles" in old_json: new_json["qualityProfiles"] = util.dict_to_list(old_json["qualityProfiles"], "language", "name") if "branches" in old_json: @@ -99,6 +97,7 @@ def convert_project_json(old_json: dict[str, Any]) -> dict[str, Any]: if AI_CODE_FIX in new_json["settings"] and not new_json["settings"][AI_CODE_FIX]: new_json["settings"].pop(AI_CODE_FIX) new_json["settings"] = util.dict_to_list(dict(sorted(new_json["settings"].items())), "key", "value") + new_json = common_json_helper.convert_common_fields(new_json) return util.order_dict(new_json, _JSON_KEY_ORDER) From db67ab3edc2b51c901178d4ea783ce5137fcd975 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 14:30:43 +0100 Subject: [PATCH 25/37] Use common attributes conversion --- sonar/util/rule_helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sonar/util/rule_helper.py b/sonar/util/rule_helper.py index a409b2ce..55b8c14f 100644 --- a/sonar/util/rule_helper.py +++ b/sonar/util/rule_helper.py @@ -21,12 +21,12 @@ from typing import Any from sonar import utilities +from sonar.util import common_json_helper def convert_rule_json(old_json: dict[str, Any]) -> dict[str, Any]: - if "tags" in old_json: - old_json["tags"] = utilities.list_to_csv(old_json["tags"]) - return old_json + """Converts a rule JSON from old to new export format""" + return common_json_helper.convert_common_fields(old_json) def convert_rules_json(old_json: dict[str, Any]) -> dict[str, Any]: From bc579c903ab050cb4960a2b9907bc2f2fcdefaef Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 14:50:44 +0100 Subject: [PATCH 26/37] Remove list inlining --- cli/config.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/cli/config.py b/cli/config.py index d815dfa9..7cc90f6c 100644 --- a/cli/config.py +++ b/cli/config.py @@ -45,7 +45,6 @@ TOOL_NAME = "sonar-config" -DONT_INLINE_LISTS = "dontInlineLists" FULL_EXPORT = "fullExport" EXPORT_EMPTY = "exportEmpty" @@ -110,14 +109,6 @@ def __parse_args(desc: str) -> object: f"By default the export will show the value as '{utilities.DEFAULT}' " "and the setting will not be imported at import time", ) - parser.add_argument( - f"--{DONT_INLINE_LISTS}", - required=False, - default=False, - action="store_true", - help="By default, sonar-config exports multi-valued settings as comma separated strings instead of arrays (if there is not comma in values). " - "Set this flag if you want to force export multi valued settings as arrays", - ) parser.add_argument( f"--{EXPORT_EMPTY}", required=False, @@ -192,10 +183,12 @@ def write_objects(queue: Queue[types.ObjectJsonRepr], fd: TextIO, object_type: s while not done: obj_json = queue.get() if not (done := obj_json is utilities.WRITE_END): + log.info("WRITING %s", utilities.json_dump(obj_json)) if object_type == "groups": obj_json = __prep_json_for_write(obj_json, {**export_settings, EXPORT_EMPTY: True}) else: obj_json = __prep_json_for_write(obj_json, export_settings) + log.info("CONVERTED WRITING %s", utilities.json_dump(obj_json)) key = "" if isinstance(obj_json, list) else obj_json.get("key", obj_json.get("login", obj_json.get("name", "unknown"))) log.debug("Writing %s key '%s'", object_type[:-1], key) if object_type in objects_exported_as_lists or object_type in objects_exported_as_whole: @@ -216,7 +209,6 @@ def export_config(endpoint: platform.Platform, what: list[str], **kwargs) -> Non export_settings = kwargs.copy() export_settings.update( { - "INLINE_LISTS": not kwargs.get(DONT_INLINE_LISTS, False), "EXPORT_DEFAULTS": True, "FULL_EXPORT": kwargs.get(FULL_EXPORT, False), "MODE": mode, @@ -274,8 +266,6 @@ def __prep_json_for_write(json_data: types.ObjectJsonRepr, export_settings: type if not export_settings.get(EXPORT_EMPTY, False): log.debug("Removing empties") json_data = utilities.clean_data(json_data, remove_empty=True) - if export_settings.get("INLINE_LISTS", True): - json_data = utilities.inline_lists(json_data, exceptions=("conditions",)) return json_data From 988f50b40251d77b906bdded9d587aedaf22b18a Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 14:52:35 +0100 Subject: [PATCH 27/37] Remove logs --- cli/config.py | 1 - sonar/util/common_json_helper.py | 1 - 2 files changed, 2 deletions(-) diff --git a/cli/config.py b/cli/config.py index 7cc90f6c..56712862 100644 --- a/cli/config.py +++ b/cli/config.py @@ -188,7 +188,6 @@ def write_objects(queue: Queue[types.ObjectJsonRepr], fd: TextIO, object_type: s obj_json = __prep_json_for_write(obj_json, {**export_settings, EXPORT_EMPTY: True}) else: obj_json = __prep_json_for_write(obj_json, export_settings) - log.info("CONVERTED WRITING %s", utilities.json_dump(obj_json)) key = "" if isinstance(obj_json, list) else obj_json.get("key", obj_json.get("login", obj_json.get("name", "unknown"))) log.debug("Writing %s key '%s'", object_type[:-1], key) if object_type in objects_exported_as_lists or object_type in objects_exported_as_whole: diff --git a/sonar/util/common_json_helper.py b/sonar/util/common_json_helper.py index 022d0e4d..6c9e9b80 100644 --- a/sonar/util/common_json_helper.py +++ b/sonar/util/common_json_helper.py @@ -30,6 +30,5 @@ def convert_common_fields(json_data: dict[str, Any], with_permissions: bool = Tr for perm in json_data["permissions"]: perm["permissions"] = utilities.csv_to_list(perm["permissions"]) if "tags" in json_data: - log.info("CONVERTING TAGS %s", json_data["tags"]) json_data["tags"] = utilities.csv_to_list(json_data["tags"]) return json_data From 35fe445c7c30ba238a24755ed6a0b99d4b9cc822 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 15:59:17 +0100 Subject: [PATCH 28/37] Use common fields conversion --- sonar/qualitygates.py | 2 ++ sonar/util/platform_helper.py | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/sonar/qualitygates.py b/sonar/qualitygates.py index 1051595d..cfe90fcc 100644 --- a/sonar/qualitygates.py +++ b/sonar/qualitygates.py @@ -38,6 +38,7 @@ from sonar.audit.rules import get_rule, RuleId from sonar.audit.problem import Problem +from sonar.util import common_json_helper __MAX_ISSUES_SHOULD_BE_ZERO = "Any numeric threshold on number of issues should be 0 or should be removed from QG conditions" @@ -565,4 +566,5 @@ def search_by_name(endpoint: pf.Platform, name: str) -> dict[str, QualityGate]: def convert_qgs_json(old_json: dict[str, Any]) -> dict[str, Any]: """Converts the sonar-config quality gates old JSON report format to the new one""" + old_json = common_json_helper.convert_common_fields(old_json, with_permissions=False) return util.dict_to_list(old_json, "name") diff --git a/sonar/util/platform_helper.py b/sonar/util/platform_helper.py index 555b2207..ac16ac4e 100644 --- a/sonar/util/platform_helper.py +++ b/sonar/util/platform_helper.py @@ -22,6 +22,7 @@ from typing import Any from sonar import settings from sonar import utilities as util +from sonar.util import common_json_helper _PERM_TPL_IMPORTABLE_PROPERTIES = ("name", "description", "pattern", "defaultFor", "permissions") @@ -47,8 +48,7 @@ def convert_basics_json(old_json: dict[str, Any]) -> dict[str, Any]: def convert_template_json(json_data: dict[str, Any], full: bool = False) -> dict[str, Any]: - if "permissions" in json_data: - json_data["permissions"] = util.perms_to_list(json_data["permissions"]) + json_data = common_json_helper.convert_common_fields(json_data) return util.remove_nones(util.filter_export(json_data, _PERM_TPL_IMPORTABLE_PROPERTIES, full)) @@ -63,8 +63,9 @@ def convert_global_settings_json(old_json: dict[str, Any], full: bool = False) - new_json[settings.LANGUAGES_SETTINGS][k] = util.sort_list_by_key(util.dict_to_list(v, "key"), "key") new_json[settings.LANGUAGES_SETTINGS] = util.dict_to_list(dict(sorted(new_json[settings.LANGUAGES_SETTINGS].items())), "language", "settings") new_json[settings.DEVOPS_INTEGRATION] = util.dict_to_list(dict(sorted(old_json[settings.DEVOPS_INTEGRATION].items())), "key") - new_json["permissions"] = util.perms_to_list(old_json["permissions"]) for k, v in new_json["permissionTemplates"].items(): new_json["permissionTemplates"][k] = convert_template_json(new_json["permissionTemplates"][k], full) new_json["permissionTemplates"] = util.dict_to_list(new_json["permissionTemplates"], "key") - return new_json + new_json = common_json_helper.convert_common_fields(new_json) + + return util.order_dict(new_json, [*settings.CATEGORIES, "permissions", "permissionTemplates"]) From 06b33a71da4dda5445ff9de8a24f88d3d0238e21 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 15:59:58 +0100 Subject: [PATCH 29/37] Use the conversion helper --- sonar/platform.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/sonar/platform.py b/sonar/platform.py index af59e441..0a6ac36d 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -499,22 +499,7 @@ def export(self, export_settings: types.ConfigSettings, full: bool = False) -> t if not self.is_sonarcloud(): json_data[settings.DEVOPS_INTEGRATION] = devops.export(self, export_settings=export_settings) - # Convert dicts to lists - special_categories = (settings.LANGUAGES_SETTINGS, settings.DEVOPS_INTEGRATION, "permissions", "permissionTemplates") - for categ in [cat for cat in settings.CATEGORIES if cat not in special_categories]: - json_data[categ] = util.sort_list_by_key(util.dict_to_list(json_data[categ], "key"), "key") - for k, v in sorted(json_data[settings.LANGUAGES_SETTINGS].items()): - json_data[settings.LANGUAGES_SETTINGS][k] = util.sort_list_by_key(util.dict_to_list(v, "key"), "key") - json_data[settings.LANGUAGES_SETTINGS] = util.dict_to_list(json_data[settings.LANGUAGES_SETTINGS], "language", "settings") - json_data[settings.LANGUAGES_SETTINGS] = util.sort_list_by_key(json_data[settings.LANGUAGES_SETTINGS], "language") - json_data[settings.DEVOPS_INTEGRATION] = util.dict_to_list(json_data[settings.DEVOPS_INTEGRATION], "key") - json_data["permissions"] = util.perms_to_list(json_data["permissions"]) - for v in json_data["permissionTemplates"].values(): - if "permissions" in v: - v["permissions"] = util.perms_to_list(v["permissions"]) - json_data["permissionTemplates"] = util.dict_to_list(json_data["permissionTemplates"], "key") - - return util.order_dict(json_data, [*settings.CATEGORIES, "permissions", "permissionTemplates"]) + return pfhelp.convert_global_settings_json(json_data) def set_webhooks(self, webhooks_data: types.ObjectJsonRepr) -> bool: """Sets global webhooks with a list of webhooks represented as JSON From b8b61c9672ef54049c531aeba42bfb0ba9039a37 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 16:00:27 +0100 Subject: [PATCH 30/37] Remove perms field for objects with no permission --- sonar/util/common_json_helper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sonar/util/common_json_helper.py b/sonar/util/common_json_helper.py index 6c9e9b80..d4816b47 100644 --- a/sonar/util/common_json_helper.py +++ b/sonar/util/common_json_helper.py @@ -25,6 +25,8 @@ def convert_common_fields(json_data: dict[str, Any], with_permissions: bool = True) -> dict[str, Any]: + if "permissions" in json_data and json_data["permissions"] is None: + json_data.pop("permissions") if with_permissions and "permissions" in json_data: json_data["permissions"] = utilities.perms_to_list(json_data["permissions"]) for perm in json_data["permissions"]: From 200eef3c55e7c0579accf6381182c2cdc1b2c4cb Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 16:00:55 +0100 Subject: [PATCH 31/37] Use common JSON conversion helper --- sonar/util/qualityprofile_helper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonar/util/qualityprofile_helper.py b/sonar/util/qualityprofile_helper.py index 10d98c40..2ad72c5a 100644 --- a/sonar/util/qualityprofile_helper.py +++ b/sonar/util/qualityprofile_helper.py @@ -22,6 +22,7 @@ from typing import Any from sonar import utilities as util from sonar.util import types +from sonar.util import common_json_helper KEY_PARENT = "parent" KEY_CHILDREN = "children" @@ -62,7 +63,7 @@ def __convert_qp_json(qp_json: dict[str, Any]) -> list[dict[str, Any]]: r.pop("severities") if "children" in v: v["children"] = __convert_qp_json(v["children"]) - qp_json[k] = util.order_keys(v, *KEY_ORDER) + qp_json[k] = util.order_keys(common_json_helper.convert_common_fields(v, with_permissions=False), *KEY_ORDER) return util.dict_to_list(qp_json, "name") From 5743981a69a3a86c4d3906bafae2ee3a12ed099f Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 16:33:25 +0100 Subject: [PATCH 32/37] Remove logs --- cli/config.py | 1 - sonar/portfolios.py | 1 - 2 files changed, 2 deletions(-) diff --git a/cli/config.py b/cli/config.py index 56712862..026047f5 100644 --- a/cli/config.py +++ b/cli/config.py @@ -183,7 +183,6 @@ def write_objects(queue: Queue[types.ObjectJsonRepr], fd: TextIO, object_type: s while not done: obj_json = queue.get() if not (done := obj_json is utilities.WRITE_END): - log.info("WRITING %s", utilities.json_dump(obj_json)) if object_type == "groups": obj_json = __prep_json_for_write(obj_json, {**export_settings, EXPORT_EMPTY: True}) else: diff --git a/sonar/portfolios.py b/sonar/portfolios.py index 5c2c4995..d71c507d 100644 --- a/sonar/portfolios.py +++ b/sonar/portfolios.py @@ -370,7 +370,6 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr if not self.is_sub_portfolio(): json_data["visibility"] = self._visibility if perms := self.permissions().export(export_settings=export_settings): - log.info("%s PERMS = %s", self, str(perms)) json_data["permissions"] = util.perms_to_list(perms) json_data["tags"] = self._tags if subportfolios: From 99d58b032cfa2504cf96f34b32d21cd9501443ca Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 17:05:49 +0100 Subject: [PATCH 33/37] Remove perms convestion (done in the generic conversion code) --- sonar/util/portfolio_helper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sonar/util/portfolio_helper.py b/sonar/util/portfolio_helper.py index 891a0ea7..68397d52 100644 --- a/sonar/util/portfolio_helper.py +++ b/sonar/util/portfolio_helper.py @@ -32,8 +32,6 @@ def convert_portfolio_json(old_json: dict[str, Any]) -> dict[str, Any]: for key in "children", "portfolios": if key in new_json: new_json[key] = convert_portfolios_json(new_json[key]) - if "permissions" in old_json: - new_json["permissions"] = util.perms_to_list(old_json["permissions"]) if "branches" in old_json: new_json["branches"] = util.dict_to_list(old_json["branches"], "name") return new_json From 707dfba3aac178db8cc62f7f3c995bb8aeb81235 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 17:22:51 +0100 Subject: [PATCH 34/37] Fix users export --- sonar/users.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sonar/users.py b/sonar/users.py index 63897587..04d21913 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -42,7 +42,7 @@ _GROUPS_API_SC = "users/groups" -SETTABLE_PROPERTIES = ("login", "name", "scmAccounts", "email", "groups", "local") +SETTABLE_PROPERTIES = ("login", "name", "email", "groups", "scmAccounts", "local") USER_API = "v2/users-management/users" @@ -448,7 +448,8 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr json_data.pop("local") for key in "sonarQubeLastConnectionDate", "externalLogin", "externalProvider", "id", "managed": json_data.pop(key, None) - return util.filter_export(json_data, SETTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False)) + json_data = util.filter_export(json_data, SETTABLE_PROPERTIES, export_settings.get("FULL_EXPORT", False)) + return convert_user_json(json_data) def search(endpoint: pf.Platform, params: types.ApiParams = None) -> dict[str, User]: @@ -571,7 +572,10 @@ def exists(endpoint: pf.Platform, login: str) -> bool: def convert_user_json(old_json: dict[str, Any]) -> dict[str, Any]: - return util.order_dict(old_json, ["name", "email", "groups", "scmAccounts", "local"]) + for k in "groups", "scmAccounts": + if k in old_json: + old_json[k] = util.csv_to_list(old_json[k]) + return util.order_dict(old_json, SETTABLE_PROPERTIES) def convert_users_json(old_json: dict[str, Any]) -> dict[str, Any]: From d6c70b7fd88f65b928b2deee2a5d20aabbac69ca Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 17:31:53 +0100 Subject: [PATCH 35/37] remove perms conversion, done in a common function --- sonar/applications.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sonar/applications.py b/sonar/applications.py index 339799b5..55eb679f 100644 --- a/sonar/applications.py +++ b/sonar/applications.py @@ -604,8 +604,6 @@ def search_by_name(endpoint: pf.Platform, name: str) -> dict[str, Application]: def convert_app_json(old_app_json: dict[str, Any]) -> dict[str, Any]: """Converts sonar-config old JSON report format to new format for a single application""" new_json = common_json_helper.convert_common_fields(old_app_json.copy()) - if "permissions" in old_app_json: - new_json["permissions"] = util.perms_to_list(old_app_json["permissions"]) if "branches" in old_app_json: new_json["branches"] = util.dict_to_list(old_app_json["branches"], "name") return new_json From eb459a71b41355c10ffee6149b598853a61baf11 Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 17:37:43 +0100 Subject: [PATCH 36/37] Fix export for SQC --- sonar/platform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar/platform.py b/sonar/platform.py index 0a6ac36d..17c6f0b6 100644 --- a/sonar/platform.py +++ b/sonar/platform.py @@ -499,7 +499,7 @@ def export(self, export_settings: types.ConfigSettings, full: bool = False) -> t if not self.is_sonarcloud(): json_data[settings.DEVOPS_INTEGRATION] = devops.export(self, export_settings=export_settings) - return pfhelp.convert_global_settings_json(json_data) + return pfhelp.convert_global_settings_json(json_data) def set_webhooks(self, webhooks_data: types.ObjectJsonRepr) -> bool: """Sets global webhooks with a list of webhooks represented as JSON From 75be34199295a880b25191ac332885138e70e71b Mon Sep 17 00:00:00 2001 From: Olivier Korach Date: Fri, 7 Nov 2025 17:38:01 +0100 Subject: [PATCH 37/37] Quality pass --- sonar/portfolios.py | 4 ++-- sonar/users.py | 1 + sonar/util/common_json_helper.py | 1 + sonar/util/platform_helper.py | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/sonar/portfolios.py b/sonar/portfolios.py index d71c507d..6ae53f71 100644 --- a/sonar/portfolios.py +++ b/sonar/portfolios.py @@ -369,8 +369,8 @@ def to_json(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr subportfolios = self.sub_portfolios() if not self.is_sub_portfolio(): json_data["visibility"] = self._visibility - if perms := self.permissions().export(export_settings=export_settings): - json_data["permissions"] = util.perms_to_list(perms) + if pf_perms := self.permissions().export(export_settings=export_settings): + json_data["permissions"] = util.perms_to_list(pf_perms) json_data["tags"] = self._tags if subportfolios: json_data["portfolios"] = {} diff --git a/sonar/users.py b/sonar/users.py index 04d21913..4412c0a7 100644 --- a/sonar/users.py +++ b/sonar/users.py @@ -572,6 +572,7 @@ def exists(endpoint: pf.Platform, login: str) -> bool: def convert_user_json(old_json: dict[str, Any]) -> dict[str, Any]: + """Converts a user JSON from old to new format""" for k in "groups", "scmAccounts": if k in old_json: old_json[k] = util.csv_to_list(old_json[k]) diff --git a/sonar/util/common_json_helper.py b/sonar/util/common_json_helper.py index d4816b47..c8b3c7d7 100644 --- a/sonar/util/common_json_helper.py +++ b/sonar/util/common_json_helper.py @@ -25,6 +25,7 @@ def convert_common_fields(json_data: dict[str, Any], with_permissions: bool = True) -> dict[str, Any]: + """Converts Sonar objects common fields from old to new JSON format""" if "permissions" in json_data and json_data["permissions"] is None: json_data.pop("permissions") if with_permissions and "permissions" in json_data: diff --git a/sonar/util/platform_helper.py b/sonar/util/platform_helper.py index ac16ac4e..6325e2c4 100644 --- a/sonar/util/platform_helper.py +++ b/sonar/util/platform_helper.py @@ -48,6 +48,7 @@ def convert_basics_json(old_json: dict[str, Any]) -> dict[str, Any]: def convert_template_json(json_data: dict[str, Any], full: bool = False) -> dict[str, Any]: + """Converts a Perm Template JSON from old to new format""" json_data = common_json_helper.convert_common_fields(json_data) return util.remove_nones(util.filter_export(json_data, _PERM_TPL_IMPORTABLE_PROPERTIES, full))