检查大小写样式2007-05-29 yycnet.yeah.net yyc译尽管对涉及文字处理的一些项目来说,前例显得比较方便,但下面要介绍的项目却能立即发挥作用,因为它执行的是一个样式检查,以确保我们的大小写形式符合“事实上”的Java样式标准。它会在当前目录中打开每个.java文件,并提取出所有类名以及标识符。若发现有不符合Java样式的情况,就向我们提出报告。
为了让这个程序正确运行,首先必须构建一个类名,将它作为一个“仓库”,负责容纳标准Java库中的所有类名。为达到这个目的,需遍历用于标准Java库的所有源码子目录,并在每个子目录都运行ClassScanner。至于参数,则提供仓库文件的名字(每次都用相同的路径和名字)和命令行开关-a,指出类名应当添加到该仓库文件中。
为了用程序检查自己的代码,需要运行它,并向它传递要使用的仓库文件的路径与名字。它会检查当前目录中的所有类和标识符,并告诉我们哪些没有遵守典型的Java大写写规范。
要注意这个程序并不是十全十美的。有些时候,它可能报告自己查到一个问题。但当我们仔细检查代码的时候,却发现没有什么需要更改的。尽管这有点儿烦人,但仍比自己动手检查代码中的所有错误强得多。
下面列出源代码,后面有详细的解释:
//: ClassScanner.java// Scans all files in directory for classes// and identifiers, to check capitalization.// Assumes properly compiling code listings.// Doesn"t do everything right, but is a very// useful aid.import java.io.*;import java.util.*;class MultiStringMap extends Hashtable {public void add(String key, String value) {if(!containsKey(key))put(key, new Vector());((Vector)get(key)).addElement(value);}public Vector getVector(String key) {if(!containsKey(key)) {System.err.println("ERROR: can"t find key: " + key);System.exit(1);}return (Vector)get(key);}public void printValues(PrintStream p) {Enumeration k = keys();while(k.hasMoreElements()) {String oneKey = (String)k.nextElement();Vector val = getVector(oneKey);for(int i = 0; i < val.size(); i++)p.println((String)val.elementAt(i));}}}public class ClassScanner {private File path;private String[] fileList;private Properties classes = new Properties();private MultiStringMap classMap = new MultiStringMap(),identMap = new MultiStringMap();private StreamTokenizer in;public ClassScanner() {path = new File(".");fileList = path.list(new JavaFilter());for(int i = 0; i < fileList.length; i++) {System.out.println(fileList[i]);scanListing(fileList[i]);}}void scanListing(String fname) {try {in = new StreamTokenizer(new BufferedReader(new FileReader(fname)));// Doesn"t seem to work:// in.slashStarComments(true);// in.slashSlashComments(true);in.ordinaryChar("/");in.ordinaryChar(".");in.wordChars("_", "_");in.eolIsSignificant(true);while(in.nextToken() != StreamTokenizer.TT_EOF) {if(in.ttype == "/")eatComments();else if(in.ttype == StreamTokenizer.TT_WORD) {if(in.sval.equals("class") ||in.sval.equals("interface")) {// Get class name: while(in.nextToken() !=StreamTokenizer.TT_EOF && in.ttype !=StreamTokenizer.TT_WORD) ; classes.put(in.sval, in.sval); classMap.add(fname, in.sval);}if(in.sval.equals("import") || in.sval.equals("package"))discardLine();else // It"s an identifier or keywordidentMap.add(fname, in.sval);}}} catch(IOException e) {e.printStackTrace();}}void discardLine() {try {while(in.nextToken() != StreamTokenizer.TT_EOF&& in.ttype != StreamTokenizer.TT_EOL); // Throw away tokens to end of line} catch(IOException e) {e.printStackTrace();}}// StreamTokenizer"s comment removal seemed// to be broken. This extracts them:void eatComments() {try {if(in.nextToken() !=StreamTokenizer.TT_EOF) {if(in.ttype == "/")discardLine();else if(in.ttype != "*")in.pushBack();else while(true) {if(in.nextToken() == StreamTokenizer.TT_EOF)break;if(in.ttype == "*")if(in.nextToken() != StreamTokenizer.TT_EOF&& in.ttype == "/")break;}}} catch(IOException e) {e.printStackTrace();}}public String[] classNames() {String[] result = new String[classes.size()];Enumeration e = classes.keys();int i = 0;while(e.hasMoreElements())result[i++] = (String)e.nextElement();return result;}public void checkClassNames() {Enumeration files = classMap.keys();while(files.hasMoreElements()) {String file = (String)files.nextElement();Vector cls = classMap.getVector(file);for(int i = 0; i < cls.size(); i++) {String className = (String)cls.elementAt(i);if(Character.isLowerCase( className.charAt(0)))System.out.println("class capitalization error, file: "+ file + ", class: " + className);}}}public void checkIdentNames() {Enumeration files = identMap.keys();Vector reportSet = new Vector();while(files.hasMoreElements()) {String file = (String)files.nextElement();Vector ids = identMap.getVector(file);for(int i = 0; i < ids.size(); i++) {String id = (String)ids.elementAt(i);if(!classes.contains(id)) {// Ignore identifiers of length 3 or// longer that are all uppercase// (probably static final values):if(id.length() >= 3 && id.equals( id.toUpperCase()))continue;// Check to see if first char is upper:if(Character.isUpperCase(id.charAt(0))){if(reportSet.indexOf(file + id)== -1){ // Not reported yetreportSet.addElement(file + id);System.out.println("Ident capitalization error in:"+ file + ", ident: " + id);}}}}}}static final String usage ="Usage:
" + "ClassScanner classnames -a
" +" Adds all the class names in this
" +" directory to the repository file
" +" called "classnames"
" +"ClassScanner classnames
" +" Checks all the java files in this
" +" directory for capitalization errors,
" +" using the repository file "classnames"";private static void usage() {System.err.println(usage);System.exit(1);}public static void main(String[] args) {if(args.length < 1 || args.length > 2)usage();ClassScanner c = new ClassScanner();File old = new File(args[0]);if(old.exists()) {try {// Try to open an existing // properties file:InputStream oldlist =new BufferedInputStream(new FileInputStream(old));c.classes.load(oldlist);oldlist.close();} catch(IOException e) {System.err.println("Could not open "+ old + " for reading");System.exit(1);}}if(args.length == 1) {c.checkClassNames();c.checkIdentNames();}// Write the class names to a repository:if(args.length == 2) {if(!args[1].equals("-a"))usage();try {BufferedOutputStream out =new BufferedOutputStream(new FileOutputStream(args[0]));c.classes.save(out,"Classes found by ClassScanner.java");out.close();} catch(IOException e) {System.err.println("Could not write " + args[0]);System.exit(1);}}}}class JavaFilter implements FilenameFilter {public boolean accept(File dir, String name) {// Strip path information:String f = new File(name).getName();return f.trim().endsWith(".java");}} ///:~
MultiStringMap类是个特殊的工具,允许我们将一组字串与每个键项对应(映射)起来。和前例一样,这里也使用了一个散列表(Hashtable),不过这次设置了继承。该散列表将键作为映射成为Vector值的单一的字串对待。add()方法的作用很简单,负责检查散列表里是否存在一个键。如果不存在,就在其中放置一个。getVector()方法为一个特定的键产生一个Vector;而printValues()将所有值逐个Vector地打印出来,这对程序的调试非常有用。
为简化程序,来自标准Java库的类名全都置入一个Properties(属性)对象中(来自标准Java库)。记住Properties对象实际是个散列表,其中只容纳了用于键和值项的String对象。然而仅需一次方法调用,我们即可把它保存到磁盘,或者从磁盘中恢复。实际上,我们只需要一个名字列表,所以为键和值都使用了相同的对象。
针对特定目录中的文件,为找出相应的类与标识符,我们使用了两个MultiStringMap:classMap以及identMap。此外在程序启动的时候,它会将标准类名仓库装载到名为classes的Properties对象中。一旦在本地目录发现了一个新类名,也会将其加入classes以及classMap。这样一来,classMap就可用于在本地目录的所有类间遍历,而且可用classes检查当前标记是不是一个类名(它标记着对象或方法定义的开始,所以收集接下去的记号——直到碰到一个分号——并将它们都置入identMap)。
ClassScanner的默认构建器会创建一个由文件名构成的列表(采用FilenameFilter的JavaFilter实现形式,参见第10章)。随后会为每个文件名都调用scanListing()。
在scanListing()内部,会打开源码文件,并将其转换成一个StreamTokenizer。根据Java帮助文档,将true传递给slashStartComments()和slashSlashComments()的本意应当是剥除那些注释内容,但这样做似乎有些问题(在Java 1.0中几乎无效)。所以相反,那些行被当作注释标记出去,并用另一个方法来提取注释。为达到这个目的,"/"必须作为一个原始字符捕获,而不是让StreamTokeinzer将其当作注释的一部分对待。此时要用ordinaryChar()方法指示StreamTokenizer采取正确的操作。同样的道理也适用于点号("."),因为我们希望让方法调用分离出单独的标识符。但对下划线来说,它最初是被StreamTokenizer当作一个单独的字符对待的,但此时应把它留作标识符的一部分,因为它在static final值中以TT_EOF等等形式使用。当然,这一点只对目前这个特殊的程序成立。wordChars()方法需要取得我们想添加的一系列字符,把它们留在作为一个单词看待的记号中。最后,在解析单行注释或者放弃一行的时候,我们需要知道一个换行动作什么时候发生。所以通过调用eollsSignificant(true),换行符(EOL)会被显示出来,而不是被StreamTokenizer吸收。
scanListing()剩余的部分将读入和检查记号,直至文件尾。一旦nextToken()返回一个final static值——StreamTokenizer.TT_EOF,就标志着已经抵达文件尾部。
若记号是个"/",意味着它可能是个注释,所以就调用eatComments(),对这种情况进行处理。我们在这儿唯一感兴趣的其他情况是它是否为一个单词,当然还可能存在另一些特殊情况。
如果单词是class(类)或interface(接口),那么接着的记号就应当代表一个类或接口名字,并将其置入classes和classMap。若单词是import或者package,那么我们对这一行剩下的东西就没什么兴趣了。其他所有东西肯定是一个标识符(这是我们感兴趣的),或者是一个关键字(对此不感兴趣,但它们采用的肯定是小写形式,所以不必兴师动众地检查它们)。它们将加入到identMap。
discardLine()方法是一个简单的工具,用于查找行末位置。注意每次得到一个新记号时,都必须检查行末。
只要在主解析循环中碰到一个正斜杠,就会调用eatComments()方法。然而,这并不表示肯定遇到了一条注释,所以必须将接着的记号提取出来,检查它是一个正斜杠(那么这一行会被丢弃),还是一个星号。但假如两者都不是,意味着必须在主解析循环中将刚才取出的记号送回去!幸运的是,pushBack()方法允许我们将当前记号“压回”输入数据流。所以在主解析循环调用nextToken()的时候,它能正确地得到刚才送回的东西。
为方便起见,classNames()方法产生了一个数组,其中包含了classes集合中的所有名字。这个方法未在程序中使用,但对代码的调试非常有用。
接下来的两个方法是实际进行检查的地方。在checkClassNames()中,类名从classMap提取出来(请记住,classMap只包含了这个目录内的名字,它们按文件名组织,所以文件名可能伴随错误的类名打印出来)。为做到这一点,需要取出每个关联的Vector,并遍历其中,检查第一个字符是否为小写。若确实为小写,则打印出相应的出错提示消息。
在checkIdentNames()中,我们采用了一种类似的方法:每个标识符名字都从identMap中提取出来。如果名字不在classes列表中,就认为它是一个标识符或者关键字。此时会检查一种特殊情况:如果标识符的长度等于3或者更长,而且所有字符都是大写的,则忽略此标识符,因为它可能是一个static final值,比如TT_EOF。当然,这并不是一种完美的算法,但它假定我们最终会注意到任何全大写标识符都是不合适的。
这个方法并不是报告每一个以大写字符开头的标识符,而是跟踪那些已在一个名为reportSet()的Vector中报告过的。它将Vector当作一个“集合”对待,告诉我们一个项目是否已在那个集合中。该项目是通过将文件名和标识符连接起来生成的。若元素不在集合中,就加入它,然后产生报告。
程序列表剩下的部分由main()构成,它负责控制命令行参数,并判断我们是准备在标准Java库的基础上构建由一系列类名构成的“仓库”,还是想检查已写好的那些代码的正确性。不管在哪种情况下,都会创建一个ClassScanner对象。
无论准备构建一个“仓库”,还是准备使用一个现成的,都必须尝试打开现有仓库。通过创建一个File对象并测试是否存在,就可决定是否打开文件并在ClassScanner中装载classes这个Properties列表(使用load())。来自仓库的类将追加到由ClassScanner构建器发现的类后面,而不是将其覆盖。如果仅提供一个命令行参数,就意味着自己想对类名和标识符名字进行一次检查。但假如提供两个参数(第二个是"-a"),就表明自己想构成一个类名仓库。在这种情况下,需要打开一个输出文件,并用Properties.save()方法将列表写入一个文件,同时用一个字串提供文件头信息。