PCART  v1.4
Automated Repair of Python API Parameter Compatibility Issues
getCall.py
Go to the documentation of this file.
1 
11 
12 
13 
14 import ast
15 from Extract.extractCall import *
16 from Tool.callsite import makeCallsiteRecord
17 
18 
19 
20 
27 def getSelfAPI(root,importDict,libName):
28  ansLst=[]
29  for node in ast.iter_child_nodes(root):
30  if isinstance(node,ast.ClassDef):
31  if len(node.bases)==0: #只关注有继承的类
32  continue
33 
34  bases=[] #可能含有多个继承
35  callLst=[]
36  defLst=[]
37 
38  #收集基类信息
39  flag=0
40  for it in node.bases:
41  base=ast.unparse(it)
42  if base.split('.')[0] in importDict:
43  base=importDict[base.split('.')[0]]
44  if libName in base:
45  flag=1
46  bases.append(base)
47  if flag==0: #基类中是否含有指定的第三方库
48  continue
49 
50  #收集定义的信息
51  for n in ast.iter_child_nodes(node):
52  if isinstance(n,ast.FunctionDef):
53  defLst.append(n.name)
54 
55  #递归搜索call节点
56  callVisitor=GetFuncCall()
57  callVisitor.dfsVisit(node)
58  callInfos=callVisitor.func_call
59  for Tuple in callInfos:
60  callLst.append(Tuple[0])
61 
62  ansLst.append((bases,defLst,callLst))
63 
64  return ansLst
65 
66 
67 
68 
84 def modifyFirstName(prefix, callName, paraStr, codeLst):
85  name_parts=callName.split('.') #按.进行字段拆分
86  #先通过赋值语句进行还原
87  #a=A(x)
88  #a.b(y) --> A(x).b(y)
89  firstModify=callName #此处考虑了第一个名字
90  index=-1 #此处改成直接从源码中按行查找 2023.6.15
91  for i in range(len(codeLst)):
92  #这个条件有点苛刻,因为这里是抽取源码中的API(目的是为了获取API在源码中的真实位置)
93  #但当源码中参数换行写的时候,这个条件就无法满足
94  if f"{prefix}({paraStr})".replace(' ','').replace("'",'').replace('"','') in codeLst[i].replace(' ','').replace("'",'').replace('"','').rstrip('\n'):
95  index=i
96  break
97 
98  if index==-1:
99  for i in range(len(codeLst)):
100  if f"{prefix}(".replace(' ','') in codeLst[i].replace(' ','').rstrip('\n'):
101  index=i
102  break
103 
104  modifyFlag=0
105  if index!=-1:
106  index-=1
107  while index>=0: #看是否能在前面找到相关的赋值语句
108  s=codeLst[index].strip() #去除字符串首尾空格以及换行符
109  if '#' in s and '=' in s:
110  try:
111  tempNode=ast.parse(s)
112  s=ast.unparse(tempNode)
113  except:
114  pass
115  s=s.replace(' ','')
116 
117  if name_parts[0]!='self':
118  if f"{name_parts[0]}="==s[0:len(name_parts[0])+1]:
119  pos=s.find('=')
120  firstModify=s[pos+1:]+'.'+'.'.join(name_parts[1:])
121  prefix=s[pos+1:]
122  prefix=s[pos+1:].split('(',1)[0]
123  paraStr=s[pos+1:].split('(',1)[-1].rstrip(')')
124  modifyFlag=1
125  break
126 
127  else: #self.a=A(), self.a.f() --> A.f()
128  if len(name_parts)>2 and f"{'.'.join(name_parts[0:2])}=" in s:
129  pos=s.find('=')
130  firstModify=s[pos+1:]+'.'+'.'.join(name_parts[2:])
131  prefix=s[pos+1:].split('(',1)[0] #更新prefix
132  paraStr=s[pos+1:].split('(',1)[-1].rstrip(')') #更新参数
133  modifyFlag=1
134  break
135  index-=1
136 
137 
138  if modifyFlag: #若找到了赋值语句,再试探一下赋值语句是否还有赋值语句
139  return modifyFirstName(prefix, firstModify, paraStr, codeLst)
140  else: #若没有找到赋值语句,则直接结束
141  return callName
142 
143 
144 
145 
157 def modifyWithName(callName, withitemCallName, lineno=None):
158  name_parts = callName.split('.') #按.进行字段拆分
159  firstModify = callName
160  modifyFlag = 0
161  if name_parts[0] in withitemCallName:
162  withitem = withitemCallName[name_parts[0]]
163  if isinstance(withitem, list):
164  candidates = withitem
165  if lineno is not None:
166  # with别名同名时,只使用覆盖当前调用行号的候选,避免外层/兄弟作用域误还原
167  scoped = []
168  for item in candidates:
169  start = item.get('lineno')
170  end = item.get('end_lineno')
171  if start is not None and end is not None and start <= lineno <= end:
172  scoped.append(item)
173  candidates = scoped
174  if candidates:
175  # 嵌套with中内层别名优先;行号越靠后的候选作用域越内层
176  candidates = sorted(candidates, key=lambda item: item.get('lineno') or -1)
177  withitem = candidates[-1].get('callName')
178  else:
179  withitem = None
180  if withitem:
181  firstModify = withitem + '.' + '.'.join(name_parts[1:])
182  modifyFlag = 1
183 
184  #找到了withitem call,重新试探一下前面是否还有withitem call语句
185  if modifyFlag:
186  return modifyWithName(firstModify, withitemCallName, lineno)
187  #若没有,则直接结束
188  else:
189  return callName
190 
191 
192 
193 
205 def getCallFunction(filePath,libName,projPath=None,pcresolveLookup=None):
206  # If PCResolve lookup is available for this file, use it directly
207  # 若PCResolve查找表中有此文件,直接使用预计算结果
208  if pcresolveLookup is not None and filePath in pcresolveLookup:
209  records = pcresolveLookup[filePath]
210  return records, records
211 
212  with open(filePath,'r',encoding='UTF-8') as f:
213  codeText=f.read()
214  f.seek(0)
215  codeLst=f.readlines()
216  try:
217  root_node=ast.parse(codeText,filename='<unknown>',mode='exec')
218 
219  #找出树中所有的模块名
220  import_visitor=Import()
221  import_visitor.visit(root_node)
222  md_names=import_visitor.get_md_name() #dict
223 
224  #找出树中所有withitem call节点 -- 2025/5/19
225  withitem_visitor = WithVisitor()
226  withitem_visitor.visit(root_node)
227  withitem_call_names = withitem_visitor.get_withitem_call() #dict
228 
229  # 找出树中所有的Call节点
230  call_visitor=GetFuncCall()
231  call_visitor.dfsVisit(root_node)
232  all_func_calls=call_visitor.func_call #[(api1,para1,callState, lineno,col,...),(api2,para2, callState, lineno,col,...),...()]
233 
234  # 通过赋值语句和import字典来还原每个调用的API
235  apiFormatDict={} #保存还原前的API后还原后的API的对应关系
236  selfAPIs=[] #保存通过self调用的API
237  for callName,paraStr,callState,lineno,colOffset,endLineNo,endColOffset in all_func_calls:
238  name_parts=callName.split('.') #按.进行字段拆分
239  if 'self' in name_parts[0]:
240  selfAPIs.append((callName,paraStr,callState,lineno,colOffset,endLineNo,endColOffset))
241 
242  # #先通过赋值语句进行还原
243  firstModify=modifyFirstName(callName,callName,paraStr,codeLst)
244  secondModify=firstModify
245 
246  # #再将withitem call的别名还原为真名(如有)-- 2025/5/19
247  if len(withitem_call_names) !=0:
248  firstModify = modifyWithName(callName, withitem_call_names, lineno)
249  secondModify = firstModify
250 
251  # #最后将import的别名还原成真名
252  # #from faker import Fake as A
253  # # A(x).b(y) --> faker.Fake(x).b(y)
254  #2024-1-29修改
255  name_parts=secondModify.split('.')
256  firstParts=name_parts[0]
257  pos=firstParts.find('(')
258  if pos!=-1:
259  temp=firstParts[0:pos]
260  res=firstParts[pos:]
261  else:
262  temp=firstParts
263  res=''
264  if temp in md_names:
265  secondModify=(md_names[temp]+res+'.'+'.'.join(name_parts[1:])).rstrip('.') #当nameparts只有一个元素的会在最后多个点,需要去掉
266 
267 
268  #函数名和参数分开放,key和value都是tuple
269  apiFormatDict[(secondModify,paraStr,callState,lineno,colOffset,endLineNo,endColOffset)]=(callName,paraStr,callState,lineno,colOffset,endLineNo,endColOffset)
270 
271  # 对self调用的API进行还原
272  if len(selfAPIs)>0:
273  selfInfo=getSelfAPI(root_node,md_names,libName)
274  if len(selfInfo)>0:
275  for callName,paraStr,callState,lineno,colOffset,endLineNo,endColOffset in selfAPIs:
276  name_parts=callName.split('.')
277  for bases,defLst,callLst in selfInfo:
278  if callName in callLst and name_parts[-1] not in defLst:
279  name=bases[0]+'.'+'.'.join(name_parts[1:])
280  apiFormatDict[(name,paraStr,callState,lineno,colOffset,endLineNo,endColOffset)]=(callName,paraStr,callState,lineno,colOffset,endLineNo,endColOffset)
281 
282  #把和指定第三方库相关的callAPI都筛选出来
283  callsiteRecords={}
284  callsiteParamRecords={}
285  for key,value in apiFormatDict.items(): #key是还原后的API,value是还原前的API
286  if key[0].split('.')[0]==libName:
287  formatAPI=f"{key[0]}({key[1]})"
288  record=makeCallsiteRecord(
289  filePath,
290  value[2],
291  formatAPI,
292  value[1],
293  value[3],
294  value[4],
295  value[5],
296  value[6],
297  projPath,
298  )
299  callsiteRecords[record['id']]=record
300  callsiteParamRecords[record['id']]=record
301 
302  #按API的行号从小到大排序,便于之后的插桩
303  sortedCallsiteRecords=dict(sorted(callsiteRecords.items(),key=lambda x:(x[1]['lineno'],x[1]['col_offset'])))
304  sortedCallsiteParamRecords=dict(sorted(callsiteParamRecords.items(),key=lambda x:(x[1]['lineno'],x[1]['col_offset'])))
305  return sortedCallsiteRecords,sortedCallsiteParamRecords
306 
307  except SyntaxError as e:
308  print(f"when extract invoked API, parsed {filePath} failed: {e}")
309  return {},{} #若对当前文件解析失败,则返回空字典
def makeCallsiteRecord(file_path, call_text, format_api, parameters, lineno, col_offset, end_lineno=None, end_col_offset=None, proj_path=None)
Make callsite record 构造调用点记录
Definition: callsite.py:190
def getSelfAPI(root, importDict, libName)
Extract method calls in a custom class inherited from a lib class 获取从库API继承的自定义类中的库API方法调用
Definition: getCall.py:27
def getCallFunction(filePath, libName, projPath=None, pcresolveLookup=None)
Extract all API calls from a given .py file 每次传进来一个.py文件,抽取所有的调用API.
Definition: getCall.py:205
def modifyWithName(callName, withitemCallName, lineno=None)
Restore the conventional API call path in the withitem (including AsyncWith) call form 还原withitemAPI调...
Definition: getCall.py:157
def modifyFirstName(prefix, callName, paraStr, codeLst)
Restore the conventional API call path by modifying the first name of the API prefix 通过修改API赋值调用前缀还原完...
Definition: getCall.py:84