How to add a new language to PMD using JAVACC grammar.
Edit me

1. Start with a new sub-module.

  • See pmd-java or pmd-vm for examples.

2. Implement an AST parser for your language

  • Ideally an AST parser should be implemented as a JJT file (see VmParser.jjt or Java.jjt for example)
  • There is nothing preventing any other parser implementation, as long as you have some way to convert an input stream into an AST tree. Doing it as a JJT simplifies maintenance down the road.
  • See this link for reference: https://javacc.java.net/doc/JJTree.html

3. Create AST node classes

  • For each AST node that your parser can generate, there should be a class
  • The name of the AST class should be “AST” + “whatever is the name of the node in JJT file”.
    • For example, if JJT contains a node called “IfStatement”, there should be a class called “ASTIfStatement”
  • Each AST class should have two constructors: one that takes an int id; and one that takes an instance of the parser, and an int id
  • It’s a good idea to create a parent AST class for all AST classes of the language. This simplifies rule creation later. (see SimpleNode for Velocity and AbstractJavaNode for Java for example)
  • Note: These AST node classes are generated usually once by javacc/jjtree and can then be modified as needed.

4. Compile your parser (if using JJT)

  • An ant script is being used to compile jjt files into classes. This is in pmd-<lang>/src/main/ant/alljavacc.xml file.
  • Create alljavacc.xml file for your language, you can use one from pmd-java as an example.
  • You would probably want to adjust contents of the <delete> tag: start with an empty <fileset> and add there <include>s for those AST nodes you had to manually rewrite (moving those node classes from autogenerated directory to the regular source tree).

5. Create a TokenManager

  • Create a new class that implements the TokenManager interface (see VmTokenManager or JavaTokenManager for example)

6. Create a PMD parser “adapter”

  • Create a new class that extends AbstractParser
  • There are two important methods to implement
    • createTokenManager method should return a new instance of a token manager for your language (see step #5)
    • parse method should return the root node of the AST tree obtained by parsing the Reader source
    • See VmParser class as an example

7. Create a rule violation factory

  • Extend AbstractRuleViolationFactory (see VmRuleViolationFactory for example)
  • The purpose of this class is to create a rule violation instance specific to your language

8. Create a version handler

  • Extend AbstractLanguageVersionHandler (see VmHandler for example)
  • This class is sort of a gateway between PMD and all parsing logic specific to your language. It has 2 purposes:
    • getRuleViolationFactory method returns an instance of your rule violation factory (see step #7)
    • getParser returns an instance of your parser adapter (see step #6)

9. Create a parser visitor adapter

  • If you use JJT to generate your parser, it should also generate an interface for a parser visitor (see VmParserVisitor for example)
  • Create a class that implements this auto-generated interface (see VmParserVisitorAdapter for example)
  • The purpose of this class is to serve as a pass-through visitor implementation, which, for all AST types in your language, just executes visit on the base AST type

10. Create a rule chain visitor

  • Extend AbstractRuleChainVisitor (see VmRuleChainVisitor for example)
  • This class should implement two important methods:
    • indexNodes generates a map of “node type” to “list of nodes of that type”. This is used to visit all applicable nodes when a rule is applied.
    • visit method should evaluate what kind of rule is being applied, and execute appropriate logic. Usually it will just check if the rule is a “parser visitor” kind of rule specific to your language, then execute the visitor. If it’s an XPath rule, then we just need to execute evaluate on that.

11. Make PMD recognize your language

  • Create your own subclass of net.sourceforge.pmd.lang.BaseLanguageModule. (see VmLanguageModule or JavaLanguageModule as an example)
  • You’ll need to refer the rule chain visitor created in step #10.
  • Add for each version of your language a call to addVersion in your language module’s constructor.
  • Create the service registration via the text file src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language. Add your fully qualified class name as a single line into it.

12. Add AST regression tests

For languages, that use an external library for parsing, the AST can easily change when upgrading the library. Also for languages, where we have the grammar under our control, it useful to have such tests.

The tests parse one or more source files and generate a textual representation of the AST. This text is compared against a previously recorded version. If there are differences, the test fails.

This helps to detect anything in the AST structure, that changed, maybe unexpectedly.

  • Create a test class in the package net.sourceforge.pmd.lang.$lang.ast with the name $langTreeDumpTest.
  • This test class must extend net.sourceforge.pmd.lang.ast.test.BaseTreeDumpTest. Note: This class is written in kotlin and is available in the module “lang-test”.
  • Add a default constructor, that calls the super constructor like so:

        public $langTreeDumpTest() {
            super(NodePrintersKt.getSimpleNodePrinter(), ".$extension");
        }
    

    Replace “$lang” and “$extension” accordingly.

  • Implement the method getParser(). It must return a subclass of net.sourceforge.pmd.lang.ast.test.BaseParsingHelper. See net.sourceforge.pmd.lang.ecmascript.ast.JsParsingHelper for a example. With this parser helper you can also specify, where the test files are searched, by using the method withResourceContext(Class<?>, String).
  • Add one or more test methods. Each test method parses one file and compares the result. The base class has a helper method doTest(String) that does all the work. This method just needs to be called:

        @Test
        public void myFirstAstTest() {
            doTest("filename-without-extension");
        }
    
  • On the first test run the test fails. A text file (with the extension .txt) is created, that records the current AST. On the next run, the text file is used as comparison and the test should pass. Don’t forget to commit the generated text file.

A complete example can be seen in the JavaScript module: net.sourceforge.pmd.lang.ecmascript.ast.JsTreeDumpTest. The test resources are in the subpackage “testdata”: pmd-javascript/src/test/resources/net/sourceforge/pmd/lang/ecmascript/ast/testdata/.

The Scala module also has a test, written in Kotlin instead of Java: net.sourceforge.pmd.lang.scala.ast.ScalaParserTests.

13. Create an abstract rule class for the language

  • Extend AbstractRule and implement the parser visitor interface for your language (see AbstractVmRule for example)
  • All other rules for your language should extend this class. The purpose of this class is to implement visit methods for all AST types to simply delegate to default behavior. This is useful because most rules care only about specific AST nodes, but PMD needs to know what to do with each node - so this just lets you use default behavior for nodes you don’t care about.

14. Create rules

  • Rules are created by extending the abstract rule class created in step 13 (see EmptyForeachStmtRule for example)
  • Creating rules is already pretty well documented in PMD - and it’s no different for a new language, except you may have different AST nodes.

15. Test the rules

  • See BasicRulesTest for example
  • You have to create a rule set for your language (see vm/basic.xml for example)
  • For each rule in this set you want to test, call addRule method in setUp of the unit test
    • This triggers the unit test to read the corresponding XML file with rule test data (see EmptyForeachStmtRule.xml for example)
    • This test XML file contains sample pieces of code which should trigger a specified number of violations of this rule. The unit test will execute the rule on this piece of code, and verify that the number of violations matches
  • To verify the validity of the created ruleset, create a subclass of AbstractRuleSetFactoryTest (see RuleSetFactoryTest in pmd-vm for example). This will load all rulesets and verify, that all required attributes are provided.

    Note: You’ll need to add your ruleset to rulesets.properties, so that it can be found.

Debugging with Rule Designer

When implementing your grammar it may be very useful to see how PMD parses your example files. This can be achieved with Rule Designer:

  • Override the getXPathNodeName in your AST nodes for Designer to show node names.
  • Make sure to override both jjtOpen and jjtClose in your AST node base class so that they set both start and end line and column for proper node bound highlighting.
  • Not strictly required but trivial and useful: implement syntax highlighting for Rule Designer:
    • Fork and clone the pmd/pmd-designer repository.
    • Add a syntax highlighter implementation to net.sourceforge.pmd.util.fxdesigner.util.codearea.syntaxhighlighting (you could use Java as an example).
    • Register it in the AvailableSyntaxHighlighters enumeration.
    • Now build your implementation and place the target/pmd-ui-<version>-SNAPSHOT.jar to the lib directory inside your pmd-bin-... distribution (you have to delete old pmd-ui-*.jar from there).

Optional features

Metrics

If you want to add support for computing metrics:

  • Create a package lang.<langname>.metrics
  • Create a utility class <langname>Metrics
  • Implement new metrics and add them as static constants. Be sure to document them.
  • Implement getLanguageMetricsProvider, to make the metrics available in the designer.

See JavaMetrics for an example.