'bug or feature? enter method of listener on labeled rule

After playing a bit with parse listeners I found a behaviour I didn't expect. My question to you is, am I wrong with my expectation and is this behaviour wanted or is it a bug? If the behaviour is wanted, please explain it.

Here the sample grammar:

grammar Labeled;

file: stmt;

stmt: stmt '+' stmt # Add
    | stmt '*' stmt # Mult
    | FLOAT         # Value
    | INTEGER       # Value
    ;

FLOAT: '-'? DIGIT* '.' DIGIT+;
INTEGER: '-'? DIGIT+;
COMMENT: (COMMENT_LINE | COMMENT_BLOCK) -> skip;
WS: [ \t\r\n] -> skip;

fragment
DIGIT: [0-9];
COMMENT_LINE: '//' ~'\n'*;
COMMENT_BLOCK: '/*' .*? '*/';`

Here the sample listener:

import org.antlr.v4.runtime.misc.NotNull;

import java.util.HashMap;
import java.util.Map;

public class TestListener extends LabeledBaseListener {

    public static final String ALL_KEY = "All";
    public static final String MULT_KEY = "Mult";
    public static final String ADD_KEY = "Add";
    public static final String VALUE_KEY = "Value";
    public static final String FILE_KEY = "File";


    public Map<String, Integer> enterValues = new HashMap<>();
    public Map<String, Integer> exitValues = new HashMap<>();

    @Override
    public void enterMult(@NotNull LabeledParser.MultContext ctx) {
        addEnter(ALL_KEY);
        addEnter(MULT_KEY);
    }

    @Override
    public void exitMult(@NotNull LabeledParser.MultContext ctx) {
        addExit(ALL_KEY);
        addExit(MULT_KEY);
    }

    @Override
    public void enterValue(@NotNull LabeledParser.ValueContext ctx) {
        addEnter(ALL_KEY);
        addEnter(VALUE_KEY);
    }

    @Override
    public void exitValue(@NotNull LabeledParser.ValueContext ctx) {
        addExit(ALL_KEY);
        addExit(VALUE_KEY);
    }

    @Override
    public void enterFile(@NotNull LabeledParser.FileContext ctx) {
        addEnter(ALL_KEY);
        addEnter(FILE_KEY);
    }

    @Override
    public void exitFile(@NotNull LabeledParser.FileContext ctx) {
        addExit(ALL_KEY);
        addExit(FILE_KEY);
    }

    @Override
    public void enterAdd(@NotNull LabeledParser.AddContext ctx) {
        addEnter(ALL_KEY);
        addEnter(ADD_KEY);
    }

    @Override
    public void exitAdd(@NotNull LabeledParser.AddContext ctx) {
        addExit(ALL_KEY);
        addExit(ADD_KEY);
    }

    // region map helper
    private static void addValue(Map<String, Integer> valueMap, String name) {
        if(valueMap.containsKey(name)) {
            valueMap.put(name, valueMap.get(name) + 1);
        } else {
            valueMap.put(name, 1);
        }
    }

    private void addEnter(String name) {
        addValue(enterValues, name);
    }

    private void addExit(String name) {
        addValue(exitValues, name);
    }
    // endregion
}

The main class:

import org.antlr.v4.runtime.ANTLRFileStream;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;

import java.io.File;
import java.io.IOException;
import java.util.Map;

public class Main {


    public static void main(String[] args) throws IOException {
        String filePath = args[0];
        ANTLRInputStream input = new ANTLRFileStream(filePath);
        LabeledLexer lexer = new LabeledLexer(input);
        CommonTokenStream token = new CommonTokenStream(lexer);
        LabeledParser parser = new LabeledParser(token);

        TestListener testListener = new TestListener();
        parser.addParseListener(testListener);

        parser.file();

        System.out.println("Enter Values:");
        System.out.println(getMapString(testListener.enterValues));

        System.out.println("Exit Values:");
        System.out.println(getMapString(testListener.exitValues));

        System.out.println("End");
    }

    private static String getMapString(Map<?, ?> map) {
        StringBuffer buffer = new StringBuffer();
        for(Map.Entry<?, ?> curEntry: map.entrySet()) {

            buffer.append("Key: " + curEntry.getKey() + "\tValue: " + curEntry.getValue() + "\n");
        }

        String result = buffer.toString();
        return result;
    }
}

Now when I execute with a file with content:

-4 + 8

The output will be:

Enter Values:
Key: File   Value: 1
Key: Add    Value: 1
Key: All    Value: 2

Exit Values:
Key: Value  Value: 2
Key: File   Value: 1
Key: Add    Value: 1
Key: All    Value: 4

End

But I expect this output:

Enter Values:
Key: Value  Value: 2
Key: File   Value: 1
Key: Add    Value: 1
Key: All    Value: 4

Exit Values:
Key: Value  Value: 2
Key: File   Value: 1
Key: Add    Value: 1
Key: All    Value: 4

End

So as you can see the enterValue() method is never called. After some more tests it seams that the enterXXX() method is not be called if there is only ONE Token/Rule in the rule's alternative.

Thanks in advance!



Solution 1:[1]

Yes, this behavior is expected (or at a minimum, is allowed). The behavior itself as well as the rationale for it are included in the documentation for the addParseListener method you used:

Parser.addParseListener(ParseTreeListener)

Solution 2:[2]

To make this work the way you expect it to, use the ParseTreeWalker directly. Something like:

ParseTree tree = parser.json();
jsonListener listener = new jsonListener();
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(listener, tree);

or in your case:

LabeledParser parser = new LabeledParser(token);
ParseTree tree = parser.file();

TestListener testListener = new TestListener();
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(testListener, tree);

This will result in enter methods being called for labeled rule alternatives.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Sam Harwell
Solution 2 Chris