为自定义语法创建自定义代码分析规则,并测试它:howto?

Creating a custom code analysis rule for a custom grammar, and test it: howto?

基于最小的 C 解析器示例,并使用以下依赖项:

compile(group: "org.codehaus.sonar.sslr", name: "sslr", version: "1.20");
compile(group: "org.codehaus.sonar.sslr", name: "sslr-testing-harness",
    version: "1.20");
compile(group: "org.codehaus.sonar.sslr", name: "sslr-examples",
    version: "1.20");

我创建了一个完全无用的语法,其中包含完全无用的标记和 运行 一个完全无用的 main(),它工作正常(警告:大量代码):

// Operators.java
@ParametersAreNonnullByDefault
public enum Operators
    implements TokenType
{
    ADD("+");

    private final String value;

    Operators(final String value)
    {
        this.value = value;
    }


    @Override
    public String getName()
    {
        return name();
    }

    @Override
    public String getValue()
    {
        return value;
    }

    @Override
    public boolean hasToBeSkippedFromAst(final AstNode node)
    {
        return false;
    }
}

// NumLiteral.java
@ParametersAreNonnullByDefault
public enum NumLiteral
    implements TokenType
{
    LITERAL;

    @Override
    public String getName()
    {
        return name();
    }

    @Override
    public String getValue()
    {
        return name();
    }

    @Override
    public boolean hasToBeSkippedFromAst(final AstNode node)
    {
        return false;
    }
}

// ExampleLexer.java
public final class ExampleLexer
{
    private ExampleLexer()
    {
        throw new Error("nice try!");
    }

    public static Lexer create()
    {
        return Lexer.builder()
            .withChannel(regexp(NumLiteral.LITERAL, "\d++"))
            .withChannel(new PunctuatorChannel(Operators.values()))
            .build();
    }
}

// ExampleGrammar.java
public enum ExampleGrammar
    implements GrammarRuleKey
{
    EXPRESSION,
    ;

    public static Grammar create()
    {
        final LexerfulGrammarBuilder builder = LexerfulGrammarBuilder.create();

        builder.rule(EXPRESSION).is(builder.sequence(NumLiteral.LITERAL,
            Operators.ADD, NumLiteral.LITERAL));

        builder.setRootRule(EXPRESSION);

        return builder.build();
    }
}

// ExampleParser.java
public final class ExampleParser
{
    private ExampleParser()
    {
        throw new Error("nice try!");
    }

    public static Parser<Grammar> create()
    {
        return Parser.builder(ExampleGrammar.create())
            .withLexer(ExampleLexer.create())
            .build();
    }
}

// ExampleToolkit.java
public final class ExampleToolkit
{
    private static final class ExampleConfigurationModel
        extends AbstractConfigurationModel
    {
        @Override
        public Parser doGetParser()
        {
            return ExampleParser.create();
        }

        @Override
        public List<Tokenizer> doGetTokenizers()
        {
            return Collections.emptyList();
        }

        @Override
        public List<ConfigurationProperty> getProperties()
        {
            return Collections.emptyList();
        }
    }

    public static void main(final String... args)
    {
        final ConfigurationModel model = new ExampleConfigurationModel();
        final Toolkit toolkit = new Toolkit("foo", model);
        toolkit.run();
    }
}

这显示了一个 window,我可以输入文本,它可以正确标记等

但是,为了让这东西少一些用处,现在我想对这一切实施一个规则。我做了一个程序:

public final class ExampleRule
    extends SquidCheck<Grammar>
{
    @Override
    public void init()
    {
        subscribeTo(NumLiteral.LITERAL);
    }

    @Override
    public void visitNode(final AstNode astNode)
    {
    }
}

规则的代码还没写呢;但这不是重点。

重点是:我该如何测试规则?

这意味着我需要能够:

不幸的是,声纳文档在这三点上都很差;虽然已经有现有语言的现有代码,但没有文档可以指导您完成自己的过程。

那么,你如何测试上面的内容,更重要的是,你如何进行测试以便在扩展语法本身时扩展你的测试?

由于文档充其量是伪劣的,我决定查看源代码,发现您只是使用

AstNode a = ExampleParser.create().parser("source code to parse");

AstNode a = ExampleParser.create().parser(new File("path/to/source"));

现在您可以直接在 AstNode you got above. The issue with doing that though is that it won't recurse like the intended API would, for that you need to use the AstWalker.walkAndVisit 上使用您的 ExampleRule.visitNode 并且隐藏在实现中。

现在在旧版本中,为了遍历您的 AstNode,您需要使用 AstScanner class 为您执行上述步骤。您可以像这样设置 AstScanner

SquidAstVisitorContextImpl<Grammar> savci = new SquidAstVisitorContextImpl<Grammar>(new SourceProject("Custom Grammar"));
AstScanner.Builder b = AstScanner.builder(savci);

b.setBaseParser(ExampleParser.create());
b.setCommentAnalyser(new CommentAnalyser {
  @override
  public bool isBlank(String line) {
    return true;
  }
  @override
  public String getContents(String comment) {
    return "";
  }
});
b.setFileMetric(FILES); // I am not sure what a 'Metric' is as both the documentation and source are unclear on that, you may have to experiment with this value.

b.withSquidAstVisitor(new ExampleRule());

AstScanner<Grammar> as = b.build();
as.scanFile(new File("path/to/source"));

然后,要检查扫描器收集的内容,您只需使用 as.getIndex() 到 return org.sonar.squid.api.SourceCodeSearchEngine 的一个实例。我会在这部分提取更多信息,但我目前没有时间这样做,我可能会稍后编辑我的答案并跟进。

不过,对于最新版本,看起来像传统访问者模式一样正确走过 ast 的唯一方法是使用 AstWalker class.

由于我对 Sonar 这个框架还不够熟悉,所以我对它的测试工具知之甚少,尽管这对于一些粗略的测试例程来说应该足够了。

好的,所以首先 所有代码挖掘和后续指针。

我的目标是测试规则, AstWalker class 确实是中央 class.

所以,首先,依赖项:

dependencies {
    compile(group: "org.codehaus.sonar.sslr", name: "sslr", version: "1.20");
    compile(group: "org.codehaus.sonar.sslr", name: "sslr-examples",
        version: "1.20");
    compile(group: "org.codehaus.sonar.sslr-squid-bridge",
        name: "sslr-squid-bridge", version: "2.5.3");
    testCompile(group: "org.testng", name: "testng", version: "6.8.21") {
        exclude(group: "org.apache.ant", module: "ant");
        exclude(group: "com.google.inject", module: "guice");
        exclude(group: "junit", module: "junit");
        exclude(group: "org.beanshell", module: "bsh");
        exclude(group: "org.yaml", module: "snakeyaml");
    };
    testCompile(group: "org.mockito", name: "mockito-core", version: "1.10.19");
    testCompile(group: "org.assertj", name: "assertj-core", version: "1.7.1");
    testCompile(group: "org.codehaus.sonar.sslr", name: "sslr-testing-harness",
        version: "1.20");
}

实际做某事的规则的修改代码:

@ParametersAreNonnullByDefault
public class ExampleRule
    extends SquidCheck<Grammar>
{
    @VisibleForTesting
    static final String MESSAGE = "0 in an addition";

    @Override
    public void init()
    {
        subscribeTo(NumLiteral.LITERAL);
    }

    @Override
    public void visitNode(final AstNode astNode)
    {
        final String value = astNode.getTokenValue();
        final int i = Integer.parseInt(value);

        if (i != 0)
            return;

        final SquidAstVisitorContext<Grammar> context = getContext();

        context.createLineViolation(this, MESSAGE, astNode);
    }
}

现在是测试;需要两件事:

  • 规则必须通过调用init()进行初始化;这似乎是 AstScanner 的工作,但我不使用(我不需要));
  • 必须有一个 SquidAstVisitorContext,因为这是规则将注入消息的地方。

我模拟上下文,并使用 mockito 的 ArgumentCaptor 来检查消息是否确实是我所期望的:

public class ExampleRuleTest
{
    private SquidCheck<Grammar> rule;
    private SquidAstVisitorContext<Grammar> context;

    @BeforeMethod
    public void init()
    {
        rule = spy(new ExampleRule());
        context = mock(SquidAstVisitorContext.class);
        doReturn(context).when(rule).getContext();
        // We need that; otherwise the list of tokens isn't accounted for
        rule.init();
    }

    @Test
    public void test()
    {
        final AstNode node = ExampleParser.create().parse("0+2");

        final AstWalker walker = new AstWalker(rule);
        walker.walkAndVisit(node);

        final ArgumentCaptor<String> captor
            = ArgumentCaptor.forClass(String.class);

        verify(context).createLineViolation(same(rule), captor.capture(),
            any(AstNode.class));

        assertThat(captor.getValue()).isEqualTo(ExampleRule.MESSAGE);
    }
}