View Javadoc
1   package org.codehaus.mojo.animal_sniffer;
2   
3   /*
4    * The MIT License
5    *
6    * Copyright (c) 2008 Kohsuke Kawaguchi and codehaus.org.
7    *
8    * Permission is hereby granted, free of charge, to any person obtaining a copy
9    * of this software and associated documentation files (the "Software"), to deal
10   * in the Software without restriction, including without limitation the rights
11   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12   * copies of the Software, and to permit persons to whom the Software is
13   * furnished to do so, subject to the following conditions:
14   *
15   * The above copyright notice and this permission notice shall be included in
16   * all copies or substantial portions of the Software.
17   *
18   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24   * THE SOFTWARE.
25   *
26   */
27  
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.ObjectInputStream;
33  import java.nio.CharBuffer;
34  import java.util.Collection;
35  import java.util.HashMap;
36  import java.util.HashSet;
37  import java.util.LinkedList;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Set;
41  import java.util.regex.Pattern;
42  import java.util.zip.GZIPInputStream;
43  
44  import org.codehaus.mojo.animal_sniffer.logging.Logger;
45  import org.codehaus.mojo.animal_sniffer.logging.PrintWriterLogger;
46  import org.objectweb.asm.AnnotationVisitor;
47  import org.objectweb.asm.ClassReader;
48  import org.objectweb.asm.ClassVisitor;
49  import org.objectweb.asm.FieldVisitor;
50  import org.objectweb.asm.Handle;
51  import org.objectweb.asm.Label;
52  import org.objectweb.asm.MethodVisitor;
53  import org.objectweb.asm.Opcodes;
54  import org.objectweb.asm.Type;
55  
56  /**
57   * Checks the signature against classes in this list.
58   *
59   * @author Kohsuke Kawaguchi
60   */
61  public class SignatureChecker
62      extends ClassFileVisitor
63  {
64      /**
65       * The fully qualified name of the annotation to use to annotate methods/fields/classes that are
66       * to be ignored by animal sniffer.
67       */
68      public static final String ANNOTATION_FQN = "org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement";
69  
70      /**
71       * Similar to {@link #ANNOTATION_FQN}. Kept for backward compatibility reasons
72       */
73      public static final String PREVIOUS_ANNOTATION_FQN = "org.jvnet.animal_sniffer.IgnoreJRERequirement";
74  
75      private final Map<String, Clazz> classes;
76  
77      private final Logger logger;
78  
79      /**
80       * Classes in this packages are considered to be resolved elsewhere and
81       * thus not a subject of the error checking when referenced.
82       */
83      private final List<MatchRule> ignoredPackageRules;
84  
85      private final Set<String> ignoredPackages;
86  
87      private final Set<String> ignoredOuterClassesOrMethods = new HashSet<>();
88  
89      private boolean hadError = false;
90  
91      private List<File> sourcePath;
92  
93      private Collection<String> annotationDescriptors;
94  
95      public static void main( String[] args )
96          throws Exception
97      {
98          Set<String> ignoredPackages = new HashSet<>();
99          ignoredPackages.add( "org.jvnet.animal_sniffer.*" );
100         ignoredPackages.add( "org.codehaus.mojo.animal_sniffer.*" );
101         ignoredPackages.add( "org.objectweb.*" );
102         new SignatureChecker( new FileInputStream( "signature" ), ignoredPackages,
103                               new PrintWriterLogger( System.out ) ).process( new File( "target/classes" ) );
104     }
105 
106     public SignatureChecker( InputStream in, Set<String> ignoredPackages, Logger logger )
107         throws IOException
108     {
109         this( loadClasses( in ), ignoredPackages, logger );
110     }
111 
112     public SignatureChecker( Map<String, Clazz> classes, Set<String> ignoredPackages, Logger logger )
113         throws IOException
114     {
115         this.classes = classes;
116         this.ignoredPackages = new HashSet<>();
117         this.ignoredPackageRules = new LinkedList<>();
118         for(String wildcard : ignoredPackages )
119         {
120             if ( wildcard.indexOf( '*' ) == -1 && wildcard.indexOf( '?' ) == -1 )
121             {
122                 this.ignoredPackages.add( wildcard.replace( '.', '/' ) );
123             }
124             else
125             {
126                 this.ignoredPackageRules.add( newMatchRule( wildcard.replace( '.', '/' ) ) );
127             }
128         }
129         this.annotationDescriptors = new HashSet<>();
130         this.annotationDescriptors.add( toAnnotationDescriptor( ANNOTATION_FQN ) );
131         this.annotationDescriptors.add( toAnnotationDescriptor( PREVIOUS_ANNOTATION_FQN ) );
132 
133         this.logger = logger;
134     }
135 
136     public static Map<String, Clazz> loadClasses( InputStream in ) throws IOException
137     {
138         Map<String, Clazz> classes = new HashMap<>();
139         try (ObjectInputStream ois = new ObjectInputStream( new GZIPInputStream( in ) ))
140         {
141             while ( true )
142             {
143                 Clazz c = (Clazz) ois.readObject();
144                 if ( c == null )
145                 {
146                     return classes; // finished
147                 }
148                 classes.put( c.getName(), c );
149             }
150         }
151         catch ( ClassNotFoundException e )
152         {
153             throw new NoClassDefFoundError( e.getMessage() );
154         }
155     }
156 
157     /** @since 1.9 */
158     public void setSourcePath( List<File> sourcePath )
159     {
160         this.sourcePath = sourcePath;
161     }
162 
163     /**
164      * Sets the annotation type(s) that this checker should consider to ignore annotated
165      * methods, classes or fields.
166      * <p>
167      * By default, the {@link #ANNOTATION_FQN} and {@link #PREVIOUS_ANNOTATION_FQN} are
168      * used.
169      * <p>
170      * If you want to <strong>add</strong> an extra annotation types, make sure to add
171      * the standard one to the specified lists.
172      *
173      * @param annotationTypes a list of the fully qualified name of the annotation types
174      *                        to consider for ignoring annotated method, class and field
175      * @since 1.11
176      */
177     public void setAnnotationTypes( Collection<String> annotationTypes )
178     {
179         this.annotationDescriptors.clear();
180         for ( String annotationType : annotationTypes )
181         {
182             annotationDescriptors.add( toAnnotationDescriptor( annotationType ) );
183         }
184     }
185 
186     protected void process( final String name, InputStream image )
187         throws IOException
188     {
189         ClassReader cr = new ClassReader( image );
190 
191         try
192         {
193             cr.accept( new CheckingVisitor( name ), 0 );
194         }
195         catch ( ArrayIndexOutOfBoundsException e )
196         {
197             logger.error( "Bad class file " + name );
198             // MANIMALSNIFFER-9 it is a pity that ASM does not throw a nicer error on encountering a malformed
199             // class file.
200             throw new IOException( "Bad class file " + name, e );
201         }
202     }
203 
204     private interface MatchRule
205     {
206         boolean matches( String text );
207     }
208 
209     private static class PrefixMatchRule
210         implements SignatureChecker.MatchRule
211     {
212         private final String prefix;
213 
214         public PrefixMatchRule( String prefix )
215         {
216             this.prefix = prefix;
217         }
218 
219         public boolean matches( String text )
220         {
221             return text.startsWith( prefix );
222         }
223     }
224 
225     private static class ExactMatchRule
226         implements SignatureChecker.MatchRule
227     {
228         private final String match;
229 
230         public ExactMatchRule( String match )
231         {
232             this.match = match;
233         }
234 
235         public boolean matches( String text )
236         {
237             return match.equals( text );
238         }
239     }
240 
241     private static class RegexMatchRule
242         implements SignatureChecker.MatchRule
243     {
244         private final Pattern regex;
245 
246         public RegexMatchRule( Pattern regex )
247         {
248             this.regex = regex;
249         }
250 
251         public boolean matches( String text )
252         {
253             return regex.matcher( text ).matches();
254         }
255     }
256 
257     private SignatureChecker.MatchRule newMatchRule( String matcher )
258     {
259         int i = matcher.indexOf( '*' );
260         if ( i == -1 )
261         {
262             return new ExactMatchRule( matcher );
263         }
264         if ( i == matcher.length() - 1 )
265         {
266             return new PrefixMatchRule( matcher.substring( 0, i ) );
267         }
268         return new RegexMatchRule( RegexUtils.compileWildcard( matcher ) );
269     }
270 
271     public boolean isSignatureBroken()
272     {
273         return hadError;
274     }
275 
276     private class CheckingVisitor
277         extends ClassVisitor
278     {
279         private final Set<String> ignoredPackageCache;
280 
281         private String packagePrefix;
282         private int line;
283         private String name;
284         private String internalName;
285 
286         private boolean ignoreClass = false;
287 
288         public CheckingVisitor( String name )
289         {
290             super(Opcodes.ASM7);
291             this.ignoredPackageCache = new HashSet<>( 50 * ignoredPackageRules.size() );
292             this.name = name;
293         }
294 
295         @Override
296         public void visit( int version, int access, String name, String signature, String superName, String[] interfaces )
297         {
298             internalName = name;
299             packagePrefix = name.substring(0, name.lastIndexOf( '/' ) + 1 );
300         }
301 
302         @Override
303         public void visitSource( String source, String debug )
304         {
305             for ( File root : sourcePath )
306             {
307                 File s = new File( root, packagePrefix + source );
308                 if ( s.isFile() )
309                 {
310                     name = s.getAbsolutePath();
311                 }
312             }
313         }
314 
315         @Override
316         public void visitOuterClass( String owner, String name, String desc )
317         {
318             if ( ignoredOuterClassesOrMethods.contains( owner ) ||
319                  ( name != null && ignoredOuterClassesOrMethods.contains ( owner + "#" + name + desc ) ) )
320             {
321                 ignoreClass = true;
322             }
323         }
324 
325         public boolean isIgnoreAnnotation(String desc)
326         {
327             for ( String annoDesc : annotationDescriptors )
328             {
329                 if ( desc.equals( annoDesc ) )
330                 {
331                     return true;
332                 }
333             }
334             return false;
335         }
336 
337         @Override
338         public AnnotationVisitor visitAnnotation(String desc, boolean visible)
339         {
340             if ( isIgnoreAnnotation( desc ) )
341             {
342                 ignoreClass = true;
343                 ignoredOuterClassesOrMethods.add( internalName );
344             }
345             return super.visitAnnotation(desc, visible);
346         }
347 
348         @Override
349         public FieldVisitor visitField(int access, String name, final String descriptor, String signature, Object value) {
350             return new FieldVisitor(Opcodes.ASM7) {
351 
352                 @Override
353                 public void visitEnd() {
354                     checkType(Type.getType(descriptor), false);
355                 }
356 
357             };
358         }
359 
360         @Override
361         public MethodVisitor visitMethod( int access, final String name, final String desc, String signature, String[] exceptions )
362         {
363             line = 0;
364             return new MethodVisitor(Opcodes.ASM7)
365             {
366                 /**
367                  * True if @IgnoreJRERequirement is set.
368                  */
369                 boolean ignoreError = ignoreClass;
370                 Label label = null;
371                 Map<Label, Set<String>> exceptions = new HashMap<>();
372 
373                 @Override
374                 public void visitEnd() {
375                     checkType(Type.getReturnType(desc), ignoreError);
376                 }
377 
378                 @Override
379                 public AnnotationVisitor visitAnnotation( String annoDesc, boolean visible )
380                 {
381                     if ( isIgnoreAnnotation(annoDesc) )
382                     {
383                         ignoreError = true;
384                         ignoredOuterClassesOrMethods.add( internalName + "#" + name + desc );
385                     }
386                     return super.visitAnnotation( annoDesc, visible );
387                 }
388 
389                 private static final String LAMBDA_METAFACTORY = "java/lang/invoke/LambdaMetafactory";
390 
391                 @Override
392                 public void visitInvokeDynamicInsn( String name, String desc, Handle bsm, Object... bsmArgs )
393                 {
394                     if ( LAMBDA_METAFACTORY.equals( bsm.getOwner() ) )
395                     {
396                         if ( "metafactory".equals( bsm.getName() ) ||
397                              "altMetafactory".equals( bsm.getName() ) )
398                         {
399                             // check the method reference
400                             Handle methodHandle = (Handle) bsmArgs[1];
401                             check( methodHandle.getOwner(), methodHandle.getName() + methodHandle.getDesc(), ignoreError );
402                             // check the functional interface type
403                             checkType( Type.getReturnType( desc ), ignoreError );
404                         }
405                     }
406                 }
407 
408                 @Override
409                 public void visitMethodInsn( int opcode, String owner, String name, String desc, boolean itf )
410                 {
411                     checkType( Type.getReturnType( desc ), ignoreError );
412                     check( owner, name + desc, ignoreError );
413                 }
414 
415                 @Override
416                 public void visitTypeInsn( int opcode, String type )
417                 {
418                     checkType( type, ignoreError );
419                 }
420 
421                 @Override
422                 public void visitFieldInsn( int opcode, String owner, String name, String desc )
423                 {
424                     check( owner, name + '#' + desc, ignoreError );
425                 }
426 
427                 @Override
428                 public void visitTryCatchBlock( Label start, Label end, Label handler, String type )
429                 {
430                     if ( type != null )
431                     {
432                         Set<String> exceptionTypes = exceptions.computeIfAbsent( handler, k -> new HashSet<>() );
433                         // we collect the types for the handler
434                         // because we do not have the line number here
435                         // and we need a list for a multi catch block
436                         exceptionTypes.add( type );
437                     }
438                 }
439 
440                 @Override
441                 public void visitFrame( int type, int nLocal, Object[] local, int nStack, Object[] stack )
442                 {
443                     Set<String> exceptionTypes = exceptions.remove(label);
444                     if ( exceptionTypes != null )
445                     {
446                         for (String exceptionType: exceptionTypes)
447                         {
448                             checkType( exceptionType, ignoreError );
449                         }
450                         for ( int i = 0; i < nStack; i++ )
451                         {
452                             Object obj = stack[i];
453                             // on the frame stack we check if we have a type which is not
454                             // present in the catch/multi catch statement
455                             if ( obj instanceof String && !exceptionTypes.contains( obj ) )
456                             {
457                                 checkType( obj.toString(), ignoreError);
458                             }
459                         }
460                     }
461                 }
462 
463                 @Override
464                 public void visitLineNumber( int line, Label start )
465                 {
466                     CheckingVisitor.this.line = line;
467                 }
468 
469                 @Override
470                 public void visitLabel( Label label ) {
471                     this.label = label;
472                 }
473 
474             };
475         }
476 
477         private void checkType(Type asmType, boolean ignoreError )
478         {
479             if ( asmType == null )
480             {
481                 return;
482             }
483             if ( asmType.getSort() == Type.OBJECT )
484             {
485                 checkType( asmType.getInternalName(), ignoreError );
486             }
487             if ( asmType.getSort() == Type.ARRAY )
488             {
489                 // recursive call
490                 checkType( asmType.getElementType(), ignoreError );
491             }
492         }
493 
494         private void checkType( String type, boolean ignoreError )
495         {
496             if ( shouldBeIgnored( type, ignoreError ) )
497             {
498                 return;
499             }
500             if ( type.charAt( 0 ) == '[' )
501             {
502                 return; // array
503             }
504             Clazz sigs = classes.get( type );
505             if ( sigs == null )
506             {
507                 error( type, null );
508             }
509         }
510 
511         private void check( String owner, String sig, boolean ignoreError )
512         {
513             if ( shouldBeIgnored( owner, ignoreError ) )
514             {
515                 return;
516             }
517             if ( find( classes.get( owner ), sig ) )
518             {
519                 return; // found it
520             }
521             error( owner, sig );
522         }
523 
524         private boolean shouldBeIgnored( String type, boolean ignoreError )
525         {
526             if ( ignoreError )
527             {
528                 return true;    // warning suppressed in this context
529             }
530             if ( type.charAt( 0 ) == '[' )
531             {
532                 return true; // array
533             }
534 
535             if ( ignoredPackages.contains( type ) || ignoredPackageCache.contains( type ) )
536             {
537                 return true;
538             }
539             for ( MatchRule rule : ignoredPackageRules )
540             {
541                 if ( rule.matches( type ) )
542                 {
543                     ignoredPackageCache.add( type );
544                     return true;
545                 }
546             }
547             return false;
548         }
549 
550         /**
551          * If the given signature is found in the specified class, return true.
552          * @param baseFind TODO
553          */
554         private boolean find( Clazz c , String sig )
555         {
556             if ( c == null )
557             {
558                 return false;
559             }
560             if ( c.getSignatures().contains( sig ) )
561             {
562                 return true;
563             }
564 
565             if ( sig.startsWith( "<" ) )
566             // constructor and static initializer shouldn't go up the inheritance hierarchy
567             {
568                 return false;
569             }
570 
571             if ( find( classes.get( c.getSuperClass() ), sig ) )
572             {
573                 return true;
574             }
575 
576             if ( c.getSuperInterfaces() != null )
577             {
578                 for ( int i = 0; i < c.getSuperInterfaces().length; i++ )
579                 {
580                     if ( find( classes.get( c.getSuperInterfaces()[i] ), sig ) )
581                     {
582                         return true;
583                     }
584                 }
585             }
586 
587             return false;
588         }
589 
590         private void error( String type, String sig )
591         {
592             hadError = true;
593             logger.error(name + (line > 0 ? ":" + line : "") + ": Undefined reference: " + toSourceForm( type, sig ) );
594         }
595     }
596 
597     static String toSourceForm( String type, String sig )
598     {
599         String sourceType = toSourceType( type );
600         if ( sig == null )
601         {
602             return sourceType;
603         }
604         int hash = sig.indexOf( '#' );
605         if ( hash != -1 )
606         {
607             return toSourceType( CharBuffer.wrap( sig, hash + 1, sig.length() ) ) + " " + sourceType + "." + sig.substring( 0, hash );
608         }
609         int lparen = sig.indexOf( '(' );
610         if ( lparen != -1 )
611         {
612             int rparen = sig.indexOf( ')' );
613             if ( rparen != -1 )
614             {
615                 StringBuilder b = new StringBuilder();
616                 String returnType = sig.substring( rparen + 1 );
617                 if ( returnType.equals( "V" ) )
618                 {
619                     b.append( "void" );
620                 }
621                 else
622                 {
623                     b.append( toSourceType( CharBuffer.wrap( returnType ) ) );
624                 }
625                 b.append( ' ' );
626                 b.append( sourceType );
627                 b.append( '.' );
628                 // XXX consider prettifying <init>
629                 b.append( sig.substring( 0, lparen ) );
630                 b.append( '(' );
631                 boolean first = true;
632                 CharBuffer args = CharBuffer.wrap( sig, lparen + 1, rparen );
633                 while ( args.hasRemaining() )
634                 {
635                     if ( first )
636                     {
637                         first = false;
638                     }
639                     else
640                     {
641                         b.append( ", " );
642                     }
643                     b.append( toSourceType( args ) );
644                 }
645                 b.append( ')' );
646                 return b.toString();
647             }
648         }
649         return "{" + type + ":" + sig + "}"; // ??
650     }
651 
652     static String toAnnotationDescriptor( String classFqn )
653     {
654         return "L" + fromSourceType( classFqn ) + ";";
655     }
656 
657     private static String toSourceType( CharBuffer type )
658     {
659         switch ( type.get() )
660         {
661             case 'L':
662                 for ( int i = type.position(); i < type.limit(); i++ )
663                 {
664                     if ( type.get( i ) == ';' )
665                     {
666                         String text = type.subSequence( 0, i - type.position() ).toString();
667                         type.position( i + 1 );
668                         return toSourceType( text );
669                     }
670                 }
671                 return "{" + type + "}"; // ??
672             case '[':
673                 return toSourceType( type ) + "[]";
674             case 'B':
675                 return "byte";
676             case 'C':
677                 return "char";
678             case 'D':
679                 return "double";
680             case 'F':
681                 return "float";
682             case 'I':
683                 return "int";
684             case 'J':
685                 return "long";
686             case 'S':
687                 return "short";
688             case 'Z':
689                 return "boolean";
690             default:
691                 return "{" + type + "}"; // ??
692         }
693     }
694 
695     private static String toSourceType( String text )
696     {
697         return text.replaceFirst( "^java/lang/([^/]+)$", "$1" ).replace( '/', '.' ).replace( '$', '.' );
698     }
699 
700     private static String fromSourceType( String text )
701     {
702         return text.replace( '.', '/' ).replace( '.', '$' );
703     }
704 
705 }