javaeclipseplugins

Eclipse RenameParticipant offset handling


When I am writing code I include trace calls at the start of every method which looks like this:

public void doOperation()
{
  Trace tr = new Trace("doOperation");
  ... method body ...
}

I am trying to write an Eclipse plugin so that when I rename a method the string constant in the trace method call is also updated. To achieve this I am implementing a RenameParticipant.

The issue I have is that the Change I produce only works correctly when the method name doesn't change length. If the method name changes length then my change ends up editing the wrong offset within the changed file.

What am I doing wrong? How can I account for the fact that the method rename may alter the offset within the file of my Trace call?


To compute the Change I use the following code:

@Override
public Change createChange(IProgressMonitor pm)
  throws CoreException,
         OperationCanceledException
{
  ICompilationUnit unit = element.getCompilationUnit();
  CompilationUnit astCompUnit = parse(unit, pm);
  ASTNode astElement = NodeFinder.perform(astCompUnit, element.getNameRange());
  MethodDeclaration astMethod = (MethodDeclaration)getParent(astElement, MethodDeclaration.class);

  String newName = getArguments().getNewName();
  List<TraceFnFixOperation> ops = new ArrayList<TraceFnFixOperation>(1);
  TraceFnCtorFinder finder = new TraceFnCtorFinder(newName, ops);
  astMethod.accept(finder);

  if (ops.size() == 0)
    return null;

  return new TraceChange("Fix Trace", unit, ops);
}

The body of the TraceFnCtorFinder:

public static class TraceFnCtorFinder extends ASTVisitor
{
  private final String methodName;
  private final List<TraceFnFixOperation> workingops;

  public TraceFnCtorFinder(String methodName, List<TraceFnFixOperation> workingops)
  {
    this.methodName = methodName;
    this.workingops = workingops;
  }

  @Override
  public boolean visit(ClassInstanceCreation ctorClass)
  {
    Type type = ctorClass.getType();

    // Only examine simple types
    if (type.isSimpleType())
    {
      SimpleType simpleType = (SimpleType)type;
      String typeName = simpleType.getName().getFullyQualifiedName();

      // Check type has correct name
      if ("Trace".equals(typeName))
      {
        List<?> arguments = ctorClass.arguments();

        // Only check a single argument
        if ((arguments != null) &&
            (arguments.size() == 1))
        {
          Object arg = arguments.get(0);

          // Only check a string literal argument
          if (arg instanceof StringLiteral)
          {
            StringLiteral literal = (StringLiteral) arg;
            String currentArg = literal.getLiteralValue();

            // Check whether argument value is valid
            if (!methodName.equals(currentArg))
            {
              workingops.add(new TraceFnFixOperation(literal.getStartPosition(),
                                                     literal.getLength(),
                                                     methodName));
            }
          }
        }
      }
    }
    return false;
  }
}

The body of TraceChange:

public static class TraceChange extends CompilationUnitChange
{
  public TraceChange(String name,
                     ICompilationUnit cunit,
                     List<TraceFnFixOperation> ops)
  {
    super(name, cunit);

    MultiTextEdit multiTextEdit= new MultiTextEdit();
    setEdit(multiTextEdit);
    for (TraceFnFixOperation op : ops)
    {
      addEdit(new ReplaceEdit(op.startPosition,
                              op.length,
                              "\"" + op.methodName + "\""));
    }
  }
}

Solution

  • I was able to get my code working by using createPreChange(...). This allows me to return a change which is executed on the source BEFORE the main refactoring is performed. This means that the changes computed by my code are still accurate at the time when they are actually applied.

    http://help.eclipse.org/helios/index.jsp?topic=%2Forg.eclipse.platform.doc.isv%2Freference%2Fapi%2Forg%2Feclipse%2Fltk%2Fcore%2Frefactoring%2Fparticipants%2FRefactoringParticipant.html

    EDIT : Using createPreChange(...) was really just a workaround as my change could still conflict with another PreChange. I have come up with a better solution by going back to using createChange(...) and calling getTextChange(...) to get the existing text edits object and adding my edits to this object. This seems to make the offsets work correctly.

    public Change createChange(IProgressMonitor pm)
      throws CoreException,
             OperationCanceledException
    {
      ICompilationUnit unit = element.getCompilationUnit();
      TextChange change = getTextChange(unit);
    
      // Failed to find existing change to add our changes to
      if (change == null)
        return null;
    
      // Find the AST version of the method being changed
      CompilationUnit astCompUnit = parse(unit, pm);
      ASTNode astElement = NodeFinder.perform(astCompUnit, element.getNameRange());
      MethodDeclaration astMethod = (MethodDeclaration)getParent(astElement, MethodDeclaration.class);
    
      // Visit the contents of the method to find changes to make
      String newName = getArguments().getNewName();
      List<TraceFnFixOperation> ops = new ArrayList<TraceFnFixOperation>(1);
      TraceFnCtorFinder finder = new TraceFnCtorFinder(newName, ops);
      astMethod.accept(finder);
    
      // Add identified edits to the overall change
      for (TraceFnFixOperation op : ops)
      {
        change.addEdit(new ReplaceEdit(op.startPosition,
                                       op.length,
                                       "\"" + op.methodName + "\""));
      }
    
      // Don't return a dedicated change
      return null;
    }