View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2014  Oliver Burn
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  package com.puppycrawl.tools.checkstyle;
20  
21  import java.io.File;
22  import java.io.Reader;
23  import java.io.StringReader;
24  import java.util.AbstractMap.SimpleEntry;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.List;
28  import java.util.Map.Entry;
29  import java.util.Set;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  
34  import antlr.CommonHiddenStreamToken;
35  import antlr.RecognitionException;
36  import antlr.Token;
37  import antlr.TokenStreamException;
38  import antlr.TokenStreamHiddenTokenFilter;
39  import antlr.TokenStreamRecognitionException;
40  
41  import com.google.common.collect.HashMultimap;
42  import com.google.common.collect.Multimap;
43  import com.google.common.collect.Sets;
44  import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
45  import com.puppycrawl.tools.checkstyle.api.Check;
46  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
47  import com.puppycrawl.tools.checkstyle.api.Configuration;
48  import com.puppycrawl.tools.checkstyle.api.Context;
49  import com.puppycrawl.tools.checkstyle.api.DetailAST;
50  import com.puppycrawl.tools.checkstyle.api.FileContents;
51  import com.puppycrawl.tools.checkstyle.api.FileText;
52  import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
53  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
54  import com.puppycrawl.tools.checkstyle.api.Utils;
55  import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaLexer;
56  import com.puppycrawl.tools.checkstyle.grammars.GeneratedJavaRecognizer;
57  
58  import static com.puppycrawl.tools.checkstyle.Utils.fileExtensionMatches;
59  
60  /**
61   * Responsible for walking an abstract syntax tree and notifying interested
62   * checks at each each node.
63   *
64   * @author Oliver Burn
65   * @version 1.0
66   */
67  public final class TreeWalker
68      extends AbstractFileSetCheck
69  {
70      /**
71       * State of AST.
72       * Indicates whether tree contains certain nodes.
73       */
74      private static enum AstState {
75          /**
76           * Ordinary tree.
77           */
78          ORDINARY,
79  
80          /**
81           * AST contains comment nodes.
82           */
83          WITH_COMMENTS
84      }
85  
86      /** default distance between tab stops */
87      private static final int DEFAULT_TAB_WIDTH = 8;
88  
89      /** maps from token name to ordinary checks */
90      private final Multimap<String, Check> tokenToOrdinaryChecks =
91          HashMultimap.create();
92  
93      /** maps from token name to comment checks */
94      private final Multimap<String, Check> tokenToCommentChecks =
95              HashMultimap.create();
96  
97      /** registered ordinary checks, that don't use comment nodes */
98      private final Set<Check> ordinaryChecks = Sets.newHashSet();
99  
100     /** registered comment checks */
101     private final Set<Check> commentChecks = Sets.newHashSet();
102 
103     /** the distance between tab stops */
104     private int tabWidth = DEFAULT_TAB_WIDTH;
105 
106     /** cache file **/
107     private PropertyCacheFile cache = new PropertyCacheFile(null, null);
108 
109     /** class loader to resolve classes with. **/
110     private ClassLoader classLoader;
111 
112     /** context of child components */
113     private Context childContext;
114 
115     /** a factory for creating submodules (i.e. the Checks) */
116     private ModuleFactory moduleFactory;
117 
118     /** logger for debug purpose */
119     private static final Log LOG =
120         LogFactory.getLog("com.puppycrawl.tools.checkstyle.TreeWalker");
121 
122     /** the file extensions that are accepted */
123     private String[] fileExtensions;
124 
125     /**
126      * Creates a new <code>TreeWalker</code> instance.
127      */
128     public TreeWalker()
129     {
130         setFileExtensions(new String[]{"java"});
131     }
132 
133     /** @param tabWidth the distance between tab stops */
134     public void setTabWidth(int tabWidth)
135     {
136         this.tabWidth = tabWidth;
137     }
138 
139     /** @param fileName the cache file */
140     public void setCacheFile(String fileName)
141     {
142         final Configuration configuration = getConfiguration();
143         cache = new PropertyCacheFile(configuration, fileName);
144     }
145 
146     /** @param classLoader class loader to resolve classes with. */
147     public void setClassLoader(ClassLoader classLoader)
148     {
149         this.classLoader = classLoader;
150     }
151 
152     /**
153      * Sets the module factory for creating child modules (Checks).
154      * @param moduleFactory the factory
155      */
156     public void setModuleFactory(ModuleFactory moduleFactory)
157     {
158         this.moduleFactory = moduleFactory;
159     }
160 
161     @Override
162     public void finishLocalSetup()
163     {
164         final DefaultContext checkContext = new DefaultContext();
165         checkContext.add("classLoader", classLoader);
166         checkContext.add("messages", getMessageCollector());
167         checkContext.add("severity", getSeverity());
168         // TODO: hmmm.. this looks less than elegant
169         // we have just parsed the string,
170         // now we're recreating it only to parse it again a few moments later
171         checkContext.add("tabWidth", String.valueOf(tabWidth));
172 
173         childContext = checkContext;
174     }
175 
176     @Override
177     public void setupChild(Configuration childConf)
178         throws CheckstyleException
179     {
180         // TODO: improve the error handing
181         final String name = childConf.getName();
182         final Object module = moduleFactory.createModule(name);
183         if (!(module instanceof Check)) {
184             throw new CheckstyleException(
185                 "TreeWalker is not allowed as a parent of " + name);
186         }
187         final Check c = (Check) module;
188         c.contextualize(childContext);
189         c.configure(childConf);
190         c.init();
191 
192         registerCheck(c);
193     }
194 
195     @Override
196     protected void processFiltered(File file, List<String> lines)
197     {
198         // check if already checked and passed the file
199         final String fileName = file.getPath();
200         final long timestamp = file.lastModified();
201         if (cache.alreadyChecked(fileName, timestamp)
202                  || !fileExtensionMatches(file, fileExtensions))
203         {
204             return;
205         }
206 
207         final String msg = "%s occurred during the analysis of file %s .";
208 
209         try {
210             final FileText text = FileText.fromLines(file, lines);
211             final FileContents contents = new FileContents(text);
212             final DetailAST rootAST = TreeWalker.parse(contents);
213 
214             getMessageCollector().reset();
215 
216             walk(rootAST, contents, AstState.ORDINARY);
217 
218             final DetailAST astWithComments = appendHiddenCommentNodes(rootAST);
219 
220             walk(astWithComments, contents, AstState.WITH_COMMENTS);
221         }
222         catch (final RecognitionException re) {
223             final String exceptionMsg = String.format(msg, "RecognitionException", fileName);
224             Utils.getExceptionLogger().error(exceptionMsg);
225             getMessageCollector().add(
226                 new LocalizedMessage(
227                     re.getLine(),
228                     re.getColumn(),
229                     Defn.CHECKSTYLE_BUNDLE,
230                     "general.exception",
231                     new String[] {re.getMessage()},
232                     getId(),
233                     this.getClass(), null));
234         }
235         catch (final TokenStreamRecognitionException tre) {
236             final String exceptionMsg = String.format(msg, "TokenStreamRecognitionException",
237                      fileName);
238             Utils.getExceptionLogger().error(exceptionMsg);
239             final RecognitionException re = tre.recog;
240             if (re != null) {
241                 getMessageCollector().add(
242                     new LocalizedMessage(
243                         re.getLine(),
244                         re.getColumn(),
245                         Defn.CHECKSTYLE_BUNDLE,
246                         "general.exception",
247                         new String[] {re.getMessage()},
248                         getId(),
249                         this.getClass(), null));
250             }
251             else {
252                 getMessageCollector().add(
253                     new LocalizedMessage(
254                         0,
255                         Defn.CHECKSTYLE_BUNDLE,
256                         "general.exception",
257                         new String[]
258                         {"TokenStreamRecognitionException occured."},
259                         getId(),
260                         this.getClass(), null));
261             }
262         }
263         catch (final TokenStreamException te) {
264             final String exceptionMsg = String.format(msg,
265                     "TokenStreamException", fileName);
266             Utils.getExceptionLogger().error(exceptionMsg);
267             getMessageCollector().add(
268                 new LocalizedMessage(
269                     0,
270                     Defn.CHECKSTYLE_BUNDLE,
271                     "general.exception",
272                     new String[] {te.getMessage()},
273                     getId(),
274                     this.getClass(), null));
275         }
276         catch (final Throwable err) {
277             final String exceptionMsg = String.format(msg, "Exception", fileName);
278             Utils.getExceptionLogger().error(exceptionMsg);
279             err.printStackTrace();
280             getMessageCollector().add(
281                 new LocalizedMessage(
282                     0,
283                     Defn.CHECKSTYLE_BUNDLE,
284                     "general.exception",
285                     new String[] {"" + err},
286                     getId(),
287                     this.getClass(), null));
288         }
289 
290         if (getMessageCollector().size() == 0) {
291             cache.checkedOk(fileName, timestamp);
292         }
293     }
294 
295     /**
296      * Register a check for a given configuration.
297      * @param check the check to register
298      * @throws CheckstyleException if an error occurs
299      */
300     private void registerCheck(Check check)
301         throws CheckstyleException
302     {
303         final int[] tokens;
304         final Set<String> checkTokens = check.getTokenNames();
305         if (!checkTokens.isEmpty()) {
306             tokens = check.getRequiredTokens();
307 
308             //register configured tokens
309             final int[] acceptableTokens = check.getAcceptableTokens();
310             Arrays.sort(acceptableTokens);
311             for (String token : checkTokens) {
312                 try {
313                     final int tokenId = TokenTypes.getTokenId(token);
314                     if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
315                         registerCheck(token, check);
316                     }
317                     // TODO: else log warning
318                 }
319                 catch (final IllegalArgumentException ex) {
320                     throw new CheckstyleException("illegal token \""
321                         + token + "\" in check " + check, ex);
322                 }
323             }
324         }
325         else {
326             tokens = check.getDefaultTokens();
327         }
328         for (int element : tokens) {
329             registerCheck(element, check);
330         }
331         if (check.isCommentNodesRequired()) {
332             commentChecks.add(check);
333         }
334         else {
335             ordinaryChecks.add(check);
336         }
337     }
338 
339     /**
340      * Register a check for a specified token id.
341      * @param tokenID the id of the token
342      * @param check the check to register
343      */
344     private void registerCheck(int tokenID, Check check)
345     {
346         registerCheck(TokenTypes.getTokenName(tokenID), check);
347     }
348 
349     /**
350      * Register a check for a specified token name
351      * @param token the name of the token
352      * @param check the check to register
353      */
354     private void registerCheck(String token, Check check)
355     {
356         if (check.isCommentNodesRequired()) {
357             tokenToCommentChecks.put(token, check);
358         }
359         else if (TokenTypes.isCommentType(token)) {
360             LOG.warn("Check '"
361                     + check.getClass().getName()
362                     + "' waits for comment type token ('"
363                     + token
364                     + "') and should override 'isCommentNodesRequred()'"
365                     + " method to return 'true'");
366         }
367         else {
368             tokenToOrdinaryChecks.put(token, check);
369         }
370     }
371 
372     /**
373      * Initiates the walk of an AST.
374      * @param ast the root AST
375      * @param contents the contents of the file the AST was generated from.
376      * @param astState state of AST.
377      */
378     private void walk(DetailAST ast, FileContents contents
379             , AstState astState)
380     {
381         notifyBegin(ast, contents, astState);
382 
383         // empty files are not flagged by javac, will yield ast == null
384         if (ast != null) {
385             processIter(ast, astState);
386         }
387 
388         notifyEnd(ast, astState);
389     }
390 
391     /**
392      * Notify checks that we are about to begin walking a tree.
393      * @param rootAST the root of the tree.
394      * @param contents the contents of the file the AST was generated from.
395      * @param astState state of AST.
396      */
397     private void notifyBegin(DetailAST rootAST, FileContents contents
398             , AstState astState)
399     {
400         Set<Check> checks;
401 
402         if (astState == AstState.WITH_COMMENTS) {
403             checks = commentChecks;
404         }
405         else {
406             checks = ordinaryChecks;
407         }
408 
409         for (Check ch : checks) {
410             ch.setFileContents(contents);
411             ch.beginTree(rootAST);
412         }
413     }
414 
415     /**
416      * Notify checks that we have finished walking a tree.
417      * @param rootAST the root of the tree.
418      * @param astState state of AST.
419      */
420     private void notifyEnd(DetailAST rootAST, AstState astState)
421     {
422         Set<Check> checks;
423 
424         if (astState == AstState.WITH_COMMENTS) {
425             checks = commentChecks;
426         }
427         else {
428             checks = ordinaryChecks;
429         }
430 
431         for (Check ch : checks) {
432             ch.finishTree(rootAST);
433         }
434     }
435 
436     /**
437      * Notify checks that visiting a node.
438      * @param ast the node to notify for.
439      * @param astState state of AST.
440      */
441     private void notifyVisit(DetailAST ast, AstState astState)
442     {
443         Collection<Check> visitors;
444         final String tokenType = TokenTypes.getTokenName(ast.getType());
445 
446         if (astState == AstState.WITH_COMMENTS) {
447             if (!tokenToCommentChecks.containsKey(tokenType)) {
448                 return;
449             }
450             visitors = tokenToCommentChecks.get(tokenType);
451         }
452         else {
453             if (!tokenToOrdinaryChecks.containsKey(tokenType)) {
454                 return;
455             }
456             visitors = tokenToOrdinaryChecks.get(tokenType);
457         }
458 
459         for (Check c : visitors) {
460             c.visitToken(ast);
461         }
462     }
463 
464     /**
465      * Notify checks that leaving a node.
466      * @param ast
467      *        the node to notify for
468      * @param astState state of AST.
469      */
470     private void notifyLeave(DetailAST ast, AstState astState)
471     {
472         Collection<Check> visitors;
473         final String tokenType = TokenTypes.getTokenName(ast.getType());
474 
475         if (astState == AstState.WITH_COMMENTS) {
476             if (!tokenToCommentChecks.containsKey(tokenType)) {
477                 return;
478             }
479             visitors = tokenToCommentChecks.get(tokenType);
480         }
481         else {
482             if (!tokenToOrdinaryChecks.containsKey(tokenType)) {
483                 return;
484             }
485             visitors = tokenToOrdinaryChecks.get(tokenType);
486         }
487 
488         for (Check ch : visitors) {
489             ch.leaveToken(ast);
490         }
491     }
492 
493     /**
494      * Static helper method to parses a Java source file.
495      *
496      * @param contents
497      *                contains the contents of the file
498      * @throws TokenStreamException
499      *                 if lexing failed
500      * @throws RecognitionException
501      *                 if parsing failed
502      * @return the root of the AST
503      */
504     public static DetailAST parse(FileContents contents)
505         throws RecognitionException, TokenStreamException
506     {
507         final String fullText = contents.getText().getFullText().toString();
508         final Reader sr = new StringReader(fullText);
509         final GeneratedJavaLexer lexer = new GeneratedJavaLexer(sr);
510         lexer.setFilename(contents.getFilename());
511         lexer.setCommentListener(contents);
512         lexer.setTreatAssertAsKeyword(true);
513         lexer.setTreatEnumAsKeyword(true);
514         lexer.setTokenObjectClass("antlr.CommonHiddenStreamToken");
515 
516         final TokenStreamHiddenTokenFilter filter =
517                 new TokenStreamHiddenTokenFilter(lexer);
518         filter.hide(TokenTypes.SINGLE_LINE_COMMENT);
519         filter.hide(TokenTypes.BLOCK_COMMENT_BEGIN);
520 
521         final GeneratedJavaRecognizer parser =
522             new GeneratedJavaRecognizer(filter);
523         parser.setFilename(contents.getFilename());
524         parser.setASTNodeClass(DetailAST.class.getName());
525         parser.compilationUnit();
526 
527         return (DetailAST) parser.getAST();
528     }
529 
530     @Override
531     public void destroy()
532     {
533         for (Check c : ordinaryChecks) {
534             c.destroy();
535         }
536         for (Check c : commentChecks) {
537             c.destroy();
538         }
539         cache.destroy();
540         super.destroy();
541     }
542 
543     /**
544      * Processes a node calling interested checks at each node.
545      * Uses iterative algorithm.
546      * @param root the root of tree for process
547      * @param astState state of AST.
548      */
549     private void processIter(DetailAST root, AstState astState)
550     {
551         DetailAST curNode = root;
552         while (curNode != null) {
553             notifyVisit(curNode, astState);
554             DetailAST toVisit = curNode.getFirstChild();
555             while ((curNode != null) && (toVisit == null)) {
556                 notifyLeave(curNode, astState);
557                 toVisit = curNode.getNextSibling();
558                 if (toVisit == null) {
559                     curNode = curNode.getParent();
560                 }
561             }
562             curNode = toVisit;
563         }
564     }
565 
566     /**
567      * Appends comment nodes to existing AST.
568      * It traverses each node in AST, looks for hidden comment tokens
569      * and appends found comment tokens as nodes in AST.
570      * @param root
571      *        root of AST.
572      * @return root of AST with comment nodes.
573      */
574     private static DetailAST appendHiddenCommentNodes(DetailAST root)
575     {
576         DetailAST result = root;
577         DetailAST curNode = root;
578         DetailAST lastNode = root;
579 
580         while (curNode != null) {
581             if (isPositionGreater(curNode, lastNode)) {
582                 lastNode = curNode;
583             }
584 
585             CommonHiddenStreamToken tokenBefore = curNode.getHiddenBefore();
586             DetailAST currentSibling = curNode;
587             while (tokenBefore != null) { // threat multiple comments
588                 final DetailAST newCommentNode =
589                          createCommentAstFromToken(tokenBefore);
590 
591                 currentSibling.addPreviousSibling(newCommentNode);
592 
593                 if (currentSibling == result) {
594                     result = newCommentNode;
595                 }
596 
597                 currentSibling = newCommentNode;
598                 tokenBefore = tokenBefore.getHiddenBefore();
599             }
600 
601             DetailAST toVisit = curNode.getFirstChild();
602             while ((curNode != null) && (toVisit == null)) {
603                 toVisit = curNode.getNextSibling();
604                 if (toVisit == null) {
605                     curNode = curNode.getParent();
606                 }
607             }
608             curNode = toVisit;
609         }
610         if (lastNode != null) {
611             CommonHiddenStreamToken tokenAfter = lastNode.getHiddenAfter();
612             DetailAST currentSibling = lastNode;
613             while (tokenAfter != null) {
614                 final DetailAST newCommentNode =
615                         createCommentAstFromToken(tokenAfter);
616 
617                 currentSibling.addNextSibling(newCommentNode);
618 
619                 currentSibling = newCommentNode;
620                 tokenAfter = tokenAfter.getHiddenAfter();
621             }
622         }
623         return result;
624     }
625 
626     /**
627      * Checks if position of first DetailAST is greater than position of
628      * second DetailAST. Position is line number and column number in source
629      * file.
630      * @param ast1
631      *        first DetailAST node.
632      * @param ast2
633      *        second DetailAST node.
634      * @return true if position of ast1 is greater than position of ast2.
635      */
636     private static boolean isPositionGreater(DetailAST ast1, DetailAST ast2)
637     {
638         if (ast1.getLineNo() > ast2.getLineNo()) {
639             return true;
640         }
641         else if (ast1.getLineNo() < ast2.getLineNo()) {
642             return false;
643         }
644         else {
645             if (ast1.getColumnNo() > ast2.getColumnNo()) {
646                 return true;
647             }
648         }
649         return false;
650     }
651 
652     /**
653      * Create comment AST from token. Depending on token type
654      * SINGLE_LINE_COMMENT or BLOCK_COMMENT_BEGIN is created.
655      * @param token
656      *        Token object.
657      * @return DetailAST of comment node.
658      */
659     private static DetailAST createCommentAstFromToken(Token token)
660     {
661         switch (token.getType()) {
662             case TokenTypes.SINGLE_LINE_COMMENT:
663                 return createSlCommentNode(token);
664             case TokenTypes.BLOCK_COMMENT_BEGIN:
665                 return createBlockCommentNode(token);
666             default:
667                 throw new IllegalArgumentException("Unknown comment type");
668         }
669     }
670 
671     /**
672      * Create single-line comment from token.
673      * @param token
674      *        Token object.
675      * @return DetailAST with SINGLE_LINE_COMMENT type.
676      */
677     private static DetailAST createSlCommentNode(Token token)
678     {
679         final DetailAST slComment = new DetailAST();
680         slComment.setType(TokenTypes.SINGLE_LINE_COMMENT);
681         slComment.setText("//");
682 
683         // column counting begins from 0
684         slComment.setColumnNo(token.getColumn() - 1);
685         slComment.setLineNo(token.getLine());
686 
687         final DetailAST slCommentContent = new DetailAST();
688         slCommentContent.initialize(token);
689         slCommentContent.setType(TokenTypes.COMMENT_CONTENT);
690 
691         // column counting begins from 0
692         // plus length of '//'
693         slCommentContent.setColumnNo(token.getColumn() - 1 + 2);
694         slCommentContent.setLineNo(token.getLine());
695         slCommentContent.setText(token.getText());
696 
697         slComment.addChild(slCommentContent);
698         return slComment;
699     }
700 
701     /**
702      * Create block comment from token.
703      * @param token
704      *        Token object.
705      * @return DetailAST with BLOCK_COMMENT type.
706      */
707     private static DetailAST createBlockCommentNode(Token token)
708     {
709         final DetailAST blockComment = new DetailAST();
710         blockComment.initialize(TokenTypes.BLOCK_COMMENT_BEGIN, "/*");
711 
712         // column counting begins from 0
713         blockComment.setColumnNo(token.getColumn() - 1);
714         blockComment.setLineNo(token.getLine());
715 
716         final DetailAST blockCommentContent = new DetailAST();
717         blockCommentContent.initialize(token);
718         blockCommentContent.setType(TokenTypes.COMMENT_CONTENT);
719 
720         // column counting begins from 0
721         // plus length of '/*'
722         blockCommentContent.setColumnNo(token.getColumn() - 1 + 2);
723         blockCommentContent.setLineNo(token.getLine());
724         blockCommentContent.setText(token.getText());
725 
726         final DetailAST blockCommentClose = new DetailAST();
727         blockCommentClose.initialize(TokenTypes.BLOCK_COMMENT_END, "*/");
728 
729         final Entry<Integer, Integer> linesColumns = countLinesColumns(
730                 token.getText(), token.getLine(), token.getColumn());
731         blockCommentClose.setLineNo(linesColumns.getKey());
732         blockCommentClose.setColumnNo(linesColumns.getValue());
733 
734         blockComment.addChild(blockCommentContent);
735         blockComment.addChild(blockCommentClose);
736         return blockComment;
737     }
738 
739     /**
740      * Count lines and columns (in last line) in text.
741      * @param text
742      *        String.
743      * @param initialLinesCnt
744      *        initial value of lines counter.
745      * @param initialColumnsCnt
746      *        initial value of columns counter.
747      * @return entry(pair), first element is lines counter, second - columns
748      *         counter.
749      */
750     private static Entry<Integer, Integer> countLinesColumns(
751             String text, int initialLinesCnt, int initialColumnsCnt)
752     {
753         int lines = initialLinesCnt;
754         int columns = initialColumnsCnt;
755         for (char c : text.toCharArray()) {
756             switch (c) {
757                 case '\n':
758                     lines++;
759                     columns = 0;
760                     break;
761                 default:
762                     columns++;
763             }
764         }
765         return new SimpleEntry<Integer, Integer>(lines, columns);
766     }
767 
768 }