Skip to content

Commit 9b1e0ca

Browse files
committed
Merge branch 'fix/static-runners-emit-col-offset' into main
Brings the col_offset emission patches for the static runners (jedi, scalpel, headergen) onto our fork's main. These commits were originally opened as PR secure-software-engineering#15 to secure-software-engineering/TypeEvalPy on 2026-05-28 and have been sitting unreviewed; since our fork controls its own main, we merge here to unblock fork-internal use without waiting on upstream. Specifically: 2451f95 jedi: emit col_offset and fix runner correctness bugs 82b5ffb headergen: emit col_offset via source-parsed enrichment 4ce1198 scalpel: emit col_offset via source-parsed enrichment 5130d1a headergen, scalpel: use stdlib ast for col_offset recovery, skip ambiguous e4b7b97 headergen: install g++ in Dockerfile to enable line-profiler build Without these patches, jedi/scalpel/headergen produce *_result.json without col_offset; the strict scorer added in TypeEvalPy commit 2f7c605 (Oct 2025) then rejects every prediction and scores them all at 0.
2 parents 3e32998 + e4b7b97 commit 9b1e0ca

6 files changed

Lines changed: 204 additions & 35 deletions

File tree

src/target_tools/headergen/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ WORKDIR /app
1010

1111
# Install dependencies
1212
RUN apt-get update \
13-
&& apt-get -y install git gcc
13+
&& apt-get -y install git gcc g++
1414

1515

1616
COPY requirements.txt /app/requirements.txt

src/target_tools/headergen/src/runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def main_runner(args):
6262
logger.info(file)
6363

6464
inferred = process_file(file)
65+
inferred = translator.enrich_with_col_offsets(file, inferred)
6566

6667
json_file_path = str(file).replace(".py", "_result.json")
6768

src/target_tools/headergen/src/translator.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import argparse
2+
import ast
23
import json
34
import os
5+
from collections import defaultdict
46
from pathlib import Path
57

68

@@ -9,6 +11,75 @@ def list_json_files(folder_path):
911
return python_files
1012

1113

14+
def build_position_map(source_path):
15+
"""Map (name, line_number) -> [1-indexed col_offsets] for every name
16+
occurrence in the source. HeaderGen's server doesn't emit col_offset, but
17+
for any (name, line) it gives us, the column is determined by the source.
18+
We keep all candidates so the enrichment can skip ambiguous cases."""
19+
positions = defaultdict(list)
20+
try:
21+
with open(source_path) as f:
22+
tree = ast.parse(f.read())
23+
except Exception:
24+
return positions
25+
26+
for node in ast.walk(tree):
27+
if isinstance(node, ast.Name):
28+
positions[(node.id, node.lineno)].append(node.col_offset + 1)
29+
elif isinstance(node, ast.arg):
30+
positions[(node.arg, node.lineno)].append(node.col_offset + 1)
31+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
32+
prefix = (
33+
"async def " if isinstance(node, ast.AsyncFunctionDef) else "def "
34+
)
35+
positions[(node.name, node.lineno)].append(
36+
node.col_offset + len(prefix) + 1
37+
)
38+
elif isinstance(node, ast.ClassDef):
39+
positions[(node.name, node.lineno)].append(
40+
node.col_offset + len("class ") + 1
41+
)
42+
return positions
43+
44+
45+
def _lookup_name(entry):
46+
"""Return the source-level name to look up for this entry's position."""
47+
if "variable" in entry:
48+
# Subscript/attribute accesses like 'h[0]' or 'self.child' are
49+
# reported as the full expression; the col_offset GT expects is
50+
# where the base name begins.
51+
name = entry["variable"]
52+
for sep in ("[", "."):
53+
if sep in name:
54+
name = name.split(sep, 1)[0]
55+
break
56+
return name
57+
if "parameter" in entry:
58+
return entry["parameter"]
59+
if "function" in entry:
60+
# Nested functions are reported as 'outer.inner'; the position
61+
# we want is the inner name's own column.
62+
return entry["function"].rsplit(".", 1)[-1]
63+
return None
64+
65+
66+
def enrich_with_col_offsets(source_path, entries):
67+
"""Augment HeaderGen entries with col_offset by looking up the position
68+
of each entry's identifying name in the source file. Skip ambiguous
69+
cases (multiple candidates) so we never guess a position."""
70+
positions = build_position_map(source_path)
71+
for entry in entries:
72+
if "col_offset" in entry:
73+
continue
74+
name = _lookup_name(entry)
75+
if name is None:
76+
continue
77+
cands = sorted(set(positions.get((name, entry["line_number"]), [])))
78+
if len(cands) == 1:
79+
entry["col_offset"] = cands[0]
80+
return entries
81+
82+
1283
def translate_content(file_path):
1384
with open(file_path) as f:
1485
data = json.load(f)

src/target_tools/jedi/src/jedi_type_inference.py

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,24 @@ def find_types_by_execute(self, jedi_obj):
100100
return _type
101101

102102
def get_function_name(self, jedi_obj):
103+
"""Return the qualified name of jedi_obj relative to its module,
104+
walking up parent scopes so nested functions become 'outer.inner'."""
103105
try:
104106
if jedi_obj.name == "<lambda>":
105-
func_name = "lambda"
106-
else:
107-
parts = jedi_obj.full_name.split(".", 1)
108-
func_name = parts[-1] if len(parts) > 1 else jedi_obj.full_name
109-
except Exception as e:
107+
return "lambda"
108+
parts = []
109+
current = jedi_obj
110+
while current is not None and current.type != "module":
111+
name = "lambda" if current.name == "<lambda>" else current.name
112+
parts.append(name)
113+
try:
114+
current = current.parent()
115+
except Exception:
116+
break
117+
return ".".join(reversed(parts)) if parts else jedi_obj.name
118+
except Exception:
110119
print("full_name not found in jedi_obj?")
111-
func_name = jedi_obj.name
112-
113-
return func_name
120+
return jedi_obj.name
114121

115122
def infer_types(self):
116123
"""
@@ -143,24 +150,46 @@ def infer_types(self):
143150
if _infer:
144151
for inferred in _infer:
145152
if inferred.type == "function":
146-
# _type = self.parse_type_hint(inferred.get_type_hint())
147-
# if not _type:
148-
# self.find_types_by_execute(inferred)
149-
150-
_type = self.find_types_by_execute(inferred)
151-
152-
_info = {
153-
"file": node.name,
154-
"line_number": pos["line"],
155-
}
156-
if inferred.name != "<lambda>":
157-
_info["function"] = self.get_function_name(inferred)
158-
_info["type"] = _type if _type else {"any"}
159-
160-
variable_name = var.split(":")[0].strip()
161-
if variable_name != self.get_function_name(inferred):
162-
_info["variable"] = variable_name
163-
if _type:
153+
# Distinguish between the function's own definition
154+
# site (return-type is what's wanted, e.g. for `def
155+
# func1():`) and a reference to it (callable is
156+
# what's wanted, e.g. for `a = func1`).
157+
at_def_site = (
158+
pos["line"] == inferred.line
159+
and pos["column"] == inferred.column
160+
)
161+
162+
if at_def_site:
163+
_type = self.find_types_by_execute(inferred)
164+
165+
_info = {
166+
"file": node.name,
167+
"line_number": pos["line"],
168+
"col_offset": pos["column"] + 1,
169+
}
170+
if inferred.name != "<lambda>":
171+
_info["function"] = self.get_function_name(inferred)
172+
_info["type"] = _type if _type else {"any"}
173+
174+
variable_name = var.split(":")[0].strip()
175+
if variable_name != self.get_function_name(inferred):
176+
_info["variable"] = variable_name
177+
if _type:
178+
output_inferred.append(_info)
179+
else:
180+
variable_name = var.split(":")[0].strip()
181+
_info = {
182+
"file": node.name,
183+
"line_number": pos["line"],
184+
"col_offset": pos["column"] + 1,
185+
"variable": variable_name,
186+
"type": {"callable"},
187+
}
188+
parent = pos["jedi_obj"].parent()
189+
if parent and parent.type != "module":
190+
parent_func = self.get_function_name(parent)
191+
if parent_func:
192+
_info["function"] = parent_func
164193
output_inferred.append(_info)
165194

166195
elif inferred.type == "instance":
@@ -187,17 +216,15 @@ def infer_types(self):
187216
_info = {
188217
"file": node.name,
189218
"line_number": pos["line"],
219+
"col_offset": pos["column"] + 1,
190220
"variable": var.split(":")[0],
191221
"type": {_type},
192222
}
193-
if (
194-
not pos["jedi_obj"].parent().name
195-
== pos["jedi_obj"].parent().module_name
196-
):
197-
if self.get_function_name(pos["jedi_obj"].parent()):
198-
_info["function"] = self.get_function_name(
199-
pos["jedi_obj"].parent()
200-
)
223+
parent = pos["jedi_obj"].parent()
224+
if parent and parent.type != "module":
225+
parent_func = self.get_function_name(parent)
226+
if parent_func:
227+
_info["function"] = parent_func
201228
if _type:
202229
output_inferred.append(_info)
203230

@@ -206,6 +233,7 @@ def infer_types(self):
206233
_info = {
207234
"file": node.name,
208235
"line_number": pos["line"],
236+
"col_offset": pos["column"] + 1,
209237
"variable": var.split(":")[0],
210238
"function": self.get_function_name(
211239
pos["jedi_obj"].parent()
@@ -225,6 +253,7 @@ def infer_types(self):
225253
_info = {
226254
"file": node.name,
227255
"line_number": pos["line"],
256+
"col_offset": pos["column"] + 1,
228257
"parameter": var.split(":")[0],
229258
"function": self.get_function_name(
230259
pos["jedi_obj"].parent()

src/target_tools/scalpel/src/runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
from pathlib import Path
66

7+
import translator
78
import utils
89
from scalpel.typeinfer.typeinfer import TypeInference
910

@@ -42,6 +43,7 @@ def main_runner(args):
4243
try:
4344
# logger.debug(file)
4445
inferred = process_file(file)
46+
inferred = translator.enrich_with_col_offsets(file, inferred)
4547
json_file_path = str(file).replace(".py", "_result.json")
4648

4749
with open(json_file_path, "w") as json_file:

src/target_tools/scalpel/src/translator.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import argparse
2+
import ast
23
import json
34
import os
5+
from collections import defaultdict
46
from pathlib import Path
57

68

@@ -9,6 +11,70 @@ def list_json_files(folder_path):
911
return python_files
1012

1113

14+
def build_position_map(source_path):
15+
"""Map (name, line_number) -> [1-indexed col_offsets] for every name
16+
occurrence in the source. Scalpel's runner doesn't emit col_offset, but
17+
for any (name, line) it gives us, the column is determined by the source.
18+
We keep all candidates so the enrichment can skip ambiguous cases."""
19+
positions = defaultdict(list)
20+
try:
21+
with open(source_path) as f:
22+
tree = ast.parse(f.read())
23+
except Exception:
24+
return positions
25+
26+
for node in ast.walk(tree):
27+
if isinstance(node, ast.Name):
28+
positions[(node.id, node.lineno)].append(node.col_offset + 1)
29+
elif isinstance(node, ast.arg):
30+
positions[(node.arg, node.lineno)].append(node.col_offset + 1)
31+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
32+
prefix = (
33+
"async def " if isinstance(node, ast.AsyncFunctionDef) else "def "
34+
)
35+
positions[(node.name, node.lineno)].append(
36+
node.col_offset + len(prefix) + 1
37+
)
38+
elif isinstance(node, ast.ClassDef):
39+
positions[(node.name, node.lineno)].append(
40+
node.col_offset + len("class ") + 1
41+
)
42+
return positions
43+
44+
45+
def _lookup_name(entry):
46+
"""Return the source-level name to look up for this entry's position."""
47+
if "variable" in entry:
48+
name = entry["variable"]
49+
for sep in ("[", "."):
50+
if sep in name:
51+
name = name.split(sep, 1)[0]
52+
break
53+
return name
54+
if "parameter" in entry:
55+
return entry["parameter"]
56+
if "function" in entry:
57+
return entry["function"].rsplit(".", 1)[-1]
58+
return None
59+
60+
61+
def enrich_with_col_offsets(source_path, entries):
62+
"""Augment entries with col_offset by looking up the position of each
63+
entry's identifying name in the source file. Skip ambiguous cases
64+
(multiple candidates) so we never guess a position."""
65+
positions = build_position_map(source_path)
66+
for entry in entries:
67+
if "col_offset" in entry:
68+
continue
69+
name = _lookup_name(entry)
70+
if name is None:
71+
continue
72+
cands = sorted(set(positions.get((name, entry["line_number"]), [])))
73+
if len(cands) == 1:
74+
entry["col_offset"] = cands[0]
75+
return entries
76+
77+
1278
def main_translator(args):
1379
json_files = list_json_files(args.bechmark_path)
1480
error_count = 0

0 commit comments

Comments
 (0)