Pārlūkot izejas kodu

Merge branch 'master' into core-algorithm-refactoring

Conflicts:
jplag.frontend.text/src/main/java/de/jplag/text/ParserAdapter.java
Jan Wittler 4 gadi atpakaļ
vecāks
revīzija
4742289c1c

+ 1 - 1
README.md

@@ -29,7 +29,7 @@ In the following, a list of all supported languages with their supported languag
 | [Scheme](http://www.scheme-reports.org)                          |       ? | scheme                | unknown | JavaCC |
 | [EMF Metamodel](https://www.eclipse.org/modeling/emf/)           |  2.25.0 | emf-metamodel         | alpha | EMF |
 | [EMF Metamodel](https://www.eclipse.org/modeling/emf/) (dynamic) |  2.25.0 | emf-metamodel-dynamic | alpha | EMF |
-| Text (naive)                                                     |       - | text                  | legacy | ANTLR |
+| Text (naive)                                                     |       - | text                  | legacy | CoreNLP |
 
 ## Download and Installation
 

+ 6 - 7
jplag.frontend.rust/src/test/java/de/jplag/rust/RustFrontendTest.java

@@ -1,6 +1,6 @@
 package de.jplag.rust;
 
-import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.File;
 import java.io.IOException;
@@ -36,6 +36,7 @@ class RustFrontendTest {
     private static final String EMPTY_STRING = "";
     private static final String RUST_SHEBANG = "#!.*$";
     private static final double EPSILON = 1E-6;
+    public static final double BASELINE_COVERAGE = 0.75;
 
     private final Logger logger = LoggerFactory.getLogger("Rust frontend test");
     private final String[] testFiles = new String[] {"deno_core_runtime.rs", COMPLETE_TEST_FILE};
@@ -85,15 +86,12 @@ class RustFrontendTest {
             } else {
                 logger.info("Coverage: %.1f%%.".formatted(coverage * 100));
                 logger.info("Missing lines {}", codeLines);
-                if (coverage - 0.9 <= EPSILON) {
-                    // TODO use fail() instead when frontend is ready
-                    logger.error("Source coverage is unsatisfactory");
-                }
+                assertTrue(coverage - BASELINE_COVERAGE >= EPSILON, "Source coverage is unsatisfactory");
             }
 
         } catch (IOException exception) {
             logger.info("Error while reading test file %s".formatted(fileName), exception);
-            fail();
+            assertTrue(false);
         }
     }
 
@@ -138,8 +136,9 @@ class RustFrontendTest {
 
         if (!allTokens.isEmpty()) {
             var notFoundTypes = allTokens.stream().map(type -> new RustToken(type, EMPTY_STRING, NOT_SET, NOT_SET, NOT_SET).type2string()).toList();
-            fail("Some %d token types were not found in the complete code example '%s':\n%s".formatted(notFoundTypes.size(), fileName,
+            logger.error("Some %d token types were not found in the complete code example '%s':\n%s".formatted(notFoundTypes.size(), fileName,
                     notFoundTypes));
+            assertTrue(false);
         }
     }
 

+ 4 - 25
jplag.frontend.text/pom.xml

@@ -9,34 +9,13 @@
     <artifactId>text</artifactId>
 
     <dependencies>
-        <dependency>
-            <groupId>antlr</groupId>
-            <artifactId>antlr</artifactId>
-        </dependency>
         <dependency>
             <groupId>de.jplag</groupId>
             <artifactId>frontend-utils</artifactId>
         </dependency>
+        <dependency>
+            <groupId>edu.stanford.nlp</groupId>
+            <artifactId>stanford-corenlp</artifactId>
+        </dependency>
     </dependencies>
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.codehaus.mojo</groupId>
-                <artifactId>antlr-maven-plugin</artifactId>
-                <configuration>
-                    <!-- Comma separated list of grammar files or pattern grammar files 
-						By default, grammar file(s) is in ${basedir}/src/main/antlr -->
-                    <!-- <grammars>*.g</grammars> -->
-                    <grammars>text.g</grammars>
-                </configuration>
-                <executions>
-                    <execution>
-                        <goals>
-                            <goal>generate</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
 </project>

+ 0 - 102
jplag.frontend.text/src/main/antlr/text.g

@@ -1,102 +0,0 @@
-header {
-package de.jplag.text;
-}
-
-// tell ANTLR that we want to generate Java source code
-options {
-  language="Java";
-}
-
-class TextParser extends Parser;
-options {
-  k = 2;			  // two token lookahead
-  //  exportVocab=Text;	          // Call its vocabulary "Text"
-  codeGenMakeSwitchThreshold = 2; // Some optimizations
-  codeGenBitsetTestThreshold = 3;
-  defaultErrorHandler = true;
-  buildAST = false;
-  ASTLabelType = "AntlrParserToken";
-}
-
-{
-    private de.jplag.text.ParserAdapter parser;
-
-    public void setParserAdapter(de.jplag.text.ParserAdapter adapter) {
-        this.parser = adapter;
-    }
-}
-
-file : ( w:WORD { parser.add(w); } | PUNCTUATION | SPECIALS )* EOF ;
-
-//----------------------------------------------------------------------------
-// The Text scanner
-//----------------------------------------------------------------------------
-
-{
-import de.jplag.text.InputState;
-import de.jplag.text.AntlrParserToken;
-}
-
-class TextLexer extends Lexer;
-options {
-    //  exportVocab=Text;    // call the vocabulary "Text"
-    testLiterals = false;    // don't automatically test for literals
-    k = 2;                   // two characters of lookahead
-    charVocabulary = '\u0000'..'\u00FF';
-}
-
-{
-    public void newline() {
-        super.newline();
-        ((InputState) inputState).setColumnIndex(1);
-    }
-
-    public void consume() throws antlr.CharStreamException {
-        if (inputState.guessing == 0) {
-            InputState state = (InputState) inputState;
-            if (text.length() == 0) {
-                // remember token start column
-                state.setTokenColumnIndex(state.getColumnIndex());
-            }
-            state.setColumnIndex(state.getColumnIndex() + 1);
-        }
-        super.consume();
-    }
-
-    protected Token makeToken(int t) {
-        AntlrParserToken token = (AntlrParserToken) super.makeToken(t);
-        token.setColumn(((InputState) inputState).getTokenColumnIndex());
-        return token;
-    }
-}
-
-WORD
-options { paraphrase = "an identifier"; } :
-  (( '0'..'9') | ('A'..'Z') | ('a'..'z') |
-   ('\300' .. '\326') | ('\330' .. '\366') | ('\370' .. '\377'))+ ;
-
-PUNCTUATION : (	'!' | '"' | '\'' | '(' | ')' | ',' | '-' | '.' |
-		':' | ';' | '?'  | '[' | ']' | '`' | '{' | '}' |
-		'\253' | '\264' | '\273' | '\277' | '\0') ;
-
-SPECIALS : ('#' | '$' | '%' | '&' | '+' | '<' | '=' | '*' |
-	    '/' | '>' | '@' | '\\' | '^' | '_' | '|' | '~' |
-	    ('\241' .. '\252') | ('\254' .. '\263') | ('\265' .. '\272') |
-	    ('\274' .. '\276') | '\327' | '\367' | ('\200' .. '\237') ) ;
-
-// Whitespace -- ignored
-SPACE : ( ' '
-	  |	'\t' //{ ((InputState)inputState).setColumnIndex((InputState)inputState).getColumnIndex() + 7); }
-	  |	'\f'
-	  |     '\240'
-	  |     ('\001' .. '\010')
-	  |     ('\016' .. '\037')
-	  |     '\013'
-	  |     '\177'
-	) { _ttype = Token.SKIP; } ;
-
-NEWLINE	: // handle newlines
-  ( "\r\n" | // Evil DOS
-    '\r'   | // Macintosh
-    '\n'   ) // Unix (the right way)
-  { newline(); _ttype = Token.SKIP; } ;

+ 0 - 79
jplag.frontend.text/src/main/java/de/jplag/text/AntlrParserToken.java

@@ -1,79 +0,0 @@
-package de.jplag.text;
-
-import antlr.Token;
-
-/**
- * Token of the ANTLR grammar, needs to be converted into a JPlag token by the parser adapter.
- */
-public class AntlrParserToken extends Token {
-    /**
-     * This variable holds the line number of the current token.
-     */
-    private int line = -1;
-
-    /**
-     * This variable holds the column of the current token in its line.
-     */
-    private int column = -1;
-
-    /**
-     * This variable holds the label of the current token.
-     */
-    private String text = null;
-
-    /**
-     * This variable holds the identifier of the current token.
-     */
-    private int id = -1;
-
-    public AntlrParserToken() {
-        super();
-    }
-
-    @Override
-    public void setLine(int line) {
-        this.line = line;
-    }
-
-    @Override
-    public void setColumn(int column) {
-        this.column = column;
-    }
-
-    public void setID(int id) {
-        this.id = id;
-    }
-
-    @Override
-    public void setText(String text) {
-        this.text = (text != null ? text.intern() : null);
-    }
-
-    @Override
-    public int getColumn() {
-        return column;
-    }
-
-    @Override
-    public int getLine() {
-        return line;
-    }
-
-    @Override
-    public String getText() {
-        return text;
-    }
-
-    public int getID() {
-        return id;
-    }
-
-    public int getLength() {
-        return text.length();
-    }
-
-    @Override
-    public String toString() {
-        return "{\"" + getText() + "\", <" + getType() + ">, " + getLine() + " " + getColumn() + "}";
-    }
-}

+ 0 - 49
jplag.frontend.text/src/main/java/de/jplag/text/InputState.java

@@ -1,49 +0,0 @@
-package de.jplag.text;
-
-import java.io.InputStream;
-import java.io.Reader;
-
-import antlr.InputBuffer;
-import antlr.LexerSharedInputState;
-
-/**
- * This object contains the data associated with an input stream of characters. Multiple lexers share a single
- * LexerSharedInputState to lex the same input stream.
- */
-public class InputState extends LexerSharedInputState {
-    private int columnIndex = 1;
-    private int tokenColumnIndex = 1;
-
-    public InputState(InputBuffer inputBuffer) {
-        super(inputBuffer);
-    }
-
-    public InputState(InputStream inputStream) {
-        super(inputStream);
-    }
-
-    public InputState(Reader inputReader) {
-        super(inputReader);
-    }
-
-    @Override
-    public int getLine() {
-        return line;
-    }
-
-    public int getColumnIndex() {
-        return columnIndex;
-    }
-
-    public void setColumnIndex(int columnIndex) {
-        this.columnIndex = columnIndex;
-    }
-
-    public int getTokenColumnIndex() {
-        return tokenColumnIndex;
-    }
-
-    public void setTokenColumnIndex(int tokenColumnIndex) {
-        this.tokenColumnIndex = tokenColumnIndex;
-    }
-}

+ 83 - 35
jplag.frontend.text/src/main/java/de/jplag/text/ParserAdapter.java

@@ -1,26 +1,48 @@
 package de.jplag.text;
 
 import java.io.File;
-import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-
-import antlr.Token;
+import java.util.Properties;
 
 import de.jplag.AbstractParser;
+import de.jplag.Token;
 import de.jplag.TokenConstants;
 
+import edu.stanford.nlp.ling.CoreLabel;
+import edu.stanford.nlp.pipeline.CoreDocument;
+import edu.stanford.nlp.pipeline.StanfordCoreNLP;
+
 public class ParserAdapter extends AbstractParser {
 
+    private static final char LF = '\n';
+    private static final char CR = '\r';
+    private static final String ANNOTATORS_KEY = "annotators";
+    private static final String ANNOTATORS_VALUE = "tokenize";
     private final Map<String, Integer> tokenTypes = new HashMap<>();
-    private int tokenTypeIndex = 1; // 0 is FILE_END token, SEPARATOR is not used as there are no methods.
+    private final StanfordCoreNLP pipeline;
+    private int tokenTypeIndex = 2; // 0 is FILE_END token, 1 is SEPARATOR_TOKEN, so start at 2.
 
-    private List<de.jplag.Token> tokens;
+    private List<Token> tokens;
     private String currentFile;
+    private int currentLine;
+    /**
+     * The position of the current line break in the content string
+     */
+    private int currentLineBreakIndex;
+
+    public ParserAdapter() {
+        Properties properties = new Properties();
+        properties.put(ANNOTATORS_KEY, ANNOTATORS_VALUE);
+        this.pipeline = new StanfordCoreNLP(properties);
+    }
 
-    public List<de.jplag.Token> parse(File directory, String[] files) {
+    public List<Token> parse(File directory, String[] files) {
         tokens = new ArrayList<>();
         errors = 0;
         for (String file : files) {
@@ -33,42 +55,69 @@ public class ParserAdapter extends AbstractParser {
         return tokens;
     }
 
+    private boolean parseFile(File directory, String file) {
+        this.currentFile = file;
+        this.currentLine = 1; // lines start at 1
+        this.currentLineBreakIndex = 0;
+        Path filePath = directory.toPath().resolve(file);
+        String content = readFile(filePath);
+        if (content == null) {
+            return false;
+        }
+        int lastTokenEnd = 0;
+        CoreDocument coreDocument = pipeline.processToCoreDocument(content);
+        for (CoreLabel token : coreDocument.tokens()) {
+            advanceLineBreaks(content, lastTokenEnd, token.beginPosition());
+            lastTokenEnd = token.endPosition();
+            if (isWord(token)) {
+                addToken(token);
+            }
+        }
+        return true;
+    }
+
     /**
-     * Converts a ANTLR token into a JPlag token and adds it to the token list.
-     * @param token is the ANTLR token to convert.
+     * Scan for line breaks and increase {@link #currentLine} and {@link #currentLineBreakIndex} accordingly.
+     * @param content the file content
+     * @param lastTokenEnd the end position of the last token
+     * @param nextTokenBegin the begin position of the next token
      */
-    public void add(Token token) {
-        if (token instanceof AntlrParserToken parserToken) {
-            String text = token.getText();
-            int type = getTokenType(text);
-            tokens.add(new TextToken(text, type, currentFile, parserToken));
-        } else {
-            throw new IllegalArgumentException("Illegal token implementation: " + token);
+    private void advanceLineBreaks(String content, int lastTokenEnd, int nextTokenBegin) {
+        for (int i = lastTokenEnd; i < nextTokenBegin; i++) {
+            if (content.charAt(i) == LF) {
+                currentLine++;
+                currentLineBreakIndex = i;
+            } else if (content.charAt(i) == CR) {
+                if (i + 1 < content.length() && content.charAt(i + 1) == LF) { // CRLF
+                    i++; // skip following LF
+                }
+                currentLine++;
+                currentLineBreakIndex = i;
+            }
         }
     }
 
-    private boolean parseFile(File directory, String file) {
-        InputState inputState = null;
-        try (FileInputStream inputStream = new FileInputStream(new File(directory, file))) {
-            currentFile = file;
-            // Create a scanner that reads from the input stream passed to us
-            inputState = new InputState(inputStream);
-            TextLexer lexer = new TextLexer(inputState);
-            lexer.setFilename(file);
-            lexer.setTokenObjectClass("de.jplag.text.AntlrParserToken");
+    private boolean isWord(CoreLabel token) {
+        // consider a token as a word if it contains any alphanumeric character
+        String text = token.originalText();
+        return text.chars().anyMatch(it -> Character.isAlphabetic(it) || Character.isDigit(it));
+    }
 
-            // Create a parser that reads from the scanner
-            TextParser parser = new TextParser(lexer);
-            parser.setFilename(file);
-            parser.setParserAdapter(this);
+    private void addToken(CoreLabel label) {
+        String text = label.originalText();
+        int type = getTokenType(text);
+        int column = label.beginPosition() - currentLineBreakIndex;
+        int length = label.endPosition() - label.beginPosition();
+        tokens.add(new TextToken(text, type, currentFile, currentLine, column, length));
+    }
 
-            // start parsing at the compilationUnit rule
-            parser.file();
-        } catch (Exception e) {
-            logger.error("Parsing Error in " + file + " (line " + (inputState != null ? "" + inputState.getLine() : "") + "):" + e.getMessage(), e);
-            return false;
+    private String readFile(Path filePath) {
+        try {
+            return Files.readString(filePath);
+        } catch (IOException e) {
+            logger.error("Error reading from file {}", filePath, e);
+            return null;
         }
-        return true;
     }
 
     private int getTokenType(String text) {
@@ -80,6 +129,5 @@ public class ParserAdapter extends AbstractParser {
             return ++tokenTypeIndex;
         });
         return tokenTypes.get(text);
-
     }
 }

+ 2 - 2
jplag.frontend.text/src/main/java/de/jplag/text/TextToken.java

@@ -12,8 +12,8 @@ public class TextToken extends Token {
         this.text = NO_TEXT;
     }
 
-    public TextToken(String text, int type, String file, AntlrParserToken parserToken) {
-        super(type, file, parserToken.getLine(), parserToken.getColumn(), parserToken.getLength());
+    public TextToken(String text, int type, String file, int line, int column, int length) {
+        super(type, file, line, column, length);
         this.text = text.toLowerCase();
     }
 

+ 25 - 2
jplag.frontend.text/src/test/java/jplag/text/TextFrontendTest.java

@@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.List;
@@ -11,6 +13,9 @@ import java.util.Map;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,8 +50,26 @@ class TextFrontendTest {
         Map<Integer, Token> tokenTypes = new HashMap<>();
         result.forEach(it -> tokenTypes.put(it.getType(), it));
 
-        assertEquals(293, result.size());
-        assertEquals(156, tokenTypes.values().size());
+        assertEquals(283, result.size());
+        assertEquals(158, tokenTypes.values().size());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"\n", "\r", "\r\n",})
+    void testLineBreakInputs(String input, @TempDir Path tempDir) throws IOException {
+        Path file = tempDir.resolve("input.txt");
+        Files.writeString(file, input);
+        List<Token> result = frontend.parse(tempDir.toFile(), new String[] {"input.txt"});
+        assertEquals(1, result.size());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"\ntoken", "\rtoken", "\r\ntoken",})
+    void testTokenAfterLineBreak(String input, @TempDir Path tempDir) throws IOException {
+        Path file = tempDir.resolve("input.txt");
+        Files.writeString(file, input);
+        List<Token> result = frontend.parse(tempDir.toFile(), new String[] {"input.txt"});
+        assertEquals(2, result.get(0).getLine());
     }
 
 }

+ 7 - 0
pom.xml

@@ -112,6 +112,13 @@
                 <version>0.9.0</version>
             </dependency>
 
+            <!-- CoreNLP -->
+            <dependency>
+                <groupId>edu.stanford.nlp</groupId>
+                <artifactId>stanford-corenlp</artifactId>
+                <version>4.5.0</version>
+            </dependency>
+
             <!-- LOGGER -->
             <dependency>
                 <groupId>org.slf4j</groupId>