/* * [SortDeclarations.java] * * Summary: compare two Java declarations, possibly with lead comment, with trailing ;. * * Copyright: (c) 2014-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2014-04-30 initial version * 1.1 2014-05-02 change sort order */ package com.mindprod.sortcode; import com.mindprod.common18.FNV1a64; import com.mindprod.common18.ST; import com.mindprod.fastcat.FastCat; import java.util.Comparator; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import static java.lang.System.*; /** * compare two Java declarations, possibly with lead comment, with trailing ;. *

* For this to work, keywords much be in canonical order i.e. * private static final int x; * Must use separator ; * Cannot handle static init. * Cannot handle classes or methods. * Sorts declarations: public before private, static before instance, final before non-final, * boolean, Boolean, char, Character, short, Short, int, Integer, long, Long, float, Float, * double, Double, variable name case-insensitive ascending * * @author Roedy Green, Canadian Mind Products * @version 1.1 2014-05-02 change sort order * @see com.mindprod.sortcode.SortMethods * @since 2014-04-30 */ public class SortDeclarations implements Comparator { // todo: ignore annotations. /** * how much space for the precomputed keys */ private static final int ALLOC_FOR_PRECOMPUTE = 1000; /** * true if want extra debugging output */ private static final boolean DEBUGGING = false; /** * set true if want lower case after upper case. * set false if want upper and lower case mixed together */ private static final boolean CASE_SENSITIVE = false; /** * parse Java declaration * keywords that apply to declarations * * @annotations (ignored) * public | protected | private ] * static * final * [ transient | volatile ] * [ int | long | String etc. */ private static final String ANNOTATION = "(?:@[\\w]+\\s*(?:\\(\\s*[ \"\\w\\{\\}]+\\s*\\))?\\s*)*"; private static final Pattern DCL_GRABBER = Pattern.compile( ANNOTATION + "(private|public|protected)?\\s*" + "(static)?\\s*" + "(final)?\\s*" + "(transient|volatile)?\\s*" + "(boolean|byte|char|int|long|float|double|[A-Za-z0-9_\\.]*)\\s*" + "(?:[\\[<][A-Za-z0-9_ ,]*[>\\]])?\\s*" // generics<> array [] + "([A-Za-z0-9_]+)" ); // var name /** * there is no point in caching segments of code that one one else is still using. * WeakHashMap gets rid of keys prematurely because noone else but us ever sees * the chunks. When we stop looking at them, WeakHashMap drops them too. The sort never * sees the chunks. We don't want chunks hanging around, so we digest with a hash. */ private static HashMap precomputedKeys = new HashMap<>( ALLOC_FOR_PRECOMPUTE ); private static char calcFinalityOrder( final String finality ) { if ( ST.isEmpty( finality ) ) { return '1'; } switch ( finality ) { case "final": return '0'; case "": /* not final */ return '1'; default: err.println( "unrecognised or misplaced final " + finality ); return 'z'; } } // /method private static char calcInstanceOrder( final String instance ) { if ( ST.isEmpty( instance ) ) { return '1'; } switch ( instance ) { case "static": return '0'; case "": /*instance */ return '1'; default: err.println( "unrecognised or misplaced static/instance " + instance ); return 'z'; } } // /method private static char calcScopeOrder( final String scope ) { if ( ST.isEmpty( scope ) ) { return '1'; } switch ( scope ) { case "public": return '0'; case "": /* package default */ return '1'; case "protected": return '2'; case "private": return '3'; default: err.println( "unrecognised or misplaced scope " + scope ); return 'z'; } } // /method private static char calcTypeOrder( final String typeName ) { if ( ST.isEmpty( typeName ) ) { return 'z'; } switch ( typeName ) { case "": return 'z'; case "boolean": return '1'; case "Boolean": return '2'; case "byte": return '3'; case "Byte": return '4'; case "char": return '5'; case "Character": return '6'; case "short": return '7'; case "Short": return '8'; case "int": return '9'; case "Integer": return 'A'; case "long": return 'B'; case "Long": return 'C'; case "float": return 'D'; case "Float": return 'E'; case "double": return 'F'; case "Double": return 'G'; case "String": return 'H'; default: // we should have a class name of some sort e.g. Double, HashMap if ( Character.isLowerCase( typeName.charAt( 0 ) ) && !typeName.contains( "." ) ) { err.println( "Class name should start with a capital letter: " + typeName ); } return 'I'; } // end switch } // /method private static char calcVarOrder( final String varName, final boolean isStatic, final boolean isFinal ) { if ( ST.isEmpty( varName ) ) { return 'z'; } if ( varName.toUpperCase().equals( varName ) ) { // all upper caps // should be static final if ( !( isStatic && isFinal ) ) { err.println( "All caps should static final" + varName ); } return '0'; } else if ( Character.isUpperCase( varName.charAt( 0 ) ) ) { // first letter is cap err.println( "Non-static final variable name should start with lower case letter " + varName ); return '3'; } else { // first letter is lower case, as it should be. return '3'; } } // /method private static String[] computeKey( String s ) { // use regex to pluck info useful for sort. // The declaration we might parse looks like: // private static final int q; // or public String = "abc"; // or private static final ArrayList COLLECTION = new ArrayList<>( COUNT_OF_FILES_TO_COLLECT ); final String clean = stripLeadComment( s ); // see if we have done this before: long hash = FNV1a64.computeHash( clean ); final String[] precomputedKey = precomputedKeys.get( hash ); if ( precomputedKey != null ) { return precomputedKey; } if ( DEBUGGING ) { // whole thing, not just first line, without comment out.println( clean ); } final Matcher m = DCL_GRABBER.matcher( clean ); final String[] computedKey; if ( m.lookingAt() ) { /** * parse Java declaration * keywords that apply to declarations * * @Nullable * public | protected | private ] * static * final * [ transient | volatile ] * [ int | long | String etc. */ int g = 1; final String scope = ST.canonical( m.group( g++ ) ); final String instance = ST.canonical( m.group( g++ ) ); final String finality = ST.canonical( m.group( g++ ) ); final String trans = ST.canonical( m.group( g++ ) );// transient/volatile) final String typeName = ST.canonical( m.group( g++ ) ); final String varName = ST.canonical( m.group( g++ ) ); if ( DEBUGGING ) { out.println( "[scope:" + scope + "|static:" + instance + "|final:" + finality + "|trans:" + trans + "|type:" + typeName + "|var:" + varName + "]" ); } // analyse keywords we found final char scopeLetter = calcScopeOrder( scope ); /* public first */ final char instanceOrder = calcInstanceOrder( instance ); /* static first */ final char finalityOrder = calcFinalityOrder( finality ); /* final first */ final char typeOrder = calcTypeOrder( typeName ); /* boolean before int before String before Pattern */ final boolean isStatic = "static".equals( instance ); final boolean isFinal = "final".equals( finality ); final char varOrder = calcVarOrder( varName, isStatic, isFinal ); /* all upper first */ // sort by scope, instance, finality, primitive/class , classname, varname // glue sort category letter together. final FastCat sb = new FastCat( 5 ); sb.append( scopeLetter ); sb.append( instanceOrder ); sb.append( finalityOrder ); // for methods, name in more important than type sb.append( typeOrder ); final String condensedKey = sb.toString(); if ( CASE_SENSITIVE ) { // case-sensitive version computedKey = new String[] { condensedKey, typeName, String.valueOf( varOrder ), varName }; } else { // case-insensitive version computedKey = new String[] { condensedKey, typeName.toUpperCase(), String.valueOf( varOrder ), varName.toUpperCase() }; } if ( DEBUGGING ) { for ( String chunk : computedKey ) { out.print( chunk + "|" ); } out.println(); } } else { err.println( "SortDeclarations parse failed." ); err.println( clean ); err.println( "--------------------------------------------------------------" ); computedKey = new String[] { "zzzz", "", "z", clean }; } // save it in case we need it again precomputedKeys.put( hash, computedKey ); return computedKey; } // /method /** * remove lead comment. * * @param s block of code. pretrimmed. * * @return block of code with any lead javadoc removed. */ private static String stripLeadComment( final String s ) { // s has already been trimmed final String clean; if ( s.startsWith( "/**" ) ) { final int end = s.indexOf( "*/", "/**".length() ); if ( end < 0 ) { // malformed comment err.println( "malformed JavaDoc comment: [-- " + s + " --]\n" ); return s; } else { return s.substring( end + "*/".length() ).trim(); } } else { // no comment present. return s; } } // /method /** * Compare two String Objects each containing a method body. * Informally, returns (a-b), or +ve if a comes after b. * * @param a first String to compare * @param b second String to compare * * @return +ve if a>b, 0 if a==b, -ve if a<b */ public final int compare( String a, String b ) { final String[] extracta = computeKey( a ); final String[] extractb = computeKey( b ); // sort on synthetic key that orders keywords int diff = 0; assert extracta != null : "null extracta"; assert extractb != null : "null extractb"; assert extracta.length == extractb.length : "extract array length mismatch"; for ( int i = 0; i < extracta.length; i++ ) { diff = extracta[ i ].compareTo( extractb[ i ] ); if ( diff != 0 ) { return diff; } } return 0; } // /method } // /methods