Skip to content

Commit 658bc91

Browse files
Use Pydantic to generate and validate CodeTF data models (pixee#357)
* Implement CodeTF data model using Pydantic * Rename codemodder.change => codemodder.codetf * Implement JSON schema validation for CodeTF results --------- Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
1 parent 9a46cce commit 658bc91

41 files changed

Lines changed: 396 additions & 301 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.pre-commit-config.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ repos:
3434
args: [--disable-error-code=has-type,--disable-error-code=import-not-found]
3535
additional_dependencies:
3636
[
37+
"types-jsonschema~=4.21.0",
3738
"types-mock==5.0.*",
3839
"types-PyYAML==6.0",
3940
"types-toml~=0.10",

‎integration_tests/conftest.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import json
2+
3+
import pytest
4+
import requests
5+
6+
7+
@pytest.fixture(scope="module")
8+
def codetf_schema():
9+
schema_path = "https://raw.githubusercontent.com/pixee/codemodder-specs/main/codetf.schema.json"
10+
response = requests.get(schema_path)
11+
yield json.loads(response.text)

‎pyproject.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies = [
1515
"isort>=5.12,<5.14",
1616
"libcst>=1.1,<1.3",
1717
"packaging>=23.2,<25.0",
18+
"pydantic~=2.6.0",
1819
"pylint>=3.0,<3.2",
1920
"python-json-logger~=2.0.0",
2021
"PyYAML~=6.0.0",
@@ -50,6 +51,7 @@ test = [
5051
"coverage>=7.3,<7.5",
5152
"Flask<4",
5253
"Jinja2~=3.1.2",
54+
"jsonschema~=4.21.0",
5355
"lxml>=4.9.3,<5.2.0",
5456
"mock==5.1.*",
5557
"pre-commit<4",
@@ -59,6 +61,7 @@ test = [
5961
"pytest-mock~=3.12.0",
6062
"pytest-randomly==3.*",
6163
"pytest-xdist==3.*",
64+
"requests~=2.31.0",
6265
"security~=1.2.0",
6366
"types-mock==5.1.*",
6467
"django>=4,<6",

‎src/codemodder/change.py‎

Lines changed: 0 additions & 71 deletions
This file was deleted.

‎src/codemodder/codemodder.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
from codemodder.code_directory import match_files
1212
from codemodder.codemods.api import BaseCodemod
1313
from codemodder.codemods.semgrep import SemgrepRuleDetector
14+
from codemodder.codetf import CodeTF
1415
from codemodder.context import CodemodExecutionContext
1516
from codemodder.dependency import Dependency
1617
from codemodder.logging import configure_logger, log_list, log_section, logger
1718
from codemodder.project_analysis.file_parsers.package_store import PackageStore
1819
from codemodder.project_analysis.python_repo_manager import PythonRepoManager
19-
from codemodder.report.codetf_reporter import report_default
2020
from codemodder.result import ResultSet
2121
from codemodder.sarifs import detect_sarif_tools
2222
from codemodder.semgrep import run as run_semgrep
@@ -210,13 +210,17 @@ def run(original_args) -> int:
210210
files_to_analyze,
211211
)
212212

213-
results = context.compile_results(codemods_to_run)
214-
215213
elapsed = datetime.datetime.now() - start
216214
elapsed_ms = int(elapsed.total_seconds() * 1000)
217215

218216
if argv.output:
219-
report_default(elapsed_ms, argv, original_args, results)
217+
codetf = CodeTF.build(
218+
context,
219+
elapsed_ms,
220+
original_args,
221+
context.compile_results(codemods_to_run),
222+
)
223+
codetf.write_report(argv.output)
220224

221225
log_report(context, argv, elapsed_ms, files_to_analyze)
222226
return 0

‎src/codemodder/codemods/base_codemod.py‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from codemodder.code_directory import file_line_patterns
1212
from codemodder.codemods.base_detector import BaseDetector
1313
from codemodder.codemods.base_transformer import BaseTransformerPipeline
14+
from codemodder.codetf import Reference
1415
from codemodder.context import CodemodExecutionContext
1516
from codemodder.file_context import FileContext
1617
from codemodder.logging import logger
@@ -23,18 +24,6 @@ class ReviewGuidance(Enum):
2324
MERGE_WITHOUT_REVIEW = 3
2425

2526

26-
@dataclass
27-
class Reference:
28-
url: str
29-
description: str = ""
30-
31-
def to_json(self):
32-
return {
33-
"url": self.url,
34-
"description": self.description or self.url,
35-
}
36-
37-
3827
@dataclass
3928
class Metadata:
4029
name: str
@@ -124,7 +113,7 @@ def describe(self):
124113
"codemod": self.id,
125114
"summary": self.summary,
126115
"description": self.description,
127-
"references": [ref.to_json() for ref in self.references],
116+
"references": [ref.model_dump() for ref in self.references],
128117
}
129118

130119
def _apply(

‎src/codemodder/codemods/base_transformer.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from abc import ABCMeta, abstractmethod
22

3-
from codemodder.change import ChangeSet
43
from codemodder.codemods.base_visitor import BaseTransformer
4+
from codemodder.codetf import ChangeSet
55
from codemodder.context import CodemodExecutionContext
66
from codemodder.file_context import FileContext
77
from codemodder.result import Result

‎src/codemodder/codemods/imported_call_modifier.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand
77
from libcst.metadata import PositionProvider
88

9-
from codemodder.change import Change
109
from codemodder.codemods.base_visitor import UtilsMixin
1110
from codemodder.codemods.utils_mixin import NameResolutionMixin
11+
from codemodder.codetf import Change
1212
from codemodder.file_context import FileContext
1313
from codemodder.result import Result
1414

@@ -75,7 +75,7 @@ def leave_Call(self, original_node: cst.Call, updated_node: cst.Call):
7575
and true_name in self.matching_functions
7676
):
7777
self.changes_in_file.append(
78-
Change(line_number, self.change_description)
78+
Change(lineNumber=line_number, description=self.change_description)
7979
)
8080

8181
new_args = self.updated_args(updated_node.args)

‎src/codemodder/codemods/libcst_transformer.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
from collections import namedtuple
2+
from typing import cast
23

34
import libcst as cst
45
from libcst import matchers
56
from libcst._position import CodeRange
67
from libcst.codemod import CodemodContext
78
from libcst.codemod.visitors import AddImportsVisitor, RemoveImportsVisitor
89

9-
from codemodder.change import Change, ChangeSet
1010
from codemodder.codemods.base_transformer import BaseTransformerPipeline
1111
from codemodder.codemods.base_visitor import BaseTransformer
1212
from codemodder.codemods.utils import get_call_name
13+
from codemodder.codetf import Change, ChangeSet
1314
from codemodder.context import CodemodExecutionContext
1415
from codemodder.dependency import Dependency
1516
from codemodder.diff import create_diff_from_tree
@@ -99,7 +100,7 @@ def leave_ClassDef(
99100

100101
def node_position(self, node):
101102
# See https://github.com/Instagram/LibCST/blob/main/libcst/_metadata_dependent.py#L112
102-
return self.get_metadata(self.METADATA_DEPENDENCIES[0], node)
103+
return cast(CodeRange, self.get_metadata(self.METADATA_DEPENDENCIES[0], node))
103104

104105
def add_change(self, node, description: str, start: bool = True):
105106
position = self.node_position(node)
@@ -125,7 +126,7 @@ def add_dependency(self, dependency: Dependency):
125126
def report_change(self, original_node):
126127
line_number = self.lineno_for_node(original_node)
127128
self.file_context.codemod_changes.append(
128-
Change(line_number, self.change_description)
129+
Change(lineNumber=line_number, description=self.change_description)
129130
)
130131

131132
def remove_unused_import(self, original_node):
@@ -275,8 +276,8 @@ def apply(
275276
return None
276277

277278
change_set = ChangeSet(
278-
str(file_context.file_path.relative_to(context.directory)),
279-
diff,
279+
path=str(file_context.file_path.relative_to(context.directory)),
280+
diff=diff,
280281
changes=file_context.codemod_changes,
281282
)
282283

‎src/codemodder/codemods/test/integration_utils.py‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from types import ModuleType
77

88
import git
9+
import jsonschema
910

1011
from codemodder import __version__, registry
1112

@@ -99,7 +100,8 @@ def _assert_results_fields(self, results, output_path):
99100
result = results[0]
100101
assert result["codemod"] == self.codemod_instance.id
101102
assert result["references"] == [
102-
ref.to_json() for ref in self.codemod_instance.references
103+
ref.model_dump(exclude_none=True)
104+
for ref in self.codemod_instance.references
103105
]
104106

105107
# TODO: once we add description for each url.
@@ -118,15 +120,15 @@ def _assert_results_fields(self, results, output_path):
118120

119121
assert len(change["changes"]) == self.num_changes
120122
line_change = change["changes"][0]
121-
assert line_change["lineNumber"] == str(self.expected_line_change)
123+
assert line_change["lineNumber"] == int(self.expected_line_change)
122124
assert line_change["description"] == self.change_description
123-
assert line_change["packageActions"] == []
124-
assert line_change["properties"] == {}
125125

126-
def _assert_codetf_output(self):
126+
def _assert_codetf_output(self, codetf_schema):
127127
with open(self.output_path, "r", encoding="utf-8") as f:
128128
codetf = json.load(f)
129129

130+
jsonschema.validate(codetf, codetf_schema)
131+
130132
assert sorted(codetf.keys()) == ["results", "run"]
131133
run = codetf["run"]
132134
self._assert_run_fields(run, self.output_path)
@@ -149,7 +151,7 @@ def check_code_after(self) -> ModuleType:
149151
path=self.code_path, allowed_exceptions=self.allowed_exceptions
150152
)
151153

152-
def test_file_rewritten(self):
154+
def test_file_rewritten(self, codetf_schema):
153155
"""
154156
Tests that file is re-written correctly with new code and correct codetf output.
155157
@@ -183,7 +185,7 @@ def test_file_rewritten(self):
183185

184186
self.check_code_after()
185187
self.check_dependencies_after()
186-
self._assert_codetf_output()
188+
self._assert_codetf_output(codetf_schema)
187189
pathlib.Path(self.output_path).unlink(missing_ok=True)
188190
self._run_idempotency_chec(command)
189191

0 commit comments

Comments
 (0)