III    Writing StyleChecks

StyleChecks can be supplied in a separate plug-in and need not be packaged in the same archives as KenyaEclipse. The only requirement for this is the availability of the KenyaEclipse plug-in during development and at runtime.

A StyleChecker is the implementation of an extension point and thus has two components: firstly the class implementing the actual check, and secondly the declaration in the plugin.xml descriptor file.

Declaring and implementing a StyleChecker

The extension point itself is declared as follows:

<extension-point
  id="styleCheckers"
  name="Kenya Style Checkers"
  schema="schema/styleCheckers.exsd"
/>

Extension point description:

Identifier: kenya.eclipse.KenyaStyle.styleCheckers

Configuration Markup:

<!ELEMENT extension (KenyaStyleChecker)>
<!ATTLIST extension
  id    CDATA #IMPLIED
  name  CDATA #IMPLIED
  point CDATA #REQUIRED>
 
<!ELEMENT KenyaStyleChecker (customAttribute)>
<!ATTLIST KenyaStyleChecker
  class          CDATA #REQUIRED
  id             CDATA #REQUIRED
  name           CDATA #REQUIRED
  defaultEnabled (true | false) >
<!ELEMENT customAttribute EMPTY>
<!ATTLIST customAttribute
  name  CDATA #REQUIRED
  value CDATA #REQUIRED>

Each extension encompasses one or more 'KenyaStyleChecker's, which require attributes as above.
The name is actually displayed on the configuration page and should be chosen to be a short description of the checker.
The defaultEnabled flag specifies whether the check is enabled by default. This may be useful if providing a StyleChecker with custom attributes (see below), where the class can be reused multiple times, but only one of them should be enabled initially.
It is usually sufficient to use the same value for id as for the class, but note that if the same class is used, then the id has to be unique for each declaration. A good example if this is the provided MethodLengthChecker.
Finally, KenyaStyleCheckers may contain multiple customAttributes, which are mappings of keys to values. These can be used to create reusable StyleCheckers.

Here, we will take the 'Long parameter list' checker as an example and see how it is declared and implemented in the plug-in. Although the extension point definition seen above looks complex, it boils down to only a few lines.

Declaration

The checker is declared in the plugin.xml file as follows:

<KenyaStyleChecker
  class="kenya.eclipse.style.checks.metrics.ParameterLengthChecker"
  name="Check for long parameter lists ( &gt; 8 )"
  id="kenya.eclipse.style.checks.metrics.ParameterLengthChecker8"
  <customAttribute name="length" value="8" />
</KenyaStyleChecker>

Note:

id is the unique identifier. Since this StyleChecker can be configured with different values for its custom length attribute, that value has been appended.
A customAttribute was added with name 'length' and value '8'
The defaultEnabled attribute is missing from the declaration since its default value is true, although it might be included for added clarity.

And that is it. This is all the markup required to integrate a StyleChecker with the framework. Only the implementation needs to be added.

Implementation

The first thing to note is that, while the extension point only requires the implementation to inherit from IStyleChecker, there is an abstract implementation available, providing various useful methods for use by its subclasses.

We can create our class, extending AbstractStyleChecker, which requires us to implement the two methods: void configure(java.util.Map)
void performCheck(mediator.ICheckedCode, IFile file).

configure is called first and passes all custom attributes declared using the customAttribute tag to the instance of the checker as a map, mapping 'name's to 'value's. In our implementation, we retrieve the value for the 'length' attribute and store it in a field:

public void configure(Map customAttributes) {
  String ln = (String)customAttributes.get("length");
  if(ln!=null) {
    try {
      int length = Integer.parseInt(ln);
      fMaxLength = length;
      //retrieve the length from the configuration
    } catch(NumberFormatException e) {
      //ignore (if fails, we will be using the default value)
    }
  }
}

The implementation of performCheck can be staged in 3 phases. First, we will perform a search to find all method declarations, since this is the point where we will find parameter lists. Second, we can single out the methods that take more parameters than fMaxLength (see above) and can then in the third phase take action (resolutions and highlighting).

Phase 1

Obtaining method or function references from the code object is simple:
IFunction[] funs = code.getFunctions();

That's phase 1 done.

Phase 2

We can store the 'offenders' in a collection to be able to treat them later. First we need to find these by examining the methods we just found:

//we will need this later
IDocument doc = getDocument(file);

//lengths will contain the offending method's location
// and the number of arguments they accept
HashMap lengths = new HashMap();

//we first examine each function in turn
for(int i = 0; i < funs.length; i++) {
  IFunction function = funs[i];
  AFuncDecDeclaration decl = function.getDeclarationNode();
  //the following is cheating as it
  // requires implicit knowledge of the AST.
  // the 'proper' way is to use a
  // visitor that counts the number of parameters

  AFormalParamList paramlist = (AFormalParamList)decl.getFormalParamList();
  if(paramlist==null) {
    continue//no parameters
  }
  Node n = paramlist.getTypeName();
  List list = paramlist.getCommaTypeName();
  
  int length = (n==null)
    ?0               //if n is null, length is 0
    :1 + (           //otherwise 1 for n, and add..
      (list==null)
        ?0           //..0 for a null list
        :list.size() //..or its length
      );
  
  if(length>fMaxLength) { //test against max length
    //we actually wish to highlight the parameter list
    ISourceCodeLocation loc
      = AdvancedPositionFinder.getFullLocation(paramlist, doc);
    lengths.put(loc, new Integer(length));
  }
}

Note that we used a map of locations to number of parameters to store the 'offenders'.
To calculate the position of the parameters in the file, we used superclass functionality to obtain the corresponding IDocument.

Phase 3

The final step is to use the gathered information to create the annotations in the editor.

//can now go through 'offenders' and create the appropriate markers
//this must be done in an IWorkSpaceRunnable

final Set offences = lengths.entrySet();
IWorkspaceRunnable runnable = new IWorkspaceRunnable() {
  public void run(IProgressMonitor monitor) throws CoreException {
    for(Iterator it = offences.iterator(); it.hasNext();) {
      Map.Entry entry = (Map.Entry)it.next();
      //retrieve the data from the map
      
      ISourceCodeLocation loc
        = (ISourceCodeLocation)entry.getKey();
      Integer length = (Integer)entry.getValue();
      
      String msg
        = MessageFormat.format(MESSAGE, new Object[]{length});
      
      //super can do the marker creation
      createKenyaStyleMarker(file, loc, msg);
      
    }
  }
};

where MESSAGE is a constant declared as:

public static final String MESSAGE =
  "This method takes {0} parameters. " +
  "\nTry splitting the method into " +
  "smaller functional parts or creating" +
  "\na class to group closely related parameters.";

The {0} is replaced by the length of the parameter list as obtained from the map by the format instruction. The message is used as a label for the created marker and is the text shown, for instance, in the problem view for that particular style warning.

Finally, this runnable needs to be executed. Again, the abstract superclass provides a method for this and we simply call:

//execute the runnable
runMarkerUpdate(runnable);

We have seen:

We have not seen:

Style Resolutions

Resolutions can be created during the execution of the IWorkspaceRunnable. The idea is to instantiate and populate a map, which is defined in the field fResolutionMap. It can be instantiated at any time and populated as required. This map is returned to the framework by a call to the predefined method Map getMarkerResolutionMap(). The map is expected to be populated once the runnable returns and maps from IMarker to IMarkerResolution[].

An IMarker is returned from createKenyaStyleMarker, while the Resolution can be created as an inner class or extension of StyleWarningResolution. This contains all information necessary to perform the resolution.

Use the field fOperation in StyleWarningResolution, which is an instance of DocumentModificationOperation (kenya.eclipse.multieditor.kenya.refactoring), to define text operations that will perform the resolution. If you do not want a modification at all, simply don't touch fOperation. It is still possible to show the resolution dialog.

Here is an example of a StyleWarningResolution implementation:

class BreakOmissionMarkerResolution1 extends StyleWarningResolution {
  public BreakOmissionMarkerResolution(IMarker marker) {
    super(null, marker, new DocumentModificationOperation());
    fLabel = "Create break statement";
    initDescription();
    init(marker);
  }
  private void initDescription() {
    fDescription = "<p><b>Explanation:</b></p>" +
      "<p>A <b>break statement</b> marks the end of a 'case' in a " +
      "switch block. This prevents cases from falling through, which " +
      "means that the <b>next</b> case is <b>also</b> executed.</p>" +
      "<p><b>Note:</b> If the case is intended to fall through, you " +
      "should create a <em>fall-through comment</em> instead.</p>";
  }
  private void init(IMarker marker) {
    IDocument doc = getDocument((IFile)marker.getResource());
    if(doc== null) { return; }
    //find the place where we are supposed to insert our generation
    int offset = marker.getAttribute(FIX_OFFSET, -1);
    if(offset<0) { return; } //can't continue if no such place defined
    String prefix; //line indent
    String postfix; //next line indent
    […] //calculate prefix and postfix
    String insertion = prefix + "break;" + postfix;
    fOperation.addOperation(
    DocumentTextOperation.newTextInsertion(offset, insertion));
  }
}

Three different text operations are defined, namely insertion, deletion and replacement. You will also have noticed that the description is written using HTML markup. Although not all tags are supported, this provides a way of formatting the displayed text.

Another point to note is the mapping to IMarkerResolution[]. Using an array allows us to specify more than one solution to the problem. In the actual implementation of the BreakOmissionChecker, another resolution is implemented, allowing the addition of a fall-through comment instead of a break statement. All resolutions from the array are added to the proposal box as shown below. Notice also the formatting resulting from the usage of HTML.

Correction proposal resulting from the implementation