PCART  v1.4
Automated Repair of Python API Parameter Compatibility Issues
preprocess.py
Go to the documentation of this file.
1 
13 
14 
15 
16 import re
17 import ast
18 import shutil
19 from Path.getPath import *
20 from Extract.getCall import getCallFunction, modifyWithName
21 from Extract.extractCall import WithVisitor
22 from Tool.tool import getAst,getParameter,getLastAPIParameter,departAPI,departAPI2,ConditionalReturnTransformer, getFileName, getRunFile
23 from Tool.workspace import getRuntimePaths
24 
25 
26 
27 
31 def countBracket(s):
32  minL=0
33  minR=0
34  midL=0
35  midR=0
36  huaL=0
37  huaR=0
38  flag=1
39  cnt=0 #计算引号的个数
40  for it in s:
41  if it=='\'': #引号内的括号不计数
42  flag=0
43  cnt+=1
44  if cnt%2==0:
45  flag=1
46  elif flag==1:
47  if it=='(':
48  minL+=1
49  elif it==')':
50  minR+=1
51  elif it=='[':
52  midL+=1
53  elif it==']':
54  midR+=1
55  elif it=='{':
56  huaL+=1
57  elif it=='}':
58  huaR+=1
59  return minL,minR,midL,midR,huaL,huaR
60 
61 
62 
63 
66 def oneLine(filePath):
67  try:
68  root=getAst(filePath)
69  #重写回文件
70  with open(filePath,'w',encoding='UTF-8') as fw:
71  newCode=ast.unparse(root)
72  # newCode=re.sub(r'""".*?"""','pass',newCode,flags=re.DOTALL) #去掉代码中的注释
73  fw.write(f"{newCode}\n")
74  except Exception as e:
75  print(f"oneLine --> {filePath} parse to ast failed: {e}")
76 
77 
78 
79 
83  try:
84  root=getAst(filePath)
85  transformer = ConditionalReturnTransformer()
86  new_root = transformer.visit(root)
87  #重写回文件
88  with open(filePath,'w',encoding='UTF-8') as fw:
89  newCode=ast.unparse(root)
90  fw.write(f"{newCode}\n")
91  except Exception as e:
92  print(f"expandConditionalReturn --> {filePath} parse to ast failed: {e}")
93 
94 
95 
96 
105 def getListVar(root,ansLst):
106  for node in ast.iter_child_nodes(root):
107  if isinstance(node,ast.ListComp):
108  s=ast.unparse(node.generators).lstrip(' ')
109  pattern="for (.*?) in"
110  lst=re.findall(pattern,s)
111  if len(lst)==1:
112  temp=lst[0]
113  if temp[0]=='(':
114  var=temp[1:-1]
115  else:
116  var=temp
117  s1=f"{var}=[{temp} {s}][0]"
118  ansLst.append(s1)
119 
120  getListVar(node,ansLst)
121 
122 
123 
124 
132 def getDictVar(root,ansLst):
133  for node in ast.iter_child_nodes(root):
134  if isinstance(node,ast.DictComp):
135  s=ast.unparse(node.generators).lstrip(' ')
136  pattern="for (.*?) in"
137  lst=re.findall(pattern,s)
138  if len(lst)==1:
139  temp=lst[0]
140  if temp[0]=='(':
141  var=temp[1:-1]
142  else:
143  var=temp
144  s1=f"{var}=[{temp} {s}][0]"
145  ansLst.append(s1)
146 
147  getDictVar(node,ansLst)
148 
149 
150 
151 
156 def convertLocalVar(filePath,libName):
157  with open(filePath,'r',encoding='UTF-8') as fr:
158  codeLst=fr.readlines()
159 
160  for i in range(len(codeLst)):
161  s=codeLst[i].lstrip(' ').rstrip(' ')
162  try:
163  root=ast.parse(s,filename='<unknown>',mode='exec')
164  except Exception as e:
165  continue
166 
167  #step1:判断代码语句中是否含有列表推导式或字典推导式
168  listComp=0
169  dictComp=0
170  for node in ast.walk(root):
171  if isinstance(node,ast.ListComp):
172  listComp=1
173  if isinstance(node,ast.DictComp):
174  dictComp=1
175 
176  if not listComp and not dictComp:
177  continue
178 
179  #step2:判断列表推导式中是否含有第三方库调用的API
180  flag=0
181  _,callDict=getCallFunction(filePath,libName)
182  callLst=[record['call_text'] for record in callDict.values()]
183  flag=0
184  for node in ast.walk(root):
185  if isinstance(node, ast.Call):
186  callState=ast.unparse(node)
187  callState=callState.replace(' ','').replace('"','').replace("'",'')
188  for it in callLst:
189  if callState==it.replace(' ','').replace('"','').replace("'",''):
190  flag=1
191  break
192  if flag==1:
193  break
194  if flag==0:
195  continue
196 
197  #step3:提取出列表推导式中的变量
198  ansLst=[]
199  if listComp:
200  getListVar(root,ansLst)
201  spaceNum=countSpace(codeLst[i])
202  temp=''
203  for it in ansLst:
204  tryStr="try:\n"
205  exceptStr="except:\n"
206  passStr="pass\n"
207  if spaceNum:
208  tryStr=' '*spaceNum*1+tryStr
209  it=' '*spaceNum*1+' '*4+it+'\n'
210  exceptStr=' '*spaceNum*1+exceptStr
211  passStr=' '*spaceNum*1+' '*4+passStr
212  else:
213  spaceNum=4
214  it=' '*spaceNum*1+it+'\n'
215  passStr=' '*spaceNum*1+passStr
216  spaceNum=0 #用完之后就置为0
217 
218  s=tryStr+it+exceptStr+passStr
219  temp=temp+s
220  temp+=codeLst[i]
221  codeLst[i]=temp
222 
223  if dictComp:
224  getDictVar(root,ansLst)
225  spaceNum=countSpace(codeLst[i])
226  temp=''
227  for it in ansLst:
228  tryStr="try:\n"
229  exceptStr="except:\n"
230  passStr="pass\n"
231  if spaceNum:
232  tryStr=' '*spaceNum*1+tryStr
233  it=' '*spaceNum*1+' '*4+it+'\n'
234  exceptStr=' '*spaceNum*1+exceptStr
235  passStr=' '*spaceNum*1+' '*4+passStr
236  else:
237  spaceNum=4
238  it=' '*spaceNum*1+it+'\n'
239  passStr=' '*spaceNum*1+passStr
240  spaceNum=0 #用完之后就置为0
241 
242  s=tryStr+it+exceptStr+passStr
243  temp=temp+s
244  temp+=codeLst[i]
245  codeLst[i]=temp
246 
247 
248  with open(filePath,'w',encoding='UTF-8') as fw:
249  for it in codeLst:
250  fw.write(it)
251 
252 
253 
254 
259 def findAssignCall(root):
260  assignLst=[]
261  for node in ast.walk(root):
262  if isinstance(node,ast.Assign) and isinstance(node.value,ast.Call):
263  target=ast.unparse(node.targets)
264  assignLst.append(target)
265  return assignLst
266 
267 
268 
276 def getAssignReceiverExpr(root,source,aliasName,lineno):
277  expr=None
278  bestLine=-1
279 
280  scopeBodies=[root.body]
281  for node in ast.walk(root):
282  if isinstance(node,(ast.FunctionDef,ast.AsyncFunctionDef,ast.ClassDef)):
283  start=node.lineno
284  end=getattr(node,'end_lineno',start)
285  if start<lineno<=end:
286  scopeBodies.append(node.body)
287 
288  for body in scopeBodies:
289  for stmt in body:
290  if not hasattr(stmt,'lineno') or stmt.lineno>=lineno:
291  continue
292  if isinstance(stmt,(ast.If,ast.For,ast.AsyncFor,ast.While,ast.Try,ast.With,ast.AsyncWith)):
293  continue
294  value=None
295  targets=[]
296  if isinstance(stmt,ast.Assign):
297  value=stmt.value
298  targets=stmt.targets
299  elif isinstance(stmt,ast.AnnAssign):
300  value=stmt.value
301  targets=[stmt.target]
302  if value is None:
303  continue
304  for target in targets:
305  if isinstance(target,ast.Name) and target.id==aliasName and stmt.lineno>bestLine:
306  sourceExpr=ast.get_source_segment(source,value)
307  if sourceExpr:
308  expr=sourceExpr.strip()
309  bestLine=stmt.lineno
310  return expr
311 
312 
313 
320 def getSelfReceiverExpr(root,lineno,methodName):
321  for node in ast.walk(root):
322  if not isinstance(node,ast.ClassDef):
323  continue
324  start=node.lineno
325  end=getattr(node,'end_lineno',start)
326  if not (start<lineno<=end) or len(node.bases)==0:
327  continue
328  defNames=set()
329  for item in ast.iter_child_nodes(node):
330  if isinstance(item,(ast.FunctionDef,ast.AsyncFunctionDef)):
331  defNames.add(item.name)
332  if methodName in defNames:
333  return None
334  return ast.unparse(node.bases[0])
335  return None
336 
337 
338 
339 
344 def countSpace(s):
345  cntSpace=0
346  for it in s:
347  if it==' ':
348  cntSpace+=1
349  else:
350  break
351  return cntSpace
352 
353 
354 
355 
363 def getImportLine(codeLst):
364  #首先判断from import语句中是否含有特殊的__future__
365  index=-1
366  for i in range(len(codeLst)):
367  if 'import' in codeLst[i] and '__future__' in codeLst[i]:
368  index=i
369 
370  #若没有future,再判断开头是否存在'"""'注释
371  count=0
372  if index==-1 and '"""' in codeLst[0]:
373  for i in range(0,len(codeLst)):
374  if '"""' in codeLst[i]:
375  index=i
376  count+=1
377  if count==2:
378  break
379 
380  index+=1
381 
382  return index
383 
384 
385 
386 
392  decoratorLst = []
393  for n in ast.walk(root):
394  if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.decorator_list:
395  for decorator in n.decorator_list:
396  if isinstance(decorator, ast.Call): # and isinstance(decorator.func.value, ast.Name):
397  decoratorLst.append(ast.unparse(decorator.func))
398 
399  return decoratorLst
400 
401 
402 
412 def getDecoratorInsertIndex(codeLst,index):
413  insertIndex=index
414  if not codeLst[index].lstrip().startswith('@'):
415  return insertIndex
416 
417  spaceNum=countSpace(codeLst[index])
418  j=index-1
419  while j>=0:
420  if codeLst[j].lstrip().startswith('@') and countSpace(codeLst[j])==spaceNum:
421  insertIndex=j
422  j-=1
423  continue
424  break
425 
426  return insertIndex
427 
428 
429 
430 
436 def addDictSingle(callAPI,filePath,callKey):
437  with open(filePath,'r',encoding='UTF-8') as fr:
438  codeLst=fr.readlines()
439  source=''.join(codeLst)
440 
441  lineno=getImportLine(codeLst)
442  importDict='from recordValue import paraValueDict\nfrom recordValue import callsiteInfoDict\n'
443  codeLst.insert(lineno,importDict)
444 
445  paraStr=getLastAPIParameter(callAPI) #获取最后一个API的参数
446  parameterLst=getParameter(paraStr,space=0) #项目参数不去空格
447  root=getAst(filePath)
448  targetLst=findAssignCall(root)
449 
450  #找出树中所有withitem call节点 -- 2025/5/19
451  withitem_visitor = WithVisitor()
452  withitem_visitor.visit(root)
453  withitem_call_names = withitem_visitor.get_withitem_call() #dict
454 
455  for i in range(len(codeLst)): #每次只会往列表中插入一个元素
456  if callAPI.replace(' ','') in codeLst[i].replace(' ','') and 'def ' not in codeLst[i] and 'paraValueDict' not in codeLst[i] and 'callsiteInfoDict' not in codeLst[i]:
457  spaceNum=countSpace(codeLst[i])
458  dicString1=''
459  l=departAPI(callAPI)
460  l2=departAPI2(callAPI)
461  firstPart=''
462  for it in l2:
463  if '(' not in it:
464  firstPart+=it+'.'
465  firstPart=firstPart.rstrip('.')
466 
467  callsiteInfo={
468  'call_text': callAPI,
469  'format_api': callAPI,
470  }
471  displayCall=callAPI.replace('\n',' ')
472  dicStringComment=f'# PCART callsite: {displayCall}\n'
473  dicStringKey=f'__pcart_runtime_callsite_key__={repr(callKey)}\n'
474  dicString0=f'callsiteInfoDict[__pcart_runtime_callsite_key__]={repr(callsiteInfo)}\n'
475  if firstPart and (firstPart.split('.')[0] in targetLst or firstPart.split('.')[0]=='self') and len(l)==1:
476  lineNo = i + 1
477  receiverExpr=None
478  if firstPart.split('.')[0] in targetLst:
479  receiverExpr=getAssignReceiverExpr(root,source,firstPart.split('.')[0],lineNo)
480  elif firstPart.split('.')[0]=='self':
481  methodName=callAPI.split('(')[0].split('.')[-1]
482  receiverExpr=getSelfReceiverExpr(root,lineNo,methodName)
483  if receiverExpr:
484  receiverExpr=receiverExpr.replace('\\','\\\\').replace('"','\\"')
485  dicString1=f'paraValueDict[\"@\"+__pcart_runtime_callsite_key__]={{}}; paraValueDict[\"@\"+__pcart_runtime_callsite_key__][\"object\"]={firstPart}; paraValueDict[\"@\"+__pcart_runtime_callsite_key__][\"expr\"]=\"{receiverExpr}\"\n'
486  else:
487  dicString1=f'paraValueDict[\"@\"+__pcart_runtime_callsite_key__]={firstPart}\n'
488  elif len(l)>1: #df.a(x).b(y), np.max(...), torch.nn.Sequential(...)
489  dicString1=f'paraValueDict[\"@\"+__pcart_runtime_callsite_key__]={l[-2]}\n'
490 
491  #判断API是否为withitem中的别名调用 -- 2025/5/19
492  if firstPart and firstPart.split('.')[0] in withitem_call_names:
493  lineNo = i + 1
494  initialCallName = modifyWithName(firstPart, withitem_call_names, lineNo).rstrip('.')
495  # withitem接收者同时保存运行时对象和还原表达式,动态阶段按可用候选依次尝试
496  initialCallName=initialCallName.replace('\\','\\\\').replace('"','\\"')
497  dicString1=f'paraValueDict[\"@\"+__pcart_runtime_callsite_key__]={{}}; paraValueDict[\"@\"+__pcart_runtime_callsite_key__][\"object\"]={firstPart}; paraValueDict[\"@\"+__pcart_runtime_callsite_key__][\"expr\"]=\"{initialCallName}\"\n'
498 
499  #再保存API的参数值
500  dicString2='paraValueDict[__pcart_runtime_callsite_key__]=['
501  for para in parameterLst: #
502  if '=' in para and "'='" not in para and '"="' not in para: #若参数的形式为key=f(x=1),只要确保=的前面不含括号即可
503  pos=para.find('=') #找到第一个=的位置
504  if '(' not in para[0:pos] and "'" not in para[0:pos] and '"' not in para[0:pos] and para[pos+1]!='=': #等号前面也不能出现引号,比如f('x= ',y=1)
505  para=para[pos+1:]
506 
507  para=para.lstrip('*') #有的参数会带*,2023-12-20
508  dicString2=dicString2+para+','
509  dicString2=dicString2.rstrip(',')+']\n'
510 
511  while spaceNum>0:
512  dicStringComment=' '+dicStringComment
513  dicStringKey=' '+dicStringKey
514  dicString0=' '+dicString0
515  if dicString1:
516  dicString1=' '+dicString1
517  dicString2=' '+dicString2
518  spaceNum-=1
519 
520  #插入的时候要考虑是否含有elif,如果有elif要把它插在elif后面
521  if 'elif' not in codeLst[i]:
522  insertIndex=getDecoratorInsertIndex(codeLst,i)
523  codeLst.insert(insertIndex,dicString2)
524  if dicString1:
525  codeLst.insert(insertIndex,dicString1)
526  codeLst.insert(insertIndex,dicString0)
527  codeLst.insert(insertIndex,dicStringKey)
528  codeLst.insert(insertIndex,dicStringComment)
529  else:
530  if dicString1:
531  dicString1=dicString1.lstrip(' ')#去掉之前添加的空格,重新计算开头的空格数
532  dicStringComment=dicStringComment.lstrip(' ')
533  dicStringKey=dicStringKey.lstrip(' ')
534  dicString0=dicString0.lstrip(' ')
535  dicString2=dicString2.lstrip(' ')
536  for j in range(i+1,len(codeLst)):
537  if codeLst[j]!='\n' and '#' not in codeLst[j]:
538  spaceNum=countSpace(codeLst[j])
539  while spaceNum>0:
540  dicStringComment=' '+dicStringComment
541  dicStringKey=' '+dicStringKey
542  dicString0=' '+dicString0
543  if dicString1:
544  dicString1=' '+dicString1
545  dicString2=' '+dicString2
546  spaceNum-=1
547 
548  codeLst.insert(j,dicString2)
549  if dicString1:
550  codeLst.insert(j,dicString1)
551  codeLst.insert(j,dicString0)
552  codeLst.insert(j,dicStringKey)
553  codeLst.insert(j,dicStringComment)
554  break
555 
556  break
557 
558 
559  if codeLst[0]=='pass\n':
560  codeLst=codeLst[1:]
561  with open(filePath,'w',encoding='UTF-8') as fw:
562  for it in codeLst:
563  fw.write(it)
564 
565 
566 
567 
579 def addDictAll(projPath,projName,filePath,copyRoot,runFileLst,libName,runPath,runCommand,pcresolveLookup=None):
580  with open(filePath,'r',encoding='UTF-8') as fr:
581  code=fr.read()
582  fr.seek(0) #将文件指针重新定位到文件的开头
583  codeLst=fr.readlines()
584  try:
585  root=ast.parse(code,filename='<unknown>',mode='exec')
586  except Exception as e:
587  print(f"addDictAll --> ast.parse failed, {filePath}: {e}")
588  return
589 
590  #处理相关路径
591  fileName = os.path.basename(filePath)[0:-3]
592  fileRelativePath=os.path.relpath(filePath,os.path.join(copyRoot,projName))
593  fileAbsolutePath=os.path.join(projPath,fileRelativePath)
594 
595  lineno=getImportLine(codeLst)
596  importDict='from recordValue import paraValueDict\nfrom recordValue import apiCoveredSet\nfrom recordValue import callsiteInfoDict\n'
597  codeLst.insert(lineno,importDict)
598  if pcresolveLookup is None:
599  _,callDict=getCallFunction(fileAbsolutePath,libName,projPath)
600  else:
601  _,callDict=getCallFunction(fileAbsolutePath,libName,projPath,pcresolveLookup=pcresolveLookup)
602 
603  targetLst=findAssignCall(root) #用来区分调用者是否来自赋值语句,比如a.f(), tf.f(), or self.f()
604 
605  #找出树中所有withitem call节点 -- 2025/5/19
606  try:
607  withitem_root = getAst(fileAbsolutePath)
608  except Exception:
609  withitem_root = root
610  withitem_visitor = WithVisitor()
611  withitem_visitor.visit(withitem_root)
612  withitem_call_names = withitem_visitor.get_withitem_call() #dict
613 
614  insertStartLine=0 #记录每次插桩的行
615  preInsertAPI='' #记录上一个插桩的API是哪个
616  preInsertAPICount=0 #记录上一个插桩行中出现了几次被插的API
617  for artifactId,record in callDict.items(): #key是调用点artifact id,value是结构化调用点记录
618  flag=0 #标记API是否找到了插桩的位置
619  lineno=int(record['lineno']) #这个lineno是原项目中的行数
620  callState=record['call_text']
621  paraStr=record['parameters']
622  callAPI=callState.replace(' ','')
623  if callAPI==preInsertAPI: #判断当前要处理的API与上一个API是否相同
624  preInsertAPICount-=1
625  if preInsertAPICount<=0:
626  insertStartLine+=1
627 
628  i=insertStartLine #从第i行开始向后找
629  while i<len(codeLst):
630  #API调用在i行代码中,且i行代码不是函数定义语句、插桩语句和运行覆盖检查语句
631  if callAPI in codeLst[i].replace(' ','') and 'def ' not in codeLst[i] and 'paraValueDict' not in codeLst[i] and 'apiCoveredSet' not in codeLst[i] and 'callsiteInfoDict' not in codeLst[i]:
632  if callAPI!=preInsertAPI:#只有当前API不等于上一个被插API时,才需要重新计算preAPICount
633  preInsertAPICount=codeLst[i].replace(' ','').count(callAPI)
634  preInsertAPI=callAPI
635  flag=1
636  spaceNum=countSpace(codeLst[i])
637  callSiteKey=artifactId
638  callsiteInfo={
639  'artifact_hash': record.get('artifact_hash',''),
640  'rel_path': record.get('rel_path',''),
641  'lineno': record.get('lineno'),
642  'col_offset': record.get('col_offset'),
643  'end_lineno': record.get('end_lineno'),
644  'end_col_offset': record.get('end_col_offset'),
645  'call_text': record.get('call_text',''),
646  'format_api': record.get('format_api',''),
647  }
648  displayPath=record.get('rel_path','')
649  displayLine=record.get('lineno')
650  displayColumn=record.get('col_offset')
651  displayCall=str(record.get('call_text','')).replace('\n',' ')
652  displayPrefix=f'{displayPath}:{displayLine}:{displayColumn} ' if displayPath else ''
653  dicStringComment=f'# PCART callsite: {displayPrefix}{displayCall}\n'
654  dicStringKey=f'__pcart_runtime_callsite_key__={repr(callSiteKey)}\n'
655  dicString0=f'callsiteInfoDict[__pcart_runtime_callsite_key__]={repr(callsiteInfo)}\n'
656  l=departAPI(callState)
657  l2=departAPI2(callState)
658  firstPart=''
659  for it in l2:
660  if '(' not in it:
661  firstPart+=it+'.'
662  firstPart=firstPart.rstrip('.')
663  dicString1=''
664 
665  #判断API是否具有上文依赖,比如self.f(x), a(x).b(y)中的a(x),或者a.f(x)中的a
666  #df.a(x).b(y)这种情况如何解决
667  #a.b.c(x)
668  # if '(' not in firstPart and (firstPart in targetLst or firstPart=='self'):
669  #self.f(x), a.f(x), a.b.c(x)
670  if firstPart and (firstPart.split('.')[0] in targetLst or firstPart.split('.')[0]=='self') and len(l)==1:
671  receiverExpr=None
672  if firstPart.split('.')[0] in targetLst:
673  receiverExpr=getAssignReceiverExpr(root,code,firstPart.split('.')[0],lineno)
674  elif firstPart.split('.')[0]=='self':
675  methodName=callState.split('(')[0].split('.')[-1]
676  receiverExpr=getSelfReceiverExpr(root,lineno,methodName)
677  if receiverExpr:
678  receiverExpr=receiverExpr.replace('\\','\\\\').replace('"','\\"')
679  dicString1=f'paraValueDict[\"@\"+__pcart_runtime_callsite_key__]={{}}; paraValueDict[\"@\"+__pcart_runtime_callsite_key__][\"object\"]={firstPart}; paraValueDict[\"@\"+__pcart_runtime_callsite_key__][\"expr\"]=\"{receiverExpr}\"\n'
680  else:
681  dicString1=f'paraValueDict[\"@\"+__pcart_runtime_callsite_key__]={firstPart}\n'
682  elif len(l)>1: #df.a(x).b(y), np.max(...), torch.nn.Sequential(...)
683  dicString1=f'paraValueDict[\"@\"+__pcart_runtime_callsite_key__]={l[-2]}\n'
684 
685  #判断API是否为withitem中的别名调用 -- 2025/5/19
686  if firstPart and firstPart.split('.')[0] in withitem_call_names:
687  initialCallName = modifyWithName(firstPart, withitem_call_names, lineno).rstrip('.')
688  initialCallName = initialCallName.rstrip('.')
689  # withitem接收者同时保存运行时对象和还原表达式,非withitem调用仍保持原有单值保存
690  initialCallName=initialCallName.replace('\\','\\\\').replace('"','\\"')
691  dicString1=f'paraValueDict[\"@\"+__pcart_runtime_callsite_key__]={{}}; paraValueDict[\"@\"+__pcart_runtime_callsite_key__][\"object\"]={firstPart}; paraValueDict[\"@\"+__pcart_runtime_callsite_key__][\"expr\"]=\"{initialCallName}\"\n'
692  #再保存API的参数值
693  dicString2='paraValueDict[__pcart_runtime_callsite_key__]=['
694  paraLst=getParameter(paraStr,space=0) #项目参数不去空格2023-12-14
695  for para in paraLst:
696  if '=' in para and "'='" not in para and '"="' not in para: #若参数的形式为key=f(x=1),只要确保=的前面不含括号即可
697  pos=para.find('=') #找到第一个=的位置,存在x=(a==b)和a==b形式
698  if '(' not in para[0:pos] and "'" not in para[0:pos] and '"' not in para[0:pos] and para[pos+1]!='=': #等号前面也不能出现引号,比如f('x= ',y=1)
699  para=para[pos+1:] #把参数的值保存下来
700 
701  para=para.lstrip('*') #有的参数会带*号,2023-12-20
702  dicString2=dicString2+para+','
703  dicString2=dicString2.rstrip(',')+']\n'
704 
705  dicString3='apiCoveredSet.add(__pcart_runtime_callsite_key__)\n'
706  while spaceNum>0:
707  dicStringComment=' '+dicStringComment
708  dicStringKey=' '+dicStringKey
709  dicString0=' '+dicString0
710  if dicString1:
711  dicString1=' '+dicString1
712  dicString2=' '+dicString2
713  dicString3=' '+dicString3
714  spaceNum-=1
715 
716  #插入的时候要考虑是否含有elif,如果有elif要把它插在elif后面
717  #不能将字典插入到if和elif之间,所以需要找到elif后的下一个非空行再插入
718  if 'elif' not in codeLst[i]:
719  insertIndex=getDecoratorInsertIndex(codeLst,i)
720  codeLst.insert(insertIndex,dicString3)
721  codeLst.insert(insertIndex,dicString2)
722  if dicString1:
723  codeLst.insert(insertIndex,dicString1)
724  codeLst.insert(insertIndex,dicString0)
725  codeLst.insert(insertIndex,dicStringKey)
726  codeLst.insert(insertIndex,dicStringComment)
727  #记录当前插在了哪一行
728  if dicString1:
729  insertStartLine=insertIndex+6
730  else:
731  insertStartLine=insertIndex+5
732  else:
733  if dicString1:
734  dicString1=dicString1.lstrip(' ') #去掉之前添加的空格,重新计算开头的空格数
735  dicStringComment=dicStringComment.lstrip(' ')
736  dicStringKey=dicStringKey.lstrip(' ')
737  dicString0=dicString0.lstrip(' ')
738  dicString2=dicString2.lstrip(' ')
739  dicString3=dicString3.lstrip(' ')
740  for j in range(i+1,len(codeLst)):
741  if codeLst[j]!='\n' and '#' not in codeLst[j]:
742  spaceNum=countSpace(codeLst[j])
743  while spaceNum>0:
744  dicStringComment=' '+dicStringComment
745  dicStringKey=' '+dicStringKey
746  dicString0=' '+dicString0
747  if dicString1:
748  dicString1=' '+dicString1
749  dicString2=' '+dicString2
750  dicString3=' '+dicString3
751  spaceNum-=1
752  codeLst.insert(j,dicString3)
753  codeLst.insert(j,dicString2)
754  if dicString1:
755  codeLst.insert(j,dicString1)
756  codeLst.insert(j,dicString0)
757  codeLst.insert(j,dicStringKey)
758  codeLst.insert(j,dicStringComment)
759  #记录当前插在了哪一行
760  if dicString1:
761  insertStartLine=j+6
762  else:
763  insertStartLine=j+5
764  break
765 
766  break
767 
768  i+=1
769 
770  if flag==0:
771  # Debug aid: log uninstrumentable callsites for diagnosis
772  # 调试辅助:记录无法插桩的调用点以便诊断
773  print(f"{fileName}#{lineno}-->{callState}\n")
774 
775  if codeLst[0]=='pass\n':
776  codeLst=codeLst[1:]
777  with open(filePath,'w',encoding='UTF-8') as fw:
778  for it in codeLst:
779  fw.write(it)
780 
781 
782 
783 
795 def handleRunFile(file,runPath,runCommand):
796  with open(file,'r',encoding='UTF-8') as fr:
797  codeLst=fr.readlines()
798  lineno=getImportLine(codeLst)
799  codeLst.insert(lineno,f"from recordValue import paraValueDict\n")
800 
801  if codeLst[0]=='pass\n':
802  codeLst=codeLst[1:]
803  with open(file,'w',encoding='UTF-8') as fw:
804  for it in codeLst:
805  fw.write(it)
806 
807 
811 def obtainDef(sourcePath):
812  with open(sourcePath,'r',encoding='UTF-8') as fr:
813  code=fr.read()
814  try:
815  root=ast.parse(code,filename='<unknown>',mode='exec') #将源码解析成AST语法树
816  except:
817  print(sourcePath)
818  return
819  fw=open('Copy/defFile.py','a',encoding='UTF-8')
820  for node in ast.iter_child_nodes(root):
821  if isinstance(node,ast.ClassDef) or isinstance(node,ast.FunctionDef):
822  s=ast.unparse(node)
823  fw.write(f"{s}\n")
824  fw.close()
825 
826 
827 
832 def modifyFromImport(filePath,importStatement):
833  # with open(filePath,'r') as fr:
834  with open(filePath, 'r', encoding='UTF-8') as fr:
835  codeLst=fr.readlines()
836 
837  s='\n'.join(importStatement)+'\n'
838  codeLst.insert(0,s)
839  # with open(filePath,'w') as fw:
840  with open(filePath, 'w', encoding='UTF-8') as fw:
841  for it in codeLst:
842  fw.write(it)
843 
844 
845 
846 
866 def saveConstantAssign(astNode, constantVar, nonConstantVar, decoratorLst):
867  flag = 0
868  astBody = []
869  for n in ast.walk(astNode):
870  if isinstance(n,ast.Assign):
871  value = n.value
872  valueAstContent = ast.dump(n.value)
873  targetAstContent = ast.dump(n.targets[0])
874  if isinstance(n.targets[0], ast.Name):
875  varName = n.targets[0].id
876  #如果赋值语句的变量名在装饰器列表中出现过,则保留该赋值语句 2025/5/13
877  if any(varName in decorator.split('.') for decorator in decoratorLst):
878  continue
879  #如果赋值语句的变量名是常量变量名,则保留该赋值语句 2025/5/31
880  if isinstance(value, ast.Constant):
881  if varName in nonConstantVar:
882  flag=1
883  break
884  else:
885  targets = n.targets
886  if targets:
887  target = targets[0]
888  if isinstance(target, ast.Name):
889  constantVar.append(target.id)
890  else:
891  # 使用正则表达式匹配单引号包围的内容
892  pattern = r"id='([^']*)'"
893  valueMatches = re.findall(pattern, valueAstContent)
894  targetMatches = re.findall(pattern, targetAstContent)
895  if not len(valueMatches):
896  if targetMatches[0] in nonConstantVar:
897  flag=1
898  break
899  else:
900  for match in valueMatches:
901  if match not in constantVar:
902  nonConstantVar.append(targetMatches[0])
903  flag=1
904  break
905  else:
906  flag=1
907  break
908  if flag==1:
909  return flag
910  else:
911  astBody.append(astNode)
912  return astBody
913 
914 
915 
916 
921 def saveStructure(projPath,libName):
922  pathObj=Path('DF')
923  pathObj.getPath(projPath)
924  filePath=[it for it in pathObj.path if it.endswith('py')]
925  for file in filePath:
926  _,callDict=getCallFunction(file,libName)
927  callLst=[record['call_text'] for record in callDict.values()]
928  # with open(file,'r') as fr:
929  with open(file, 'r', encoding='UTF-8') as fr:
930  code=fr.read()
931  try:
932  root=ast.parse(code,filename='<unknown>',mode='exec')
933  except Exception as e:
934  print(f"saveStructure --> ast parse failed in {file}: {e}")
935  continue
936  newBody=[]
937  constantVar = []
938  nonConstantVar = []
939  decoratorLst = extractDecorator(root)
940  #保留Import语句、函数和类定义
941  for node in root.body:
942  #增加AsyncFunctionDef节点信息保存 -- 2025/5/19
943  if isinstance(node,ast.Import) or isinstance(node,ast.ImportFrom) or isinstance(node,ast.ClassDef) or isinstance(node,ast.FunctionDef) or isinstance(node,ast.AsyncFunctionDef):
944  newBody.append(node)
945 
946  #保留包含常量的全局赋值语句和装饰器调用相关的赋值语句,例如:
947  # Case 1:
948  #1. a = 1
949  #2. b = 1
950  #3. c = a + b
951  #4. c = func(a,b)
952  #仅保留1,2,3行代码
953  # Case 2:
954  #1. app = Flask(__name__)
955  #2. @app.route("/")
956  #保留 app = Flask(__name__)以解决NameError
957  if isinstance(node,ast.Assign):
958  result = saveConstantAssign(node,constantVar,nonConstantVar,decoratorLst)
959  if result==1:
960  continue
961  else:
962  newBody+=result
963 
964  #保留包含常量的局部赋值语句
965  for node in ast.walk(root):
966  if isinstance(node,ast.Assign):
967  result = saveConstantAssign(node,constantVar,nonConstantVar,decoratorLst)
968  if result==1:
969  continue
970  else:
971  for item in result:
972  if ast.dump(item) not in [ast.dump(i) for i in newBody]:
973  newBody.append(item)
974 
975  root.body=newBody
976  newFile=ast.unparse(root)
977  with open(file,'w',encoding='utf-8') as fw:
978  fw.write(f"{newFile}\n")
979 
980 
981 
982 
988 def ignore_sym_links(directory, files):
989  return [f for f in files if os.path.islink(os.path.join(directory, f))]
990 
991 
992 
993 
999 def getLibImportLst(projPath,libName):
1000  lst=[]
1001  pathObj=Path('DF')
1002  pathObj.getPath(projPath)
1003  filePath=[file for file in pathObj.path if file.endswith('.py')]
1004  pattern=rf"(from {libName}|import {libName})" #确保库的前面不会出现其它字符
1005  for file in filePath:#下面的所有操作都是对项目副本进行的
1006  # with open(file,'r') as fr:
1007  with open(file, 'r', encoding='UTF-8') as fr:
1008  code=fr.read()
1009  try:
1010  root=ast.parse(code,filename='<unknown>',mode='exec')
1011  for node in ast.walk(root):
1012  if isinstance(node,ast.Import) or isinstance(node,ast.ImportFrom):
1013  s=ast.unparse(node)
1014  if bool(re.search(pattern,s)):
1015  lst.append(s)
1016  except Exception as e:
1017  print(f"getLibImportLst: ast parse failed, {file}, {e}")
1018 
1019 
1020  ansLst=list(set(lst))
1021  ansLst.sort(key=lst.index)
1022  return ansLst
1023 
1024 
1025 
1026 
1030 def convertTabsToSpaces(directory):
1031  for root, dirs, files in os.walk(directory):
1032  for file in files:
1033  if file.endswith('.py'):
1034  file_path = os.path.join(root, file)
1035  try:
1036  with open(file_path, 'r', encoding='utf-8') as f:
1037  content = f.read()
1038  # 将制表符转换为4个空格
1039  content = content.expandtabs(4)
1040  with open(file_path, 'w', encoding='utf-8') as f:
1041  f.write(content)
1042  except Exception as e:
1043  print(f"Error converting file {file_path}: {e}")
1044 
1045 
1046 
1047 
1053 def writeRecordValue(filePath,pklRelPath,useCallsiteName):
1054  scriptPath=os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
1055  'Script','recordValue.py')
1056  with open(scriptPath,'r',encoding='UTF-8') as fr:
1057  content=fr.read()
1058  content=content.replace("'__PCART_PKL_REL_PATH__'",repr(pklRelPath))
1059  content=content.replace('__PCART_USE_CALLSITE_NAME__',str(bool(useCallsiteName)))
1060  with open(filePath,'w',encoding='UTF-8') as fw:
1061  fw.write(content)
1062 
1063 
1064 
1069 def scriptPath(scriptName):
1070  return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
1071  'Script',scriptName)
1072 
1073 
1074 
1094 def codeProcess(projPath,runCommand,runPath,libName,workspace,pcresolveLookup=None):
1095  runtimePaths=getRuntimePaths(workspace)
1096  copyRoot=runtimePaths['copy_root']
1097  dynamicRoot=runtimePaths['dynamic_root']
1098  dataDir=runtimePaths['data_dir']
1099  #提取运行的文件
1100  runFileLst=[]
1101  runFile=getRunFile(runCommand)
1102  prefix='' #运行文件所在的目录,默认是在项目的一级子目录下
1103  # if '/' in runFile:
1104  if runFile and ('/' in runFile or '\\' in runFile):
1105  # prefix=runFile.rsplit('/',1)[0]
1106  prefix = os.path.dirname(runFile)
1107  # runFile=runFile.rsplit('/',1)[1] #去掉路径前缀,只保留文件名即run.py
1108  runFile = os.path.basename(runFile)
1109  if runFile:
1110  runFileLst.append(runFile)
1111  #这种情况针对于python run.py, run.py在其它目录中比如src,则prefix就是src
1112  #若是python src/run.py, 则prexfix和runPath是一致的
1113  if runPath!='' and prefix!=runPath:
1114  prefix=runPath
1115  # projName=projPath.split('/')[-1]
1116  projName = os.path.basename(projPath)
1117  copyProjPath=os.path.join(copyRoot,projName)
1118  importStatement=''
1119  for it in runFileLst:
1120  # it=it.rstrip('.py') #把文件名中的后缀去掉,但遇到display.py会变成displa
1121  it=it[0:-3]
1122  importStatement+=f"from {it} import *\n"
1123 
1124 
1125  #找出项目中所有和第三方库相关的import语句
1126  libImportLst=getLibImportLst(projPath,libName)
1127  libImportLst.append(importStatement)
1128 
1129  #清除Copy和Dynamic中遗留的项目信息,然后把新项目的信息拷贝进去
1130  if os.path.isdir(copyRoot):
1131  shutil.rmtree(copyRoot)
1132  os.makedirs(os.path.dirname(copyRoot) or '.',exist_ok=True)
1133  shutil.copytree(projPath,os.path.join(copyRoot,projName),ignore=ignore_sym_links)
1134  os.mkdir(os.path.join(copyRoot,'pkl'))
1135  if os.path.isdir(dynamicRoot):
1136  shutil.rmtree(dynamicRoot)
1137  os.makedirs(os.path.dirname(dynamicRoot) or '.',exist_ok=True)
1138  shutil.copytree(projPath,os.path.join(dynamicRoot,projName),ignore=ignore_sym_links)
1139 
1140  #去掉项目代码中的冗余信息,仅保存项目代码的结构信息(import,functionDef, classDef)
1141  saveStructure(os.path.join(dynamicRoot,projName),libName)
1142 
1143  dynamicScriptDir=os.path.join(dynamicRoot,projName,prefix)
1144  shutil.copy2(scriptPath('addValueForAPI.py'),dynamicScriptDir)
1145  shutil.copy2(scriptPath('codeUtils.py'),dynamicScriptDir)
1146  shutil.copy2(scriptPath('dynamicMatch.py'),dynamicScriptDir)
1147  shutil.copy2(scriptPath('verifySingle.py'),dynamicScriptDir)
1148 
1149  #更新脚本中的from ... import ...语句,因为加载pkl的时候需要依赖于项目的结构信息
1150  modifyFromImport(os.path.join(dynamicScriptDir,'addValueForAPI.py'),libImportLst)
1151  modifyFromImport(os.path.join(dynamicScriptDir,'dynamicMatch.py'),libImportLst)
1152  modifyFromImport(os.path.join(dynamicScriptDir,'verifySingle.py'),libImportLst)
1153 
1154  #清除data中的数据
1155  if os.path.isdir(dataDir):
1156  shutil.rmtree(dataDir)
1157  os.makedirs(dataDir)
1158 
1159 
1160  #然后再把Copy中的项目制表符统一转化为空格,目的是为了插入字典的时候计算空格缩进
1161  convertTabsToSpaces(copyRoot)
1162 
1163  #把代码换行写的合成一行,并添加字典
1164  pathObj=Path('DF')
1165  pathObj.getPath(copyProjPath)
1166  filePath=[file for file in pathObj.path if file.endswith('.py')]
1167  for file in filePath:#下面的所有操作都是对项目副本进行的
1168  oneLine(file)
1169 
1170  #处理单行条件返回语句
1171  for file in filePath:
1173 
1174  #处理局部变量
1175  for file in filePath:
1176  convertLocalVar(file,libName)
1177 
1178 
1179  shutil.copytree(os.path.join(copyRoot,projName),os.path.join(copyRoot,f'bak_{projName}'))
1180 
1181  # 计算recordValue.py到Copy/pkl的相对路径
1182  pklDepth=1
1183  if prefix:
1184  pklDepth+=len([s for s in prefix.replace('\\','/').strip('/').split('/') if s])
1185  pklRelPath='/'.join(['..']*pklDepth+['pkl'])
1186  writeRecordValue(os.path.join(copyRoot,f'bak_{projName}',prefix,'recordValue.py'),pklRelPath,0)
1187 
1188 
1189  for file in filePath:
1190  if pcresolveLookup is None:
1191  addDictAll(projPath,projName,file,copyRoot,runFileLst,libName,runPath,runCommand)
1192  else:
1193  addDictAll(projPath,projName,file,copyRoot,runFileLst,libName,runPath,runCommand,pcresolveLookup=pcresolveLookup)
1194 
1195  #再对bak_proj中的运行文件进行插桩
1196  for file in runFileLst:
1197  file=os.path.join(copyRoot,f'bak_{projName}',prefix,file)
1198  handleRunFile(file,runPath,runCommand)
1199  # bak项目会在current pkl生成后换回Copy/{projName},其运行文件也依赖codeUtils
1200  shutil.copy2(scriptPath('codeUtils.py'),os.path.join(copyRoot,f'bak_{projName}',prefix))
1201 
1202  #处理完项目所有文件后,再给项目添加一个新的文件
1203  writeRecordValue(os.path.join(copyRoot,projName,prefix,'recordValue.py'),pklRelPath,1)
1204 
1205  shutil.copy2(scriptPath('codeUtils.py'),os.path.join(copyRoot,projName,runPath))
1206  if prefix!=runPath:
1207  shutil.copy2(scriptPath('codeUtils.py'),os.path.join(copyRoot,projName,prefix))
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 convertLocalVar(filePath, libName)
Convert the ListComp and DictComp statements to variable assignment statements 将列表推导式listComp和字典推导式Di...
Definition: preprocess.py:156
def codeProcess(projPath, runCommand, runPath, libName, workspace, pcresolveLookup=None)
Code processing 代码预处理
Definition: preprocess.py:1094
def convertTabsToSpaces(directory)
Convert tabs to spaces in all Python files within a directory 将目录下所有Python文件中的制表符转换为空格
Definition: preprocess.py:1030
def getDictVar(root, ansLst)
Get the local variable in the dictionary comprehension 获取字典推导式中的局部变量
Definition: preprocess.py:132
def oneLine(filePath)
Convert the multi-line parameter calls in the code into a single line to facilitate insertion into di...
Definition: preprocess.py:66
def expandConditionalReturn(filePath)
Expand the single-line conditional return statement into a multi-line if-else structure 展开单行条件retur...
Definition: preprocess.py:82
def obtainDef(sourcePath)
Extract class and function definitions from a source file 从源文件中提取类和函数定义
Definition: preprocess.py:811
def getAssignReceiverExpr(root, source, aliasName, lineno)
Resolve the source expression assigned to a receiver alias 还原调用者别名在当前作用域内的赋值来源表达式
Definition: preprocess.py:276
def addDictSingle(callAPI, filePath, callKey)
Code instrumentation for a single API call within a source file 源文件单个API调用代码插桩
Definition: preprocess.py:436
def getImportLine(codeLst)
Determine the instrumentation line for import statements 确定import语句插桩行
Definition: preprocess.py:363
def writeRecordValue(filePath, pklRelPath, useCallsiteName)
Write recordValue.py used by instrumented project files 写入插桩文件共享的recordValue.py.
Definition: preprocess.py:1053
def getLibImportLst(projPath, libName)
Get all lib-related import statements 获取所有第三方库相关的import语句
Definition: preprocess.py:999
def getListVar(root, ansLst)
Get the local variable in the list comprehension 获取列表推导式中的局部变量
Definition: preprocess.py:105
def countBracket(s)
Count the number of different types of brackets ((), [], {}) in a string 计算字符串中各类括号((),...
Definition: preprocess.py:31
def getSelfReceiverExpr(root, lineno, methodName)
Resolve the visible base-class expression for an inherited self receiver 还原继承场景中self调用者可见的基类表达式
Definition: preprocess.py:320
def scriptPath(scriptName)
Get source script path from PCART repository 获取PCART仓库中的辅助脚本路径
Definition: preprocess.py:1069
def countSpace(s)
Count the number of spaces at the beginning of a string 计算字符串的前面有多少个空格
Definition: preprocess.py:344
def saveConstantAssign(astNode, constantVar, nonConstantVar, decoratorLst)
Save assignment statement with constant values 保存常量赋值语句
Definition: preprocess.py:866
def addDictAll(projPath, projName, filePath, copyRoot, runFileLst, libName, runPath, runCommand, pcresolveLookup=None)
Code instrumentation for all API calls within a project source file 项目源文件所有API调用代码插桩
Definition: preprocess.py:579
def modifyFromImport(filePath, importStatement)
Save import statement to source file 将import语句保存到源码中
Definition: preprocess.py:832
def ignore_sym_links(directory, files)
Determine the soft link files in a directory 确定文件夹中的软链接文件
Definition: preprocess.py:988
def handleRunFile(file, runPath, runCommand)
Code instrumentation for project run file 项目运行文件代码插桩
Definition: preprocess.py:795
def findAssignCall(root)
Find assignment call statements 获取赋值调用语句
Definition: preprocess.py:259
def extractDecorator(root)
Extract decorator API calls 抽取项目中第三方库装饰器调用
Definition: preprocess.py:391
def saveStructure(projPath, libName)
Save structure information of the project 保存项目的结构信息
Definition: preprocess.py:921
def getDecoratorInsertIndex(codeLst, index)
Get decorator block start index 获取装饰器块起始行号
Definition: preprocess.py:412
def getAst(filePath, strFlag=0)
Get AST for code 将代码转化为Ast树
Definition: tool.py:173
def getParameter(p_string, separator=',', space=1)
Split parameter string into list of separated parameters 将参数字符串拆分成单个的参数
Definition: tool.py:85
def departAPI2(s, separator='.')
Split API call string based on separator "." 根据"."拆分API调用字符串
Definition: tool.py:340
def departAPI(s)
Split API call string based on parameter passing 根据参数传递拆分API调用字符串
Definition: tool.py:292
def getRunFile(runCommand)
Extract the .py script filename from a run command 从运行命令中提取.py脚本文件名
Definition: tool.py:799
def getLastAPIParameter(apiStr)
Get parameter(s) of the last API from the API call string 获取最后一个API参数
Definition: tool.py:262
def getRuntimePaths(workspace)
Return runtime artifact paths for the current execution 返回当前执行使用的运行产物路径
Definition: workspace.py:207