def readRangeMap(filename, srcRoot):
    rangeMap = {}
    for line in file(filename).readlines():
        tokens = line.strip().split("|")
        if tokens[0] != "@": continue
        absFilename, startRangeStr, endRangeStr, expectedOldText, kind = tokens[1:6]
        filename = getProjectRelativePath(absFilename, srcRoot)
        startRange = int(startRangeStr)
        endRange = int(endRangeStr)
        info = tokens[6:]

        if not rangeMap.has_key(filename):
            rangeMap[filename] = []


        # Build unique identifier for symbol

        if kind == "package":
            packageName, forClass = info

            #key = "package "+packageName # ignore old name (unique identifier is filename)
            if forClass == "(file)":
                forClass = getTopLevelClassForFilename(filename)
            else:
                forClass = srglib.sourceName2Internal(forClass)  # . -> / 
   

            # 'forClass' is the class that is in this package; when the class is
            # remapped to a different package, this range should be updated
            key = "package "+forClass
        elif kind == "class":
            className, = info
            key = "class "+srglib.sourceName2Internal(className)
        elif kind == "field":
            className, fieldName = info
            key = "field "+srglib.sourceName2Internal(className)+"/"+fieldName
        elif kind == "method":
            if expectedOldText in ("super", "this"): continue # hack: avoid erroneously replacing super/this calls
            className, methodName, methodSignature = info
            key = "method "+srglib.sourceName2Internal(className)+"/"+methodName+" "+methodSignature
        elif kind == "param":
            className, methodName, methodSignature, parameterName, parameterIndex = info
            key = "param "+srglib.sourceName2Internal(className)+"/"+methodName+" "+methodSignature+" "+str(parameterIndex)  # ignore old name (positional)
        elif kind == "localvar":
            className, methodName, methodSignature, variableName, variableIndex = info
            key = "localvar "+srglib.sourceName2Internal(className)+"/"+methodName+" "+methodSignature+" "+str(variableIndex) # ignore old name (positional)
        else:
            assert False, "Unknown kind: "+kind

        # Map to range
        rangeMap[filename].append((startRange, endRange, expectedOldText, key))

    # Sort and check
    for filename in sorted(rangeMap.keys()):
        sortRangeList(rangeMap[filename], filename)

    return rangeMap
def readRangeMap(filename, srcRoot):
    rangeMap = {}
    for line in file(filename).readlines():
        tokens = line.strip().split("|")
        if tokens[0] != "@": continue
        absFilename, startRangeStr, endRangeStr, expectedOldText, kind = tokens[1:6]
        filename = getProjectRelativePath(absFilename, srcRoot)
        startRange = int(startRangeStr)
        endRange = int(endRangeStr)
        info = tokens[6:]

        if not rangeMap.has_key(filename):
            rangeMap[filename] = []


        # Build unique identifier for symbol

        if kind == "package":
            packageName, forClass = info

            #key = "package "+packageName # ignore old name (unique identifier is filename)
            if forClass == "(file)":
                forClass = getTopLevelClassForFilename(filename)
            else:
                forClass = srglib.sourceName2Internal(forClass)  # . -> / 
   

            # 'forClass' is the class that is in this package; when the class is
            # remapped to a different package, this range should be updated
            key = "package "+forClass
        elif kind == "class":
            className, = info
            key = "class "+srglib.sourceName2Internal(className)
        elif kind == "field":
            className, fieldName = info
            key = "field "+srglib.sourceName2Internal(className)+"/"+fieldName
        elif kind == "method":
            if expectedOldText in ("super", "this"): continue # hack: avoid erroneously replacing super/this calls
            className, methodName, methodSignature = info
            key = "method "+srglib.sourceName2Internal(className)+"/"+methodName+" "+methodSignature
        elif kind == "param":
            className, methodName, methodSignature, parameterName, parameterIndex = info
            key = "param "+srglib.sourceName2Internal(className)+"/"+methodName+" "+methodSignature+" "+str(parameterIndex)  # ignore old name (positional)
        elif kind == "localvar":
            className, methodName, methodSignature, variableName, variableIndex = info
            key = "localvar "+srglib.sourceName2Internal(className)+"/"+methodName+" "+methodSignature+" "+str(variableIndex) # ignore old name (positional)
        else:
            assert False, "Unknown kind: "+kind

        # Map to range
        rangeMap[filename].append((startRange, endRange, expectedOldText, key))

    # Sort and check
    for filename in sorted(rangeMap.keys()):
        sortRangeList(rangeMap[filename], filename)

    return rangeMap
def readLocalVariableMap(filename, renameMaps, invClassMap, invMethodMap, invMethodSigMap, srcRoot):
    for line in file(filename).readlines():
        tokens = line.strip().split("|")
        if tokens[0] != "@": continue
        absFilename, startRangeStr, endRangeStr, expectedOldText, kind = tokens[1:6]
        filename = getProjectRelativePath(absFilename, srcRoot)
        startRange = int(startRangeStr)
        endRange = int(endRangeStr)
        info = tokens[6:]

        if kind != "localvar": continue

        mcpClassName, mcpMethodName, mcpMethodSignature, variableName, variableIndex = info

        mcpClassName = srglib.sourceName2Internal(mcpClassName)

        # Range map has MCP names, but we need to map from CB

        if not invClassMap.has_key(mcpClassName):
            className = "net.minecraft.server." + srglib.splitBaseName(mcpClassName) # fake it, assuming CB mc-dev will choose similar name to MCP
            print "WARNING: readLocalVariableMap: no CB class name for MCP class name '%s', using %s" % (mcpClassName, className)
        else:
            className = invClassMap[mcpClassName]

        if mcpMethodName == "{}": 
            # Initializer - no name
            methodName = className + "/{}"
            methodSignature = ""
        elif srglib.splitBaseName(mcpClassName) == mcpMethodName:
            # Constructor - same name as class
            methodName = className + "/" + srglib.splitBaseName(className)
            methodSignature = srglib.remapSig(mcpMethodSignature, invClassMap)
        else:
            # Normal method
            key = mcpClassName+"/"+mcpMethodName+" "+mcpMethodSignature
            if not invMethodMap.has_key(key):
                print "NOTICE: local variables available for %s but no inverse method map; skipping" % (key,)
                # probably a changed signature
                continue

            methodName = invMethodMap[key]
            methodSignature = invMethodSigMap[key]

        key = "localvar "+methodName+" "+methodSignature+" "+str(variableIndex)

        renameMaps[key] = expectedOldText  # existing name
def readLocalVariableMap(filename, renameMaps, invClassMap, invMethodMap, invMethodSigMap, srcRoot):
    for line in file(filename).readlines():
        tokens = line.strip().split("|")
        if tokens[0] != "@": continue
        absFilename, startRangeStr, endRangeStr, expectedOldText, kind = tokens[1:6]
        filename = getProjectRelativePath(absFilename, srcRoot)
        startRange = int(startRangeStr)
        endRange = int(endRangeStr)
        info = tokens[6:]

        if kind != "localvar": continue

        mcpClassName, mcpMethodName, mcpMethodSignature, variableName, variableIndex = info

        mcpClassName = srglib.sourceName2Internal(mcpClassName)

        # Range map has MCP names, but we need to map from CB

        if not invClassMap.has_key(mcpClassName):
            className = "net.minecraft.server." + srglib.splitBaseName(mcpClassName) # fake it, assuming CB mc-dev will choose similar name to MCP
            print "WARNING: readLocalVariableMap: no CB class name for MCP class name '%s', using %s" % (mcpClassName, className)
        else:
            className = invClassMap[mcpClassName]

        if mcpMethodName == "{}": 
            # Initializer - no name
            methodName = className + "/{}"
            methodSignature = ""
        elif srglib.splitBaseName(mcpClassName) == mcpMethodName:
            # Constructor - same name as class
            methodName = className + "/" + srglib.splitBaseName(className)
            methodSignature = srglib.remapSig(mcpMethodSignature, invClassMap)
        else:
            # Normal method
            key = mcpClassName+"/"+mcpMethodName+" "+mcpMethodSignature
            if not invMethodMap.has_key(key):
                print "NOTICE: local variables available for %s but no inverse method map; skipping" % (key,)
                # probably a changed signature
                continue

            methodName = invMethodMap[key]
            methodSignature = invMethodSigMap[key]

        key = "localvar "+methodName+" "+methodSignature+" "+str(variableIndex)

        renameMaps[key] = expectedOldText  # existing name
def updateImports(data, newImports, importMap):
    lines = data.split("\n")
    # Parse the existing imports and find out where to add ours
    # This doesn't use Psi.. but the syntax is easy enough to parse here
    newLines = []
    addedNewImports = False

    newImportLines = []
    for imp in sorted(list(newImports)):
        newImportLines.append("import %s;" % (imp,))

    for i, line in enumerate(lines):
        if line.startswith("import net.minecraft"):
            # Add our new imports first, before existing NMS imports
            if not addedNewImports:
                print "Adding %s imports" % (len(newImportLines))
                newLines.extend(newImportLines)

                addedNewImports = True

            # If no import map, *remove* NMS imports (OBC rewritten with fully-qualified names)
            if len(importMap) == 0:
                continue

            # Rewrite NMS imports
            oldClass = line.replace("import ", "").replace(";", "")
            print oldClass
            if oldClass == "net.minecraft.server.*":
                # wildcard NMS imports (CraftWorld, CraftEntity, CraftPlayer).. bad idea
                continue
            else:
                newClass = importMap["class " + srglib.sourceName2Internal(oldClass)]

            newLine = "import %s;" % (newClass,)
            if newLine not in newImportLines:  # if not already added
                newLines.append(newLine)
        else:
            newLines.append(line)

    if not addedNewImports:
        newLines = newLines[0:2] + newImportLines + newLines[2:]

    return "\n".join(newLines)
def processJavaSourceFile(srcRoot, filename, rangeList, renameMap, importMap, shouldAnnotate, options):
    path = os.path.join(options.srcRoot, filename)
    data = file(path).read()

    if "\r" in data:
        # BlockJukebox is the only file with CRLF line endings in NMS.. and.. IntelliJ IDEA treats offsets 
        # as line endings being one character, whether LF or CR+LF. So remove the extraneous character or
        # offsets will be all off :.
        print "Warning: %s has CRLF line endings; consider switching to LF" % (filename,)
        data = data.replace("\r", "")

    importsToAdd = set()

    shift = 0

    # Existing package/class name (with package, internal) derived from filename
    oldTopLevelClassFullName = getTopLevelClassForFilename(filename)
    oldTopLevelClassPackage = srglib.splitPackageName(oldTopLevelClassFullName)
    oldTopLevelClassName = srglib.splitBaseName(oldTopLevelClassFullName)

    # New package/class name through mapping
    newTopLevelClassPackage = srglib.sourceName2Internal(renameMap.get("package "+oldTopLevelClassFullName))
    newTopLevelClassName = renameMap.get("class "+oldTopLevelClassFullName)
    if newTopLevelClassPackage is not None and newTopLevelClassName is None:
        assert False, "filename %s found package %s->%s but no class map for %s" % (filename, oldTopLevelClassPackage, newTopLevelClassPackage, newTopLevelClassName)
    if newTopLevelClassPackage is None and newTopLevelClassName is not None:
        assert False, "filename %s found class map %s->%s but no package map for %s" % (filename, oldTopLevelClassName, newTopLevelClassName, oldTopLevelClassPackage)

    for start,end,expectedOldText,key in rangeList:
        if renameMap.has_key(key) and len(renameMap[key]) == 0:
            # Replacing a symbol with no text = removing a symbol
            assert key.startswith("package "), "unable to remove non-package symbol %s" % (key,)
            # Remove that pesky extra period after qualified package names
            end += 1
            expectedOldText += "."

        oldName = data[start+shift:end+shift]

        assert oldName == expectedOldText, "Rename sanity check failed: expected '%s' at [%s,%s] (shifted %s to [%s,%s]) in %s, but found '%s'\nRegenerate symbol map on latest sources or start with fresh source and try again" % (
            expectedOldText, start, end, shift, start+shift, end+shift, filename, oldName)

        newName = getNewName(key, oldName, renameMap, shouldAnnotate)
        if newName is None:
            print "No rename for "+key
            continue

        print "Rename",key,[start+shift,end+shift],"::",oldName,"->",newName

        if importMap.has_key(key):
            # This rename requires adding an import, if it crosses packages
            importPackage = srglib.splitPackageName(srglib.sourceName2Internal(importMap[key]))
            if importPackage != newTopLevelClassPackage:
                importsToAdd.add(importMap[key])

        # Rename algorithm: 
        # 1. textually replace text at specified range with new text
        # 2. shift future ranges by difference in text length
        data = data[0:start+shift] + newName + data[end+shift:]
        shift += len(newName) - len(oldName)

    # Lastly, update imports - this is separate from symbol range manipulation above
    data = updateImports(data, importsToAdd, importMap)

    if options.rewriteFiles:
        print "Writing",filename
        file(path,"w").write(data)

    if options.renameFiles:
        if newTopLevelClassPackage is not None: # rename if package changed
            newFilename = os.path.join("src/main/java/", newTopLevelClassPackage, newTopLevelClassName + ".java")
            newPath = os.path.join(options.srcRoot, newFilename)

            print "Rename file",filename,"->",newFilename

            if filename != newFilename:
                # Create any missing directories in new path
                dirComponents = os.path.dirname(newPath).split(os.path.sep)
                for i in range(2,len(dirComponents)+1):
                    intermediateDir = os.path.sep.join(dirComponents[0:i])
                    if not os.path.exists(intermediateDir):
                        os.mkdir(intermediateDir)

                #os.rename(path, newPath)

                wd = os.getcwd()
                os.chdir(options.srcRoot)
                cmd = "git mv '%s' '%s'" % (filename, newFilename)  # warning: if filename contains quotes..
                status = os.system(cmd)
                assert status == 0, "Failed to execute rename command: %s" % (cmd,)
                os.chdir(wd)
def updateImports(data, newImports, importMap):
    lines = data.split("\n")
    # Parse the existing imports and find out where to add ours
    # This doesn't use Psi.. but the syntax is easy enough to parse here
    newLines = []
    addedNewImports = False

    newImportLines = []
    for imp in sorted(list(newImports)):
        newImportLines.append("import %s;" % (imp,))

    sawImports = False

    for i, line in enumerate(lines):
        if line.startswith("import "):
            sawImports = True

            if line.startswith("import net.minecraft."):
                # If no import map, *remove* NMS imports (OBC rewritten with fully-qualified names)
                if len(importMap) == 0:
                    continue

                # Rewrite NMS imports
                oldClass = line.replace("import ", "").replace(";", "");
                print oldClass
                if oldClass == "net.minecraft.server.*":
                    # wildcard NMS imports (CraftWorld, CraftEntity, CraftPlayer).. bad idea
                    continue
                else:
                    newClass = importMap["class "+srglib.sourceName2Internal(oldClass)]

                newLine = "import %s;" % (newClass,)
                if newLine not in newImportLines:  # if not already added
                    newLines.append(newLine)
            else:
                newLines.append(line)
        else:
            if sawImports and not addedNewImports:
                # Add our new imports right after the last import
                print "Adding %s imports" % (len(newImportLines,))
                newLines.extend(newImportLines)

                addedNewImports = True


            newLines.append(line)

    if not addedNewImports:
        newLines = newLines[0:2] + newImportLines + newLines[2:]

    newData = "\n".join(newLines)

    # Warning: ugly hack ahead
    # The symbol range map extractor is supposed to emit package reference ranges, which we can 
    # update with the correct new package names. However, it has a bug where the package ranges
    # are not always emitted on fully-qualified names. For example: (net.minecraft.server.X)Y - a
    # cast - will fail to recognize the net.minecraft.server package, so it won't be processed by us.
    # This leads to some qualified names in the original source to becoming "overqualified", that is,
    # net.minecraft.server.net.minecraft.X; the NMS class is replaced with its fully-qualified name
    # (in non-NMS source, where we want it to always be fully-qualified): original package name isn't replaced.
    # Occurs in OBC source which uses fully-qualified NMS names already, and NMS source which (unnecessarily)
    # uses fully-qualified NMS names, too. Attempted to fix this problem for longer than I should.. 
    # maybe someone smarter can figure it out -- but until then, in the interest of expediency, I present 
    # this ugly workaround, replacing the overqualified names after-the-fact.
    # Fortunately, this pattern is easy enough to reliably detect and replace textually!
    newData = newData.replace("net.minecraft.server.net", "net")  # OBC overqualified symbols
    newData = newData.replace("net.minecraft.server.Block", "Block") # NMS overqualified symbols
    # ..and qualified inner classes, only one.... last ugly hack, I promise :P
    newData = newData.replace("net.minecraft.block.BlockSapling/*was:BlockSapling*/.net.minecraft.block.BlockSapling.TreeGenerator", "net.minecraft.block.BlockSapling.TreeGenerator")
    newData = newData.replace("net.minecraft.block.BlockSapling.net.minecraft.block.BlockSapling.TreeGenerator", "net.minecraft.block.BlockSapling.TreeGenerator")

    return newData
def processJavaSourceFile(srcRoot, filename, rangeList, renameMap, importMap, shouldAnnotate, options):
    path = os.path.join(options.srcRoot, filename)
    data = file(path).read()

    if "\r" in data:
        # BlockJukebox is the only file with CRLF line endings in NMS.. and.. IntelliJ IDEA treats offsets 
        # as line endings being one character, whether LF or CR+LF. So remove the extraneous character or
        # offsets will be all off :.
        print "Warning: %s has CRLF line endings; consider switching to LF" % (filename,)
        data = data.replace("\r", "")

    importsToAdd = set()

    shift = 0

    # Existing package/class name (with package, internal) derived from filename
    oldTopLevelClassFullName = getTopLevelClassForFilename(filename)
    oldTopLevelClassPackage = srglib.splitPackageName(oldTopLevelClassFullName)
    oldTopLevelClassName = srglib.splitBaseName(oldTopLevelClassFullName)

    # New package/class name through mapping
    newTopLevelClassPackage = srglib.sourceName2Internal(renameMap.get("package "+oldTopLevelClassFullName))
    newTopLevelClassName = renameMap.get("class "+oldTopLevelClassFullName)
    if newTopLevelClassPackage is not None and newTopLevelClassName is None:
        assert False, "filename %s found package %s->%s but no class map for %s" % (filename, oldTopLevelClassPackage, newTopLevelClassPackage, newTopLevelClassName)
    if newTopLevelClassPackage is None and newTopLevelClassName is not None:
        assert False, "filename %s found class map %s->%s but no package map for %s" % (filename, oldTopLevelClassName, newTopLevelClassName, oldTopLevelClassPackage)

    for start,end,expectedOldText,key in rangeList:
        if renameMap.has_key(key) and len(renameMap[key]) == 0:
            # Replacing a symbol with no text = removing a symbol
            assert key.startswith("package "), "unable to remove non-package symbol %s" % (key,)
            # Remove that pesky extra period after qualified package names
            end += 1
            expectedOldText += "."

        oldName = data[start+shift:end+shift]

        assert oldName == expectedOldText, "Rename sanity check failed: expected '%s' at [%s,%s] (shifted %s to [%s,%s]) in %s, but found '%s'\nRegenerate symbol map on latest sources or start with fresh source and try again" % (
            expectedOldText, start, end, shift, start+shift, end+shift, filename, oldName)

        newName = getNewName(key, oldName, renameMap, shouldAnnotate)
        if newName is None:
            print "No rename for "+key
            continue

        print "Rename",key,[start+shift,end+shift],"::",oldName,"->",newName

        if importMap.has_key(key):
            # This rename requires adding an import, if it crosses packages
            importPackage = srglib.splitPackageName(srglib.sourceName2Internal(importMap[key]))
            if importPackage != newTopLevelClassPackage:
                importsToAdd.add(importMap[key])

        # Rename algorithm: 
        # 1. textually replace text at specified range with new text
        # 2. shift future ranges by difference in text length
        data = data[0:start+shift] + newName + data[end+shift:]
        shift += len(newName) - len(oldName)

    # Lastly, update imports - this is separate from symbol range manipulation above
    data = updateImports(data, importsToAdd, importMap)

    if options.rewriteFiles:
        print "Writing",filename
        file(path,"w").write(data)

    if options.renameFiles:
        if newTopLevelClassPackage is not None: # rename if package changed
            newFilename = os.path.join("src/main/java/", newTopLevelClassPackage, newTopLevelClassName + ".java")
            newPath = os.path.join(options.srcRoot, newFilename)

            print "Rename file",filename,"->",newFilename

            if filename != newFilename:
                # Create any missing directories in new path
                dirComponents = os.path.dirname(newPath).split(SEP)
                for i in range(2,len(dirComponents)+1):
                    intermediateDir = os.path.sep.join(dirComponents[0:i])
                    if not os.path.exists(intermediateDir):
                        os.mkdir(intermediateDir)

                #os.rename(path, newPath)

                wd = os.getcwd()
                os.chdir(options.srcRoot)
                cmd = "%s mv '%s' '%s'" % (options.git, filename, newFilename)  # warning: if filename contains quotes..
                status = os.system(cmd)
                assert status == 0, "Failed to execute rename command: %s" % (cmd,)
                os.chdir(wd)
def updateImports(data, newImports, importMap):
    lines = data.split("\n")
    # Parse the existing imports and find out where to add ours
    # This doesn't use Psi.. but the syntax is easy enough to parse here
    newLines = []
    addedNewImports = False

    newImportLines = []
    for imp in sorted(list(newImports)):
        newImportLines.append("import %s;" % (imp,))

    sawImports = False

    for i, line in enumerate(lines):
        if line.startswith("import "):
            sawImports = True

            if line.startswith("import net.minecraft."):
                # If no import map, *remove* NMS imports (OBC rewritten with fully-qualified names)
                if len(importMap) == 0:
                    continue

                # Rewrite NMS imports
                oldClass = line.replace("import ", "").replace(";", "");
                print oldClass
                if oldClass == "net.minecraft.server.*":
                    # wildcard NMS imports (CraftWorld, CraftEntity, CraftPlayer).. bad idea
                    continue
                else:
                    newClass = importMap["class "+srglib.sourceName2Internal(oldClass)]

                newLine = "import %s;" % (newClass,)
                if newLine not in newImportLines:  # if not already added
                    newLines.append(newLine)
            else:
                newLines.append(line)
        else:
            if sawImports and not addedNewImports:
                # Add our new imports right after the last import
                print "Adding %s imports" % (len(newImportLines,))
                newLines.extend(newImportLines)

                addedNewImports = True


            newLines.append(line)

    if not addedNewImports:
        newLines = newLines[0:2] + newImportLines + newLines[2:]

    newData = "\n".join(newLines)

    # Warning: ugly hack ahead
    # The symbol range map extractor is supposed to emit package reference ranges, which we can 
    # update with the correct new package names. However, it has a bug where the package ranges
    # are not always emitted on fully-qualified names. For example: (net.minecraft.server.X)Y - a
    # cast - will fail to recognize the net.minecraft.server package, so it won't be processed by us.
    # This leads to some qualified names in the original source to becoming "overqualified", that is,
    # net.minecraft.server.net.minecraft.X; the NMS class is replaced with its fully-qualified name
    # (in non-NMS source, where we want it to always be fully-qualified): original package name isn't replaced.
    # Occurs in OBC source which uses fully-qualified NMS names already, and NMS source which (unnecessarily)
    # uses fully-qualified NMS names, too. Attempted to fix this problem for longer than I should.. 
    # maybe someone smarter can figure it out -- but until then, in the interest of expediency, I present 
    # this ugly workaround, replacing the overqualified names after-the-fact.
    # Fortunately, this pattern is easy enough to reliably detect and replace textually!
    newData = newData.replace("net.minecraft.server.net", "net")  # OBC overqualified symbols
    newData = newData.replace("net.minecraft.server.Block", "Block") # NMS overqualified symbols
    # ..and qualified inner classes, only one.... last ugly hack, I promise :P
    newData = newData.replace("net.minecraft.block.BlockSapling/*was:BlockSapling*/.net.minecraft.block.BlockSapling.TreeGenerator", "net.minecraft.block.BlockSapling.TreeGenerator")
    newData = newData.replace("net.minecraft.block.BlockSapling.net.minecraft.block.BlockSapling.TreeGenerator", "net.minecraft.block.BlockSapling.TreeGenerator")

    return newData
def processJavaSourceFile(filename, rangeList, renameMap, importMap, renameLocalVars, shouldAnnotate):
    path = os.path.join(srcRoot, filename)
    data = file(path).read()

    if "\r" in data:
        # BlockJukebox is the only file with CRLF line endings in NMS.. and.. IntelliJ IDEA treats offsets
        # as line endings being one character, whether LF or CR+LF. So remove the extraneous character or
        # offsets will be all off :.
        print "Warning: %s has CRLF line endings; consider switching to LF" % (filename,)
        data = data.replace("\r", "")

    importsToAdd = set()

    shift = 0

    # Existing package/class name (with package, internal) derived from filename
    oldTopLevelClassFullName = getTopLevelClassForFilename(filename)
    oldTopLevelClassPackage = srglib.splitPackageName(oldTopLevelClassFullName)
    oldTopLevelClassName = srglib.splitBaseName(oldTopLevelClassFullName)

    # New package/class name through mapping
    newTopLevelClassPackage = srglib.sourceName2Internal(renameMap.get("package " + oldTopLevelClassFullName))
    newTopLevelClassName = renameMap.get("class " + oldTopLevelClassFullName)
    if newTopLevelClassPackage is not None and newTopLevelClassName is None:
        assert False, "filename %s found package %s->%s but no class map for %s" % (
            filename,
            oldTopLevelClassPackage,
            newTopLevelClassPackage,
            newTopLevelClassName,
        )
    if newTopLevelClassPackage is None and newTopLevelClassName is not None:
        assert False, "filename %s found class map %s->%s but no package map for %s" % (
            filename,
            oldTopLevelClassName,
            newTopLevelClassName,
            oldTopLevelClassPackage,
        )

    for start, end, expectedOldText, key in rangeList:
        if renameMap.has_key(key) and len(renameMap[key]) == 0:
            # Replacing a symbol with no text = removing a symbol
            assert key.startswith("package "), "unable to remove non-package symbol %s" % (key,)
            # Remove that pesky extra period after qualified package names
            end += 1
            expectedOldText += "."

        if renameLocalVars == False and key.startswith("localvar"):
            # non-NMS, no need to rename local variables
            continue

        oldName = data[start + shift : end + shift]

        if oldName != expectedOldText:
            print "Rename sanity check failed: expected '%s' at [%s,%s] (shifted %s to [%s,%s]) in %s, but found '%s'" % (
                expectedOldText,
                start,
                end,
                shift,
                start + shift,
                end + shift,
                filename,
                oldName,
            )
            print "Regenerate symbol map on latest sources or start with fresh source and try again"
            # file("/tmp/a","w").write(data)
            raise SystemExit

        newName = getNewName(key, oldName, renameMap, shouldAnnotate)
        if newName is None:
            print "No rename for " + key
            continue

        print "Rename", key, [start + shift, end + shift], "::", oldName, "->", newName

        if importMap.has_key(key):
            # this rename requires adding an import
            importsToAdd.add(importMap[key])

        # Rename algorithm:
        # 1. textually replace text at specified range with new text
        # 2. shift future ranges by difference in text length
        data = data[0 : start + shift] + newName + data[end + shift :]
        shift += len(newName) - len(oldName)

    # Lastly, update imports - this is separate from symbol range manipulation above
    data = updateImports(data, importsToAdd, importMap)

    if rewriteFiles:
        print "Writing", filename
        file(path, "w").write(data)

    if renameFiles:
        if newTopLevelClassPackage is not None:  # rename if package changed
            newFilename = os.path.join(
                srcRoot, "src/main/java/", newTopLevelClassPackage, newTopLevelClassName + ".java"
            )
            newPath = os.path.join(srcRoot, newFilename)

            print "Rename file", filename, "->", newFilename
            srglib.rename_path(path, newPath)