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:
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) >
- class - Fully qualified name of a class that implements IStyleChecker
- id - unique identifier for the checker
- name - A name which will be used in the preference panel, a short description would be good
- defaultEnabled - whether or not this check should be enabled by default, the default value is 'true'
<!ELEMENT customAttribute EMPTY>
<!ATTLIST customAttribute
name CDATA #REQUIRED
value CDATA #REQUIRED>
- name - name of the attribute (map key)
- value - value of the attribute (map value)
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:
class="kenya.eclipse.style.checks.metrics.ParameterLengthChecker"
name="Check for long parameter lists ( > 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:
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:
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.
//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:
"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:
runMarkerUpdate(runnable);
We have seen:
- how easy it is to write a StyleChecker
- that for more complicated checkers you will need to use the AST and visitors
- that
AbstractStyleChecker
provides various helpful methods
We have not seen:
- How to create resolutions
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:
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.
