RubyAddHints

Writing New Ruby Hints

NetBeans' Ruby support has hints and quickfixes as described in the he RubyHints document. The following document attempts to explain how additional hints can be contributed.

If you are new to the project, you may want to also read about how to join, how to build the code and perhaps the code ideas document.

Ideas

Here are some ideas for hints we need:

  • Error fixers (might help new Ruby programmers)
  • On Syntax Error where the syntax error line begins with (whitespace)=begin, point out that =begin must be in column 0 and offer
to shift it (and the =end) over. See issue 114947.
(Done)
  • Look for typos: incorrect spelling of "initialize", or perhaps an assignment to a variable that is close in spelling to another symbol.
 (Note that there is a spellchecker CVS module in netbeans.org which contains a lot of the stuff needed; perhaps this should
 be implemented more as a syntax highlighting option than a quicktip mechanism)
  • Spell check all text in comments and documentation (=begin/=end) sections
  • Spell check words in string literals
  • Spell check Constant and method names (this would probably yield a lot of false positives, so perhaps only warn about these when the
   word is -close- to a letter in the dictionary, e.g. missing a repeated letter such as "Leter" instead of "Letter", and obviously
   needs to be tolerant of camelcase conventions
  • Offer to add parentheses to a code construct where the lack of parentheses results in ambiguity (such as nested method calls without parentheses)
    (Done)
  • Offer to remove parentheses where that's okay (some developers prefer not to use them)
  • Offer non-Railsy deprecations: Use fileutils instead of ftools,
cgi instead of cgi-lib, avoid importenv,
(Done) ... Anything else?
  • Offer to remove unused variables (the left hand side of the assignment, or if the right hand side is known not to
  have side effects, the entire statement)
  • Warn about Ruby 1.8 versus Ruby 1.9 changes for language constructs that are changing; see this blog entry for a summary of the changes
  • Warn about calls to methods that are thought to not have side effects (e.g. has corresponding bang method) where you're ignoring the return value
  (but see this blog entry)
  • Style warnings: Using method names containing uppercase/camelcase names, or constants containing lowercase characters
    (Done)
  • Warn about using multibyte characters etc in identifiers - these are not safe and can lead to runtime errors
    (Done)
  • Camelcase warnings should perhaps not kick in for projects enabled for Java API calls (e.g. with JRuby)
    (Done)
  • Check whether file references from RHTML files (such as in HTML attributes, from helper method calls for CSS and JavaScript tags etc) exist
  • And related, from controller files, check whether a corresponding view file exists and if not offer to Fix it by invoking the generator
    (Done)
    here's another, and yet another
  • Split multiple statements on a line into separate lines (I have this for defs and classes but x=y; foo should be splittable.)
  • Warn about questionable combinations of "=" and "and" (see this blog entry)
  • (JRuby projects) For an unknown class, check the Java index and offer to "import" the Java class into the Ruby name space
  • In migrations, offer to create table, add columns, etc. (Perhaps this should be supported via code completion instead)
  • Offer to replace a { } block with a do-end and vice versa (unless it's a single-line block where braces are most common)
    (Done)
  • Warn about a variable that is being side-effected in a block (e.g. a possible name clash) - and offer to rename it
    (Done)
  • For same-line class definitions, offer to expand it to multiple lines (and vice versa)
    (Done)
  • Detect usages of deprecated Rails constructs
    (Done)
  • RHTML or ERB hints
  • Offer to extract selection as a partial
  • Look for unsafe output and offer to sanitize it - <%h %>

Terminology

  • Hint: A suggestion which appears in the editor related to some problem or potential improvement
  • Rule: The code responsible for identifying suggestions and creating hints
  • Fix: A set of actions which can be performed on a hint

Hints versus Fixes

Here's a typical hint:

http://wiki.netbeans.org/wiki/attach/RubyAddHints/blockvar_RubyAddHints.png

The hint has an icon on the left side of the editor, and an underline under the relevant text on the line. If you hover over the icon, you'll also get a description of the hint:

http://wiki.netbeans.org/wiki/attach/RubyAddHints/deprecated-fields_RubyAddHints.png

Hints can have fixes associated with them. If you type Alt-Enter, you get to see not only the hint text (which was also used as the tooltip for the lightbulb), but actual fix proposals as well:

http://wiki.netbeans.org/wiki/attach/RubyAddHints/blockvar-fixes_RubyAddHints.png

Not all hints have fixes - such as some of the Rails deprecation warnings. The key thing to understand is that a hint has a description, a type (error versus warning etc.), an optional set of fixes, and document begin and end locations.

Fixes on the other hand have descriptions, as well as a handler method which is invoked when the user chooses to apply that particular fix.

Hints are created by several different kinds of rules:

  • Current Line Rules: These are run when the caret moves and only apply to the code around the caret. These types of hints typically offer to perform some service that you
 would not want listed as light bulbs on every single line. For example, it can offer to change a { block } into a do-end block.
  • AST Rules: These are run on specific abstract syntax tree (AST) nodes after the AST has been updated (as a result of editing, opening files etc.).
  • Error Rules: These are based on errors or warnings generated during parsing. Error Hints are keyed by the error code number.
  • Selection Rules: These are run when there is a selection, and you want to apply something to the code in the selection (for example,
 extracting the code into a separate method, or perhaps realigning the assignments in the selection to match up horizontally.)

Code

(For instructions on how to check out and build the Ruby code, first see the RubyBuildInstructions document)

The "built in" rules in the IDE are located in the ruby/hints module. The module also contains some infrastructure around hints, such as options. Additional rules should first be added to the "Ruby Experimental Hints" module, which is is located in ruby/hints/extrahints.

Thus, you'll want to open both the ruby/hints and ruby/hints/extrahints projects, which will display as "Ruby Hints" and "Ruby Experimental Hints" respectively in your project navigator.

You should be adding your rules to the extrahints module, but you might want to look at the existing rules implementations in the hints module for inspiration. You can ignore the infrastructure and options packages.

Ruly Anatomy

Briefly, a rule consists of a single class (which implements the Rule interface defined in the hints SPI package). This class is registered with the IDE in the layer.xml file, which you can easily locate in the "Important Files" list in the project navigator under your project.

The rule can add a number of Hint descriptions, and each hint can have an associated list of Fix implementations. If the fix-list is not empty, the hint will show up with a lightbulb in the editor, and the user can press Alt-Enter to view the available fixes and choose among them.

Thus, a quickfix typically consists of the rule class itself, as well as one or more fix classes which provide alternative fixes for the issue identified by the rule.

Creating a New Rule

To create a new rule, you'll first need to create a class for it it in the org.netbeans.modules.ruby.extrahints package. Take a look at the NestedLocal.java file there for inspiration.

The class should implement AstRule. You then need to implement some key methods:

    public String getId() {
        return "Nested_Local"; // NOI18N
    }

The getId method returns a unique ID for this rule. It's used by the hints infrastructure to for example identify the rule when storing settings related to it. The // NOI18N comment marks this string as "not to be internationalized" (i18n stands for "i" + 18 letters "n", as in internationalization). In other words, this string is only used internally and is never shown to the user.

Next, you've gotta implement a couple of user visible display methods:

    public String getDisplayName() {
        return NbBundle.getMessage(NestedLocal.class, "NestedLocal");
    }

    public String getDescription() {
        return NbBundle.getMessage(NestedLocal.class, "NestedLocalDesc");
    }

These two methods provide user-visible strings of the hints (which show up in the customizer dialog and as the editor lightbulb tooltip). The tips should be internationalized rather than hardcoded for English, which is what the NbBundle calls are doing. There is already a Bundle.properties file in the same package which contains translations for these two strings:

NestedLocal=Nested local variable
NestedLocalDesc=Detects local variable usages that are "nested" (such as in for loops) \
 where the loop variable is being reused

Next, we need to write some methods that control what kind of rule we're dealing with:

    public boolean getDefaultEnabled() {
        return true;
    }

    public HintSeverity getDefaultSeverity() {
        return HintSeverity.WARNING;
    }

    public JComponent getCustomizer(Preferences node) {
        return null;
    }

These methods indicate whether the rule should be enabled by default, whether these hints should (by default) be shown as an error, or a warning, or a warning on the current line only. And finally, the getCustomizer method can be used to offer more detailed configuration editing in the Options dialog - just return null there for now.

Now on to the interesting parts of the rule:

    public boolean appliesTo(CompilationInfo info) {
        return true;
    }

This method is called for each file being hinted to see whether this rule applies to the file at all. If this method returns false, it won't be involved as the rules infrastructure is traversing the parse tree. Most rules will just return true, but this lets you make rules that apply to only some files. For example, rules that only apply in Rails projects, or even only to controller files, can do some logic here. Similarly, files that need to do some setup once can do that here, and then use that result whenever the individual parse tree nodes are going to be checked.

    public Set<Integer> getKinds() {
        return Collections.singleton(NodeTypes.FORNODE);
    }

The getKinds method returns a set of AST nodes that this rule should be applied to. In this example, this rule will be asked to look at each for node in the AST.

Finally, the run method computes the actual hints:

    public void run(CompilationInfo info, Node node, AstPath path, List<Description> result) {
        ...

This method will be run repeatedly on a single AST, once for each occurrence of the node types we listed in the getKinds method. In our case, this means it will be called for each for node in the AST. Here we can look at the node itself, or any of the ancestors in the AST (by looking at the AstPath parameter we're handed). We can then add in hint descriptions to the List<Description> we're passed in.

You might want to look at the existing rules (1 in the ruby/hints/extrahints module, 3 more in the ruby/hints module, for inspiration on what you can do here. Basically, you'll probably look at the AST and decide whether it violates some rule you want to enforce. You can also use some extra services available from the ruby/editing module, such as lexical operations, or even direct document operations (to for example check formatting or whitespace issues). You can also use the RubyIndex method to consult with the global code index to look up libraries etc.

There is one useful trick you should know about, which is used by the RailsDeprecations rule. If your rule needs to look at the whole AST rather specific node types, your getKinds method should return NodeTypes.ROOTNODE. Your rule will then be called once, on the root node of the AST, and from there you can look at the whole AST yourself.

    public Set<Integer> getKinds() {
        return Collections.singleton(NodeTypes.ROOTNODE);
    }

At this point you may be wondering how you can identify AST nodes to do what you're after. An invaluable tool for this is the AST Viewer; open it up on some code which contains the problem you want your rule to identify, and you can now drill around to figure out the AST pattern you need to identify. Here's how the AST viewer looks:

http://wiki.netbeans.org/wiki/attach/RubyAstViewer/astviewer.png

Fixes

So far, you've added a hint that shows up in the editor when a given code construct is identified. You may want to offer fix suggestions which can automatically handle the problem. To do this, your hint Description should return a nonempty list from the getFixes method.

A fix is really simple - it has the following methods:

    String getDescription();

This should return a localized message which describes the fix; these are shown in the popup that the user can navigate in when pressing Alt-Enter.

    boolean isSafe();
    boolean isInteractive();

These two methods indicate whether the fix is interactive (e.g. pops up a dialog where the user enters more information, or perhaps goes to synchronized editing mode), and whether the fix is "safe" (e.g. reformatting a same-line code block is safe, and replacing a deprecated call with the correct new call is safe, but replacing a potential typo (initialise to initialize) may not be).

These methods are not currently used, but I am thinking of adding a command line driver for the hints, which would make use of these to help decide whether hints can be applied automatically across a code base etc.

Finally, the guts of the implementation:

    void implement() throws Exception;

This method performs whatever logic is necessary to perform the hint. It can operate on the document, or on other files, or pop up a browser on a URL or open up dialogs or do whatever else is necessary. I'm thinking this API will change soon, to pass an optional ModificationResult (used in the refactoring module). Which is a perfect segway into the next topic:

Recent Changes

In NetBeans 6.1, there is a new API your fixes can implement: PreviewableFix. It adds two methods,

    boolean canPreview();
    EditList getEditList() throws Exception;

The first method normally just returns true. The second method returns an EditList object which contains a number of edits. (These are easy to generate; the methods on EditList mimicks those on Document, but ensures that the offsets are handled correctly.

If you implement the above methods, then your quickfix is marked as previewable, and a separate Preview action is offered to the user where a diff view is showing the proposed changes for your quickfix. It's also trivial to implement the above implement method once you've implemented getEditList - just call getEditList().apply from your implement method.

API Stability

The fix API is not stable - which is why the hints module does not have a public API; it only exposes its SPI package as a friend to the extrahints module. As we write more hints I'm sure we'll run into usecases we'd like to improve. In particular, I know I want to improve the Fix API to make it easier to offer previews (where applicable) in a similar way to what is done for refactoring.

Hint Categories

In the Hints options panel you'll notice that there are two categories: "General" and "Rails". These were registered by the hints module in the layer.xml file. You can create additional categories in the extrahints module by adding "folders" in the same way in the layer file. Be sure to remember to localize the name of the folder in the associated Bundle.properties file - see the "localizingBundle" attribute used in the hints module's layer.xml file for an example.

Testing

Your hints should have a unit test. Look at the existing hints' unit test. By extending HintTestBase you don't have to do much work - just supply some sample source files, run the hint once (which will generate a golden file listing the hint matches on the file), check that the golden file looks correct, and then move the golden file into the test directory. From now on, the test infrastructure will ensure that the golden file matches the output resulting from applying the rule to the corresponding input file.

To test your fix-implementations you'll need to do a bit more work - look at the HintTestBase for inspiration.

There's a lot more documentation on unit test in NetBeans in NetBeansDeveloperTestFAQ.

You can run the tests from the project menu (Run Unit Tests) or Shift-F6 your own test file to test just your own rule.

Getting Your Hints Into the Build

See the RubyParticipation document for more information on how to submit code etc. Note that we are after feature freeze for NetBeans 6.0, so at this point the hints will go on the Auto Update center rather than into the 6.0 stable build. But there is always version 6.1 :)

Questions

If you have any questions please bring them up on dev@ruby.netbeans.org as described in RubyParticipation.

Not logged in. Log in, Register

By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2012, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo