ANTLR4 - VisitChildren returns null,即使 child returns 一些 object

ANTLR4 - VisitChildren returns null, even when child returns some object

我一直在尝试实现访问者模式以将一些特定的 SQL 语句解析为由 TableDefinition 和 ColumnDefinition object 组成的内部 object 结构。

这是语法中的一小部分(精简):

column_definition
 : column_name datatype? column_constraint*
 ;
 
column_constraint
 : ( K_CONSTRAINT name )?
   ( K_PRIMARY K_KEY ( K_CLUSTERED | K_NONCLUSTERED )? ( K_ASC | K_DESC )? K_AUTOINCREMENT?
   | K_AUTOINCREMENT
   | K_NOT? K_NULL
   )
 ;

datatype
 : K_CHAR ( '(' unsigned_integer ')' )?                                     #char
 | K_DATE                                                                   #date
 ;

这是派生的 BaseVisitors 之一,意在 return ColumnDefinitions:

namespace SqlParser.Visitor
{
    public class DataTypeVisitor: SqlAnywhereParserBaseVisitor<ColumnDefinition>
    {
        public override ColumnDefinition VisitColumn_definition([NotNull] SqlAnywhereParser.Column_definitionContext context)
        {
            var res = VisitChildren(context);
            var constraint = (SqlAnywhereParser.Column_constraintContext[])context.column_constraint();

            if (res != null) // Add NULL attributes
            {
                if (constraint.Any(c => c.K_NULL() != null && c.K_NOT() == null))
                    res.IsNullable = true;

                if (constraint.Any(c => c.K_NULL() != null && c.K_NOT() != null))
                    res.IsNullable = false;
            }

            return res;
        }

        public override ColumnDefinition VisitChar([NotNull] SqlAnywhereParser.CharContext context)
        {
            return new ColumnDefinition()
            {
                DataType = DbType.StringFixedLength,
                Length = int.Parse(context.unsigned_integer()?.GetText() ?? "1") 
            };
        }
   }
}

当我调试进程时,我可以观察到对 VisitChildren 的调用如何进入 VisitChar,其中 return 是一个 ColumnDefinition object。当 VisitChar 完成并且光标跳回以在 VisitColumn_definition 中继续时,变量 res 为空。

我是不是漏掉了一些重要的东西,还是我误解了访问者模式? 在尝试 VisitChildren 之前,我曾经使用 base.VisitColumn_definition(context) 调用,它基本上只调用 VisitChildren。

有没有人提示我犯了哪些错误?为什么我在 VisitChar 叶中创建的 ColumnDefinition 结果没有冒泡?

下面是我的测试输入:

CREATE TABLE "DBA"."pbcattbl" (
    "pbt_tnam"                       char(129) NOT NULL
   ,"pbt_tid"                        char(5) NULL
);

您必须覆盖所有 Visit...(... context) 调用(至少是您的解析树中包含的所有调用)。假设你有这个语法:

grammar T;

parse
 : expr EOF
 ;

expr
 : expr ( '*' | '/' ) expr #multExpr
 | expr ( '+' | '-' ) expr #addExpr
 | NUMBER                  #numberExpr
 ;
 
NUMBER
 : [0-9]+ ( '.' [0-9]+ )?
 ;

SPACES
 : [ \t\r\n]+ -> skip
 ;

并且您正在解析表达式 "42"。那么仅覆盖方法 VisitNumberExpr(TParser.NumberExprContext context):

是不够的
using System;
using Antlr4.Runtime;

namespace AntlrTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var lexer = new TLexer(CharStreams.fromstring("42"));
            var parser = new TParser(new CommonTokenStream(lexer));
            var root = parser.parse();
            var evaluated = new CustomVisitor().Visit(root);
            
            Console.WriteLine($"evaluated: {evaluated}");
        }
    }

    class CustomVisitor : TBaseVisitor<decimal>
    {
        public override decimal VisitNumberExpr(TParser.NumberExprContext context)
        {
            return decimal.Parse(context.GetText());
        }
    }
}

它将 return 默认 0。在这种情况下,您还应该覆盖 VisitParse(TParser.ParseContext context):

class CustomVisitor : TBaseVisitor<decimal>
{
    public override decimal VisitParse(TParser.ParseContext context)
    {
        return Visit(context.expr());
    }
    
    public override decimal VisitNumberExpr(TParser.NumberExprContext context)
    {
        return decimal.Parse(context.GetText());
    }
}

现在 returns 42.

如果你不想override/implement太多规则,你可以改用监听器:

using System;
using Antlr4.Runtime;
using Antlr4.Runtime.Tree;

namespace AntlrTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var lexer = new TLexer(CharStreams.fromstring("42"));
            var parser = new TParser(new CommonTokenStream(lexer));
            var root = parser.parse();
            var listener = new CustomListener();
            
            ParseTreeWalker.Default.Walk(listener, root);
            
            Console.WriteLine($"Result: {listener.Result}");
        }
    }

    class CustomListener : TBaseListener
    {
        public decimal Result { get; private set; }

        public override void EnterNumberExpr(TParser.NumberExprContext context)
        {
            Result = decimal.Parse(context.GetText());
        }
    }
}

我找到了解决方案:

protected override List<ColumnDefinition> AggregateResult(List<ColumnDefinition> aggregate, List<ColumnDefinition> nextResult)
{
    if (aggregate != null && nextResult != null) aggregate.AddRange(nextResult);
    return aggregate ?? nextResult;
}

我将结果转换为 List 并向 AggregateResult 添加了适当的覆盖。

感谢@kaby76 用你的评论为我指明了正确的方向。 也感谢所有其他人的反馈和快速回复!