import re
import compiler
from bike.parsing import visitor
from bike.query.common import getScopeForLine
from bike.parsing.parserutils import generateLogicalLines,\
makeLineParseable, maskStringsAndRemoveComments
from parser import ParserError
from bike.parsing.fastparserast import Class
from bike.transformer.undo import getUndoStack
from bike.refactor.utils import getTabWidthOfLine,getLineSeperator,\
reverseCoordsIfWrongWayRound
from bike.transformer.save import queueFileToSave
from bike.parsing.load import getSourceNode
TABSIZE = 4
class coords:
def __init__(self, line, column):
self.column = column
self.line = line
def __str__(self):
return "("+str(self.column)+","+str(self.line)+")"
commentRE = re.compile(r"#.*?$")
class ParserException(Exception): pass
def extractMethod(filename, startcoords, endcoords, newname):
ExtractMethod(getSourceNode(filename),
startcoords, endcoords, newname).execute()
class ExtractMethod(object):
def __init__(self,sourcenode, startcoords, endcoords, newname):
self.sourcenode = sourcenode
startcoords, endcoords = \
reverseCoordsIfWrongWayRound(startcoords,endcoords)
self.startline = startcoords.line
self.endline = endcoords.line
self.startcol = startcoords.column
self.endcol= endcoords.column
self.newfn = NewFunction(newname)
self.getLineSeperator()
self.adjustStartColumnIfLessThanTabwidth()
self.adjustEndColumnIfStartsANewLine()
self.fn = self.getFunctionObject()
self.getRegionToBuffer()
#print "-"*80
#print self.extractedLines
#print "-"*80
self.deduceIfIsMethodOrFunction()
def execute(self):
self.deduceArguments()
getUndoStack().addSource(self.sourcenode.filename,
self.sourcenode.getSource())
srclines = self.sourcenode.getLines()
newFnInsertPosition = self.fn.getEndLine()-1
self.insertNewFunctionIntoSrcLines(srclines, self.newfn,
newFnInsertPosition)
self.writeCallToNewFunction(srclines)
src = "".join(srclines)
queueFileToSave(self.sourcenode.filename,src)
def getLineSeperator(self):
line = self.sourcenode.getLines()[self.startline-1]
linesep = getLineSeperator(line)
self.linesep = linesep
def adjustStartColumnIfLessThanTabwidth(self):
tabwidth = getTabWidthOfLine(self.sourcenode.getLines()[self.startline-1])
if self.startcol < tabwidth: self.startcol = tabwidth
def adjustEndColumnIfStartsANewLine(self):
if self.endcol == 0:
self.endline -=1
nlSize = len(self.linesep)
self.endcol = len(self.sourcenode.getLines()[self.endline-1])-nlSize
def getFunctionObject(self):
return getScopeForLine(self.sourcenode,self.startline)
def getTabwidthOfParentFunction(self):
line = self.sourcenode.getLines()[self.fn.getStartLine()-1]
match = re.match("\s+",line)
if match is None:
return 0
else:
return match.end(0)
# should be in the transformer module
def insertNewFunctionIntoSrcLines(self,srclines,newfn,insertpos):
tabwidth = self.getTabwidthOfParentFunction()
while re.match("\s*"+self.linesep,srclines[insertpos-1]):
insertpos -= 1
srclines.insert(insertpos, self.linesep)
insertpos +=1
fndefn = "def "+newfn.name+"("
if self.isAMethod:
fndefn += "self"
if newfn.args != []:
fndefn += ", "+", ".join(newfn.args)
else:
fndefn += ", ".join(newfn.args)
fndefn += "):"+self.linesep
srclines.insert(insertpos,tabwidth*" "+fndefn)
insertpos +=1
tabwidth += TABSIZE
if self.extractedCodeIsAnExpression(srclines):
assert len(self.extractedLines) == 1
fnbody = [tabwidth*" "+ "return "+self.extractedLines[0]]
else:
fnbody = [tabwidth*" "+line for line in self.extractedLines]
if newfn.retvals != []:
fnbody.append(tabwidth*" "+"return "+
", ".join(newfn.retvals) + self.linesep)
for line in fnbody:
srclines.insert(insertpos,line)
insertpos +=1
def writeCallToNewFunction(self, srclines):
startline = self.startline
endline = self.endline
startcol = self.startcol
endcol= self.endcol
fncall = self.constructFunctionCallString(self.newfn.name, self.newfn.args,
self.newfn.retvals)
self.replaceCodeWithFunctionCall(srclines, fncall,
startline, endline, startcol, endcol)
def replaceCodeWithFunctionCall(self, srclines, fncall,
startline, endline, startcol, endcol):
if startline == endline: # i.e. extracted code part of existing line
line = srclines[startline-1]
srclines[startline-1] = self.replaceSectionOfLineWithFunctionCall(line,
startcol, endcol, fncall)
else:
self.replaceLinesWithFunctionCall(srclines, startline, endline, fncall)
def replaceLinesWithFunctionCall(self, srclines, startline, endline, fncall):
tabwidth = getTabWidthOfLine(srclines[startline-1])
line = tabwidth*" " + fncall + self.linesep
srclines[startline-1:endline] = [line]
def replaceSectionOfLineWithFunctionCall(self, line, startcol, endcol, fncall):
line = line[:startcol] + fncall + line[endcol:]
if not line.endswith(self.linesep):
line+=self.linesep
return line
def constructFunctionCallString(self, fnname, fnargs, retvals):
fncall = fnname + "("+", ".join(fnargs)+")"
if self.isAMethod:
fncall = "self." + fncall
if retvals != []:
fncall = ", ".join(retvals) + " = "+fncall
return fncall
def deduceArguments(self):
lines = self.fn.getLinesNotIncludingThoseBelongingToChildScopes()
# strip off comments
lines = [commentRE.sub(self.linesep,line) for line in lines]
extractedLines = maskStringsAndRemoveComments("".join(self.extractedLines)).splitlines(1)
linesbefore = lines[:(self.startline - self.fn.getStartLine())]
linesafter = lines[(self.endline - self.fn.getStartLine()) + 1:]
# split into logical lines
linesbefore = [line for line in generateLogicalLines(linesbefore)]
extractedLines = [line for line in generateLogicalLines(extractedLines)]
linesafter = [line for line in generateLogicalLines(linesafter)]
if self.startline == self.endline:
# need to include the line code is extracted from
line = generateLogicalLines(lines[self.startline - self.fn.getStartLine():]).next()
linesbefore.append(line[:self.startcol] + "dummyFn()" + line[self.endcol:])
assigns = getAssignments(linesbefore)
fnargs = getFunctionArgs(linesbefore)
candidateArgs = assigns + fnargs
refs = getVariableReferencesInLines(extractedLines)
self.newfn.args = [ref for ref in refs if ref in candidateArgs]
assignsInExtractedBlock = getAssignments(extractedLines)
usesAfterNewFunctionCall = getVariableReferencesInLines(linesafter)
usesInPreceedingLoop = getVariableReferencesInLines(
self.getPreceedingLinesInLoop(linesbefore,line))
self.newfn.retvals = [ref for ref in usesInPreceedingLoop+usesAfterNewFunctionCall
if ref in assignsInExtractedBlock]
def getPreceedingLinesInLoop(self,linesbefore,firstLineToExtract):
if linesbefore == []: return []
tabwidth = getTabWidthOfLine(firstLineToExtract)
rootTabwidth = getTabWidthOfLine(linesbefore[0])
llines = [line for line in generateLogicalLines(linesbefore)]
startpos = len(llines)-1
loopTabwidth = tabwidth
for idx in range(startpos,0,-1):
line = llines[idx]
if re.match("(\s+)for",line) is not None or \
re.match("(\s+)while",line) is not None:
candidateLoopTabwidth = getTabWidthOfLine(line)
if candidateLoopTabwidth < loopTabwidth:
startpos = idx
return llines[startpos:]
def getRegionToBuffer(self):
startline = self.startline
endline = self.endline
startcol = self.startcol
endcol= self.endcol
self.extractedLines = self.sourcenode.getLines()[startline-1:endline]
match = re.match("\s*",self.extractedLines[0])
tabwidth = match.end(0)
self.extractedLines = [line[startcol:] for line in self.extractedLines]
# above cropping can take a blank line's newline off.
# this puts it back
for idx in range(len(self.extractedLines)):
if self.extractedLines[idx] == '':
self.extractedLines[idx] = self.linesep
if startline == endline:
# need to crop the end
# (n.b. if region is multiple lines, then whole lines are taken)
self.extractedLines[-1] = self.extractedLines[-1][:endcol-startcol]
if self.extractedLines[-1][-1] != '\n':
self.extractedLines[-1] += self.linesep
def extractedCodeIsAnExpression(self,lines):
if len(self.extractedLines) == 1:
charsBeforeSelection = lines[self.startline-1][:self.startcol]
if re.match("^\s*$",charsBeforeSelection) is not None:
return 0
if re.search(":\s*$",charsBeforeSelection) is not None:
return 0
return 1
return 0
def deduceIfIsMethodOrFunction(self):
if isinstance(self.fn.getParent(),Class):
self.isAMethod = 1
else:
self.isAMethod = 0
# holds information about the new function
class NewFunction:
def __init__(self,name):
self.name = name
# lines = list of lines.
# Have to have strings masked and comments removed
def getAssignments(lines):
class AssignVisitor:
def __init__(self):
self.assigns = []
def visitAssTuple(self, node):
for a in node.nodes:
if a.name not in self.assigns:
self.assigns.append(a.name)
def visitAssName(self, node):
if node.name not in self.assigns:
self.assigns.append(node.name)
def visitAugAssign(self, node):
if isinstance(node.node, compiler.ast.Name):
if node.node.name not in self.assigns:
self.assigns.append(node.node.name)
assignfinder = AssignVisitor()
for line in lines:
doctoredline = makeLineParseable(line)
try:
ast = compiler.parse(doctoredline)
except ParserError:
raise ParserException("couldnt parse:"+doctoredline)
visitor.walk(ast, assignfinder)
return assignfinder.assigns
# lines = list of lines.
# Have to have strings masked and comments removed
def getFunctionArgs(lines):
if lines == []: return []
class FunctionVisitor:
def __init__(self):
self.result = []
def visitFunction(self, node):
for n in node.argnames:
if n != "self":
self.result.append(n)
fndef = generateLogicalLines(lines).next()
doctoredline = makeLineParseable(fndef)
try:
ast = compiler.parse(doctoredline)
except ParserError:
raise ParserException("couldnt parse:"+doctoredline)
return visitor.walk(ast, FunctionVisitor()).result
# lines = list of lines. Have to have strings masked and comments removed
def getVariableReferencesInLines(lines):
class NameVisitor:
def __init__(self):
self.result = []
def visitName(self, node):
if node.name not in self.result:
self.result.append(node.name)
reffinder = NameVisitor()
for line in lines:
doctoredline = makeLineParseable(line)
try:
ast = compiler.parse(doctoredline)
except ParserError:
raise ParserException("couldnt parse:"+doctoredline)
visitor.walk(ast, reffinder)
return reffinder.result
|