001: package tide.stats;
002:
003: import snow.utils.CollectionUtils;
004: import java.awt.EventQueue;
005: import javax.swing.text.*;
006: import java.awt.BorderLayout;
007: import javax.swing.*;
008: import snow.utils.gui.*;
009: import snow.utils.StringUtils;
010: import tide.utils.SyntaxUtils;
011: import tide.editor.styler.*;
012: import snow.texteditor.*;
013: import tide.editor.*;
014: import tide.sources.*;
015: import java.util.*;
016:
017: /** TODO: add metrics [See JavaSpectrum Feb/Marz 08 (named erroneously 07)]
018: * TODO: graphical representation
019: * TODO: multidimensional search for outsiders => for example detect code that may be pasted or written from other person, may be good to analyse...
020: */
021: public final class CodeStats {
022: private CodeStats() {
023: }
024:
025: public static void analyse(final List<SourceFile> allFiles,
026: final boolean showStatForEachFile) {
027: //EDT
028:
029: //OLD: MainEditorFrame.instance.outputPanels.selectToolsTab(true);
030: //final SimpleDocument doc = MainEditorFrame.instance.outputPanels.toolsOutputPanel.doc;
031:
032: final JDialog d = new JDialog(MainEditorFrame.instance,
033: "Code statistics", false);
034: final SimpleDocument doc = new SimpleDocument();
035: final JTextPane tp = new JTextPane(doc);
036: tp.setEditable(false);
037: d.add(new JScrollPane(tp), BorderLayout.CENTER);
038:
039: CloseControlPanel ccpd = new CloseControlPanel(d, false, true,
040: "Close");
041: d.add(ccpd, BorderLayout.SOUTH);
042: d.setSize(900, 600);
043: d.setLocationRelativeTo(MainEditorFrame.instance);
044:
045: TabStop[] tabs = new TabStop[5];
046: tabs[0] = new TabStop(50f, TabStop.ALIGN_RIGHT,
047: TabStop.LEAD_NONE);
048: tabs[1] = new TabStop(110f, TabStop.ALIGN_RIGHT,
049: TabStop.LEAD_NONE);
050: tabs[2] = new TabStop(180f, TabStop.ALIGN_RIGHT,
051: TabStop.LEAD_NONE);
052: tabs[3] = new TabStop(210f, TabStop.ALIGN_LEFT,
053: TabStop.LEAD_NONE);
054: tabs[4] = new TabStop(260f, TabStop.ALIGN_LEFT,
055: TabStop.LEAD_NONE);
056: TabSet tabSet = new TabSet(tabs);
057: doc.setTabsForDoc(tabSet);
058:
059: final ProgressModalDialog progress = new ProgressModalDialog(d,
060: "Code statistics", false);
061: Thread t = new Thread(new Runnable() {
062: public void run() {
063: try {
064: progress.start();
065: progress.setProgressBounds(allFiles.size() * 2);
066:
067: // pass to fetch median !
068: int[] stat0 = _analyse(allFiles,
069: showStatForEachFile, true, null, null, doc,
070: progress);
071: if (stat0 == null)
072: return;
073:
074: EventQueue.invokeLater(new Runnable() {
075: public void run() {
076: d.setVisible(true);
077: }
078: });
079:
080: Map<String, WordStat> commentsWordsStat = null;
081: if (MainEditorFrame.enableExperimental) {
082: commentsWordsStat = new HashMap<String, WordStat>();
083: }
084: _analyse(allFiles, showStatForEachFile, false,
085: stat0, commentsWordsStat, doc, progress);
086: } catch (Exception e) {
087: progress.closeDialog();
088: JOptionPane.showMessageDialog(
089: MainEditorFrame.instance, ""
090: + e.getMessage(), "Error",
091: JOptionPane.ERROR_MESSAGE);
092: e.printStackTrace();
093: } finally {
094: tp.setCaretPosition(0);
095: progress.closeDialog();
096: }
097: }
098: });
099: t.setName("Code statistics");
100: t.setPriority(Thread.NORM_PRIORITY - 1);
101: t.start();
102: }
103:
104: /** @return {nonempty lines, code words, comment words, percent of comment words}
105: * @param stat0 if non null (hunted from a previous pass with silent=true), displays stat outsiders in red.
106: * @param commentsWordsStat if non null, a stat of all comment words {lowercase word, count}
107: */
108: private static int[] _analyse(final List<SourceFile> allFiles,
109: boolean showStatForEachFile, boolean silent, int[] stat0,
110: final Map<String, WordStat> commentsWordsStat,
111: final SimpleDocument doc, final ProgressModalDialog pmd) {
112:
113: // TODO: semantics (using the javaCC parser), per method
114: // TODO: metrics (control count, recursivity, cyclotomic, ...)
115:
116: if (!silent) {
117: doc.append("Code statistics for ");
118: doc.append(" " + allFiles.size() + " file"
119: + (allFiles.size() == 1 ? "" : "s") + ":");
120: doc
121: .append("\n lines: non empty lines, CodeW: number of code words, ComW%: fraction of comment words");
122: }
123:
124: int totNonEmptyLines = 0;
125: int totCodeWords = 0;
126: int totCommentWords = 0;
127: int classes = 0;
128:
129: // "pack", {nel, codeW, comW}
130: Map<String, int[]> perPackage = new HashMap<String, int[]>();
131:
132: if (!silent && showStatForEachFile && allFiles.size() > 1) {
133: //header
134: doc.append("\n\n\tLines\tCodeW\tComW%\tsource");
135: }
136:
137: for (final SourceFile fi : allFiles) {
138: if (pmd.getWasCancelled()) {
139: doc.appendErrorLine("\nSTATS CANCELLED BY USER.");
140: return null;
141: }
142: pmd.incrementProgress(1);
143:
144: classes += fi.sourceFileDependencies
145: .getDeclaredTypesNames_REF_().size();
146:
147: int lines = 0;
148: int emptyLines = 0;
149: int words = 0; // variables, classes
150: int keywords = 0;
151: int commentsC = 0; // c -like and javadoc like
152: int commentsJ = 0;
153: int commentsJWords = 0;
154: int commentsCWords = 0;
155:
156: int litterals = 0;
157: int chars = 0;
158:
159: try {
160: String cont = fi.getContent();
161: chars = cont.length();
162: lines = StringUtils.count(cont, '\n');
163: emptyLines = StringUtils.count(cont, "\n\r\n"); // CR LF // small bug: ignores empty beginning lines.
164: SimpleCodeParser scp = new SimpleCodeParser(cont);
165:
166: SimpleCodeParser.Item w;
167: wl: while ((w = scp.getNextItem()) != null) {
168: switch (w.type) {
169: case Word:
170: if (SyntaxUtils.isKeyWord(w.word)) {
171: keywords++;
172: } else {
173: words++;
174: }
175: break;
176: case Comment:
177: if (w.word.startsWith("/**")) // TODO: minor bug: this causes the tIDE completion to be disabled below !! (parsedID recognizes as comment!)
178: {
179: commentsJ++;
180: commentsJWords += StringUtils
181: .countWords(w.word) - 2;
182: if (commentsWordsStat != null) {
183: stat(w.word.substring(3, w.word
184: .length() - 2),
185: commentsWordsStat);
186: }
187: } else if (w.word.startsWith("/*")) {
188: commentsC++;
189: commentsCWords += StringUtils
190: .countWords(w.word) - 2;
191: if (commentsWordsStat != null) {
192: if (w.word.length() > 3) {
193: stat(w.word.substring(2, w.word
194: .length() - 2),
195: commentsWordsStat);
196: }
197: }
198: } else {
199: commentsC++;
200: commentsCWords += StringUtils
201: .countWords(w.word) - 1;
202: if (commentsWordsStat != null) {
203: stat(w.word.substring(2, w.word
204: .length()), commentsWordsStat);
205: }
206: }
207: break;
208: case Litteral:
209: litterals++;
210: break;
211: }
212: }
213:
214: if (!perPackage.containsKey(fi.getPackageName())) {
215: perPackage.put(fi.getPackageName(), new int[3]);
216: }
217: int[] sp = perPackage.get(fi.getPackageName());
218: sp[0] += lines - emptyLines;
219: sp[1] += keywords + words;
220: sp[2] += commentsJWords + commentsCWords;
221:
222: int nonEmptyLines = lines - emptyLines;
223: int codeWords = keywords + words;
224: int commentWords = commentsJWords + commentsCWords;
225:
226: totNonEmptyLines += nonEmptyLines;
227: totCodeWords += codeWords;
228: totCommentWords += commentWords;
229:
230: if (!silent && showStatForEachFile) {
231: if (allFiles.size() > 1) {
232: if (nonEmptyLines > 2000) {
233: doc.appendError("\n\t" + nonEmptyLines);
234: } else {
235: doc.append("\n\t" + nonEmptyLines);
236: }
237: doc.append("\t" + codeWords);
238: //+" \t"+commentWords
239: int perc = (commentWords * 100 / (commentWords + codeWords));
240: if (stat0 != null
241: && (perc < stat0[3] / 3 || perc < 10)) {
242: doc.appendError("\t" + perc);
243: } else {
244: doc.append("\t" + perc);
245: }
246: doc.append("\t" + fi.getJavaName());
247: } else {
248: doc.append("\n\nSource " + fi.getJavaName()
249: + ": ");
250: doc.append("\n " + words + " words");
251: doc.append("\n " + lines + " lines, "
252: + emptyLines + " empty lines, " + chars
253: + " characters, ");
254: doc.append("\n " + keywords + " keywords");
255: doc.append("\n " + commentsJ
256: + " javadoc comments ("
257: + commentsJWords + " words)");
258: doc.append("\n " + commentsC
259: + " c-like comments (" + commentsCWords
260: + " words)");
261: doc.append("\n " + litterals + " litterals");
262: }
263: }
264:
265: } catch (Exception e) {
266: e.printStackTrace();
267: return null;
268: }
269: }
270:
271: if (!silent) {
272: if (perPackage.size() > 1) {
273: List<String> packs = new ArrayList<String>(perPackage
274: .keySet());
275: Collections.sort(packs);
276: doc
277: .append("\n\nStatistics per package\n\tlines\tcodeW\tComW%\tpackage");
278: for (String pi : packs) {
279: int[] sp = perPackage.get(pi);
280: doc.append("\n\t" + sp[0]);
281: doc.append("\t" + sp[1]);
282: //doc.append(" \t"+sp[2]);
283: int perc = (sp[2] * 100 / (sp[1] + sp[2]));
284: if (stat0 != null
285: && (perc < stat0[3] / 3 || perc < 10)) {
286: doc.appendError("\t" + perc);
287: } else {
288: doc.append("\t" + perc);
289: }
290: doc.append("\t" + pi);
291: }
292: }
293:
294: doc.append("\n\nOverall:\n " + totNonEmptyLines
295: + " non empty lines\n " + totCodeWords
296: + " code words, "); //+ totCommentWords+" comment words");
297: doc
298: .append(""
299: + (100 * totCommentWords / (totCodeWords + totCommentWords))
300: + "% comment words");
301: doc.append("\n " + classes + " declared types.");
302:
303: if (commentsWordsStat != null) {
304: doc.append("\n\nComment words histogram: "
305: + commentsWordsStat.size()
306: + " different words, most frequent are:\n ");
307: if (commentsWordsStat.size() > 0) {
308: ArrayList<WordStat> sortedFreqs = new ArrayList<WordStat>(
309: commentsWordsStat.values());
310: Collections.sort(sortedFreqs); // ascending
311:
312: List<WordStat> greatestFreqs = CollectionUtils
313: .takeLast(sortedFreqs, 20);
314:
315: int i = 0;
316: for (WordStat wi : greatestFreqs) {
317: doc.append("" + wi + ", ");
318: i++;
319: if (i % 10 == 0)
320: doc.append("\n ");
321: }
322: }
323: }
324:
325: }
326:
327: return new int[] {
328: totNonEmptyLines,
329: totCodeWords,
330: totCommentWords,
331: 100 * totCommentWords
332: / (totCodeWords + totCommentWords) };
333: }
334:
335: static class WordStat implements Comparable<WordStat> {
336: final String word;
337: int count = 1;
338:
339: public WordStat(String word) {
340: this .word = word;
341: }
342:
343: @Override
344: public boolean equals(Object o) {
345: return ((WordStat) o).word.equals(word);
346: }
347:
348: @Override
349: public final String toString() {
350: StringBuilder sb = new StringBuilder();
351: sb.append("(" + word + ": " + count + ")");
352: return sb.toString();
353: }
354:
355: public int compareTo(WordStat w2) {
356: return Integer.valueOf(count).compareTo(w2.count);
357: }
358:
359: }
360:
361: /** Only words made of at least 4 letters and not starting with a special char.
362: */
363: static void stat(String w, Map<String, WordStat> stat) {
364: // TODO: may be quicker and more accurate to char iterate and look at isLetterOrDigit()
365: String[] ws = w.toLowerCase().trim().split("\\s+");
366: for (String wi : ws) {
367: if (wi.length() < 4)
368: continue;
369: if (wi.startsWith("@"))
370: continue;
371: if (SyntaxUtils.isKeyWord(wi))
372: continue;
373:
374: if (stat.containsKey(wi)) {
375: stat.get(wi).count++;
376: } else {
377: stat.put(wi, new WordStat(wi));
378: }
379: }
380: }
381:
382: }
|