Skip to content

Commit 160ada0

Browse files
authored
Working draft for CodeTFv3 data model (#1012)
* Move current CodeTF implementation to v2 * Add initial draft of CodeTF v3 model
1 parent 937a5a0 commit 160ada0

File tree

4 files changed

+213
-22
lines changed

4 files changed

+213
-22
lines changed

src/codemodder/codetf/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .v2.codetf import * # noqa: F403

src/codemodder/codetf/common.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from abc import ABCMeta
2+
from enum import Enum
3+
from pathlib import Path
4+
5+
from pydantic import BaseModel
6+
7+
from codemodder.logging import logger
8+
9+
10+
class CaseInsensitiveEnum(str, Enum):
11+
@classmethod
12+
def _missing_(cls, value: object):
13+
if not isinstance(value, str):
14+
return super()._missing_(value)
15+
16+
return cls.__members__.get(value.upper())
17+
18+
19+
class CodeTFWriter(BaseModel, metaclass=ABCMeta):
20+
def write_report(self, outfile: Path | str) -> int:
21+
try:
22+
Path(outfile).write_text(self.model_dump_json(exclude_none=True))
23+
except Exception:
24+
logger.exception("failed to write report file.")
25+
# Any issues with writing the output file should exit status 2.
26+
return 2
27+
logger.debug("wrote report to %s", outfile)
28+
return 0

src/codemodder/codetf.py src/codemodder/codetf/v2/codetf.py

+3-22
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,18 @@
99
import os
1010
import sys
1111
from enum import Enum
12-
from pathlib import Path
1312
from typing import TYPE_CHECKING, Optional
1413

1514
from pydantic import BaseModel, ConfigDict, model_validator
1615

1716
from codemodder import __version__
18-
from codemodder.logging import logger
17+
18+
from ..common import CaseInsensitiveEnum, CodeTFWriter
1919

2020
if TYPE_CHECKING:
2121
from codemodder.context import CodemodExecutionContext
2222

2323

24-
class CaseInsensitiveEnum(str, Enum):
25-
@classmethod
26-
def _missing_(cls, value: object):
27-
if not isinstance(value, str):
28-
return super()._missing_(value)
29-
30-
return cls.__members__.get(value.upper())
31-
32-
3324
class Action(CaseInsensitiveEnum):
3425
ADD = "add"
3526
REMOVE = "remove"
@@ -221,7 +212,7 @@ class Run(BaseModel):
221212
sarifs: list[Sarif] = []
222213

223214

224-
class CodeTF(BaseModel):
215+
class CodeTF(CodeTFWriter, BaseModel):
225216
run: Run
226217
results: list[Result]
227218

@@ -247,13 +238,3 @@ def build(
247238
sarifs=[],
248239
)
249240
return cls(run=run, results=results)
250-
251-
def write_report(self, outfile: Path | str):
252-
try:
253-
Path(outfile).write_text(self.model_dump_json(exclude_none=True))
254-
except Exception:
255-
logger.exception("failed to write report file.")
256-
# Any issues with writing the output file should exit status 2.
257-
return 2
258-
logger.debug("wrote report to %s", outfile)
259-
return 0

src/codemodder/codetf/v3/codetf.py

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
from __future__ import annotations
2+
3+
from enum import Enum
4+
from typing import Optional
5+
6+
from pydantic import BaseModel, model_validator
7+
8+
from ..common import CaseInsensitiveEnum, CodeTFWriter
9+
10+
11+
class Run(BaseModel):
12+
"""Metadata about the analysis run that produced the results"""
13+
14+
vendor: str
15+
tool: str
16+
version: str
17+
# Optional free-form metadata about the project being analyzed
18+
# e.g. project name, directory, commit SHA, etc.
19+
projectMetadata: Optional[str] = None
20+
# Analysis duration in milliseconds
21+
elapsed: Optional[int] = None
22+
# Optional free-form metadata about the inputs used for the analysis
23+
# e.g. command line, environment variables, etc.
24+
inputMetadata: Optional[dict] = None
25+
# Optional free-form metadata about the analysis itself
26+
# e.g. timeouts, memory usage, etc.
27+
analysisMetadata: Optional[dict] = None
28+
29+
30+
class FixStatusType(str, Enum):
31+
"""Status of a fix"""
32+
33+
fixed = "fixed"
34+
skipped = "skipped"
35+
failed = "failed"
36+
wontfix = "wontfix"
37+
38+
39+
class FixStatus(BaseModel):
40+
"""Metadata describing fix outcome"""
41+
42+
status: FixStatus
43+
reason: Optional[str]
44+
details: Optional[str]
45+
46+
47+
class Rule(BaseModel):
48+
id: str
49+
name: str
50+
url: Optional[str] = None
51+
52+
53+
class Finding(BaseModel):
54+
id: str
55+
rule: Optional[Rule] = None
56+
57+
58+
class Action(CaseInsensitiveEnum):
59+
ADD = "add"
60+
REMOVE = "remove"
61+
62+
63+
class PackageResult(CaseInsensitiveEnum):
64+
COMPLETED = "completed"
65+
FAILED = "failed"
66+
SKIPPED = "skipped"
67+
68+
69+
class DiffSide(CaseInsensitiveEnum):
70+
LEFT = "left"
71+
RIGHT = "right"
72+
73+
74+
class PackageAction(BaseModel):
75+
action: Action
76+
result: PackageResult
77+
package: str
78+
79+
80+
class Change(BaseModel):
81+
lineNumber: int
82+
description: Optional[str]
83+
diffSide: DiffSide = DiffSide.RIGHT
84+
properties: Optional[dict] = None
85+
packageActions: Optional[list[PackageAction]] = None
86+
87+
@model_validator(mode="after")
88+
def validate_lineNumber(self):
89+
if self.lineNumber < 1:
90+
raise ValueError("lineNumber must be greater than 0")
91+
return self
92+
93+
@model_validator(mode="after")
94+
def validate_description(self):
95+
if self.description is not None and not self.description:
96+
raise ValueError("description must not be empty")
97+
return self
98+
99+
100+
class ChangeSet(BaseModel):
101+
path: str
102+
diff: str
103+
changes: list[Change]
104+
105+
106+
class Reference(BaseModel):
107+
url: str
108+
description: Optional[str] = None
109+
110+
@model_validator(mode="after")
111+
def validate_description(self):
112+
self.description = self.description or self.url
113+
return self
114+
115+
116+
class Strategy(Enum):
117+
ai = "ai"
118+
hybrid = "hybrid"
119+
deterministic = "deterministic"
120+
121+
122+
class AIMetadata(BaseModel):
123+
provider: Optional[str] = None
124+
models: Optional[list[str]] = None
125+
total_tokens: Optional[int] = None
126+
completion_tokens: Optional[int] = None
127+
prompt_tokens: Optional[int] = None
128+
129+
130+
class GenerationMetadata(BaseModel):
131+
strategy: Strategy
132+
ai: Optional[AIMetadata] = None
133+
provisional: bool
134+
135+
136+
class FixMetadata(BaseModel):
137+
# Fix provider ID, corresponds to legacy codemod ID
138+
id: str
139+
# A brief summary of the fix
140+
summary: str
141+
# A detailed description of the fix
142+
description: str
143+
references: list[Reference]
144+
generation: GenerationMetadata
145+
146+
147+
class Rating(BaseModel):
148+
score: int
149+
description: Optional[str] = None
150+
151+
152+
class FixQuality(BaseModel):
153+
safetyRating: Rating
154+
effectivenessRating: Rating
155+
cleanlinessRating: Rating
156+
157+
158+
class FixResult(BaseModel):
159+
"""Result corresponding to a single finding"""
160+
161+
finding: Finding
162+
fixStatus: FixStatus
163+
changeSets: list[ChangeSet]
164+
fixMetadata: Optional[FixMetadata] = None
165+
fixQuality: Optional[FixQuality] = None
166+
# A description of the reasoning process that led to the fix
167+
reasoningSteps: Optional[list[str]] = None
168+
169+
@model_validator(mode="after")
170+
def validate_fixMetadata(self):
171+
if self.fixStatus.status == FixStatusType.fixed:
172+
if not self.changeSets:
173+
raise ValueError("changeSets must be provided for fixed results")
174+
if not self.fixMetadata:
175+
raise ValueError("fixMetadata must be provided for fixed results")
176+
return self
177+
178+
179+
class CodeTF(CodeTFWriter, BaseModel):
180+
run: Run
181+
results: list[FixResult]

0 commit comments

Comments
 (0)