How can I replace all matches of variables programatically?

367 views
Skip to first unread message

Nico Rodsevich

unread,
Jan 26, 2019, 2:09:30 AM1/26/19
to Dart Analyzer Discussion
The name refactor thing is well know to all, but where is it defined and how does ot works?
I'm willing to replace all references to some variable "foo" with "variablesMap['foo']"

As far as I'm approaching, I did this:
/// A visitor that will be used to find all the references
class
VariableReferencesLocator extends GeneralizingAstVisitor {
 List<SimpleIdentifier> nodes = [];
 String name;

  VariableReferencesLocator(this.name);

  visitSimpleIdentifier(node) {
   if (node.name == this.name) {
     nodes.add(node);
   }
   super.visitSimpleIdentifier(node);
 }
}

String replaceAll(ExecutableElement elem){
   FunctionBody body = elem.computeNode().body;
   var code = StringBuffer();
   var sourceWriter = ToSourceVisitor2(code);
   for (var g in getters) {
     var referencesLocator = VariableReferencesLocator(g.name);
     body.accept(referencesLocator);
     referencesLocator.nodes.forEach((n) {
       IndexExpression replacement = _createReplacement(n);
       var replacer = NodeReplacer(n, replacement);
       body.accept(replacer);
     });
   }
   body.accept(sourceWriter);
   return code.toString();
 }


However, I don't know where to keep going after that when writing the _createReplacement function, how could I create StringTokens (it's not exported)? Am I doing all wrong?
IndexExpression _createReplacement(SimpleIdentifier n) {
   int offset = n.offset;
   Expression e1 = astFactory.simpleIdentifier(StringToken("variablesMap"));
   Token e2 = Token(TokenType.OPEN_SQUARE_BRACKET, offset++);
   Expression e3 = astFactory.simpleStringLiteral(StringToken(n.name), n.name);
   Token e4 = Token(TokenType.OPEN_SQUARE_BRACKET, offset++);
   return astFactory.indexExpressionForTarget(e1, e2, e3, e4);
 }

Thanks in advance

Brian Wilkerson

unread,
Jan 26, 2019, 1:06:45 PM1/26/19
to analyzer...@dartlang.org
The name refactor thing is well know to all, but where is it defined and how does ot works?

Refactoring support is defined in the 'analysis_server' package. That package is not published to pub, so you can't depend on it, but the source is publicly available. The rename refactoring is implemented by a number of different classes, depending on what kind of thing is being renamed. For example, local variables are renamed by `RenameLocalRefactoringImpl` and class members are renamed by `RenameClassMemberRefactoringImpl`.

All of the classes work in a similar way. They first locate all of the places that the name is being referenced (similar to what you're doing), then they build a sequence of edits that can be applied to the source code. We do it that way because of the way the analysis server works with clients (typically editors). Server never modifies source code, leaving that to the client, because the source code to be modified is often sitting in the client's edit buffer rather than being on disk.

But even if you're not writing this to work with the analysis server it might still be a good idea to model the changes as source edits rather than updates to the AST. There is no support for converting the edited AST back into source code that is similar to what the user wrote. Writing such support basically amounts to producing a diff of two AST structures and converting that into a sequence of edits, and we chose to skip the intermediate step of modifying the AST.

If you choose to go with creating edits directly we do have some support to make that easier. The package 'analyzer_plugin' (which is published) contains a class that makes it easier to build source edits (DartChangeBuilder). After it has built the edits, you can use SourceEdit.applySequence to apply those edits to the source code.

VariableReferencesLocator

The one other thing I'll point out is that you're currently matching based on name. That might work fine for your application, but in general such an approach won't work if a user has created multiple variables with the same name (there are typically a lot of commonly repeated names like `list`, `point`, or even `i`). The safe solution is to use the element. In a resolved AST every `SimpleIdentifier` will have a non-null `staticElement` (unless the name is undefined). That element uniquely represents the object to which the name was resolved. If you find the element of the variable whose references you want to change, then you can tell whether any given identifier is referencing that variable and change only the ones that ought to be changed.

In server code we can typically do this because the user has selected an identifier and we know the range of the selection. We can then use a NodeLocator2 to find a `SimpleIdentifier` whose element will be the element we need to search for.

--
You received this message because you are subscribed to the Google Groups "Dart Analyzer Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to analyzer-discu...@dartlang.org.
Visit this group at https://groups.google.com/a/dartlang.org/group/analyzer-discuss/.

Nico Rodsevich

unread,
Jan 26, 2019, 11:51:49 PM1/26/19
to Dart Analyzer Discussion
Thank you sooooooooo much for your help, I'm almost done thanks to it! :-D
The last thing I'm lacking is with this part:

The one other thing I'll point out is that you're currently matching based on name. That might work fine for your application, but in general such an approach won't work if a user has created multiple variables with the same name (there are typically a lot of commonly repeated names like `list`, `point`, or even `i`). The safe solution is to use the element. In a resolved AST every `SimpleIdentifier` will have a non-null `staticElement` (unless the name is undefined). That element uniquely represents the object to which the name was resolved. If you find the element of the variable whose references you want to change, then you can tell whether any given identifier is referencing that variable and change only the ones that ought to be changed.

I have sth similar to this code:
class VariableReferencesLocator extends GeneralizingAstVisitor {
  List<SimpleIdentifier> nodes = [];
  String name;

  VariableReferencesLocator(this.name);

  visitSimpleIdentifier(node) {
    if (node.name == this.name) {
      nodes.add(node);
    }
    super.visitSimpleIdentifier(node);
  }
}

MethodElement elem;
FieldElement getter;
MethodDeclaration method = elem.computeNode();
var referencesLocator = VariableReferencesLocator(getter.name);
method.accept(referencesLocator);
referencesLocator.nodes.forEach((ref) {
  if (ref.staticElement != (getter.computeNode() as VariableDeclaration).name.staticElement) {
    print("locally defined, ignore...");
  }else{
    print("FOUND!");
  }
});

but clearly I'm doing sth wrong. Could you give me a little last help with a bit more details on how to use that staticElement thing (if possible)?

Brian Wilkerson

unread,
Jan 27, 2019, 4:42:08 PM1/27/19
to analyzer...@dartlang.org
Glad I could help.

The `VariableReferencesLocator` looks good, but I'll suggest one possible improvement. If instead of passing in to the constructor the name of the field you want to rename you were to pass in the element for the field (which I'll call `targetElement`), and changed the condition inside `visitSimpleIdentifier` to compare elements (`node.staticElement == this.targetElement`), then it would only find references that you need to change, and you wouldn't need to filter them later. But as I said, I don't think there's a problem with the locator.

The problem is likely happening because of the arguably non-intuitive way we handle fields in analyzer.

As you know, in Dart every field induces a getter and, if it isn't final or const, a setter. Most references to a field are really invocations of the induced getter or setter. But users can also define getters and setters that are not backed by a field. And, there are a few place where fields can be referenced directly without becoming invocations of a getter or setter (such as in a constructor's initializer list). Similar semantics apply to top-level variables (but not to local variables).

Those semantics led us to defining two different classes of element: a `FieldElement` to represent the field itself, and a `PropertyAccessorElement` to represent a getter or setter. If you have a non-final field named `foo`, there will be three elements: a `FieldElement` for the field, a `PropertyAccessorElement` for the getter and a second `PropertyAccessorElement` for the setter.

When you're visiting an AST and you find an identifier that references a field, except in special cases like constructor initializer lists, we set the `staticElement` of the `SimpleIdentifier` to either the getter or the setter (depending on which of the two would be invoked at that point). The other special case, which is probably what's causing the problem, is that the `SimpleIdentifier` in the declaration of the field has it's `staticElement` set to the `FieldElement`.

So, looking at the code outside the locator:

FieldElement getter;

If you're interested in the getter (which I think you are), the element will be a `PropertyAccessorElement`. If you have a `FieldElement` (named `field`), you can access the getter via `field.correspondingGetter`.

On the other hand, if `getter` really is a `FieldElement`, then the expression

(getter.computeNode() as VariableDeclaration).name.staticElement

should always return the same element as `getter`. (The `name` of the declaration will have been associated with the `FieldElement` that is declared there.)

The last thing I'll point out is that `computeNode` is a very expensive operation. While it seems reasonable, on the surface, to start with the element model and then access the ASTs as you need them, it's far more efficient if you can structure your code to process ASTs and use the ASTs to access the element model.

Nico Rodsevich

unread,
Jan 28, 2019, 4:01:14 PM1/28/19
to analyzer...@dartlang.org
Thank you so much again, it's working like a charm ^_^
Reply all
Reply to author
Forward
0 new messages