View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2015 the original author or authors.
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                 final int tokenId = TokenTypes.getTokenId(token);
313                 if (Arrays.binarySearch(acceptableTokens, tokenId) >= 0) {
314                     registerCheck(token, check);
315                 }
316                 else {
317                     throw new CheckstyleException("Token \""
318                         + token + "\" was not found in Acceptable tokens list"
319                                 + " in check " + check);
320                 }
321             }
322         }
323         else {
324             tokens = check.getDefaultTokens();
325         }
326         for (int element : tokens) {
327             registerCheck(element, check);
328         }
329         if (check.isCommentNodesRequired()) {
330             commentChecks.add(check);
331         }
332         else {
333             ordinaryChecks.add(check);
334         }
335     }
336 
337     /**
338      * Register a check for a specified token id.
339      * @param tokenID the id of the token
340      * @param check the check to register
341      */
342     private void registerCheck(int tokenID, Check check)
343     {
344         registerCheck(TokenTypes.getTokenName(tokenID), check);
345     }
346 
347     /**
348      * Register a check for a specified token name
349      * @param token the name of the token
350      * @param check the check to register
351      */
352     private void registerCheck(String token, Check check)
353     {
354         if (check.isCommentNodesRequired()) {
355             tokenToCommentChecks.put(token, check);
356         }
357         else if (TokenTypes.isCommentType(token)) {
358             LOG.warn("Check '"
359                     + check.getClass().getName()
360                     + "' waits for comment type token ('"
361                     + token
362                     + "') and should override 'isCommentNodesRequred()'"
363                     + " method to return 'true'");
364         }
365         else {
366             tokenToOrdinaryChecks.put(token, check);
367         }
368     }
369 
370     /**
371      * Initiates the walk of an AST.
372      * @param ast the root AST
373      * @param contents the contents of the file the AST was generated from.
374      * @param astState state of AST.
375      */
376     private void walk(DetailAST ast, FileContents contents
377             , AstState astState)
378     {
379         notifyBegin(ast, contents, astState);
380 
381         // empty files are not flagged by javac, will yield ast == null
382         if (ast != null) {
383             processIter(ast, astState);
384         }
385 
386         notifyEnd(ast, astState);
387     }
388 
389     /**
390      * Notify checks that we are about to begin walking a tree.
391      * @param rootAST the root of the tree.
392      * @param contents the contents of the file the AST was generated from.
393      * @param astState state of AST.
394      */
395     private void notifyBegin(DetailAST rootAST, FileContents contents
396             , AstState astState)
397     {
398         Set<Check> checks;
399 
400         if (astState == AstState.WITH_COMMENTS) {
401             checks = commentChecks;
402         }
403         else {
404             checks = ordinaryChecks;
405         }
406 
407         for (Check ch : checks) {
408             ch.setFileContents(contents);
409             ch.beginTree(rootAST);
410         }
411     }
412 
413     /**
414      * Notify checks that we have finished walking a tree.
415      * @param rootAST the root of the tree.
416      * @param astState state of AST.
417      */
418     private void notifyEnd(DetailAST rootAST, AstState astState)
419     {
420         Set<Check> checks;
421 
422         if (astState == AstState.WITH_COMMENTS) {
423             checks = commentChecks;
424         }
425         else {
426             checks = ordinaryChecks;
427         }
428 
429         for (Check ch : checks) {
430             ch.finishTree(rootAST);
431         }
432     }
433 
434     /**
435      * Notify checks that visiting a node.
436      * @param ast the node to notify for.
437      * @param astState state of AST.
438      */
439     private void notifyVisit(DetailAST ast, AstState astState)
440     {
441         Collection<Check> visitors;
442         final String tokenType = TokenTypes.getTokenName(ast.getType());
443 
444         if (astState == AstState.WITH_COMMENTS) {
445             if (!tokenToCommentChecks.containsKey(tokenType)) {
446                 return;
447             }
448             visitors = tokenToCommentChecks.get(tokenType);
449         }
450         else {
451             if (!tokenToOrdinaryChecks.containsKey(tokenType)) {
452                 return;
453             }
454             visitors = tokenToOrdinaryChecks.get(tokenType);
455         }
456 
457         for (Check c : visitors) {
458             c.visitToken(ast);
459         }
460     }
461 
462     /**
463      * Notify checks that leaving a node.
464      * @param ast
465      *        the node to notify for
466      * @param astState state of AST.
467      */
468     private void notifyLeave(DetailAST ast, AstState astState)
469     {
470         Collection<Check> visitors;
471         final String tokenType = TokenTypes.getTokenName(ast.getType());
472 
473         if (astState == AstState.WITH_COMMENTS) {
474             if (!tokenToCommentChecks.containsKey(tokenType)) {
475                 return;
476             }
477             visitors = tokenToCommentChecks.get(tokenType);
478         }
479         else {
480             if (!tokenToOrdinaryChecks.containsKey(tokenType)) {
481                 return;
482             }
483             visitors = tokenToOrdinaryChecks.get(tokenType);
484         }
485 
486         for (Check ch : visitors) {
487             ch.leaveToken(ast);
488         }
489     }
490 
491     /**
492      * Static helper method to parses a Java source file.
493      *
494      * @param contents
495      *                contains the contents of the file
496      * @throws TokenStreamException
497      *                 if lexing failed
498      * @throws RecognitionException
499      *                 if parsing failed
500      * @return the root of the AST
501      */
502     public static DetailAST parse(FileContents contents)
503         throws RecognitionException, TokenStreamException
504     {
505         final String fullText = contents.getText().getFullText().toString();
506         final Reader sr = new StringReader(fullText);
507         final GeneratedJavaLexer lexer = new GeneratedJavaLexer(sr);
508         lexer.setFilename(contents.getFilename());
509         lexer.setCommentListener(contents);
510         lexer.setTreatAssertAsKeyword(true);
511         lexer.setTreatEnumAsKeyword(true);
512         lexer.setTokenObjectClass("antlr.CommonHiddenStreamToken");
513 
514         final TokenStreamHiddenTokenFilter filter =
515                 new TokenStreamHiddenTokenFilter(lexer);
516         filter.hide(TokenTypes.SINGLE_LINE_COMMENT);
517         filter.hide(TokenTypes.BLOCK_COMMENT_BEGIN);
518 
519         final GeneratedJavaRecognizer parser =
520             new GeneratedJavaRecognizer(filter);
521         parser.setFilename(contents.getFilename());
522         parser.setASTNodeClass(DetailAST.class.getName());
523         parser.compilationUnit();
524 
525         return (DetailAST) parser.getAST();
526     }
527 
528     @Override
529     public void destroy()
530     {
531         for (Check c : ordinaryChecks) {
532             c.destroy();
533         }
534         for (Check c : commentChecks) {
535             c.destroy();
536         }
537         cache.destroy();
538         super.destroy();
539     }
540 
541     /**
542      * Processes a node calling interested checks at each node.
543      * Uses iterative algorithm.
544      * @param root the root of tree for process
545      * @param astState state of AST.
546      */
547     private void processIter(DetailAST root, AstState astState)
548     {
549         DetailAST curNode = root;
550         while (curNode != null) {
551             notifyVisit(curNode, astState);
552             DetailAST toVisit = curNode.getFirstChild();
553             while ((curNode != null) && (toVisit == null)) {
554                 notifyLeave(curNode, astState);
555                 toVisit = curNode.getNextSibling();
556                 if (toVisit == null) {
557                     curNode = curNode.getParent();
558                 }
559             }
560             curNode = toVisit;
561         }
562     }
563 
564     /**
565      * Appends comment nodes to existing AST.
566      * It traverses each node in AST, looks for hidden comment tokens
567      * and appends found comment tokens as nodes in AST.
568      * @param root
569      *        root of AST.
570      * @return root of AST with comment nodes.
571      */
572     private static DetailAST appendHiddenCommentNodes(DetailAST root)
573     {
574         DetailAST result = root;
575         DetailAST curNode = root;
576         DetailAST lastNode = root;
577 
578         while (curNode != null) {
579             if (isPositionGreater(curNode, lastNode)) {
580                 lastNode = curNode;
581             }
582 
583             CommonHiddenStreamToken tokenBefore = curNode.getHiddenBefore();
584             DetailAST currentSibling = curNode;
585             while (tokenBefore != null) { // threat multiple comments
586                 final DetailAST newCommentNode =
587                          createCommentAstFromToken(tokenBefore);
588 
589                 currentSibling.addPreviousSibling(newCommentNode);
590 
591                 if (currentSibling == result) {
592                     result = newCommentNode;
593                 }
594 
595                 currentSibling = newCommentNode;
596                 tokenBefore = tokenBefore.getHiddenBefore();
597             }
598 
599             DetailAST toVisit = curNode.getFirstChild();
600             while ((curNode != null) && (toVisit == null)) {
601                 toVisit = curNode.getNextSibling();
602                 if (toVisit == null) {
603                     curNode = curNode.getParent();
604                 }
605             }
606             curNode = toVisit;
607         }
608         if (lastNode != null) {
609             CommonHiddenStreamToken tokenAfter = lastNode.getHiddenAfter();
610             DetailAST currentSibling = lastNode;
611             while (tokenAfter != null) {
612                 final DetailAST newCommentNode =
613                         createCommentAstFromToken(tokenAfter);
614 
615                 currentSibling.addNextSibling(newCommentNode);
616 
617                 currentSibling = newCommentNode;
618                 tokenAfter = tokenAfter.getHiddenAfter();
619             }
620         }
621         return result;
622     }
623 
624     /**
625      * Checks if position of first DetailAST is greater than position of
626      * second DetailAST. Position is line number and column number in source
627      * file.
628      * @param ast1
629      *        first DetailAST node.
630      * @param ast2
631      *        second DetailAST node.
632      * @return true if position of ast1 is greater than position of ast2.
633      */
634     private static boolean isPositionGreater(DetailAST ast1, DetailAST ast2)
635     {
636         if (ast1.getLineNo() > ast2.getLineNo()) {
637             return true;
638         }
639         else if (ast1.getLineNo() < ast2.getLineNo()) {
640             return false;
641         }
642         else {
643             if (ast1.getColumnNo() > ast2.getColumnNo()) {
644                 return true;
645             }
646         }
647         return false;
648     }
649 
650     /**
651      * Create comment AST from token. Depending on token type
652      * SINGLE_LINE_COMMENT or BLOCK_COMMENT_BEGIN is created.
653      * @param token
654      *        Token object.
655      * @return DetailAST of comment node.
656      */
657     private static DetailAST createCommentAstFromToken(Token token)
658     {
659         switch (token.getType()) {
660             case TokenTypes.SINGLE_LINE_COMMENT:
661                 return createSlCommentNode(token);
662             case TokenTypes.BLOCK_COMMENT_BEGIN:
663                 return createBlockCommentNode(token);
664             default:
665                 throw new IllegalArgumentException("Unknown comment type");
666         }
667     }
668 
669     /**
670      * Create single-line comment from token.
671      * @param token
672      *        Token object.
673      * @return DetailAST with SINGLE_LINE_COMMENT type.
674      */
675     private static DetailAST createSlCommentNode(Token token)
676     {
677         final DetailAST slComment = new DetailAST();
678         slComment.setType(TokenTypes.SINGLE_LINE_COMMENT);
679         slComment.setText("//");
680 
681         // column counting begins from 0
682         slComment.setColumnNo(token.getColumn() - 1);
683         slComment.setLineNo(token.getLine());
684 
685         final DetailAST slCommentContent = new DetailAST();
686         slCommentContent.initialize(token);
687         slCommentContent.setType(TokenTypes.COMMENT_CONTENT);
688 
689         // column counting begins from 0
690         // plus length of '//'
691         slCommentContent.setColumnNo(token.getColumn() - 1 + 2);
692         slCommentContent.setLineNo(token.getLine());
693         slCommentContent.setText(token.getText());
694 
695         slComment.addChild(slCommentContent);
696         return slComment;
697     }
698 
699     /**
700      * Create block comment from token.
701      * @param token
702      *        Token object.
703      * @return DetailAST with BLOCK_COMMENT type.
704      */
705     private static DetailAST createBlockCommentNode(Token token)
706     {
707         final DetailAST blockComment = new DetailAST();
708         blockComment.initialize(TokenTypes.BLOCK_COMMENT_BEGIN, "/*");
709 
710         // column counting begins from 0
711         blockComment.setColumnNo(token.getColumn() - 1);
712         blockComment.setLineNo(token.getLine());
713 
714         final DetailAST blockCommentContent = new DetailAST();
715         blockCommentContent.initialize(token);
716         blockCommentContent.setType(TokenTypes.COMMENT_CONTENT);
717 
718         // column counting begins from 0
719         // plus length of '/*'
720         blockCommentContent.setColumnNo(token.getColumn() - 1 + 2);
721         blockCommentContent.setLineNo(token.getLine());
722         blockCommentContent.setText(token.getText());
723 
724         final DetailAST blockCommentClose = new DetailAST();
725         blockCommentClose.initialize(TokenTypes.BLOCK_COMMENT_END, "*/");
726 
727         final Entry<Integer, Integer> linesColumns = countLinesColumns(
728                 token.getText(), token.getLine(), token.getColumn());
729         blockCommentClose.setLineNo(linesColumns.getKey());
730         blockCommentClose.setColumnNo(linesColumns.getValue());
731 
732         blockComment.addChild(blockCommentContent);
733         blockComment.addChild(blockCommentClose);
734         return blockComment;
735     }
736 
737     /**
738      * Count lines and columns (in last line) in text.
739      * @param text
740      *        String.
741      * @param initialLinesCnt
742      *        initial value of lines counter.
743      * @param initialColumnsCnt
744      *        initial value of columns counter.
745      * @return entry(pair), first element is lines counter, second - columns
746      *         counter.
747      */
748     private static Entry<Integer, Integer> countLinesColumns(
749             String text, int initialLinesCnt, int initialColumnsCnt)
750     {
751         int lines = initialLinesCnt;
752         int columns = initialColumnsCnt;
753         for (char c : text.toCharArray()) {
754             switch (c) {
755                 case '\n':
756                     lines++;
757                     columns = 0;
758                     break;
759                 default:
760                     columns++;
761             }
762         }
763         return new SimpleEntry<>(lines, columns);
764     }
765 
766 }