W tym wpisie opiszę pierwszy etap w wykrywaniu plagiatów w Plag-Detectorze.
O co adekwatnie chodzi?
Tokenizacja jest terminem występującym często w tematyce związanej z bezpieczeństwem czy też szyfrowaniem. W tym przypadku chodzi o coś trochę innego. Wspomniałem o tym w jednym z poprzednich wpisów.
Poniżej użyję tego samego przykładu, którego użyłem w tamtym wpisie.
String var; VARIABLE_DECLARATION int i; VARIABLE_DECLARATION i = 3; IDENTIFIER ASSIGN IDENTIFIER if (i > 2) { IF_SWITCH_STATEMENT_BEGIN OPEN_PARENTHESIS IDENTIFIER CONDITION IDENTIFIER CLOSE_PARENTHESIS var = "more than 2"; IDENTIFIER ASSIGN STRING } IF_SWITCH_STATEMENT_END else { IF_SWITCH_STATEMENT_BEGIN var = "less than 2"; IDENTIFIER ASSIGN STRING } IF_SWITCH_STATEMENT_ENDIdeą tokenizacji jest zamiana kodu źródłowego na ciąg tokenów. W listingu powyżej można zobaczyć jak może wyglądać przykładowa tokenizacja kawałka kodu.
Jak to zrobić w praktyce?
Oczywiście można by samemu napisać tokenizer, ale byłaby to dość skomplikowana i długa robota. Oczywiście zakładając, iż tokenizer powinien uwzględniać gramatykę danego języka. Z pomocą przychodzi nam ANTLR, czyli ANother Tool for Language Recognition.
Dla ANTLRa możemy zdefiniować gramatykę, według której będzie przeprowadzona tokenizacja. Zdefiniowanie gramatyki dla wszystkich języki również byłoby dosyć karkołomnym zadaniem. Na szczęście większość gramatyk dla popularnych języków programowania została już zdefiniowana i jest dostępna tutaj: https://github.com/antlr/grammars-v4.
Konfiguracja i użycie ANTLRa
Ponieważ jesteśmy leniwi, chcemy żeby wszystko nam się robiło automatycznie. I tak jest w tym przypadku
Zależności
Konfiguracje musimy standardowo zacząć od dodania zależności do naszego pliku pom.xml.
<dependency> <groupId>org.antlr</groupId> <artifactId>antlr4</artifactId> <version>4.7</version> </dependency>Gramatyka
Następnie musimy znaleźć interesującą nas gramatykę i wrzucić ja do folderu antlr4, który umieszczamy w katalogu serc/main. Dodatkowo, o ile chcemy, żeby kod wygenerowany na podstawie naszej gramatyki był w konkretnym pakiecie to musimy gramatykę wrzucić właśnie pod taką ścieżką. Czyli np. o ile chcę, żeby klasy związane z gramatyką były w pakiecie pl.lantkowiak.plagdetector.algorithm.grammar, to powinienem mieć taką strukturę katalogów jak przedstawiona poniżej.
src |- main |- antlr4 |- pl |- lantkowiak |- algorithm |- grammarPluginy
Teraz potrzebujemy dodać do naszego poma kolejne dwa pluginy.
antlr4-maven-plugin
<plugin> <groupId>org.antlr</groupId> <artifactId>antlr4-maven-plugin</artifactId> <version>4.7</version> <executions> <execution> <id>antlr-generate</id> <phase>generate-sources</phase> <goals> <goal>antlr4</goal> </goals> </execution> </executions> </plugin>Plugin ten będzie odpowiedzialny za wygenerowanie klas związanych z naszą gramatyką.
build-helper-maven-plugin
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>${project.build.directory}/generated-sources/antlr/</source> </sources> </configuration> </execution> </executions> </plugin>Ten plugin sprawia, iż nasze wygenerowane źródła będą widoczne w Eclipsie jako klasy, których możemy użyć.
Mały hack
Jeżeli będziemy wykorzystać wygenerowane klasy w naszym kodzie Kotlinowym to nasz projekt niestety się nie zbuduje. Maven nie będzie widział wygenerowanych źródeł. Aby to naprawić musimy w naszym pomie zamienić kolejnością pluginy odpowiedzialne za kompilacje Javy i Kotlina. W wpisie Hello Kotlin pisałem, iż plugin do kompilacji kodu Kotlina powinien być przed pluginem Javovym, żeby kod kotlina mógł być użyty w Javie. Niestety póki co musiałem z tego zrezygnować, aby móc zautomatyzować proces generacji kodów źródłowych dla gramatyk ANTLRowych.
Użycie w kodzie
Dla np. gramatyki dla Java 8 ANTLR wygeneruje następujące pliki:
Java8.tokens Java8Lexer.tokens Java8BaseListener.java Java8Lexer.java Java8Listener.java Java8Parser.javaKlasa, która będzie nas interesowała do przeprowadzenia tokenizacji to Java8Lexer.
Ponieważ Plag-Detector będzie wspierał wiele języków programowania stworzyłem enuma, który będzie zawierał wszystkie wspierane języki.
enum class LexerType(val title: String) { JAVA_8("Java 8") }Następnie stworzyłem prostą fabrykę, która na podstawie przekazanego enuma zwróci mi instancje oczekiwanego leexera.
class LexerFactory { fun getLexer(type: LexerType, cs: CharStream): Lexer { when (type) { LexerType.JAVA_8 -> return Java8Lexer(cs) } } }Klasa Lexer jest klasą abstrakcyjną, po której dziedziczą wszystkie lexery wygenerowane przez ANTLRa.
Sama tokenizacja jest już bardzo prosta:
class TokenizerImpl : Tokenizer { override fun tokenize(lexerType: LexerType, input: String): List<Int> { val lexer = LexerFactory().getLexer(lexerType, CharStreams.fromString(input)) return lexer.allTokens.map { t -> t.type } } }W pierwszej linii metody tworze instancje fabryki, a następnie pobieram lexer. Następnie wywołuję metodę getallTokens(), która zwraca mi listę wszystkich tokenów z przetworzonego kodu źródłowego. Ponieważ do dalszych potrzeb istotne są dla mnie tylko typy tokenów, a nie całe obiekty z nadmiarowymi danymi, mapuję tokeny na ich typ, czyli int. Taką listę intów jest zwracana przez metodę i będzie dalej użyta w procesie wykrywania plagiatów.
Podsumowanie
W tym wpisie przedstawiłem ANTLRa oraz pokazałem jak go skonfigurować. Dodatkowo pokazałem w jaki sposób można użyć ANTLRa, aby dokonać tokenizacji ciągu znaków na wejściu – w naszym przypadku kodu źródłowego.