为自定义语法创建自定义代码分析规则,并测试它: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);
}
}
基于最小的 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);
}
}