Skip to content

Latest commit

 

History

History
420 lines (322 loc) · 11.5 KB

File metadata and controls

420 lines (322 loc) · 11.5 KB

How to write rules when you do not know the JavaParser AST node name

This guide helps you move from Java code to a formatter DSL rule when it is not clear what JavaParser calls the required construct or which fields it has.

Short algorithm

  1. Take the smallest possible Java code sample that contains the required construct.
  2. Parse it with JavaParser.
  3. Print the AST tree: node class names and their .toString() output.
  4. Find the node that corresponds to the required construct.
  5. Print the properties of this node.
  6. Use the node class name in the DSL pattern.
  7. Use property names as field names in the DSL.

Minimal code for finding the node name

Paste this code:

JavaParser parser = new JavaParser(
        new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_25)
);

String code = """
        class Sample {
            void run() {
                while (i < limit) {
                    i++;
                }
            }
        }
        """;

CompilationUnit compilationUnit = parser.parse(code)
        .getResult()
        .orElseThrow();

compilationUnit.findAll(Node.class).forEach(node -> {
    String source = node.toString().replace('\n', ' ');
    System.out.println(node.getClass().getSimpleName() + " -> " + source);
});

The output will be:

CompilationUnit -> class Sample {      void run() {         while (i < limit) {             i++;         }     } } 
ClassOrInterfaceDeclaration -> class Sample {      void run() {         while (i < limit) {             i++;         }     } }
SimpleName -> Sample
MethodDeclaration -> void run() {     while (i < limit) {         i++;     } }
SimpleName -> run
VoidType -> void
BlockStmt -> {     while (i < limit) {         i++;     } }
WhileStmt -> while (i < limit) {     i++; }
BinaryExpr -> i < limit
NameExpr -> i
SimpleName -> i
NameExpr -> limit
SimpleName -> limit
BlockStmt -> {     i++; }
ExpressionStmt -> i++;
UnaryExpr -> i++
NameExpr -> i
SimpleName -> i

So the JavaParser node name for while is:

WhileStmt

How to inspect node fields

Once the node name is found, you need to understand which fields can be used in the DSL. Add this to the previous code (example for WhileStmt):

Node node = compilationUnit.findFirst(WhileStmt.class).orElseThrow();

for (PropertyMetaModel property : node.getMetaModel().getAllPropertyMetaModels()) {
    String name = property.getName();

    if (List.of(
            "metaModel",            // description of the node model itself
            "range",                // position in the source file
            "tokenRange",           // token range
            "parsed",               // whether the node was produced by the parser or created programmatically
            "comment",              // comment attached to the node
            "orphanComments",       // comment that was near the node
            "allContainedComments", // all comments in the node subtree
            "childNodes",           // all child nodes without field names
            "parentNode"            // parent node
    ).contains(name)) {
        continue;
    }

    Object value = property.getValue(node);
    System.out.println(name + " -> " + value);
}

Output (fields for WhileStmt):

body -> {
    i++;
}
condition -> i < limit

How to write a DSL rule when you know the node name

If the JavaParser class is called:

WhileStmt

write this in the DSL pattern:

WhileStmt(...)

If the properties are called:

condition
body

write this in the DSL:

condition=<Expression>, body=<Statement>

Full example:

<Statement> ::= WhileStmt(condition=<Expression>, body=<Statement>)
  => "while" sp "(" <Expression> ")" sp <Statement>;

Lists and optional fields

If a property contains a list, the DSL uses:

statements=[<Statement>*]

Example:

<Statement> ::= BlockStmt(statements=[<Statement>*])
  => "{" nl indent join(<Statement>, nl) nl dedent "}";

If a property may or may not be present, use ? after the field name:

elseStmt?=<ElseStmt>

Example:

<Statement> ::= IfStmt(condition=<Expression>, thenStmt=<ThenStmt>, elseStmt?=<ElseStmt>)
  => "if" sp "(" <Expression> ")" <ThenStmt>
     ifpresent(ElseStmt, nl "else" <ElseStmt>);

Important: if two fields may contain different values, do not give their placeholders the same name

Bad:

<BinaryExpr> ::= BinaryExpr(left=<Expression>, right=<Expression>)
  => <Expression> sp "+" sp <Expression>;

This rule may conflict in bindings because the left and right expressions are different.

Better:

<BinaryExpr> ::= BinaryExpr(left=<LeftExpr>, right=<RightExpr>)
  => <LeftExpr> sp "+" sp <RightExpr>;

If a part of a construct is not a Node

Not all JavaParser properties are nodes. For example:

Modifier.keyword    -> FINAL
PrimitiveType.type  -> INT
BinaryExpr.operator -> PLUS
AssignExpr.operator -> PLUS
UnaryExpr.operator  -> PREFIX_INCREMENT

To understand exactly what is stored in such a field, print not only the value but also the Java class of this value, because the same textual value may mean different things in different JavaParser classes.

For example:

BinaryExpr.operator -> PLUS
AssignExpr.operator -> PLUS

The same name PLUS in different classes:

com.github.javaparser.ast.expr.BinaryExpr.Operator
com.github.javaparser.ast.expr.AssignExpr.Operator

has different meanings:

a + b   // BinaryExpr.Operator.PLUS
a += b  // AssignExpr.Operator.PLUS

The following set of functions is suitable for determining the names of non-Node constructs:

import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.metamodel.PropertyMetaModel;

import java.util.List;
import java.util.Optional;

private static void printNonNodeProperties(Node node) {
    for (PropertyMetaModel property : node.getMetaModel().getAllPropertyMetaModels()) {
        String name = property.getName();

        if (List.of(
                "metaModel",            // description of the node model itself
                "range",                // position in the source file
                "tokenRange",           // token range
                "parsed",               // whether the node was produced by the parser or created programmatically
                "comment",              // comment attached to the node
                "orphanComments",       // comment that was near the node
                "allContainedComments", // all comments in the node subtree
                "childNodes",           // all child nodes without field names
                "parentNode"            // parent node
        ).contains(name)) {
            continue;
        }

        printNonNodeValue(name, property.getValue(node));
    }
}

private static void printNonNodeValue(String name, Object value) {
    switch (value) {
        case null -> {
            return;
        }
        case Optional<?> optionalValue -> {
            optionalValue.ifPresent(innerValue -> printNonNodeValue(name, innerValue));
            return;
        }
        case NodeList<?> values -> {
            for (int i = 1; i <= values.size(); i++) {
                printNonNodeValue(name + "[" + i + "]", values.get(i - 1));
            }
            return;
        }
        case Node node -> {
            return;
        }
        default -> {
        }
    }

    Class<?> valueClass = value instanceof Enum<?> enumValue
            ? enumValue.getDeclaringClass()
            : value.getClass();
    
    String className = valueClass.getCanonicalName();
    // className may be unavailable for anonymous, lambda, or local classes
    if (className == null) {
        className = valueClass.getName().replace('$', '.');
    }

    String enumConstant = value instanceof Enum<?> enumValue
            ? "." + enumValue.name()
            : "";

    System.out.println(name + " -> " + className + enumConstant + " = " + value);
}

Usage example:

String code = "public class Sample{public int one(){if (a == b) return 1;}}";

CompilationUnit compilationUnit = StaticJavaParser.parse(code);
BinaryExpr binaryExpr = compilationUnit.findFirst(BinaryExpr.class).orElseThrow();
printNonNodeProperties(binaryExpr);

The output will be:

operator -> com.github.javaparser.ast.expr.BinaryExpr.Operator.EQUALS = EQUALS

Example of writing a rule for such constructs

Determine the name:

import com.github.javaparser.ast.Modifier;

String code = "final class Empty{}";

CompilationUnit compilationUnit = StaticJavaParser.parse(code);
Modifier modifier = compilationUnit.findFirst(Modifier.class).orElseThrow();
printNonNodeProperties(modifier);                                               // keyword -> com.github.javaparser.ast.Modifier.Keyword.FINAL = FINAL

In the DSL, this can still be moved into a placeholder:

<Modifier> ::= Modifier(keyword=<Keyword>)                 // writes keyword on a separate line
  => nl indent <Keyword> dedent nl;                        // with one-tab indentation

Summary

Quick template for a new rule

  1. Find the node in the AST:
SomeNode
  1. Inspect the properties of this node:
left     -> a
operator -> PLUS
right    -> b
  1. For each property, understand what it contains:
public class Example {
    void example(Node node) {
        for (PropertyMetaModel property : node.getMetaModel().getAllPropertyMetaModels()) {
            String name = property.getName();

            if (List.of(
                    "metaModel", "range", "tokenRange", "parsed",
                    "comment", "orphanComments", "allContainedComments",
                    "childNodes", "parentNode"
            ).contains(name)) {
                continue;
            }
            
            Object value = property.getValue(node);

            if (value == null) {
                System.out.println(name + " -> null");
            } else if (value instanceof Node childNode) {
                System.out.println(name
                        + " -> Node "
                        + childNode.getClass().getSimpleName()
                        + " = "
                        + childNode);
            } else {
                Class<?> valueClass = value instanceof Enum<?> enumValue
                        ? enumValue.getDeclaringClass()
                        : value.getClass();

                String className = valueClass.getCanonicalName();
                if (className == null) {
                    className = valueClass.getName().replace('$', '.');
                }

                String enumConstant = value instanceof Enum<?> enumValue
                        ? "." + enumValue.name()
                        : "";

                System.out.println(name
                        + " -> "
                        + className
                        + enumConstant
                        + " = "
                        + value);
            }
        }
    }
}
  1. For example, for BinaryExpr(a + b) the output will be:
left -> Node NameExpr = a
operator -> com.github.javaparser.ast.expr.BinaryExpr.Operator.PLUS = PLUS
right -> Node NameExpr = b
  1. If a property contains a Node, move it into a placeholder. If a property contains a non-Node value, for example an enum for operator, keyword, or type, it can also be moved into a placeholder.
  2. Example rule:
<SomeRule> ::= SomeNode(left=<LeftExpr>, operator=<Operator>, right=<RightExpr>)
  => <LeftExpr> sp <Operator> sp <RightExpr>;