Skip to content

Commit b8a62db

Browse files
authored
Update codetf bindings (#490)
* Update CodeTF bindings to reflect new spec * Update rule metadata to accept multiple rules
1 parent b7514e1 commit b8a62db

File tree

16 files changed

+145
-93
lines changed

16 files changed

+145
-93
lines changed

src/codemodder/codemods/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Reference,
1010
ReviewGuidance,
1111
ToolMetadata,
12+
ToolRule,
1213
)
1314
from codemodder.codemods.libcst_transformer import (
1415
LibcstResultTransformer,

src/codemodder/codemods/base_codemod.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from codemodder.code_directory import file_line_patterns
1414
from codemodder.codemods.base_detector import BaseDetector
1515
from codemodder.codemods.base_transformer import BaseTransformerPipeline
16-
from codemodder.codetf import DetectionTool, Reference, Rule
16+
from codemodder.codetf import DetectionTool, Reference
1717
from codemodder.context import CodemodExecutionContext
1818
from codemodder.file_context import FileContext
1919
from codemodder.logging import logger
@@ -37,12 +37,21 @@ class Metadata:
3737
language: str = "python"
3838

3939

40+
@dataclass
41+
class ToolRule:
42+
id: str
43+
name: str
44+
url: str | None = None
45+
46+
4047
@dataclass
4148
class ToolMetadata:
4249
name: str
43-
rule_id: str
44-
rule_name: str
45-
rule_url: str | None = None
50+
rules: list[ToolRule]
51+
52+
@property
53+
def rule_ids(self):
54+
return [rule.id for rule in self.rules]
4655

4756

4857
class BaseCodemod(metaclass=ABCMeta):
@@ -115,11 +124,6 @@ def detection_tool(self) -> DetectionTool | None:
115124

116125
return DetectionTool(
117126
name=self._metadata.tool.name,
118-
rule=Rule(
119-
id=self._metadata.tool.rule_id,
120-
name=self._metadata.tool.rule_name,
121-
url=self._metadata.tool.rule_url,
122-
),
123127
)
124128

125129
@cached_property

src/codemodder/codemods/base_visitor.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import ClassVar, Collection
1+
from functools import cache
2+
from typing import ClassVar, Collection, cast
23

34
import libcst as cst
45
from libcst import MetadataDependent
@@ -23,13 +24,22 @@ def __init__(
2324
self.line_exclude = line_exclude
2425
self.line_include = line_include
2526

26-
def filter_by_result(self, node):
27+
def filter_by_result(self, node: cst.CSTNode) -> bool:
28+
# Codemods with detectors will only run their transformations if there are results.
29+
return self.results is None or any(self.results_for_node(node))
30+
31+
@cache
32+
def results_for_node(self, node: cst.CSTNode) -> list[Result]:
2733
pos_to_match = self.node_position(node)
28-
if self.results is None:
29-
# Returning True here means codemods without detectors (and results)
30-
# will still run their transformations.
31-
return True
32-
return any(result.match_location(pos_to_match, node) for result in self.results)
34+
return (
35+
[
36+
result
37+
for result in self.results
38+
if result.match_location(pos_to_match, node)
39+
]
40+
if self.results
41+
else []
42+
)
3343

3444
def filter_by_path_includes_or_excludes(self, pos_to_match):
3545
"""
@@ -55,13 +65,17 @@ def node_position(self, node):
5565
# By default a function's position includes the entire
5666
# function definition. Instead, we will only use the first line
5767
# of the function definition.
58-
params_end = self.get_metadata(PositionProvider, node.params).end
68+
params_end = cast(
69+
CodeRange, self.get_metadata(PositionProvider, node.params)
70+
).end
5971
return CodeRange(
60-
start=self.get_metadata(PositionProvider, node).start,
72+
start=cast(
73+
CodeRange, self.get_metadata(PositionProvider, node)
74+
).start,
6175
end=CodePosition(params_end.line, params_end.column + 1),
6276
)
6377
case _:
64-
return self.get_metadata(PositionProvider, node)
78+
return cast(CodeRange, self.get_metadata(PositionProvider, node))
6579

6680
def lineno_for_node(self, node):
6781
return self.node_position(node).start.line

src/codemodder/codemods/libcst_transformer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,15 @@ def report_change(self, original_node, description: str | None = None):
124124
Change(
125125
lineNumber=line_number,
126126
description=description or self.change_description,
127+
# TODO: add fixed findings
127128
)
128129
)
129130

131+
def report_unfixed(self, original_node: cst.CSTNode, reason: str):
132+
pass
133+
# results = self.results_for_node(original_node)
134+
# self.file_context.unfixed_findings.extend(results)
135+
130136
def remove_unused_import(self, original_node):
131137
RemoveImportsVisitor.remove_unused_import_by_node(self.context, original_node)
132138

src/codemodder/codemods/test/integration_utils.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,9 @@ def check_sonar_issues(cls):
288288
)
289289

290290
assert (
291-
cls.codemod.rule_id in sonar_results
291+
cls.codemod.requested_rules[-1] in sonar_results
292292
), f"Make sure to add a sonar issue/hotspot for {cls.codemod.rule_id} in {cls.sonar_issues_json} or {cls.sonar_hotspots_json}"
293-
results_for_codemod = sonar_results[cls.codemod.rule_id]
293+
results_for_codemod = sonar_results[cls.codemod.requested_rules[-1]]
294294
file_path = pathlib.Path(cls.code_filename)
295295
assert (
296296
file_path in results_for_codemod
@@ -300,19 +300,9 @@ def _assert_sonar_fields(self, result):
300300
assert self.codemod_instance._metadata.tool is not None
301301
assert (
302302
result["references"][-1]["description"]
303-
== self.codemod_instance._metadata.tool.rule_name
303+
== self.codemod_instance._metadata.tool.rules[0].name
304304
)
305305
assert result["detectionTool"]["name"] == "Sonar"
306-
assert (
307-
result["detectionTool"]["rule"]["id"]
308-
== self.codemod_instance._metadata.tool.rule_id
309-
)
310-
assert (
311-
result["detectionTool"]["rule"]["name"]
312-
== self.codemod_instance._metadata.tool.rule_name
313-
)
314-
# TODO: empty array until we add findings metadata
315-
assert result["detectionTool"]["findings"] == []
316306

317307

318308
def original_and_expected_from_code_path(code_path, replacements):

src/codemodder/codetf.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class Change(BaseModel):
5252
diffSide: DiffSide = DiffSide.RIGHT
5353
properties: Optional[dict] = None
5454
packageActions: Optional[list[PackageAction]] = None
55+
finding: Optional[Finding] = None
5556

5657

5758
class ChangeSet(BaseModel):
@@ -80,19 +81,17 @@ class Rule(BaseModel):
8081

8182
class Finding(BaseModel):
8283
id: str
83-
fixed: bool
84-
reason: Optional[str] = None
84+
rule: Rule
8585

86-
@model_validator(mode="after")
87-
def validate_reason(self):
88-
assert self.fixed or self.reason, "reason is required if fixed is False"
89-
return self
86+
87+
class UnfixedFinding(Finding):
88+
path: str
89+
lineNumber: Optional[int] = None
90+
reason: str
9091

9192

9293
class DetectionTool(BaseModel):
9394
name: str
94-
rule: Rule
95-
findings: list[Finding] = []
9695

9796

9897
class Result(BaseModel):
@@ -104,6 +103,7 @@ class Result(BaseModel):
104103
properties: Optional[dict] = None
105104
failedFiles: Optional[list[str]] = None
106105
changeset: list[ChangeSet]
106+
unfixedFindings: Optional[list[UnfixedFinding]] = None
107107

108108

109109
class Sarif(BaseModel):

src/codemodder/file_context.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from dataclasses import dataclass, field
22
from pathlib import Path
33

4-
from codemodder.codetf import Change, ChangeSet
4+
from codemodder.codetf import Change, ChangeSet, UnfixedFinding
55
from codemodder.dependency import Dependency
66
from codemodder.result import Result
77
from codemodder.utils.timer import Timer
@@ -20,6 +20,7 @@ class FileContext:
2020
findings: list[Result] | None = field(default_factory=list)
2121
dependencies: set[Dependency] = field(default_factory=set)
2222
codemod_changes: list[Change] = field(default_factory=list)
23+
unfixed_findings: list[UnfixedFinding] = field(default_factory=list)
2324
results: list[ChangeSet] = field(default_factory=list)
2425
failures: list[Path] = field(default_factory=list)
2526
timer: Timer = field(default_factory=Timer)

src/codemodder/result.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class Location(ABCDataclass):
2727
end: LineInfo
2828

2929

30-
@dataclass
30+
@dataclass(kw_only=True)
3131
class Result(ABCDataclass):
3232
rule_id: str
3333
locations: list[Location]
@@ -47,6 +47,11 @@ def match_location(self, pos: CodeRange, node: cst.CSTNode) -> bool:
4747
)
4848

4949

50+
@dataclass(kw_only=True)
51+
class SASTResult(Result):
52+
finding_id: str
53+
54+
5055
def same_line(pos: CodeRange, location: Location) -> bool:
5156
return pos.start.line == location.start.line and pos.end.line == location.end.line
5257

src/core_codemods/defectdojo/api.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from typing_extensions import override
88

9-
from codemodder.codemods.api import Metadata, Reference, ToolMetadata
9+
from codemodder.codemods.api import Metadata, Reference, ToolMetadata, ToolRule
1010
from codemodder.codemods.base_detector import BaseDetector
1111
from codemodder.context import CodemodExecutionContext
1212
from codemodder.result import ResultSet
@@ -62,9 +62,13 @@ def from_core_codemod(
6262
+ other.description,
6363
tool=ToolMetadata(
6464
name="DefectDojo",
65-
rule_id=rule_id,
66-
rule_name=rule_name,
67-
rule_url=rule_url,
65+
rules=[
66+
ToolRule(
67+
id=rule_id,
68+
name=rule_name,
69+
url=rule_url,
70+
)
71+
],
6872
),
6973
),
7074
transformer=other.transformer,
@@ -82,5 +86,5 @@ def apply(
8286
context,
8387
files_to_analyze,
8488
# We know this has a tool because we created it with `from_core_codemod`
85-
[cast(ToolMetadata, self._metadata.tool).rule_id],
89+
cast(ToolMetadata, self._metadata.tool).rule_ids,
8690
)

src/core_codemods/defectdojo/results.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from libcst._position import CodeRange
77
from typing_extensions import Self, override
88

9-
from codemodder.result import LineInfo, Location, Result, ResultSet
9+
from codemodder.result import LineInfo, Location, ResultSet, SASTResult
1010

1111

1212
class DefectDojoLocation(Location):
@@ -20,10 +20,11 @@ def from_finding(cls, finding: dict) -> Self:
2020
)
2121

2222

23-
class DefectDojoResult(Result):
23+
class DefectDojoResult(SASTResult):
2424
@classmethod
2525
def from_finding(cls, finding: dict) -> Self:
2626
return cls(
27+
finding_id=finding["id"],
2728
rule_id=finding["title"],
2829
locations=[DefectDojoLocation.from_finding(finding)],
2930
)

0 commit comments

Comments
 (0)