improved PHP Completion for -> operator
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpeclipse / builder / IdentifierIndexManager.java
1 package net.sourceforge.phpeclipse.builder;
2
3 import java.io.BufferedInputStream;
4 import java.io.BufferedReader;
5 import java.io.FileNotFoundException;
6 import java.io.FileReader;
7 import java.io.FileWriter;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.util.ArrayList;
11 import java.util.Collection;
12 import java.util.Comparator;
13 import java.util.HashMap;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.Set;
17 import java.util.SortedMap;
18 import java.util.StringTokenizer;
19 import java.util.TreeMap;
20
21 import net.sourceforge.phpdt.core.compiler.ITerminalSymbols;
22 import net.sourceforge.phpdt.core.compiler.InvalidInputException;
23 import net.sourceforge.phpdt.internal.compiler.parser.Scanner;
24 import net.sourceforge.phpdt.internal.compiler.parser.SyntaxError;
25 import net.sourceforge.phpdt.internal.compiler.util.Util;
26 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
27 import net.sourceforge.phpeclipse.obfuscator.PHPIdentifier;
28
29 import org.eclipse.core.resources.IFile;
30 import org.eclipse.core.runtime.CoreException;
31 import org.eclipse.core.runtime.IStatus;
32
33 /**
34  * Manages the identifer index information for a specific project
35  *  
36  */
37 public class IdentifierIndexManager {
38   public class LineCreator implements ITerminalSymbols {
39     private Scanner fScanner;
40
41     private int fToken;
42
43     public LineCreator() {
44       fScanner = new Scanner(true, false, false, false, true, null, null);
45     }
46
47     /**
48      * Add the information of the current identifier to the line
49      * 
50      * @param typeOfIdentifier
51      *          the type of the identifier ('c'lass, 'd'efine, 'f'unction, 'm'ethod(class), 'v'ariable(class) 'g'lobal variable)
52      * @param identifier
53      *          current identifier
54      * @param line
55      *          Buffer for the current index line
56      * @param phpdocOffset
57      *          the offset of the PHPdoc comment if available
58      * @param phpdocLength
59      *          the length of the PHPdoc comment if available
60      */
61     private void addIdentifierInformation(char typeOfIdentifier, char[] identifier, StringBuffer line, int phpdocOffset,
62         int phpdocLength) {
63       line.append('\t');
64       line.append(typeOfIdentifier);
65       line.append(identifier);
66       line.append("\to"); // Offset
67       line.append(fScanner.getCurrentTokenStartPosition());
68       if (phpdocOffset >= 0) {
69         line.append("\tp"); // phpdoc offset
70         line.append(phpdocOffset);
71         line.append("\tl"); // phpdoc length
72         line.append(phpdocLength);
73       }
74     }
75     
76     private void addClassVariableInformation(char typeOfIdentifier, char[] identifier, StringBuffer line, int phpdocOffset,
77         int phpdocLength) {
78       line.append('\t');
79       line.append(typeOfIdentifier);
80       line.append(identifier);
81       line.append("\to"); // Offset
82       // we don't store the '$' in the index for class variables:
83       line.append(fScanner.getCurrentTokenStartPosition()+1);
84       if (phpdocOffset >= 0) {
85         line.append("\tp"); // phpdoc offset
86         line.append(phpdocOffset);
87         line.append("\tl"); // phpdoc length
88         line.append(phpdocLength);
89       }
90     }
91
92     /**
93      * Get the next token from input
94      */
95     private void getNextToken() {
96       try {
97         fToken = fScanner.getNextToken();
98         if (Scanner.DEBUG) {
99           int currentEndPosition = fScanner.getCurrentTokenEndPosition();
100           int currentStartPosition = fScanner.getCurrentTokenStartPosition();
101           System.out.print(currentStartPosition + "," + currentEndPosition + ": ");
102           System.out.println(fScanner.toStringAction(fToken));
103         }
104         return;
105       } catch (InvalidInputException e) {
106         // ignore errors
107         //        e.printStackTrace();
108       }
109       fToken = TokenNameERROR;
110     }
111
112     private void parseDeclarations(char[] parent, StringBuffer buf, boolean goBack) {
113       char[] ident;
114       char[] classVariable;
115       int counter = 0;
116       int phpdocOffset = -1;
117       int phpdocLength = -1;
118       try {
119         while (fToken != TokenNameEOF && fToken != TokenNameERROR) {
120           phpdocOffset = -1;
121           if (fToken == TokenNameCOMMENT_PHPDOC) {
122             phpdocOffset = fScanner.getCurrentTokenStartPosition();
123             phpdocLength = fScanner.getCurrentTokenEndPosition() - fScanner.getCurrentTokenStartPosition() + 1;
124             getNextToken();
125             if (fToken == TokenNameEOF || fToken == TokenNameERROR) {
126               break;
127             }
128           }
129           if (fToken == TokenNamevar || fToken == TokenNamestatic || fToken == TokenNamefinal || fToken == TokenNamepublic
130               || fToken == TokenNameprotected || fToken == TokenNameprivate) {
131             while (fToken == TokenNamevar || fToken == TokenNamestatic || fToken == TokenNamefinal || fToken == TokenNamepublic
132                 || fToken == TokenNameprotected || fToken == TokenNameprivate) {
133               getNextToken();
134             }  
135             if (fToken == TokenNameVariable) {
136               ident = fScanner.getCurrentIdentifierSource();
137               classVariable = new char[ident.length - 1];
138               System.arraycopy(ident, 1, classVariable, 0, ident.length - 1);
139               addClassVariableInformation('v', classVariable, buf, phpdocOffset, phpdocLength);
140               getNextToken();
141             }
142           } else if (fToken == TokenNamefunction) {
143             getNextToken();
144             if (fToken == TokenNameAND) {
145               getNextToken();
146             }
147             if (fToken == TokenNameIdentifier) {
148               ident = fScanner.getCurrentIdentifierSource();
149               if (parent != null && equalCharArrays(parent, ident)) {
150                 // constructor function
151                 addIdentifierInformation('k', ident, buf, phpdocOffset, phpdocLength);
152               } else {
153                 if (parent != null) {
154                   // class method function
155                   addIdentifierInformation('m', ident, buf, phpdocOffset, phpdocLength);
156                 } else {
157                   // nested function ?!
158                   addIdentifierInformation('f', ident, buf, phpdocOffset, phpdocLength);
159                 }
160               }
161               getNextToken();
162               parseDeclarations(null, buf, true);
163             }
164           } else if (fToken == TokenNameclass || fToken == TokenNameinterface) {
165             getNextToken();
166             if (fToken == TokenNameIdentifier) {
167               ident = fScanner.getCurrentIdentifierSource();
168               addIdentifierInformation('c', ident, buf, phpdocOffset, phpdocLength);
169               getNextToken();
170               //skip tokens for classname, extends and others until we have
171               // the opening '{'
172               while (fToken != TokenNameLBRACE && fToken != TokenNameEOF && fToken != TokenNameERROR) {
173                 getNextToken();
174               }
175               parseDeclarations(ident, buf, true);
176             }
177           } else if (fToken == TokenNameIdentifier) {
178             ident = fScanner.getCurrentIdentifierSource();
179             getNextToken();
180             if (ident.length == 6 && ident[0] == 'd' && ident[1] == 'e' && ident[2] == 'f' && ident[3] == 'i' && ident[4] == 'n'
181                 && ident[5] == 'e') {
182               if (fToken == TokenNameLPAREN) {
183                 getNextToken();
184                 if (fToken == TokenNameStringDoubleQuote) {
185                   ident = fScanner.getCurrentStringLiteralSource();
186                   addIdentifierInformation('d', ident, buf, phpdocOffset, phpdocLength);
187                   getNextToken();
188                 }
189               }
190             }
191           } else if (fToken == TokenNameglobal) {
192             // global variable
193             while (fToken != TokenNameEOF && fToken != TokenNameERROR && fToken != TokenNameSEMICOLON && fToken != TokenNameLBRACE
194                 && fToken != TokenNameRBRACE) {
195               getNextToken();
196               if (fToken == TokenNameVariable) {
197                 ident = fScanner.getCurrentIdentifierSource();
198                 addIdentifierInformation('g', ident, buf, phpdocOffset, phpdocLength);
199               }
200             }
201           } else if (fToken == TokenNameLBRACE) {
202             getNextToken();
203             counter++;
204           } else if (fToken == TokenNameRBRACE) {
205             getNextToken();
206             --counter;
207             if (counter == 0 && goBack) {
208               return;
209             }
210           } else {
211             getNextToken();
212           }
213         }
214       } catch (SyntaxError e) {
215         // TODO Auto-generated catch block
216         e.printStackTrace();
217       }
218     }
219
220     synchronized public void parseIdentifiers(char[] charArray, StringBuffer buf) {
221       char[] ident;
222       String identifier;
223       int counter = 0;
224       int phpdocOffset = -1;
225       int phpdocLength = -1;
226       fScanner.setSource(charArray);
227       fScanner.setPHPMode(false);
228       fToken = TokenNameEOF;
229       getNextToken();
230       try {
231         while (fToken != TokenNameEOF) { // && fToken != TokenNameERROR) {
232           phpdocOffset = -1;
233           if (fToken == TokenNameCOMMENT_PHPDOC) {
234             phpdocOffset = fScanner.getCurrentTokenStartPosition();
235             phpdocLength = fScanner.getCurrentTokenEndPosition() - fScanner.getCurrentTokenStartPosition() + 1;
236             getNextToken();
237             if (fToken == TokenNameEOF || fToken == TokenNameERROR) {
238               break;
239             }
240           }
241           if (fToken == TokenNamefunction) {
242             getNextToken();
243             if (fToken == TokenNameAND) {
244               getNextToken();
245             }
246             if (fToken == TokenNameIdentifier) {
247               ident = fScanner.getCurrentIdentifierSource();
248               addIdentifierInformation('f', ident, buf, phpdocOffset, phpdocLength);
249               getNextToken();
250               parseDeclarations(null, buf, true);
251             }
252           } else if (fToken == TokenNameclass || fToken == TokenNameinterface) {
253             getNextToken();
254             if (fToken == TokenNameIdentifier) {
255               ident = fScanner.getCurrentIdentifierSource();
256               addIdentifierInformation('c', ident, buf, phpdocOffset, phpdocLength);
257               getNextToken();
258               //skip fTokens for classname, extends and others until we have
259               // the opening '{'
260               while (fToken != TokenNameLBRACE && fToken != TokenNameEOF && fToken != TokenNameERROR) {
261                 getNextToken();
262               }
263               parseDeclarations(ident, buf, true);
264             }
265           } else if (fToken == TokenNameVariable) {
266             // global variable
267             ident = fScanner.getCurrentIdentifierSource();
268             addIdentifierInformation('g', ident, buf, phpdocOffset, phpdocLength);
269             getNextToken();
270           } else if (fToken == TokenNameIdentifier) {
271             ident = fScanner.getCurrentIdentifierSource();
272             getNextToken();
273             if (ident.length == 6 && ident[0] == 'd' && ident[1] == 'e' && ident[2] == 'f' && ident[3] == 'i' && ident[4] == 'n'
274                 && ident[5] == 'e') {
275               if (fToken == TokenNameLPAREN) {
276                 getNextToken();
277                 if (fToken == TokenNameStringDoubleQuote) {
278                   ident = fScanner.getCurrentStringLiteralSource();
279                   addIdentifierInformation('d', ident, buf, phpdocOffset, phpdocLength);
280                   getNextToken();
281                 }
282               }
283             }
284           } else {
285             getNextToken();
286           }
287         }
288       } catch (SyntaxError e) {
289         // TODO Auto-generated catch block
290         e.printStackTrace();
291       }
292     }
293   }
294
295   class StringComparator implements Comparator {
296     public int compare(Object o1, Object o2) {
297       String s1 = (String) o1;
298       String s2 = (String) o2;
299       return s1.compareTo(s2);
300       //        return s1.toUpperCase().compareTo(s2.toUpperCase());
301     }
302
303     public boolean equals(Object o) {
304       String s = (String) o;
305       return compare(this, o) == 0;
306     }
307   }
308
309   private HashMap fFileMap;
310
311   private String fFilename;
312
313   private TreeMap fIndentifierMap;
314
315   public IdentifierIndexManager(String filename) {
316     fFilename = filename;
317     initialize();
318     readFile();
319   }
320
321   /**
322    * Check if 2 char arrays are equal
323    * 
324    * @param a
325    * @param b
326    * @return
327    */
328   private static boolean equalCharArrays(char[] a, char[] b) {
329     if (a.length != b.length) {
330       return false;
331     }
332     for (int i = 0; i < b.length; i++) {
333       if (a[i] != b[i]) {
334         return false;
335       }
336     }
337     return true;
338   }
339
340   public LineCreator createLineCreator() {
341     return new LineCreator();
342   }
343
344   /**
345    * Add the information for a given IFile resource
346    *  
347    */
348   public void addFile(IFile fileToParse) {
349     //    InputStream iStream;
350     LineCreator lineCreator = createLineCreator();
351     try {
352       addInputStream(new BufferedInputStream(fileToParse.getContents()), fileToParse.getProjectRelativePath().toString(),
353           lineCreator);
354     } catch (CoreException e1) {
355       // TODO Auto-generated catch block
356       e1.printStackTrace();
357     }
358   }
359
360   /**
361    * @param fileToParse
362    * @param lineCreator
363    * @throws CoreException
364    */
365   public void addInputStream(InputStream stream, String filePath, LineCreator lineCreator) throws CoreException {
366     try {
367       StringBuffer lineBuffer = new StringBuffer();
368       lineBuffer.append(filePath);
369       int lineLength = lineBuffer.length();
370       lineCreator.parseIdentifiers(Util.getInputStreamAsCharArray(stream, -1, null), lineBuffer);
371       //      if (lineLength != lineBuffer.length()) {
372       // always add the file for Open Include Action
373       addLine(lineBuffer.toString());
374       //      }
375     } catch (IOException e) {
376       e.printStackTrace();
377     } finally {
378       try {
379         if (stream != null) {
380           stream.close();
381         }
382       } catch (IOException e) {
383       }
384     }
385   }
386
387   /**
388    * Adds a line of the index file for function, class, class-method and class-variable names
389    * 
390    * @param line
391    */
392   private void addLine(String line) {
393     StringTokenizer tokenizer;
394     String phpFileName = null;
395     String token;
396     String identifier = null;
397     String classname = null;
398     String offset = null;
399     PHPIdentifierLocation phpIdentifier = null;
400     boolean tokenExists = false;
401     tokenizer = new StringTokenizer(line, "\t");
402     // first token contains the filename:
403     if (tokenizer.hasMoreTokens()) {
404       phpFileName = tokenizer.nextToken();
405       //System.out.println(token);
406     } else {
407       return;
408     }
409     // all the other tokens are identifiers:
410     while (tokenizer.hasMoreTokens()) {
411       token = tokenizer.nextToken();
412       //System.out.println(token);
413       switch (token.charAt(0)) {
414       case 'c':
415         // class name
416         identifier = token.substring(1);
417         classname = identifier;
418         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.CLASS, phpFileName);
419         break;
420       case 'd':
421         // define
422         identifier = token.substring(1);
423         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.DEFINE, phpFileName);
424         break;
425       case 'f':
426         // function name
427         identifier = token.substring(1);
428         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.FUNCTION, phpFileName);
429         break;
430       case 'g':
431         // global variable
432         identifier = token.substring(1);
433         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.GLOBAL_VARIABLE, phpFileName);
434         break;
435       case 'k':
436         // constructor function name
437         identifier = token.substring(1);
438         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.CONSTRUCTOR, phpFileName);
439         break;
440       case 'm':
441         //method inside a class
442         identifier = token.substring(1);
443         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.METHOD, phpFileName, classname);
444         break;
445       case 'v':
446         // variable inside a class
447         identifier = token.substring(1);
448         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.VARIABLE, phpFileName, classname);
449         break;
450       case 'o':
451         // offset information
452         identifier = null;
453         if (phpIdentifier != null) {
454           offset = token.substring(1);
455           phpIdentifier.setOffset(Integer.parseInt(offset));
456         }
457         break;
458       case 'p':
459         // PHPdoc offset information
460         identifier = null;
461         if (phpIdentifier != null) {
462           offset = token.substring(1);
463           phpIdentifier.setPHPDocOffset(Integer.parseInt(offset));
464         }
465         break;
466       case 'l':
467         // PHPdoc length information
468         identifier = null;
469         if (phpIdentifier != null) {
470           offset = token.substring(1);
471           phpIdentifier.setPHPDocLength(Integer.parseInt(offset));
472         }
473         break;
474       default:
475         PHPeclipsePlugin.log(IStatus.ERROR, "Unknown token character in IdentifierIndexManager: " + token.charAt(0));
476         identifier = null;
477         phpIdentifier = null;
478         classname = null;
479       }
480       if (identifier != null && phpIdentifier != null) {
481         tokenExists = true;
482         ArrayList list = (ArrayList) fIndentifierMap.get(identifier);
483         if (list == null) {
484           list = new ArrayList();
485           list.add(phpIdentifier);
486           fIndentifierMap.put(identifier, list);
487         } else {
488           boolean flag = false;
489           for (int i = 0; i < list.size(); i++) {
490             if (list.get(i).equals(phpIdentifier)) {
491               flag = true;
492               break;
493             }
494           }
495           if (flag == false) {
496             list.add(phpIdentifier);
497           }
498         }
499       }
500     }
501     //    if (tokenExists) {
502     fFileMap.put(phpFileName, line);
503     //    }
504   }
505
506   /**
507    * Change the information for a given IFile resource
508    *  
509    */
510   public void changeFile(IFile fileToParse) {
511     removeFile(fileToParse);
512     addFile(fileToParse);
513   }
514
515   /**
516    * Get a list of all PHPIdentifierLocation object's associated with an identifier
517    * 
518    * @param identifier
519    * @return
520    */
521   public List getLocations(String identifier) {
522     return (List) fIndentifierMap.get(identifier);
523   }
524
525   /**
526    * Initialize (i.e. clear) the current index information
527    *  
528    */
529   public void initialize() {
530     fIndentifierMap = new TreeMap(new StringComparator());
531     fFileMap = new HashMap();
532   }
533
534   private void readFile() {
535     FileReader fileReader;
536     try {
537       fileReader = new FileReader(fFilename);
538       BufferedReader bufferedReader = new BufferedReader(fileReader);
539       String line;
540       while (bufferedReader.ready()) {
541         // all entries for one file are in a line
542         // separated by tabs !
543         line = bufferedReader.readLine();
544         addLine(line);
545       }
546       fileReader.close();
547     } catch (FileNotFoundException e) {
548       // ignore this
549       // TODO DialogBox which asks the user if she/he likes to build new index?
550     } catch (IOException e) {
551       // TODO Auto-generated catch block
552       e.printStackTrace();
553     }
554   }
555
556   /**
557    * Remove the information for a given IFile resource
558    *  
559    */
560   public void removeFile(IFile fileToParse) {
561     //    String line = (String)
562     // fFileMap.get(fileToParse.getLocation().toString());
563     String line = (String) fFileMap.get(fileToParse.getProjectRelativePath().toString());
564     if (line != null) {
565       removeLine(line);
566     }
567   }
568
569   /**
570    * Removes a line of the index file for function, class, class-method and class-variable names
571    * 
572    * @param line
573    */
574   private void removeLine(String line) {
575     StringTokenizer tokenizer;
576     String phpFileName = null;
577     String token;
578     String identifier = null;
579     String classname = null;
580     PHPIdentifier phpIdentifier = null;
581     boolean tokenExists = false;
582     tokenizer = new StringTokenizer(line, "\t");
583     // first token contains the filename:
584     if (tokenizer.hasMoreTokens()) {
585       phpFileName = tokenizer.nextToken();
586       //System.out.println(token);
587     } else {
588       return;
589     }
590     int offset = -1;
591     // all the other tokens are identifiers:
592     while (tokenizer.hasMoreTokens()) {
593       token = tokenizer.nextToken();
594       //System.out.println(token);
595       switch (token.charAt(0)) {
596       case 'c':
597         // class name
598         identifier = token.substring(1);
599         classname = identifier;
600         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.CLASS, phpFileName);
601         break;
602       case 'd':
603         // define
604         identifier = token.substring(1);
605         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.DEFINE, phpFileName);
606         break;
607       case 'f':
608         // function name
609         identifier = token.substring(1);
610         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.FUNCTION, phpFileName);
611         break;
612       case 'g':
613         // global variable
614         identifier = token.substring(1);
615         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.GLOBAL_VARIABLE, phpFileName);
616         break;
617       case 'k':
618         // constructor function name
619         identifier = token.substring(1);
620         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.CONSTRUCTOR, phpFileName);
621         break;
622       case 'm':
623         //method inside a class
624         identifier = token.substring(1);
625         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.METHOD, phpFileName, classname);
626         break;
627       case 'o':
628         // offset information
629         identifier = null;
630         break;
631       case 'p':
632         // PHPdoc offset information
633         identifier = null;
634         break;
635       case 'l':
636         // PHPdoc length information
637         identifier = null;
638         break;
639       case 'v':
640         // variable inside a class
641         identifier = token.substring(1);
642         phpIdentifier = new PHPIdentifierLocation(identifier, PHPIdentifier.VARIABLE, phpFileName, classname);
643         break;
644       default:
645         PHPeclipsePlugin.log(IStatus.ERROR, "Unknown token character in IdentifierIndexManager: " + token.charAt(0));
646         identifier = null;
647         phpIdentifier = null;
648         classname = null;
649       }
650       if (identifier != null && phpIdentifier != null) {
651         ArrayList list = (ArrayList) fIndentifierMap.get(identifier);
652         if (list == null) {
653         } else {
654           for (int i = 0; i < list.size(); i++) {
655             if (list.get(i).equals(phpIdentifier)) {
656               list.remove(i);
657               break;
658             }
659           }
660           if (list.size() == 0) {
661             fIndentifierMap.remove(identifier);
662           }
663         }
664       }
665     }
666     fFileMap.remove(phpFileName);
667   }
668
669   /**
670    * Save the current index information in the projects index file
671    *  
672    */
673   public void writeFile() {
674     FileWriter fileWriter;
675     try {
676       fileWriter = new FileWriter(fFilename);
677       String line;
678       Collection collection = fFileMap.values();
679       Iterator iterator = collection.iterator();
680       while (iterator.hasNext()) {
681         line = (String) iterator.next();
682         fileWriter.write(line + '\n');
683       }
684       fileWriter.close();
685     } catch (FileNotFoundException e) {
686       // ignore exception; project is deleted by user
687     } catch (IOException e) {
688       // TODO Auto-generated catch block
689       e.printStackTrace();
690     }
691   }
692
693   /**
694    * @param fromKey
695    * @param toKey
696    * @return
697    */
698   public SortedMap getIdentifierMap() {
699     return fIndentifierMap;
700   }
701
702   synchronized public List getFileList(String filePattern) {
703     Set set = fFileMap.keySet();
704     if (set.isEmpty()) {
705       return null;
706     }
707     Iterator iter = set.iterator();
708     ArrayList list = new ArrayList();
709     String fileName;
710     int index;
711     while (iter.hasNext()) {
712       fileName = (String) iter.next();
713       if ((index = fileName.indexOf(filePattern)) != -1 && fileName.length() == (index + filePattern.length())) {
714         list.add(fileName);
715       }
716     }
717     return list;
718   }
719 }