30 if not isinstance(fixedAPI,str)
or repairStatus
not in (
'Successful',
'Unknown',
'Failed'):
33 original=ast.parse(record[
'call_text'],mode=
'eval').body
34 fixed=ast.parse(fixedAPI,mode=
'eval').body
35 except (KeyError,SyntaxError,ValueError):
37 if not isinstance(original,ast.Call)
or not isinstance(fixed,ast.Call):
39 if ast.dump(original)==ast.dump(fixed):
41 edit={key:record.get(key)
for key
in (
42 'id',
'rel_path',
'lineno',
'col_offset',
'end_lineno',
'end_col_offset',
'call_text',
44 edit[
'fixed_api']=fixedAPI
45 edit[
'repair_status']=repairStatus
61 source=sourceBytes.decode(
'utf-8-sig')
62 root=ast.parse(source)
63 lines=source.splitlines(keepends=
True)
64 byteLines=sourceBytes.splitlines(keepends=
True)
67 for line
in byteLines:
68 lineOffsets.append(offset)
70 if sourceBytes.startswith(b
'\xef\xbb\xbf'):
72 callNodes={(node.lineno,node.col_offset,node.end_lineno,node.end_col_offset):node
73 for node
in ast.walk(root)
if isinstance(node,ast.Call)}
75 for token
in tokenize.generate_tokens(io.StringIO(source).readline):
76 if token.type==tokenize.COMMENT:
77 line,column=token.start
78 commentOffsets.append(lineOffsets[line-1]+len(lines[line-1][:column].encode(
'utf-8')))
79 except (UnicodeError,SyntaxError,ValueError,tokenize.TokenError)
as e:
80 return sourceBytes,[f
'Patch skipped: cannot parse source: {e}\n']
84 callId=edit.get(
'id',
'unknown')
86 span=tuple(edit.get(key)
for key
in (
'lineno',
'col_offset',
'end_lineno',
'end_col_offset'))
87 if span
not in callNodes:
88 raise ValueError(
'missing or invalid callsite span')
89 original=ast.parse(edit[
'call_text'],mode=
'eval').body
90 if ast.dump(original)!=ast.dump(callNodes[span]):
91 raise ValueError(
'original call does not match the source span')
92 fixed=ast.parse(edit[
'fixed_api'],mode=
'eval').body
93 if not isinstance(fixed,ast.Call):
94 raise ValueError(
'repair result is not a single call expression')
95 start=lineOffsets[span[0]-1]+span[1]
96 end=lineOffsets[span[2]-1]+span[3]
97 if any(start<=offset<end
for offset
in commentOffsets):
98 raise ValueError(
'callsite contains comments that replacement would remove')
99 candidates.append((start,end,edit))
100 except (KeyError,TypeError,SyntaxError,ValueError)
as e:
101 errLst.append(f
'Patch skipped for {callId}: {e}\n')
103 candidates.sort(key=
lambda item:item[0])
105 for index,(start,end,edit)
in enumerate(candidates):
106 for other
in range(index+1,len(candidates)):
107 if candidates[other][0]>=end:
109 overlaps.update((index,other))
111 newline=
'\r\n' if b
'\r\n' in sourceBytes
else '\n'
112 for index
in range(len(candidates)-1,-1,-1):
113 start,end,edit=candidates[index]
114 if index
in overlaps:
115 errLst.append(f
"Patch skipped for {edit['id']}: overlapping callsite spans\n")
117 replacement=edit[
'fixed_api'].replace(
'\r\n',
'\n').replace(
'\n',newline).encode(
'utf-8')
118 modified=modified[:start]+replacement+modified[end:]
120 ast.parse(modified.decode(
'utf-8-sig'))
121 except (SyntaxError,ValueError)
as e:
122 errLst.append(f
'Patch skipped: modified file has invalid syntax: {e}\n')
123 return sourceBytes,errLst
124 return modified,errLst
140 projPath=os.path.realpath(projPath)
141 reportDir=os.path.realpath(reportDir)
143 if os.path.commonpath((projPath,reportDir))==projPath:
144 return [
'Patch skipped: report directory is inside the original project\n']
147 patchDir=os.path.join(reportDir,
'patches')
148 fixedDir=os.path.join(reportDir,
'fixed_project')
150 os.makedirs(patchDir,exist_ok=
True)
151 os.makedirs(fixedDir,exist_ok=
True)
153 return [f
'Patch output failed: {e}\n']
158 relativePath=edit[
'rel_path'].replace(
'\\',
'/')
159 if ntpath.isabs(relativePath)
or ntpath.splitdrive(relativePath)[0] \
160 or any(part
in (
'',
'..',
'.')
for part
in relativePath.split(
'/')) \
161 or any(char
in relativePath
for char
in '\t\r\n'):
162 raise ValueError(
'invalid project-relative path')
163 file=os.path.join(projPath,*relativePath.split(
'/'))
164 if os.path.commonpath((projPath,os.path.realpath(file)))!=projPath:
165 raise ValueError(
'source file is outside the original project')
166 if os.path.realpath(file)!=os.path.abspath(file):
167 raise ValueError(
'source file uses a symbolic link')
168 status=edit[
'repair_status']
169 if status
not in (
'Successful',
'Unknown',
'Failed'):
170 raise ValueError(
'invalid repair status')
171 fileEdits.setdefault(relativePath,{}).setdefault(status,[]).append(edit)
172 except (KeyError,TypeError,AttributeError,ValueError)
as e:
173 errLst.append(f
"Patch skipped for {edit.get('id','unknown')}: {e}\n")
175 patches={status:[]
for status
in (
'Successful',
'Unknown',
'Failed')}
177 for relativePath
in sorted(fileEdits):
178 file=os.path.join(projPath,*relativePath.split(
'/'))
180 with open(file,
'rb')
as fr:
181 originalBytes=fr.read()
182 for status,categoryEdits
in fileEdits[relativePath].items():
183 modifiedBytes,errors=
applyEdits(originalBytes,categoryEdits)
184 errLst.extend(f
'{relativePath} <{status}>: {error}' for error
in errors)
185 if modifiedBytes==originalBytes:
188 diff=difflib.unified_diff(originalBytes.decode(
'utf-8').splitlines(keepends=
True),modifiedBytes.decode(
'utf-8').splitlines(keepends=
True),fromfile=f
'a/{relativePath}\t',tofile=f
'b/{relativePath}\t')
191 patch.append(line
if line.endswith(
'\n')
else line+
'\n\\ No newline at end of file\n')
192 patches[status].extend(patch)
193 if status==
'Successful':
194 successfulFiles[relativePath]=modifiedBytes
195 except (OSError,UnicodeError,ValueError)
as e:
196 errLst.append(f
'Patch skipped for {relativePath}: {e}\n')
201 fixedProject=os.path.join(fixedDir,projName)
202 shutil.copytree(projPath,fixedProject,ignore=
lambda directory,names:[
203 name
for name
in names
if os.path.islink(os.path.join(directory,name))
205 for relativePath,modifiedBytes
in successfulFiles.items():
206 with open(os.path.join(fixedProject,*relativePath.split(
'/')),
'wb')
as fw:
207 fw.write(modifiedBytes)
209 errLst.append(f
'Fixed project output failed: {e}\n')
211 for status,patch
in patches.items():
214 with open(os.path.join(patchDir,f
'{status.lower()}.patch'),
'wb')
as fw:
215 fw.write(
''.join(patch).encode(
'utf-8'))
217 errLst.append(f
'Patch output failed for {status}: {e}\n')
def makePatchEdit(record, fixedAPI, repairStatus)
Collect one unambiguous repair result with its original source span 收集唯一修复表达式及其原始源码范围
def writeRepairArtifacts(projPath, projName, edits, reportDir)
Write independent category patches and a Successful-only project copy 写出三类独立补丁及仅含Successful修复的项目副本
def applyEdits(sourceBytes, edits)
Apply non-overlapping edits using UTF-8 byte offsets from the original AST 根据原始AST的UTF-8字节位置应用不重叠的修复