/* XBN Java: Generically useful, non-GUI Java code. http://sourceforge.net/projects/xbnjava Copyright (C) 1997-2003, Jeff Epstein All rights reserved. Modifications: No Redistribution in binary form, with or without modifications, are permitted provided that the following conditions are met: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * If modifications are made to source code then this license should indicate that fact in the "Modifications" section above. * Neither the author, nor the contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [NOTE: This license contains NO advertising clause.] */ package xbn.programs; import xbn.array.VWObject; import xbn.array.AOSLCreator; import xbn.jdlcode.JDFile; import xbn.named.Named; import xbn.output.Outputter; import xbn.output.OWFile; import xbn.string.FLRString; import xbn.string.SOBStringBuffer; import xbn.string.TrimChars; import xbn.string.UtilSOB; import xbn.string.UtilString; import xbn.util.DirFile; import xbn.util.Utility; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import java.io.File; import java.io.FileFilter; /**

An application to analyze a directory of Java code, reporting on all unneeded/redundant import statements. It detects imports that are not used, duplicate import statements, and also needless self-referencing star ('.*;') imports.

Source code:  ReportBadImports.java.

Notes

This application does the same thing as PMD's rulesets/imports.xml ruleset, but has more specific error messages.

This application uses string analysis only, no reflection is involved. Specifically, it searches for the last word in each specific import statement ('AssertException' is the last word in 'import xbn.AssertException;'), to see that it exists elsewhere in the code (ignoring comments). It can tell the difference between 'MyAssertException' and 'AssertException'.

'.*;' import statements (star imports, the opposite of specific imports) are also analyzed for duplicates, and also that the package it is importing is one other than the package in which the containing class exists.

In order for this code to work properly...

This application will only work (as expected!) on Java code that can be successfully compiled.

Although the compiler knows that // contained in a string is not the beginning of a comment, you must denote this with

'/' + '/'

and not

'//'

The same is true with the multi-line comment delimiter:

'/' + '*' and '*' + '/'

instead of

'/*' and '*/'

Why? Because I don't want to have to write complicated string parsing, which can tell the difference between true comment delimiters, and those existing inside strings. :'  )

Error codes

Key:

Precedence: Duplicate state is always reported. Self-referencing trumps unneeded, and unneeded trumps sub-import. When one error type trumps another, the trumped error condition is not analyzed, and therefore unknown (denoted by a ?).

Code for import type Error type
Star Specific Self-referencing Unneeded Sub-import Duplicate
f f Yes ? ? no
F F Yes ? ? Yes
n/a i no no Yes no
n/a I no no Yes Yes
n/a x no Yes ? no
n/a X no Yes ? Yes
d d no no no Yes

Import statements for testing

Adding these import statements into VWChar (right below existing import statements), and then running this application (or PMD) against it, will result in every possible error. There is no need to compile the code.

      //To test xbn.programs.ReportBadImports...START
         import xbn.array.*;
         import xbn.array.*;
         import xbn.array.UNNEEDED_and_SUB_IMPORT_and_SELF_REFERENCE;
         import xbn.array.UNNEEDED_and_SUB_IMPORT_and_SELF_REFERENCE_and_DUPLICATE;
         import xbn.array.UNNEEDED_and_SUB_IMPORT_and_SELF_REFERENCE_and_DUPLICATE;

         import java.util.UNNEEDED;

         //Duplicate, because of the actual import statement above
         //this testing section.
         import java.util.Arrays;

         import java.util.UNNEEDED_and_DUPLICATE;
         import java.util.UNNEEDED_and_DUPLICATE;

         //No errors.
         import java.bla.*;

         import java.bla.UNNEEDED_and_SUB_IMPORT;
         import java.bla.UNNEEDED_and_SUB_IMPORT_and_DUPLICATE;
         import java.bla.UNNEEDED_and_SUB_IMPORT_and_DUPLICATE;
      //To test xbn.programs.ReportBadImports...END

@version 0.9b @author Jeff Epstein, http://sourceforge.net/projects/xbnjava. **/ public class ReportBadImports extends XBNStatic { private static final String sSLASH = "/"; private static final String sSTAR = "*"; private static SOBStringBuffer ssb = new SOBStringBuffer(sES); private static final TrimChars tc = new TrimChars(); private static final UtilSOB uSOB = new UtilSOB(); private static final Utility util = new Utility(); private static final ACjdfni acjdfni = new ACjdfni(); private static final StringBuffer sbErrors = new StringBuffer(sES); private static final StringBuffer sbClassErr = new StringBuffer(sES); private static int iErrors = 0; private static int iValid = 0; //Define and describe all command line parameters...START private static final Option optDirToAnalyze = OptionBuilder.withArgName("dir") .hasArg() .isRequired() .withDescription("Directory to analyze") .create("dir2nlz"); private static final Option optOutputFile = OptionBuilder.withArgName("file") .hasArg() .withDescription("Also send output to this file") .create("outfile"); private static final Option optSubDirLvls = OptionBuilder.withArgName("int") .withDescription("Sub-directory levels, provided as i_levelsToAnalyze to DirFile") .create("subdirlvls"); private static final Options opts = new Options(); //Define and describe all command line parameters...END /**

Create a ReportBadImports. This constructor does nothing.

**/ public ReportBadImports() { } /**

Run the application.

**/ public final static void main(String[] as_cmdLineParams) { sopl("ReportBadImports...START"); //Prepare for testing and get command line parameters...START opts.addOption(optDirToAnalyze); opts.addOption(optOutputFile); opts.addOption(optSubDirLvls); UtilCommandLine ucl = new UtilCommandLine(opts, 80, "xbn.programs.ReportBadImports", null, "\nFor documentation on the meaning of the reported error codes, see the JavaDoc for xbn.programs.ReportBadImports.", true); CommandLine cl = ucl.getCommandLine(as_cmdLineParams); Outputter optr = ucl.getOSDOOrOSDOAndFile("outfile", false); String sDir = cl.getOptionValue("dir2nlz"); int iSubDirLevels = ucl.getInt("subdirlvls", -1); //Prepare for testing and get command line parameters...END DirFile df = new DirFile(sDir, iSubDirLevels, (new FFJava()), optr); optr.write("------------------\nListing all files\n------------------"); optr.write(df.toString()); analyzeFiles(optr, df); optr.write("\n------------------\nSTART REPORT (" + iErrors + " errors, " + iValid + " valid)\n------------------\n" + sbErrors.toString() + "\n------------------\nEND REPORT (" + iErrors + " errors, " + iValid + " valid)\n------------------\n"); if(cl.hasOption("outfile")) { optr.write("\n\nOutput written to '" + ((OWFile)optr.getOWriter()).getPath() + "'"); } optr.write("(For the meanings of error codes, see the JavaDoc for xbn.programs.ReportBadImports)\n\nReportBadImports...END"); } private static void analyzeFiles(Outputter optr_dbg, DirFile df) { String sDebugPfx = " " + (new UtilString()).getDuped(" ", df.getLevelsBelowBaseDir()); for(int i = 0; i < df.getCountSubFiles(); i++) { File f = df.getSubFile(i).getFileObjectThis(); String sFilename = f.getName().substring(0, f.getName().length() - 5); optr_dbg.writeNoln(sDebugPfx + sFilename + " "); ssb.deleteAll(); util.appendFileText(ssb, f.getPath()); //Although the compiler knows that [slash][slash] //contained *in* a string is not the beginning of a //comment, I don't want to have to write code that //complicated, so make sure that [slash][slash] is //really only comments. util.stripSLCs(ssb, sSLASH + sSLASH); //Same with the multi-line comment delimiter. util.stripMLCs(ssb, sSLASH + sSTAR, sSTAR + sSLASH); //Trim all lines of tabs and spaces. This causes the //replaceUntil line to eliminate all empty *and* //whitespace only lines. tc.trimAllLines(ssb); //Eliminate all empty lines (except the very first and last) uSOB.replaceUntil(ssb, sLINE_SEP + sLINE_SEP, sLINE_SEP); //Eliminate empty lines at the very start and end. ssb.trim(); FLRString flrs = new FLRString(ssb.getStringBuffer()); AOSLCreator aoslc = new AOSLCreator(true, true); acjdfni.initializeVector(); JDFile jdfPackage = null; SOBStringBuffer ssbLine = null; while(flrs.hasMoreLines()) { ssbLine = new SOBStringBuffer(flrs.getNextLine()); if(ssbLine.startsWith("package")) { String sFQPackage = ssbLine.substring("package ".length(), ssbLine.length()); //Eliminate whitespace, and then the semicolon ("- 1"). sFQPackage = sFQPackage.trim(); sFQPackage = sFQPackage.substring(0, sFQPackage.length() - 1) + ".*"; jdfPackage = new JDFile(sFQPackage); //This is the first line in the file, and it is //a package statement. continue; } if(!ssbLine.startsWith("import")) { //All import statements have been retrieved. //This is the first line that does not start with //"import". break; } //This line is an import statement. //7 is the character *2* after the "t", because //there's expected to be a space or tab there. String sFQClass = ssbLine.substring(7, ssbLine.length()); //We don't know how many tabs or spaces there are //between import and the class. We are assuming //at least one, though. This also eliminates //the ending newline characters (and any other //whitespace). sFQClass = sFQClass.trim(); //Eliminate the ending semicolon sFQClass = sFQClass.substring(0, sFQClass.length() - 1); //Used exclusively so we can call //wasLastStringUnique(), below. aoslc.addString(sFQClass); JDFNImport jdfni = null; if(sFQClass.endsWith(sSTAR)) { jdfni = new JDFNIStar(sFQClass); } else { //This does not end in *, and is therefore //specific. jdfni = new JDFNISpecific(sFQClass); } if(aoslc.wasLastStringUnique()) { //This is a new import statement. acjdfni.add(jdfni); //Is this self-referencing (importing a class in //the same package as the class in which the //import statement exists)? if(jdfPackage != null && jdfPackage.isInSamePackageAs(jdfni.getJDFile())) { //It is. jdfni.declareSelfReference(); } } else { //This is a *duplicate* import statement. Find //the previous (existing) import, and mark it as //a duplicate. for(int j = 0; j < acjdfni.size(); j++) { if(acjdfni.getJDFNImport(j).getName().equals(sFQClass)) { acjdfni.getJDFNImport(j).declareDuplicate(); break; } } } } //All import statements have been retrieved. //Determine sub-imports. (Even though self-references //trump sub-imports [sub-imports are not reported when //it's a self-reference], were still analyzing if it is //a sub-import here, for diagnostic and debugging.) for(int j = 0; j < acjdfni.size(); j++) { if(acjdfni.getJDFNImport(j).isSpecific()) { //Only specific imports can be sub-import... for(int k = 0; k < acjdfni.size(); k++) { //...and they can only be sub-import with //star imports. if(acjdfni.getJDFNImport(k).isStar()) { //Okay, so is it sub-import? J is specific, K is star if(acjdfni.getJDFNISpecific(j).isInSamePackageAs(acjdfni.getJDFNIStar(k))) { //Yup. acjdfni.getJDFNISpecific(j).declareSubImport(); break; } } } } } //All sub-imports determined. //Now...determine which *specific* import statements //are actually being used in the code (below the import //statements). while(true) { //Note we're not getting the next line here, because //we still have it from the previous while. boolean bAllSpecificsFound = true; for(int j = 0; j < acjdfni.size(); j++) { if(acjdfni.getJDFNImport(j).isSpecific() && !acjdfni.getJDFNISpecific(j).wasFound()) { //This is a specific import statement, and it //was not yet found (used by the code). :' ( bAllSpecificsFound = false; //(Being found is moot for star imports.) } } if(bAllSpecificsFound) { //Every *specifically* imported class was found. //No need to analyze any more lines. break; } //At least one specific was not yet found was not yet found. for(int j = 0; j < acjdfni.size(); j++) { if(acjdfni.getJDFNImport(j).isSpecific() && !acjdfni.getJDFNISpecific(j).wasFound() && uSOB.indexOfWord(ssbLine, acjdfni.getJDFNISpecific(j).getJDFile().getFileName(), 0, ssbLine.length()) != -1) { acjdfni.getJDFNISpecific(j).declareFound(); } } if(!flrs.hasMoreLines()) { break; } //There is at least one more line. ssbLine = new SOBStringBuffer(flrs.getNextLine()); tc.trim(ssbLine); } //All potential errors have been analyzed for. /** //Debugging...START sopl(sES); int z = 0; for(; z < acjdfni.size(); z++) { sopl(acjdfni.getJDFNImport(z).getName() + " (isStar()=" + acjdfni.getJDFNImport(z).isStar() + ", isSpecific()=" + acjdfni.getJDFNImport(z).isSpecific() + ")"); sopl("\tisSelfReferencing()= " + acjdfni.getJDFNImport(z).isSelfReferencing()); sopl("\tisDuplicate()= " + acjdfni.getJDFNImport(z).isDuplicate()); if(acjdfni.getJDFNImport(z).isSpecific()) { sopl("\tisSubImport()= " + acjdfni.getJDFNISpecific(z).isSubImport()); sopl("\tisUnneeded()= " + acjdfni.getJDFNISpecific(z).isUnneeded()); } } sopl(sES + z + " total"); //Debugging...END **/ //Report on all errors, based on the state of each //JDFNImport. For error documentation and precedence, //see the main JavaDoc for this class. for(int j = 0; j < acjdfni.size(); j++) { char cErrorCode = ' '; //Is this import statement self-referencing? //(both star and specific) if(acjdfni.getJDFNImport(j).isSelfReferencing()) { //Self-referencing trumps everything but duplicate. if(acjdfni.getJDFNImport(j).isDuplicate()) { //Both self-referencing AND duplicate. cErrorCode = 'F'; } else { //Just self-referencing. cErrorCode = 'f'; } } //It is not self-referencing. //Check only specific imports for unneeded-ness, and //sub-import-ed-ness. if(cErrorCode == ' ' && acjdfni.getJDFNImport(j).isSpecific()) { if(acjdfni.getJDFNISpecific(j).isUnneeded()) { //Unneeded trumps sub-import (but not self- //referencing). if(acjdfni.getJDFNISpecific(j).isDuplicate()) { //Both self-referencing AND duplicate. cErrorCode = 'X'; } else { //Just self-referencing. cErrorCode = 'x'; } } //It is not unneeded. Is it a sub-import? if(cErrorCode == ' ' && acjdfni.getJDFNISpecific(j).isSubImport()) { //Unneeded trumps sub-import (but not self- //referencing). if(acjdfni.getJDFNISpecific(j).isDuplicate()) { //Both self-referencing AND duplicate. cErrorCode = 'I'; } else { //Just self-referencing. cErrorCode = 'i'; } } //It is not a sub-import. } //This import statement is definitely not self- //referencing, unneeded, nor is it a sub-import. Is //it a duplicate? if(cErrorCode == ' ' && acjdfni.getJDFNImport(j).isDuplicate()) { //Yup, it's duplicate. cErrorCode = 'd'; } //Done getting error codes for this import. Report. if(cErrorCode != ' ') { //This import statement is in error somehow, some way. iErrors++; optr_dbg.writeNoln(new Character(cErrorCode).toString()); sbClassErr.append(" " + cErrorCode + " " + acjdfni.getJDFNImport(j).getName() + sLINE_SEP); } else { iValid++; optr_dbg.writeNoln("."); } } if(sbClassErr.length() > 0) { //There is at least one error for this class. sbErrors.append(f.getPath() + sLINE_SEP + sbClassErr + sLINE_SEP); sbClassErr.setLength(0); } optr_dbg.newln(); } //Recursion. Only call again for sub-directories. for(int i = 0; i < df.getCountSubDirs(); i++) { optr_dbg.write(sDebugPfx + "[" + df.getSubDir(i).getFileObjectThis().getName() + "]"); analyzeFiles(optr_dbg, df.getSubDir(i)); } } } class FFJava implements FileFilter { public boolean accept(File f_ile) { if(f_ile.isDirectory() && !f_ile.getName().toLowerCase().startsWith("cvs") && !f_ile.getName().toLowerCase().endsWith("cvs")) { return true; } if(f_ile.getPath().toLowerCase().endsWith(".java")) { return true; } else { return false; } } } /** Represents an import statement. **/ class JDFNImport implements Named { private String sName = null; private boolean bDuplicate = false; private boolean bSelfReference = false; private JDFile jdf = null; public JDFNImport(String s_fqClassName) { jdf = new JDFile(s_fqClassName); sName = jdf.getList("."); } public final boolean isStar() { return getName().endsWith("*"); } public final boolean isSpecific() { return !isStar(); } public final String getName() { return sName; } public final JDFile getJDFile() { return jdf; } public final void declareDuplicate() { bDuplicate = true; } public final boolean isDuplicate() { return bDuplicate; } public final void declareSelfReference() { bSelfReference = true; } public final boolean isSelfReferencing() { return bSelfReference; } public final boolean isInSamePackageAs(JDFNImport jdfn_import) { return jdfn_import.getJDFile().isInSamePackageAs(getJDFile()); } } /** Represents a "specific" import statement. That is, an import statement that ends with anything other than ".*;" **/ class JDFNISpecific extends JDFNImport { private boolean bFound = false; private boolean bSubImport = false; public JDFNISpecific(String s_fqClassName) { super(s_fqClassName); } public final void declareFound() { bFound = true; } public final boolean wasFound() { return bFound; } public final boolean isUnneeded() { return !wasFound(); } public final void declareSubImport() { bSubImport = true; } public final boolean isSubImport() { return bSubImport; } } /** Represents a "star" import statement. That is, an import statement that ends with ".*;" This empty class is simply to make JDFNImport a "generic" name. The (way) above code is more clear because of it. **/ class JDFNIStar extends JDFNImport { public JDFNIStar(String s_fqClassName) { super(s_fqClassName); } } class ACjdfni extends VWObject { public final ACjdfni[] getAOACjdfn() { Object[] ao = getAOObject(); if(ao == null) { return null; } if(ao.length == 0) { return (new ACjdfni[0]); } //The array is at least one element in length. ACjdfni[] aACjdfn = new ACjdfni[ao.length]; for(int i = 0; i < aACjdfn.length; i++) { aACjdfn[i] = (ACjdfni)ao[i]; } return aACjdfn; } public final JDFNImport getJDFNImport(int i_dx) { return (JDFNImport)getObject(i_dx); } public final JDFNISpecific getJDFNISpecific(int i_dx) { return (JDFNISpecific)getJDFNImport(i_dx); } public final JDFNIStar getJDFNIStar(int i_dx) { return (JDFNIStar)getJDFNImport(i_dx); } }