Donnerstag, 2. September 2010

CCD Syntax Checks

Die CCD-Prinzipien setzen sich aus Syntax und Semantik zusammen. Syntax-Prüfungen sind automatisiert durch einen Parser ausführbar. Semantische Prüfungen hingegen sind nur sehr viel schwerer und mit deutlich höherer Software-Intelligenz prüfbar.

In der CCD-Workbench, der Arbeitsumgebung für CCD-Entwickler, wurden automatisierte Syntaxprüfungen zunächst nach dem KISS-Prinzip (Keep it simple) mit regulären Ausdrücken durchgeführt. Aufgrund der vielfältig und frei formatierbaren Java Quellcodedateien haben sich reguläre Ausdrücke im Integrationstest als zu schwach erwiesen. Parserkonforme Konstrukte wie das Lookahead im Parseprozess anzuwenden haben zwar die Güte des Parsers verbessert, aber nicht alle Ausreißer eingefangen.

Als Alternative zu regulären Ausdrücken, hat sich ein Parsergenerator und eine Grammatik für das CCD-konforme parsen angeboten. Dabei wird im ersten Schritt die Java-Syntax geprüft um sicherzustellen, dass im Java-Quellcode keine Syntaxfehler vorhanden sind und anschliessend die Einhaltung definierter CCD-Regeln überprüft.

Der Parsevorgang läuft in folgenden Stufen ab:
  1. Zerlegen der Java Quellcodedatei in Tokens
  2. Überprüfen der Java Quellcodedatei auf Syntaxfehler
  3. Erkennen von Tokenmuster
  4. Anwenden der hinterlegten CCD-Regeln auf die Tokenmuster
  5. Bei Regelverstößen, Erzeugen und Speichern eines CCD-Statuseintrages im Statusreport
Der Kern des CCD-Parsers setzt sich aus einer Provider-Schnittstelle, der CCD-Parserimplementierung und den CCD-Parserregelklassen zusammen. Über die Providerschnittstelle wird der vom Parsergenerator erzeugte Lexer und Java-Parser an den CCD-Parser angebunden.

Der Lexer liest die Java-Quellcodedatei und erzeugt aus dem Eingabestrom die Tokens zur Weiterverarbeitung im Parseprozess. Für den Parser sind die Tokens atomare Eingabezeichen, die zur Syntaxprüfung herangezogen werden. Im Parseprozess wird nach der Syntaxprüfung ein Ableitungsbaum (Parsebaum) erstellt, der zur Weiterverarbeitung dient. Auf Basis des Parsebaums und des CCD-Regelsets kann der CCD-Parser Regelverstöße gegen CCD-Prinzipien erkennen.

Exemplarisch wird die folgende Java Quellcodedatei durch den CCD-Parser überprüft:

package ccd.jee.parser;

public class CcdParserTestInput {

    public int ihpField; 
    private int field;

    public void lod() {

        new CcdParserTestInput().getField().getField();
        new CcdParserTestInput().getField().getField().getField().getField();
        new CcdParserTestInput().getField();                     
    }

    public void multipleParameter(int nArg, int mArg, int lArg, int oArg) {}
    public void boolSwitchParameter(int valueArg, boolean switchArg) {} 

    public CcdParserTestInput getField() {

        return this;
    }

    public int getPrivateField() {

        return field;
    }
}

In der Quellocdedatei erkennt man folgende Verstöße gegen CCD-Prinzipien:
  1. Öffentliches Feld (Kapselung - IHP)
  2. Transitive Navigation (Law of Demeter - LOD)
  3. Methoden mit langer Parametersignatur (MUP)
  4. Methode mit Boolean-Verzweigungsargument (BOA)
Der CCD-Parser setzt sich aus der Parser-Schnittstelle und Implementierungsklasse zusammen. Der Parserreport wird in Statusklassen festgehalten, die JAXB-fähig sind und deshalb in das XML-Format umgewandelt werden können.

Schnittstelle des CCD-Parsers:

public interface CcdParser {
   
    public CcdParserReport parse(String filename);
}

Parserreport:

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement(name= "CcdParserReport")
@XmlType(propOrder = { "javaSyntaxError", "ccdState"})
public class CcdParserReport {
      
    private boolean javaSyntaxError = false;
  
    @XmlElement(name = "CcdState")
    private List<CcdState> ccdState = new CopyOnWriteArrayList<CcdState>();
  
    @XmlElement(name = "JavaSyntaxError")
    public boolean isJavaSyntaxError() {
        return javaSyntaxError;
    }

    public List<CcdState> getStates() {
        return ccdState;
    }

    public void add(CcdState state) {
        ccdState.add(state);
    }
  
    public void clear() {
        ccdState.clear();
    }

    public void setJavaSyntaxError(boolean javaSyntaxError) {
        this.javaSyntaxError = javaSyntaxError;
    }

    @Override
    public String toString() {
        return "CcdStates [states="
                    + ccdState + ", syntaxError=" + javaSyntaxError + " ]";
    }
}

Einzelstatus im Parserreport:

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class CcdState {

    @XmlElement(name = "Violation")
    private final String violation;
  
    @XmlElement(name = "Line")
    private final int line;
  
    public CcdState() {      
        this("",0);
    }

    public CcdState(String violation, int line) {
      
        this.violation = violation;
        this.line = line;
    }

    public String getViolation() {
        return violation;
    }

    public int getLine() {
        return line;
    }
}

Einfacher Unit-Test des CCD-Parsers:

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import org.junit.Before;
import org.junit.Test;

public class CcdParserTest {

    private static final String PARSER_INPUT = "CcdParserTestInput.java";
    private CcdParser parser;

    @Before
    public void setUp() {

        parser = CcdParserImpl.newInstance();
    }

    @Test
    public void testCcdParser() throws JAXBException {

        final CcdParserReport report = parser.parse(PARSER_INPUT);

        final JAXBContext ctx = JAXBContext.newInstance(CcdParserReport.class);
        final Marshaller m = ctx.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        m.marshal(report, System.out);
    }
}

Ausgabe des Unit-Tests auf der Konsole:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<CcdParserReport>
    <JavaSyntaxError>false</JavaSyntaxError>
    <CcdState>
        <Violation>MUP</Violation>
        <Line>15</Line>
    </CcdState>
    <CcdState>
        <Violation>BOA</Violation>
        <Line>16</Line>
    </CcdState>
    <CcdState>
        <Violation>LOD</Violation>
        <Line>11</Line>
    </CcdState>
    <CcdState>
        <Violation>IHP</Violation>
        <Line>5</Line>
    </CcdState>
</CcdParserReport>

Der Blogeintrag definiert ein sehr einfaches Testszenario. Mit JAXB lassen sich wesentlich komplexere Testszenarien umsetzen. JAXB  1.x war schwierig anzuwenden. Alternative Frameworks wie Castor wurden deshalb ebenfalls zum Binden von Java Objekten und XML-Dokumenten herangezogen. Mit dem in Java SE 6 integrierten JAXB 2.x lassen sich Bindungen von Java Objekten und XML-Strukturen einfacher und ohne zusätzliche Generate per Annotationen definieren. Wesentlich dabei ist die Standardisierung von JAXB durch die Verankerung im I-Stack.

Zurück zum CCD-Parser! Die CCD-Parserimplementierung (Parser Brain) und der Parserprovider bleiben verborgen. Beide Komponenten sind nicht Gegenstand des Blogeintrages. Die CCD-Parserimplementierung zeigt, dass es durchaus möglich ist,  automatisiert gegen CCD-Regelverstöße zu testen. Plugins wie PMD haben teilweise ähnliche Funktionen. PMD findet Quellcode-Duplikate (Copy/Paste Quellcode) und zeigt dadurch einen Verstoß gegen das DRY-Prinzip an.


Der Rechtshinweis des Java Blog für Clean Code Developer ist bei der Verwendung und Weiterentwicklung des Quellcodes des Blogeintrages zu beachten.