Skip to content

Commit 17b08b3

Browse files
authored
Kill-td (#2022)
* Remove useless ruff rule * Reduce complexity * Align CSV export * Remove useless import * Remove built-in shadow * Remove built-in shadow * Import for type checking only * Add docstrings * Kill TD * Disable 2 ruff rules * Quality pass * Add tests on import projects * Add test on import projects * Fix regression * New reference config * Fix test import projects * Revert format * Fix regression
1 parent 76a5e6a commit 17b08b3

18 files changed

+3987
-3443
lines changed

cli/audit.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ def write_csv(queue: Queue[list[problem.Problem]], fd: TextIO, settings: types.C
100100
problems = __filter_problems(problems, settings)
101101
for p in problems:
102102
json_data = p.to_json(with_url)
103-
data = [] if not server_id else [server_id]
104-
data += [json_data[k] for k in ("problem", "type", "severity", "message") if k in json_data]
103+
data = [server_id] if server_id else []
104+
data += [json_data[k] for k in ("problem", "type", "severity", "message", "url") if k in json_data]
105105
csvwriter.writerow(data)
106106
queue.task_done()
107107
queue.task_done()
@@ -246,7 +246,7 @@ def main() -> None:
246246
errcode = errcodes.SIF_AUDIT_ERROR
247247
(settings["SERVER_ID"], problems) = _audit_sif(file, settings)
248248
problems = __filter_problems(problems, settings)
249-
problem.dump_report(problems, file=ofile, server_id=settings["SERVER_ID"], format=fmt)
249+
problem.dump_report(problems, file=ofile, server_id=settings["SERVER_ID"], fmt=fmt)
250250
else:
251251
sq = platform.Platform(**kwargs)
252252
sq.verify_connection()

cli/findings_export.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,12 @@ def main() -> None:
373373
key_regexp=params.get(options.KEY_REGEXP, None),
374374
branch_regexp=branch_regexp,
375375
)
376+
if params[options.COMPONENT_TYPE] == "portfolios":
377+
components = []
378+
for comp in components_list:
379+
components += comp.components()
380+
components_list = components
381+
376382
if len(components_list) == 0:
377383
br = f"and branch matching regexp '{params[options.BRANCH_REGEXP]}'" if options.BRANCH_REGEXP in params else ""
378384
raise exceptions.SonarException(

cli/housekeeper.py

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
from cli import options
3333
import sonar.logging as log
3434
from sonar import platform, tokens, users, projects, branches, pull_requests, version, errcodes
35-
from sonar.util import types
3635
import sonar.util.constants as c
3736
import sonar.utilities as util
3837
import sonar.exceptions as ex
@@ -145,41 +144,58 @@ def _parse_arguments() -> object:
145144
return options.parse_and_check(parser=parser, logger_name=TOOL_NAME)
146145

147146

148-
def _delete_objects(problems: problem.Problem, mode: str, settings: types.ConfigSettings) -> tuple[int, int, int, int, int]:
149-
"""Deletes objects (that should be housekept)"""
147+
def _revoke_tokens(problems: problem.Problem, mode: str) -> int:
148+
"""Revokes user tokens (that should be housekept)"""
150149
revoked_token_count = 0
151-
deleted_projects = {}
152-
deleted_branch_count = 0
153-
deleted_pr_count = 0
154-
deleted_loc = 0
155-
for p in problems:
150+
for p in [p for p in problems if isinstance(p.concerned_object, tokens.UserToken)]:
156151
obj = p.concerned_object
157-
if obj is None:
158-
continue # BUG
159152
try:
160-
if isinstance(obj, projects.Project):
161-
loc = int(obj.get_measure("ncloc", fallback="0"))
162-
if mode == "delete":
163-
log.info("Deleting %s, %d LoC", str(obj), loc)
164-
else:
165-
log.info("%s, %d LoC should be deleted", str(obj), loc)
166-
if mode != "delete" or obj.delete():
167-
deleted_projects[obj.key] = obj
168-
deleted_loc += loc
169-
if isinstance(obj, (tokens.UserToken, users.User)) and (mode != "delete" or obj.revoke()):
153+
if mode != "delete" or obj.revoke():
170154
revoked_token_count += 1
171-
elif settings[PROJ_MAX_AGE] > 0 and obj.project().key in deleted_projects:
172-
log.info("%s deleted, so no need to delete %s", str(obj.project()), str(obj))
173-
elif mode != "delete" or obj.delete():
174-
log.info("%s to delete", str(obj))
175-
if isinstance(obj, branches.Branch):
176-
deleted_branch_count += 1
177-
elif isinstance(obj, pull_requests.PullRequest):
178-
deleted_pr_count += 1
155+
except ex.ObjectNotFound:
156+
log.warning("Token %s does not exist, revocation skipped...", obj)
157+
return revoked_token_count
158+
159+
160+
def _delete_projects(problems: problem.Problem, mode: str) -> tuple[list[str], int]:
161+
"""Deletes projects (that should be housekept)"""
162+
deleted_projects = []
163+
loc_total = 0
164+
for obj in [p.concerned_object for p in problems if isinstance(p.concerned_object, projects.Project)]:
165+
try:
166+
loc = int(obj.get_measure("ncloc", fallback="0"))
167+
log.info("Deleting %s, %d LoC" if mode == "delete" else "%s, %d LoC should be deleted", str(obj), loc)
168+
if mode != "delete" or obj.delete():
169+
deleted_projects.append(obj.key)
170+
loc_total += loc
171+
except ex.ObjectNotFound:
172+
log.warning("%s does not exist, deletion skipped...", str(obj))
173+
return deleted_projects, loc_total
179174

175+
176+
def _delete_class(problems: problem.Problem, mode: str, proj_list: list[str], object_class: object) -> int:
177+
"""Deletes branches or PRs (that should be housekept)"""
178+
counter = 0
179+
for obj in [p.concerned_object for p in problems if isinstance(p.concerned_object, object_class)]:
180+
try:
181+
if obj.project().key in proj_list:
182+
log.info("%s deleted, so no need to delete %s", str(obj.project()), str(obj))
183+
continue
184+
log.info("%s to delete", str(obj))
185+
if mode != "delete" or obj.delete():
186+
counter += 1
180187
except ex.ObjectNotFound:
181188
log.warning("%s does not exist, deletion skipped...", str(obj))
189+
return counter
190+
182191

192+
def _delete_objects(problems: problem.Problem, mode: str) -> tuple[int, int, int, int, int]:
193+
"""Deletes objects (that should be housekept)"""
194+
deleted_projects = {}
195+
revoked_token_count = _revoke_tokens(problems, mode)
196+
deleted_projects, deleted_loc = _delete_projects(problems, mode)
197+
deleted_branch_count = _delete_class(problems, mode, deleted_projects, branches.Branch)
198+
deleted_pr_count = _delete_class(problems, mode, deleted_projects, pull_requests.PullRequest)
183199
return (
184200
len(deleted_projects),
185201
deleted_loc,
@@ -224,13 +240,11 @@ def main() -> None:
224240
if token_age:
225241
problems += get_user_problems(settings, sq)
226242

227-
problem.dump_report(problems, file=kwargs[options.REPORT_FILE], format="csv")
243+
problem.dump_report(problems, file=kwargs[options.REPORT_FILE], fmt="csv")
228244

229-
op = "to delete"
230-
if mode == "delete":
231-
op = "deleted"
232-
(deleted_proj, deleted_loc, deleted_branches, deleted_prs, revoked_tokens) = _delete_objects(problems, mode, settings=settings)
245+
(deleted_proj, deleted_loc, deleted_branches, deleted_prs, revoked_tokens) = _delete_objects(problems, mode)
233246

247+
op = "deleted" if mode == "delete" else "to delete"
234248
if proj_age > 0:
235249
log.info("%d projects older than %d days (%d LoCs) %s", deleted_proj, proj_age, deleted_loc, op)
236250
if branch_age > 0:

cli/support.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def main():
166166
if problems:
167167
found_problems = True
168168
log.warning("%d issues found during audit", len(problems))
169-
problem.dump_report(problems, file=None, format="csv")
169+
problem.dump_report(problems, file=None, fmt="csv")
170170
comment += build_jira_comments(problems)
171171
else:
172172
log.info("%d issues found during audit", len(problems))

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ extend-ignore = [
172172
"I001",
173173
"TRY003",
174174
"EM102",
175+
"C901", # Complexity rule, better covered by SonarQube
176+
"ANN401", # Disallow Any type annotation
177+
"TD001", # Disallow TODO/FIXME comments
175178
]
176179

177180
exclude = [

sonar/app_branches.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,22 @@
2121
"""Abstraction of Sonar Application Branch"""
2222

2323
from __future__ import annotations
24-
from typing import Optional
25-
from datetime import datetime
26-
24+
from typing import Optional, TYPE_CHECKING
2725
import json
2826
from requests.utils import quote
2927

3028
import sonar.logging as log
31-
from sonar.util import types, cache
29+
from sonar.util import cache
3230

3331
from sonar.components import Component
3432

3533
from sonar.branches import Branch
3634
from sonar import exceptions, projects, utilities
3735
import sonar.util.constants as c
3836

37+
if TYPE_CHECKING:
38+
from sonar.util import types
39+
from datetime import datetime
3940

4041
_NOT_SUPPORTED = "Applications not supported in community edition"
4142

sonar/applications.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ def delete(self) -> bool:
260260
return super().delete()
261261

262262
def get_hotspots(self, filters: Optional[dict[str, str]] = None) -> dict[str, object]:
263+
"""Returns the security hotspots of the application (ie of its projects or branches)"""
263264
new_filters = filters.copy() if filters else {}
264265
pattern = new_filters.pop("branch", None) if new_filters else None
265266
if not pattern:
@@ -271,6 +272,7 @@ def get_hotspots(self, filters: Optional[dict[str, str]] = None) -> dict[str, ob
271272
return findings_list
272273

273274
def get_issues(self, filters: Optional[dict[str, str]] = None) -> dict[str, object]:
275+
"""Returns the issues of the application (ie of its projects or branches)"""
274276
new_filters = filters.copy() if filters else {}
275277
pattern = new_filters.pop("branch", None) if new_filters else None
276278
if not pattern:

sonar/audit/problem.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def to_json(self, with_url=False):
6363

6464

6565
def dump_report(
66-
problems: list[Problem], file: str, server_id: Optional[str] = None, format: str = "csv", with_url: bool = False, separator: str = ","
66+
problems: list[Problem], file: str, server_id: Optional[str] = None, fmt: str = "csv", with_url: bool = False, separator: str = ","
6767
) -> None:
6868
"""Dumps to file a report about a list of problems
6969
@@ -74,7 +74,7 @@ def dump_report(
7474
:rtype: None
7575
"""
7676
log.info("Writing report to %s", f"file '{file}'" if file else "stdout")
77-
if format == "json":
77+
if fmt == "json":
7878
__dump_json(problems=problems, file=file, server_id=server_id, with_url=with_url)
7979
else:
8080
__dump_csv(problems=problems, file=file, server_id=server_id, with_url=with_url, separator=separator)
@@ -91,14 +91,13 @@ def __dump_csv(problems: list[Problem], file: str, server_id: Optional[str] = No
9191
with utilities.open_file(file, "w") as fd:
9292
csvwriter = csv.writer(fd, delimiter=separator)
9393
header = ["Server Id"] if server_id else []
94-
header += ["Audit Check", "Category", "Severity", "Message"]
94+
header += ["Problem", "Category", "Severity", "Message"]
9595
header += ["URL"] if with_url else []
9696
csvwriter.writerow(header)
9797
for p in problems:
98-
data = []
99-
if server_id is not None:
100-
data = [server_id]
101-
data += list(p.to_json(with_url).values())
98+
json_data = p.to_json(with_url)
99+
data = [server_id] if server_id else []
100+
data += [json_data[k] for k in ("problem", "type", "severity", "message", "url") if k in json_data]
102101
csvwriter.writerow(data)
103102

104103

sonar/branches.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,27 @@
2222

2323
from __future__ import annotations
2424
from http import HTTPStatus
25-
from typing import Optional
26-
from datetime import datetime
25+
from typing import Optional, TYPE_CHECKING
26+
2727
import json
2828
import re
2929
from urllib.parse import unquote
3030
import requests.utils
3131

3232
from sonar import platform
33-
from sonar.util import types, cache
33+
from sonar.util import cache
3434
import sonar.logging as log
3535
from sonar import components, settings, exceptions, tasks
36-
from sonar import projects
36+
from sonar import projects as proj
3737
import sonar.utilities as util
3838

3939
from sonar.audit.problem import Problem
4040
from sonar.audit.rules import get_rule, RuleId
4141
import sonar.util.constants as c
4242

43+
if TYPE_CHECKING:
44+
from sonar.util import types
45+
from datetime import datetime
4346

4447
_UNSUPPORTED_IN_CE = "Branches not available in Community Edition"
4548

@@ -57,7 +60,7 @@ class Branch(components.Component):
5760
"get_new_code": "new_code_periods/list",
5861
}
5962

60-
def __init__(self, project: projects.Project, name: str) -> None:
63+
def __init__(self, project: proj.Project, name: str) -> None:
6164
"""Don't use this, use class methods to create Branch objects
6265
6366
:raises UnsupportedOperation: When attempting to branches on Community Edition
@@ -76,10 +79,10 @@ def __init__(self, project: projects.Project, name: str) -> None:
7679
log.debug("Created object %s", str(self))
7780

7881
@classmethod
79-
def get_object(cls, concerned_object: projects.Project, branch_name: str) -> Branch:
82+
def get_object(cls, concerned_object: proj.Project, branch_name: str) -> Branch:
8083
"""Gets a SonarQube Branch object
8184
82-
:param projects.Project concerned_object: projects.Project concerned by the branch
85+
:param Project concerned_object: Project concerned by the branch
8386
:param str branch_name: The branch name
8487
:raises UnsupportedOperation: If trying to manipulate branches on a community edition
8588
:raises ObjectNotFound: If project key or branch name not found in SonarQube
@@ -97,10 +100,10 @@ def get_object(cls, concerned_object: projects.Project, branch_name: str) -> Bra
97100
return cls.load(concerned_object, branch_name, br)
98101

99102
@classmethod
100-
def load(cls, concerned_object: projects.Project, branch_name: str, data: types.ApiPayload) -> Branch:
103+
def load(cls, concerned_object: proj.Project, branch_name: str, data: types.ApiPayload) -> Branch:
101104
"""Gets a Branch object from JSON data gotten from a list API call
102105
103-
:param projects.Project concerned_object: the projects.Project the branch belonsg to
106+
:param Project concerned_object: the Project the branch belonsg to
104107
:param str branch_name: Name of the branch
105108
:param dict data: Data received from API call
106109
:return: The Branch object
@@ -122,7 +125,7 @@ def __hash__(self) -> int:
122125
"""Computes a uuid for the branch that can serve as index"""
123126
return hash((self.concerned_object.key, self.name, self.base_url()))
124127

125-
def project(self) -> projects.Project:
128+
def project(self) -> proj.Project:
126129
"""Returns the project key"""
127130
return self.concerned_object
128131

@@ -186,7 +189,7 @@ def get(
186189
except exceptions.ObjectNotFound as e:
187190
if re.match(r"Project .+ not found", e.message):
188191
log.warning("Clearing project cache")
189-
projects.Project.CACHE.clear()
192+
proj.Project.CACHE.clear()
190193
raise
191194

192195
def post(self, api: str, params: types.ApiParams = None, mute: tuple[HTTPStatus] = (), **kwargs: str) -> requests.Response:
@@ -196,7 +199,7 @@ def post(self, api: str, params: types.ApiParams = None, mute: tuple[HTTPStatus]
196199
except exceptions.ObjectNotFound as e:
197200
if re.match(r"Project .+ not found", e.message):
198201
log.warning("Clearing project cache")
199-
projects.Project.CACHE.clear()
202+
proj.Project.CACHE.clear()
200203
raise
201204

202205
def new_code(self) -> str:
@@ -396,8 +399,7 @@ def audit(self, audit_settings: types.ConfigSettings) -> list[Problem]:
396399
try:
397400
if audit_settings.get(c.AUDIT_MODE_PARAM, "") == "housekeeper":
398401
return self.__audit_last_analysis(audit_settings)
399-
else:
400-
return self.__audit_last_analysis(audit_settings) + self.__audit_never_analyzed() + self._audit_component(audit_settings)
402+
return self.__audit_last_analysis(audit_settings) + self.__audit_never_analyzed() + self._audit_component(audit_settings)
401403
except Exception as e:
402404
log.error("%s while auditing %s, audit skipped", util.error_msg(e), str(self))
403405
return []
@@ -414,10 +416,10 @@ def last_task(self) -> Optional[tasks.Task]:
414416
return task
415417

416418

417-
def get_list(project: projects.Project) -> dict[str, Branch]:
419+
def get_list(project: proj.Project) -> dict[str, Branch]:
418420
"""Retrieves the list of branches of a project
419421
420-
:param projects.Project project: projects.Project the branch belongs to
422+
:param Project project: Project the branch belongs to
421423
:raises UnsupportedOperation: Branches not supported in Community Edition
422424
:return: List of project branches
423425
:rtype: dict{branch_name: Branch}
@@ -436,13 +438,13 @@ def exists(endpoint: platform.Platform, branch_name: str, project_key: str) -> b
436438
437439
:param Platform endpoint: Reference to the SonarQube platform
438440
:param str branch_name: Branch name
439-
:param str project_key: projects.Project key
441+
:param str project_key: Project key
440442
:raises UnsupportedOperation: Branches not supported in Community Edition
441443
:return: Whether the branch exists in SonarQube
442444
:rtype: bool
443445
"""
444446
try:
445-
project = projects.Project.get_object(endpoint, project_key)
447+
project = proj.Project.get_object(endpoint, project_key)
446448
except exceptions.ObjectNotFound:
447449
return False
448450
return branch_name in get_list(project)

0 commit comments

Comments
 (0)