package org.eclipse.swt.tools.internal; | |
import java.io.*; | |
import java.util.*; | |
import org.eclipse.jdt.core.dom.*; | |
import org.eclipse.jface.text.*; | |
/** | |
* Bashes the javadoc from one source tree into another. Only produces new | |
* source files for compilation units that have changed. | |
* | |
* How to use: 1) make sure you have the latest org.eclipse.swt (master branch) | |
* in your workspace, and that you have no outstanding org.eclipse.swt changes | |
* 2) create a Bugzilla bug called | |
* "Do the annual javadoc/copyright bash for x.x" 3) make a version (tag) of the | |
* org.eclipse.swt project before you bash here is a sample tag name: | |
* BEFORE_JAVADOC_BASH_FOR_43RC3 use the Bugzilla bug for the tag comment 4) | |
* modify the code in main, below, so that 'workspaceDir' and 'outputDir' point | |
* to the (git) directory that contains org.eclipse.swt in your workspace, | |
* typically C:/git/eclipse.platform.swt/bundles (prior to 3.8/4.2, these | |
* pointed to the workspace directory) 5) make sure 'sourceSubdir' (usually | |
* win32), 'targetSubdirs' (all others), and 'folders' are correct (note: there | |
* are typically a few new targetSubdirs and folders every year... although | |
* nothing new for 4.3) 6) run JavadocBasher (for a more verbose output, set | |
* fVerbose to true) 7) refresh (F5) the org.eclipse.swt project inside eclipse | |
* 8) search for *** in console output to see results of API consistency | |
* checking 9) synchronize, carefully reviewing every change. Watch out for: - | |
* duplicated comments - // comments that have been removed (if they appear | |
* before a javadoc comment) 10) use the Bugzilla bug as the commit comment for | |
* javadoc and copyright bash commits 11) make a version of the org.eclipse.swt | |
* project after bashing (use tag name AFTER_...) | |
* | |
* 12) Copyright bash (tag before and after): NOTE: JavadocBasher does not fix | |
* copyrights. Use the "Fix Copyrights" tool in org.eclipse.releng.tools for | |
* that (always fix copyrights after bash). Use Help->Install New Software... to | |
* install "Releng Tools" from the "Eclipse Project Updates" site (for release - | |
* 1). Select org.eclipse.swt project and choose "Fix Copyrights" from the | |
* context menu. See http://wiki.eclipse.org/Development_Resources/ | |
* How_to_Use_Eclipse_Copyright_Tool for more info. NOTE: The copyright tool | |
* takes about 45 minutes to run (for SWT). NOTE 2: Check console for possible | |
* errors/warnings, refresh (F5), synchronize, and browse all changes. Use | |
* keyboard (Ctrl+.) for next diff instead of mouse (keyboard is faster because | |
* there are fewer focus changes). Only use git History view as needed - if it | |
* is open and linked with editor, it gets bogged down and lags behind. NOTE 3: | |
* SWT anomalies that confuse the tool: - Some ns*.h files in | |
* Mozilla/common/library do not contain the word "copyright" so the tool tries | |
* to add one - don't keep it (the text is fine as-is). - Other ns*.h files in | |
* Mozilla/common/library have a copyright line that should not be updated | |
* (Initial Developer) - don't keep the change suggested by the tool (the text | |
* is fine as-is). - The ns*.java and some other *.java files in | |
* internal/mozilla have 2 copyright lines and the tool tries to change the 1st | |
* - don't keep the 1st change (Netscape 1998-1999), but update the 2nd (IBM) | |
* manually. | |
* | |
* NOTE: JavadocBasher now does a fairly good job of checking API consistency. | |
* We used to use org.eclipse.swt.diff for API consistency checking, but it was | |
* difficult to maintain. | |
*/ | |
public class JavadocBasher { | |
static final boolean fVerbose = false; // set to true for verbose output | |
List<String> fBashed; | |
List<String> fUnchanged; | |
List<String> fSkipped; | |
public JavadocBasher() { | |
fBashed = new ArrayList<String>(); | |
fUnchanged = new ArrayList<String>(); | |
fSkipped = new ArrayList<String>(); | |
} | |
public static class Edit { | |
int start, length; | |
String text; | |
public Edit(int start, int length, String text) { | |
this.start = start; | |
this.length = length; | |
this.text = text; | |
} | |
} | |
public static void main(String[] args) { | |
String workspaceDir = ".."; // use forward slashes, no final slash | |
String outputDir = ".."; // can point to another directory for debugging | |
String[] folders = new String[] { // commented folders do not need to be | |
// bashed | |
"Eclipse SWT", "Eclipse SWT Accessibility", | |
"Eclipse SWT AWT", | |
"Eclipse SWT Browser", | |
// "Eclipse SWT Custom Widgets", | |
"Eclipse SWT Drag and Drop", "Eclipse SWT Effects", | |
"Eclipse SWT Mozilla", | |
// "Eclipse SWT OLE Win32", | |
"Eclipse SWT OpenGL", | |
// "Eclipse SWT PI", | |
"Eclipse SWT Printing", "Eclipse SWT Program", | |
"Eclipse SWT Theme", "Eclipse SWT WebKit", }; | |
String sourceSubdir = "win32"; | |
String[] targetSubdirs = new String[] { "cairo", // used by gtk | |
// "carbon", // we are no longer maintaining carbon | |
"cde", // used by gtk | |
"cocoa", | |
// "common", | |
// "common_j2me", | |
// "common_j2se", | |
"emulated", "emulated/bidi", // used by carbon, cocoa | |
"emulated/coolbar", // used by carbon, cocoa, gtk | |
"emulated/expand", // used by carbon, cocoa | |
"emulated/taskbar", // used by carbon, gtk | |
"emulated/tooltip", // used by cocoa (?!) | |
"gnome", // used by gtk | |
"glx", // used by gtk | |
"gtk", | |
"mozilla", // used by carbon, cocoa, gtk, win32 | |
// "qt", // folder should be deleted | |
}; | |
System.out.println("==== Start Bashing ===="); | |
int totalBashed = 0; | |
for (int t = 0; t < targetSubdirs.length; t++) { | |
for (int f = 0; f < folders.length; f++) { | |
String targetSubdir = folders[f] + "/" + targetSubdirs[t]; | |
File source = new File(workspaceDir + "/org.eclipse.swt/" | |
+ folders[f] + "/" + sourceSubdir); | |
File target = new File(workspaceDir + "/org.eclipse.swt/" | |
+ targetSubdir); | |
File out = new File(outputDir + "/org.eclipse.swt/" | |
+ targetSubdir); | |
JavadocBasher basher = new JavadocBasher(); | |
System.out.println("\n==== Start Bashing " + targetSubdir); | |
basher.bashJavaSourceTree(source, target, out); | |
List<String> bashedList = basher.getBashed(); | |
basher.status("Bashed", bashedList, targetSubdir); | |
if (bashedList.size() > 0) { | |
totalBashed += bashedList.size(); | |
if (fVerbose) | |
basher.status("Didn't change", basher.getUnchanged(), | |
targetSubdir); | |
basher.status("Skipped", basher.getSkipped(), targetSubdir); | |
} | |
System.out.println("==== Done Bashing " + targetSubdir); | |
} | |
} | |
System.out.println("\n==== Done Bashing (Bashed " + totalBashed | |
+ " files in total) - Be sure to Refresh (F5) project(s) ===="); | |
} | |
void status(String label, List<String> list, String targetSubdir) { | |
int count = list.size(); | |
System.out.println(label + " " + count | |
+ ((count == 1) ? " file" : " files") + " in " + targetSubdir | |
+ ((count > 0) ? ":" : ".")); | |
if (count > 0) { | |
Iterator<String> i = list.iterator(); | |
while (i.hasNext()) | |
System.out.println(label + ": " + i.next()); | |
System.out.println(); | |
} | |
} | |
char[] readFile(File file) { | |
try { | |
Reader in = new FileReader(file); | |
CharArrayWriter storage = new CharArrayWriter(); | |
char[] chars = new char[8192]; | |
int read = in.read(chars); | |
while (read > 0) { | |
storage.write(chars, 0, read); | |
storage.flush(); | |
read = in.read(chars); | |
} | |
in.close(); | |
return storage.toCharArray(); | |
} catch (IOException ioe) { | |
System.out.println("*** Could not read " + file); | |
} | |
return null; | |
} | |
void writeFile(char[] contents, File file) { | |
try { | |
Writer out = new FileWriter(file); | |
out.write(contents); | |
out.flush(); | |
out.close(); | |
} catch (IOException ioe) { | |
System.out.println("*** Could not write to " + file); | |
if (fVerbose) { | |
System.out.println("<dump filename=\"" + file + "\">"); | |
System.out.println(contents); | |
System.out.println("</dump>"); | |
} | |
} | |
} | |
void bashJavaSourceTree(File sourceDir, File targetDir, File outDir) { | |
if (fVerbose) | |
System.out.println("Reading source javadoc from " + sourceDir); | |
if (!sourceDir.exists()) { | |
System.out.println("Source: " + sourceDir + " was missing"); | |
return; | |
} | |
if (!targetDir.exists()) { | |
System.out.println("Target: " + targetDir + " was missing"); | |
return; | |
} | |
String[] list = sourceDir.list(); | |
if (list != null) { | |
int count = list.length; | |
for (int i = 0; i < count; i++) { | |
String filename = list[i]; | |
if (filename.equals("CVS") || filename.equals("internal") | |
|| filename.equals("library")) | |
continue; | |
File source = new File(sourceDir, filename); | |
File target = new File(targetDir, filename); | |
File out = new File(outDir, filename); | |
if (source.exists() && target.exists()) { | |
if (source.isDirectory()) { | |
if (target.isDirectory()) { | |
bashJavaSourceTree(source, target, out); | |
} else { | |
System.out.println("*** " + target | |
+ " should have been a directory."); | |
} | |
} else { | |
if (filename.toLowerCase().endsWith(".java")) { | |
bashFile(source, target, out); | |
} else { | |
fSkipped.add(source + " (not a java file)"); | |
} | |
} | |
} else { | |
if (source.exists()) { | |
fSkipped.add(target + " (does not exist)"); | |
} else { | |
fSkipped.add(source + " (does not exist)"); | |
} | |
} | |
} | |
} | |
} | |
void bashFile(final File source, final File target, File out) { | |
char[] contents = readFile(source); | |
if (contents == null) return; | |
ASTParser parser = ASTParser.newParser(AST.JLS8); | |
final Document sourceDocument = new Document(new String(contents)); | |
parser.setSource(contents); | |
CompilationUnit sourceUnit = (CompilationUnit)parser.createAST(null); | |
contents = readFile(target); | |
if (contents == null) return; | |
String targetContents = new String(contents); | |
final Document targetDocument = new Document(targetContents); | |
parser.setSource(contents); | |
CompilationUnit targetUnit = (CompilationUnit)parser.createAST(null); | |
final HashMap<String, String> comments = new HashMap<String, String>(); | |
sourceUnit.accept(new ASTVisitor() { | |
String prefix = ""; | |
@Override | |
public boolean visit(Block node) { | |
return false; | |
} | |
@Override | |
public boolean visit(VariableDeclarationFragment node) { | |
FieldDeclaration field = (FieldDeclaration)node.getParent(); | |
int mods = field.getModifiers(); | |
if (Modifier.isPublic(mods) || Modifier.isProtected(mods)) { | |
Javadoc javadoc = field.getJavadoc(); | |
if (field.fragments().size() > 1 && javadoc != null) { | |
System.err.println("Field declaration with multiple variables is not supported. -> " + source + " " + node.getName().getFullyQualifiedName()); | |
} | |
try { | |
String key = prefix + "." + node.getName().getFullyQualifiedName(); | |
comments.put(key, javadoc != null ? sourceDocument.get(javadoc.getStartPosition(), getJavadocLength(sourceDocument, javadoc)) : ""); | |
} catch (BadLocationException e) {} | |
return true; | |
} | |
return false; | |
} | |
@Override | |
public boolean visit(MethodDeclaration node) { | |
int mods = node.getModifiers(); | |
if (Modifier.isPublic(mods) || Modifier.isProtected(mods)) { | |
Javadoc javadoc = node.getJavadoc(); | |
try { | |
String key = prefix + "." + node.getName().getFullyQualifiedName(); | |
for (Iterator<SingleVariableDeclaration> iterator = node.parameters().iterator(); iterator.hasNext();) { | |
SingleVariableDeclaration param = iterator.next(); | |
key += param.getType().toString(); | |
} | |
comments.put(key, javadoc != null ? sourceDocument.get(javadoc.getStartPosition(), getJavadocLength(sourceDocument, javadoc)) : ""); | |
} catch (BadLocationException e) {} | |
return true; | |
} | |
return false; | |
} | |
@Override | |
public boolean visit(TypeDeclaration node) { | |
int mods = node.getModifiers(); | |
if (Modifier.isPublic(mods) || Modifier.isProtected(mods)) { | |
Javadoc javadoc = node.getJavadoc(); | |
try { | |
String key = prefix + "." + node.getName().getFullyQualifiedName(); | |
comments.put(key, javadoc != null ? sourceDocument.get(javadoc.getStartPosition(), getJavadocLength(sourceDocument, javadoc)) : ""); | |
} catch (BadLocationException e) {} | |
prefix = node.getName().getFullyQualifiedName(); | |
return true; | |
} | |
return false; | |
} | |
}); | |
final List<Edit> edits = new ArrayList<Edit>(); | |
targetUnit.accept(new ASTVisitor() { | |
String prefix = ""; | |
@Override | |
public boolean visit(Block node) { | |
return false; | |
} | |
@Override | |
public boolean visit(VariableDeclarationFragment node) { | |
FieldDeclaration field = (FieldDeclaration)node.getParent(); | |
int mods = field.getModifiers(); | |
if (Modifier.isPublic(mods) || Modifier.isProtected(mods)) { | |
Javadoc javadoc = field.getJavadoc(); | |
if (field.fragments().size() > 1 && javadoc != null) { | |
System.err.println("Field declaration with multiple variables is not supported. -> " + target + " " + node.getName().getFullyQualifiedName()); | |
} | |
String key = prefix + "." + node.getName().getFullyQualifiedName(); | |
String newComment = comments.get(key); | |
if (newComment != null) { | |
comments.remove(key); | |
if (javadoc != null) { | |
edits.add(new Edit(javadoc.getStartPosition(), getJavadocLength(targetDocument, javadoc), newComment)); | |
} else { | |
edits.add(new Edit(field.getStartPosition(), 0, newComment)); | |
} | |
} | |
return true; | |
} | |
return false; | |
} | |
@Override | |
public boolean visit(MethodDeclaration node) { | |
int mods = node.getModifiers(); | |
if (Modifier.isPublic(mods) || Modifier.isProtected(mods)) { | |
Javadoc javadoc = node.getJavadoc(); | |
String key = prefix + "." + node.getName().getFullyQualifiedName(); | |
for (Iterator<SingleVariableDeclaration> iterator = node.parameters().iterator(); iterator.hasNext();) { | |
SingleVariableDeclaration param = iterator.next(); | |
key += param.getType().toString(); | |
} | |
String newComment = comments.get(key); | |
if (newComment != null) { | |
comments.remove(key); | |
if (javadoc != null) { | |
edits.add(new Edit(javadoc.getStartPosition(), getJavadocLength(targetDocument, javadoc), newComment)); | |
} else { | |
edits.add(new Edit(node.getStartPosition(), 0, newComment)); | |
} | |
} | |
return true; | |
} | |
return false; | |
} | |
@Override | |
public boolean visit(TypeDeclaration node) { | |
int mods = node.getModifiers(); | |
if (Modifier.isPublic(mods) || Modifier.isProtected(mods)) { | |
Javadoc javadoc = node.getJavadoc(); | |
String key = prefix + "." + node.getName().getFullyQualifiedName(); | |
String newComment = comments.get(key); | |
if (newComment != null) { | |
comments.remove(key); | |
if (javadoc != null) { | |
edits.add(new Edit(javadoc.getStartPosition(), getJavadocLength(targetDocument, javadoc), newComment)); | |
} else { | |
edits.add(new Edit(node.getStartPosition(), 0, newComment)); | |
} | |
} | |
prefix = node.getName().getFullyQualifiedName(); | |
return true; | |
} | |
return false; | |
} | |
}); | |
for (int i = edits.size() - 1; i >=0 ; i--) { | |
Edit edit = edits.get(i); | |
try { | |
targetDocument.replace(edit.start, edit.length, edit.text); | |
} catch (BadLocationException e) { | |
e.printStackTrace(); | |
} | |
} | |
/* Rudimentary API consistency checker. | |
* This assumes that: | |
* a) the sourceSubdir (typically win32) API is correct | |
* b) all sourceSubdir API classes, methods and fields do have a comment | |
* c) names that are in the filter list are never API, | |
* or they are old API that is defined in the super on some platforms | |
*/ | |
if (comments.size() > 0) { | |
String [] filter = new String [] { | |
"Color.win32_newDeviceint", | |
"Cursor.win32_newDeviceint", | |
"Device.hPalette", | |
"Font.win32_newDevicelong", | |
"FontData.data", | |
"FontData.win32_newLOGFONTfloat", | |
"FontMetrics.handle", | |
"FontMetrics.win32_newTEXTMETRIC", | |
"GC.win32_newlongGCData", | |
"GC.win32_newDrawableGCData", | |
"Image.win32_newDeviceintlong", | |
"Pattern.handle", | |
"Region.win32_newDeviceint", | |
"Control.handle", | |
"Display.getSystemFont", | |
"Display.msg", | |
"Menu.handle", | |
"Shell.win32_newDisplaylong", | |
"Accessible.internal_WM_GETOBJECTlonglong", | |
"TransferData.result", | |
"TransferData.stgmedium", | |
"TransferData.pIDataObject", | |
"TransferData.formatetc", | |
"Printer.handle", | |
"Printer.checkDevice", | |
"TableDragSourceEffect.dragFinishedDragSourceEvent", | |
"TableDragSourceEffect.dragStartDragSourceEvent", | |
"TableDropTargetEffect.dragOverDropTargetEvent", | |
"TableDropTargetEffect.dragEnterDropTargetEvent", | |
"TableDropTargetEffect.dragLeaveDropTargetEvent", | |
"Transfer.validateObject", | |
"TransferData.result", | |
"TransferData.stgmedium", | |
"TransferData.pIDataObject", | |
"TransferData.formatetc", | |
"TreeDragSourceEffect.dragFinishedDragSourceEvent", | |
"TreeDragSourceEffect.dragStartDragSourceEvent", | |
"TreeDropTargetEffect.dragLeaveDropTargetEvent", | |
"TreeDropTargetEffect.dragEnterDropTargetEvent", | |
"TreeDropTargetEffect.dragOverDropTargetEvent", | |
"Printer.createDeviceData", | |
"Printer.internal_dispose_GClongGCData", | |
"Printer.release", | |
"Printer.destroy", | |
"Image.handle", | |
"Display.getClientArea", | |
"TreeItem.handle", | |
}; | |
for (Iterator<String> iterator = comments.keySet().iterator(); iterator.hasNext();) { | |
String name = iterator.next(); | |
if (comments.get(name).length() > 0){ | |
int i = 0; | |
for (i = 0; i < filter.length; i++) { | |
if (name.equals(filter[i])) break; | |
} | |
if (i >= filter.length) { | |
System.err.println("***No target for " + name); | |
} | |
} | |
} | |
} | |
String newContents = targetDocument.get(); | |
if (!targetContents.equals(newContents)) { | |
if (makeDirectory(out.getParentFile())) { | |
writeFile(newContents.toCharArray(), out); | |
fBashed.add(target.toString()); | |
} else { | |
System.out.println("*** Could not create " + out.getParent()); | |
} | |
} else { | |
fUnchanged.add(target.toString()); | |
} | |
} | |
int getJavadocLength(Document sourceDocument, Javadoc javadoc) { | |
return skipWhitespace(sourceDocument, javadoc.getStartPosition() + javadoc.getLength()) - javadoc.getStartPosition(); | |
} | |
int skipWhitespace(Document doc, int offset) { | |
try { | |
while (Character.isWhitespace(doc.getChar(offset))){ | |
offset++; | |
} | |
} catch (BadLocationException e) { | |
} | |
return offset; | |
} | |
boolean makeDirectory(File directory) { | |
if (directory.exists()) | |
return true; | |
return directory.mkdirs(); | |
} | |
List<String> getBashed() { | |
return fBashed; | |
} | |
List<String> getUnchanged() { | |
return fUnchanged; | |
} | |
List<String> getSkipped() { | |
return fSkipped; | |
} | |
} |