/* * [SortMethods.java] * * Summary: Sort Methods in order in Java source code. * * 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.*; /** * Sort Methods in order in Java source code. *

* compare two Java methods, possibly with lead JavaDoc comment, with trailing // /method * For this to work, keywords much be in canonical order i.e. * private static final int getWeight * Must use separator [// /method] * Cannot handle classes or declarations. *

* Sorts methods: abstract before implemented, public before private, static before instance, native before non-native, * boolean, Boolean, char, Character, short, Short, int, Integer, long, Long, float, Float, * double, Double, method name case-insensitive ascending. * * @author Roedy Green, Canadian Mind Products * @version 1.1 2014-05-02 change sort order * @since 2014-04-30 */ public class SortMethods implements Comparator { // todo: tie breaker using method parms /** * 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; /** * how much space for the precomputed keys */ private static final int ALLOC_FOR_PRECOMPUTE = 1000; private static final String ANNOTATION = "(?:@[\\w]+\\s*(?:\\(\\s*[ \"\\w\\{\\}]+\\s*\\))?\\s*)*"; /** * parse Java declaration * keywords that apply to methods * public | protected | private ] * abstract * static * final * synchronized * native * strictfp * int | long | String | etc. */ private static final Pattern METHOD_GRABBER = Pattern.compile( ANNOTATION + "(private|public|protected)?\\s*" + "(abstract)?\\s*" + "(static)?\\s*" + "(final)?\\s*" + "(synchronized)?\\s*" + "(native)?\\s*" + "(strictfp)?\\s*" + "(void|boolean|byte|char|int|long|float|double|[A-Za-z0-9_\\.]*)\\s*" + "(?:[\\[<][A-Za-z0-9_ ,]*[>\\]])?\\s*" // generics<> array [] + "([A-Za-z0-9_]+)" ); // method 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 ); /** * put abstract first, part of contract with outside world. * * @param abstr "abstract" or "" * * @return sort order */ private static char calcAbstrOrder( final String abstr ) { if ( ST.isEmpty( abstr ) ) { return '1'; } else { return '0'; } } // /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 calcMainOrder( final String methodName, final String context ) { if ( ST.isEmpty( methodName ) ) { return 'z'; } // put main at the end. else if ( methodName.equals( "main" ) ) { return '2'; } else { if ( Character.isUpperCase( methodName.charAt( 0 ) ) ) { // first letter is cap err.println( "method name name should start with lower case letter " + methodName ); err.println( context ); } return '1'; } } // /method private static char calcNativityOrder( final String nativity ) { if ( ST.isEmpty( nativity ) ) { return '1'; } else { return '0'; } } // /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 /** * calculate ordering letter based on the type name * * @param typeName name of typue e.g. int Integer HashMap * @param context itemfor error message * * @return latter giving priority order based on type */ private static char calcTypeOrder( final String typeName, final String context ) { if ( ST.isEmpty( typeName ) ) { return 'z'; } switch ( typeName ) { case "": return 'z'; case "void": return '0'; 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 ); err.println( context ); } return 'I'; } // end switch } // /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 ) { // show just the first line. int place = clean.indexOf( '\n' ); out.println( clean.substring( 0, place ) ); } final String[] computedKey; final Matcher m = METHOD_GRABBER.matcher( clean ); if ( m.lookingAt() ) { /** * parse Java declaration * keywords that apply to methods * public | protected | private ] * abstract * static * final * synchronized * native * strictfp * int | long | String | etc. */ int g = 1; final String scope = ST.canonical( m.group( g++ ) ); final String abstr = ST.canonical( m.group( g++ ) ); final String instance = ST.canonical( m.group( g++ ) ); final String finality = ST.canonical( m.group( g++ ) ); final String sync = ST.canonical( m.group( g++ ) ); final String nativity = ST.canonical( m.group( g++ ) ); final String strict = ST.canonical( m.group( g++ ) ); final String typeName = ST.canonical( m.group( g++ ) ); final String methodName = ST.canonical( m.group( g++ ) ); if ( DEBUGGING ) { out.println( "[scope:" + scope + "|abstr:" + abstr + "|static:" + instance + "|final:" + finality + "|sync:" + sync + "|native:" + nativity + "|strict:" + strict + "|type:" + typeName + "|method:" + methodName + "]" ); } /** analyse the keywords we found */ final char mainOrder = calcMainOrder( methodName, clean ); // main last final char abstrOrder = calcAbstrOrder( abstr ); // abstract first final char scopeOrder = calcScopeOrder( scope ); // public first final char instanceOrder = calcInstanceOrder( instance ); /// static first final char nativityOrder = calcNativityOrder( nativity ); // native first final char typeOrder = calcTypeOrder( typeName, clean ); // boolean before int before String // sort by main, scope, instance, nativity final FastCat sb = new FastCat( 5 ); sb.append( mainOrder ); sb.append( abstrOrder ); sb.append( scopeOrder ); sb.append( instanceOrder ); sb.append( nativityOrder ); // for declarations, type is more important than name , for methods the reverse. // typeOrder comes later. final String condensedKey = sb.toString(); if ( CASE_SENSITIVE ) { // case-sensitive version computedKey = new String[] { condensedKey, methodName, String.valueOf( typeOrder ), typeName }; } else { // case-insensitive version computedKey = new String[] { condensedKey, methodName.toUpperCase(), String.valueOf( typeOrder ), typeName.toUpperCase() } ; } if ( DEBUGGING ) { for ( String chunk : computedKey ) { out.print( chunk + "|" ); } out.println(); } } else { err.println( "SortMethods parse failed." ); // show just the first line. int place = clean.indexOf( '\n' ); if ( place < 0 ) { place = Math.min( clean.length(), 200 ); } err.println( clean.substring( 0, place ) ); err.println( "--------------------------------------------------------------------------------" ); computedKey = new String[] { "zzzzz", clean, "z", "" }; } // save it in case we need it again without recomputing. It will not be in there already. 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; for ( int i = 0; i < extracta.length; i++ ) { diff = extracta[ i ].compareTo( extractb[ i ] ); if ( diff != 0 ) { return diff; } } return 0; } // /method } // /methods