/******************************************************************************* * Copyright (c) 2014, 2016 GK Software AG. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Stephan Herrmann - initial API and implementation * Lars Vogel <Lars.Vogel@vogella.com> - Contributions for * Bug 473178 *******************************************************************************/ package org.eclipse.jdt.internal.compiler.classfmt; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.internal.compiler.env.IBinaryAnnotation; import org.eclipse.jdt.internal.compiler.env.IBinaryElementValuePair; import org.eclipse.jdt.internal.compiler.env.ITypeAnnotationWalker; import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment; import org.eclipse.jdt.internal.compiler.lookup.SignatureWrapper; import org.eclipse.jdt.internal.compiler.util.Util; public class ExternalAnnotationProvider { public static final String ANNOTATION_FILE_EXTENSION= "eea"; //$NON-NLS-1$ public static final String CLASS_PREFIX = "class "; //$NON-NLS-1$ public static final String SUPER_PREFIX = "super "; //$NON-NLS-1$ /** Representation of a 'nullable' annotation, independent of the concrete annotation name used in Java sources. */ public static final char NULLABLE = '0'; /** Representation of a 'nonnull' annotation, independent of the concrete annotation name used in Java sources. */ public static final char NONNULL = '1'; /** * Represents absence of a null annotation. Useful for removing an existing null annotation. * This character is used only internally, it is not part of the Eclipse External Annotation file format. */ public static final char NO_ANNOTATION = '@'; public static final String ANNOTATION_FILE_SUFFIX = ".eea"; //$NON-NLS-1$ private static final String TYPE_PARAMETER_PREFIX = " <"; //$NON-NLS-1$ private String typeName; String typeParametersAnnotationSource; Map<String,String> supertypeAnnotationSources; private Map<String,String> methodAnnotationSources; private Map<String,String> fieldAnnotationSources; /** * Create and initialize. * @param input open input stream to read the annotations from, will be closed by the constructor. * @param typeName slash-separated qualified name of a type * @throws IOException various issues when accessing the annotation file */ public ExternalAnnotationProvider(InputStream input, String typeName) throws IOException { this.typeName = typeName; initialize(input); } private void initialize(InputStream input) throws IOException { try (LineNumberReader reader = new LineNumberReader(new InputStreamReader(input))) { assertClassHeader(reader.readLine(), this.typeName); String line; if ((line = reader.readLine()) == null) { return; } if (line.startsWith(TYPE_PARAMETER_PREFIX)) { if ((line = reader.readLine()) == null) // skip first line, second line may contain type parameter annotations return; if (line.startsWith(TYPE_PARAMETER_PREFIX)) { this.typeParametersAnnotationSource = line.substring(TYPE_PARAMETER_PREFIX.length()); if ((line = reader.readLine()) == null) return; } } String pendingLine; do { pendingLine = null; line = line.trim(); if (line.isEmpty()) continue; String rawSig = null, annotSig = null; // selector: String selector = line; boolean isSuper = selector.startsWith(SUPER_PREFIX); if (isSuper) selector = selector.substring(SUPER_PREFIX.length()); int errLine = -1; try { // raw signature: line = reader.readLine(); if (line != null && !line.isEmpty() && line.charAt(0) == ' ') // first signature line is mandatory rawSig = line.substring(1); else errLine = reader.getLineNumber(); // annotated signature: line = reader.readLine(); if (line == null || line.isEmpty()) continue; // skip since optional line with annotations is missing if (line.charAt(0) != ' ') { pendingLine = line; // push back what appears to be the next selector, not a signature continue; } annotSig = line.substring(1); } catch (Exception ex) { // continue to escalate below } if (rawSig == null || annotSig == null) { if (errLine == -1) errLine = reader.getLineNumber(); throw new IOException("Illegal format for annotation file at line "+errLine); //$NON-NLS-1$ } // discard optional meta data (separated by whitespace): annotSig = trimTail(annotSig); if (isSuper) { if (this.supertypeAnnotationSources == null) this.supertypeAnnotationSources = new HashMap<>(); this.supertypeAnnotationSources.put('L'+selector+rawSig+';', annotSig); } else if (rawSig.contains("(")) { //$NON-NLS-1$ if (this.methodAnnotationSources == null) this.methodAnnotationSources = new HashMap<>(); this.methodAnnotationSources.put(selector+rawSig, annotSig); } else { if (this.fieldAnnotationSources == null) this.fieldAnnotationSources = new HashMap<>(); this.fieldAnnotationSources.put(selector+':'+rawSig, annotSig); } } while (((line = pendingLine) != null) || (line = reader.readLine()) != null); } } /** * Assert that the given line is a class header for 'typeName' (slash-separated qualified name). */ public static void assertClassHeader(String line, String typeName) throws IOException { if (line != null && line.startsWith(CLASS_PREFIX)) { line = line.substring(CLASS_PREFIX.length()); } else { throw new IOException("missing class header in annotation file"); //$NON-NLS-1$ } if (!trimTail(line).equals(typeName)) { throw new IOException("mismatching class name in annotation file, expected "+typeName+", but header said "+line); //$NON-NLS-1$ //$NON-NLS-2$ } } /** * Extract the signature from a line of an external annotation file. * Answers null if line is not in the expected format. */ public static String extractSignature(String line) { if (line == null || line.isEmpty() || line.charAt(0) != ' ') return null; return trimTail(line.substring(1)); } /** Lines may contain arbitrary trailing data, separated by white space. */ protected static String trimTail(String line) { int tail = line.indexOf(' '); if (tail == -1) tail = line.indexOf('\t'); if (tail != -1) return line.substring(0, tail); return line; } public ITypeAnnotationWalker forTypeHeader(LookupEnvironment environment) { if (this.typeParametersAnnotationSource != null || this.supertypeAnnotationSources != null) return new DispatchingAnnotationWalker(environment); return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } public ITypeAnnotationWalker forMethod(char[] selector, char[] signature, LookupEnvironment environment) { Map<String, String> sources = this.methodAnnotationSources; if (sources != null) { String source = sources.get(String.valueOf(CharOperation.concat(selector, signature))); if (source != null) return new MethodAnnotationWalker(source.toCharArray(), 0, environment); } return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } public ITypeAnnotationWalker forField(char[] selector, char[] signature, LookupEnvironment environment) { if (this.fieldAnnotationSources != null) { String source = this.fieldAnnotationSources.get(String.valueOf(CharOperation.concat(selector, signature, ':'))); if (source != null) return new FieldAnnotationWalker(source.toCharArray(), 0, environment); } return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } public String toString() { StringBuilder sb = new StringBuilder(); sb.append("External Annotations for ").append(this.typeName).append('\n'); //$NON-NLS-1$ sb.append("Methods:\n"); //$NON-NLS-1$ if (this.methodAnnotationSources != null) for (Entry<String,String> e : this.methodAnnotationSources.entrySet()) sb.append('\t').append(e.getKey()).append('\n'); return sb.toString(); } abstract class SingleMarkerAnnotation implements IBinaryAnnotation { @Override public IBinaryElementValuePair[] getElementValuePairs() { return ElementValuePairInfo.NoMembers; } protected char[] getBinaryTypeName(char[][] name) { return CharOperation.concat('L', CharOperation.concatWith(name, '/'), ';'); } } SingleMarkerAnnotation NULLABLE_ANNOTATION, NONNULL_ANNOTATION; void initAnnotations(final LookupEnvironment environment) { if (this.NULLABLE_ANNOTATION == null) { this.NULLABLE_ANNOTATION = new SingleMarkerAnnotation() { @Override public char[] getTypeName() { return getBinaryTypeName(environment.getNullableAnnotationName()); } }; } if (this.NONNULL_ANNOTATION == null) { this.NONNULL_ANNOTATION = new SingleMarkerAnnotation() { @Override public char[] getTypeName() { return getBinaryTypeName(environment.getNonNullAnnotationName()); } }; } } /** * Walker for top-level elements of a type (type parameters & super types), * which dispatches to specialized walkers for those details. */ class DispatchingAnnotationWalker implements ITypeAnnotationWalker { private LookupEnvironment environment; private TypeParametersAnnotationWalker typeParametersWalker; public DispatchingAnnotationWalker(LookupEnvironment environment) { this.environment = environment; } public ITypeAnnotationWalker toTypeParameter(boolean isClassTypeParameter, int rank) { String source = ExternalAnnotationProvider.this.typeParametersAnnotationSource; if (source != null) { if (this.typeParametersWalker == null) this.typeParametersWalker = new TypeParametersAnnotationWalker(source.toCharArray(), 0, 0, null, this.environment); return this.typeParametersWalker.toTypeParameter(isClassTypeParameter, rank); } return this; } public ITypeAnnotationWalker toTypeParameterBounds(boolean isClassTypeParameter, int parameterRank) { if (this.typeParametersWalker != null) return this.typeParametersWalker.toTypeParameterBounds(isClassTypeParameter, parameterRank); return this; } public ITypeAnnotationWalker toSupertype(short index, char[] superTypeSignature) { Map<String, String> sources = ExternalAnnotationProvider.this.supertypeAnnotationSources; if (sources != null) { String source = sources.get(String.valueOf(superTypeSignature)); if (source != null) return new SuperTypesAnnotationWalker(source.toCharArray(), this.environment); } return this; } // the rest is borrowed from EMPTY_ANNOTATION_WALKER: public ITypeAnnotationWalker toField() { return this; } public ITypeAnnotationWalker toThrows(int rank) { return this; } public ITypeAnnotationWalker toTypeArgument(int rank) { return this; } public ITypeAnnotationWalker toMethodParameter(short index) { return this; } public ITypeAnnotationWalker toTypeBound(short boundIndex) { return this; } public ITypeAnnotationWalker toMethodReturn() { return this; } public ITypeAnnotationWalker toReceiver() { return this; } public ITypeAnnotationWalker toWildcardBound() { return this; } public ITypeAnnotationWalker toNextArrayDimension() { return this; } public ITypeAnnotationWalker toNextNestedType() { return this; } public IBinaryAnnotation[] getAnnotationsAtCursor(int currentTypeId) { return NO_ANNOTATIONS; } } abstract class BasicAnnotationWalker implements ITypeAnnotationWalker { char[] source; SignatureWrapper wrapper; int pos; int prevTypeArgStart; int currentTypeBound; LookupEnvironment environment; BasicAnnotationWalker(char[] source, int pos, LookupEnvironment environment) { this.source = source; this.pos = pos; this.environment = environment; initAnnotations(environment); } SignatureWrapper wrapperWithStart(int start) { if (this.wrapper == null) this.wrapper = new SignatureWrapper(this.source); this.wrapper.start = start; return this.wrapper; } @Override public ITypeAnnotationWalker toReceiver() { return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toTypeParameter(boolean isClassTypeParameter, int rank) { return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toTypeParameterBounds(boolean isClassTypeParameter, int parameterRank) { return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toTypeBound(short boundIndex) { return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toSupertype(short index, char[] superTypeSignature) { return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toTypeArgument(int rank) { if (rank == 0) { int start = CharOperation.indexOf('<', this.source, this.pos) + 1; this.prevTypeArgStart = start; return new MethodAnnotationWalker(this.source, start, this.environment); } int next = this.prevTypeArgStart; switch (this.source[next]) { case '*': break; case '-': case '+': next++; //$FALL-THROUGH$ default: next = wrapperWithStart(next).computeEnd(); } next++; this.prevTypeArgStart = next; return new MethodAnnotationWalker(this.source, next, this.environment); } @Override public ITypeAnnotationWalker toWildcardBound() { switch (this.source[this.pos]) { case '-': case '+': return new MethodAnnotationWalker(this.source, this.pos+1, this.environment); default: // includes unbounded '*' return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } } @Override public ITypeAnnotationWalker toNextArrayDimension() { if (this.source[this.pos] == '[') { int newPos = this.pos+1; switch (this.source[newPos]) { case NULLABLE: case NONNULL: newPos++; break; } return new MethodAnnotationWalker(this.source, newPos, this.environment); } return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toNextNestedType() { return this; // FIXME(stephan) } @Override public IBinaryAnnotation[] getAnnotationsAtCursor(int currentTypeId) { if (this.pos != -1 && this.pos < this.source.length-2) { switch (this.source[this.pos]) { case 'T': case 'L': case '[': case '*': case '+': case '-': switch (this.source[this.pos+1]) { case NULLABLE: return new IBinaryAnnotation[]{ ExternalAnnotationProvider.this.NULLABLE_ANNOTATION }; case NONNULL: return new IBinaryAnnotation[]{ ExternalAnnotationProvider.this.NONNULL_ANNOTATION }; } } } return NO_ANNOTATIONS; } } /** * Walker that may serve the annotations on type parameters of the current class or method. */ public class TypeParametersAnnotationWalker extends BasicAnnotationWalker { int[] rankStarts; // indices of start positions for type parameters per rank int currentRank; TypeParametersAnnotationWalker(char[] source, int pos, int rank, int[] rankStarts, LookupEnvironment environment) { super(source, pos, environment); this.currentRank = rank; if (rankStarts != null) { this.rankStarts = rankStarts; } else { // eagerly scan all type parameters: int length = source.length; rankStarts = new int[length]; int curRank = 0; // next block cf. BinaryTypeBinding.createTypeVariables(): int depth = 0; boolean pendingVariable = true; scanVariables: { for (int i = pos; i < length; i++) { switch(this.source[i]) { case Util.C_GENERIC_START : depth++; break; case Util.C_GENERIC_END : if (--depth < 0) break scanVariables; break; case Util.C_NAME_END : if ((depth == 0) && (i +1 < length) && (this.source[i+1] != Util.C_COLON)) pendingVariable = true; break; case Util.C_COLON : if (depth == 0) pendingVariable = true; // end of variable name // skip optional bound ReferenceTypeSignature i++; // peek next while (i < length && this.source[i] == Util.C_ARRAY) i++; if (i < length && this.source[i] == Util.C_RESOLVED) { int currentdepth = depth; while (i < length && (currentdepth != depth || this.source[i] != Util.C_NAME_END)) { if(this.source[i] == Util.C_GENERIC_START) currentdepth++; if(this.source[i] == Util.C_GENERIC_END) currentdepth--; i++; } } i--; // unget break; default: if (pendingVariable) { pendingVariable = false; rankStarts[curRank++] = i; } } } } System.arraycopy(rankStarts, 0, this.rankStarts = new int[curRank], 0, curRank); } } @Override public ITypeAnnotationWalker toTypeParameter(boolean isClassTypeParameter, int rank) { if (rank == this.currentRank) return this; if (rank < this.rankStarts.length) return new TypeParametersAnnotationWalker(this.source, this.rankStarts[rank], rank, this.rankStarts, this.environment); return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toTypeParameterBounds(boolean isClassTypeParameter, int parameterRank) { return new TypeParametersAnnotationWalker(this.source, this.rankStarts[parameterRank], parameterRank, this.rankStarts, this.environment); } @Override public ITypeAnnotationWalker toTypeBound(short boundIndex) { // assume we are positioned either at the start of the bounded type parameter // or at the start of a previous type bound int p = this.pos; int i = this.currentTypeBound; while(true) { // each bound is prefixed with ':' int colon = CharOperation.indexOf(Util.C_COLON, this.source, p); if (colon != -1) p = colon + 1; if (++i > boundIndex) break; // skip next type: p = wrapperWithStart(p).computeEnd()+1; } this.pos = p; this.currentTypeBound = boundIndex; return this; } @Override public ITypeAnnotationWalker toField() { throw new UnsupportedOperationException("Cannot navigate to fields"); //$NON-NLS-1$ } @Override public ITypeAnnotationWalker toMethodReturn() { throw new UnsupportedOperationException("Cannot navigate to method return"); //$NON-NLS-1$ } @Override public ITypeAnnotationWalker toMethodParameter(short index) { throw new UnsupportedOperationException("Cannot navigate to method parameter"); //$NON-NLS-1$ } @Override public ITypeAnnotationWalker toThrows(int index) { throw new UnsupportedOperationException("Cannot navigate to throws"); //$NON-NLS-1$ } @Override public IBinaryAnnotation[] getAnnotationsAtCursor(int currentTypeId) { if (this.pos != -1 && this.pos < this.source.length-1) { switch (this.source[this.pos]) { case NULLABLE: return new IBinaryAnnotation[]{ ExternalAnnotationProvider.this.NULLABLE_ANNOTATION }; case NONNULL: return new IBinaryAnnotation[]{ ExternalAnnotationProvider.this.NONNULL_ANNOTATION }; } } return super.getAnnotationsAtCursor(currentTypeId); } } /** Walker serving type annotations on a type's supertypes. */ class SuperTypesAnnotationWalker extends BasicAnnotationWalker { SuperTypesAnnotationWalker(char[] source, LookupEnvironment environment) { super(source, 0, environment); } // actual implementation is inherited, main entries: toTypeArgument & getAnnotationsAtCursor @Override public ITypeAnnotationWalker toField() { throw new UnsupportedOperationException("Supertype has no field annotations"); //$NON-NLS-1$ } @Override public ITypeAnnotationWalker toMethodReturn() { throw new UnsupportedOperationException("Supertype has no method return"); //$NON-NLS-1$ } @Override public ITypeAnnotationWalker toMethodParameter(short index) { throw new UnsupportedOperationException("Supertype has no method parameter"); //$NON-NLS-1$ } @Override public ITypeAnnotationWalker toThrows(int index) { throw new UnsupportedOperationException("Supertype has no throws"); //$NON-NLS-1$ } } public interface IMethodAnnotationWalker extends ITypeAnnotationWalker { int getParameterCount(); } class MethodAnnotationWalker extends BasicAnnotationWalker implements IMethodAnnotationWalker { int prevParamStart; TypeParametersAnnotationWalker typeParametersWalker; MethodAnnotationWalker(char[] source, int pos, LookupEnvironment environment) { super(source, pos, environment); } int typeEnd(int start) { while (this.source[start] == '[') { start++; char an = this.source[start]; if (an == NULLABLE || an == NONNULL) start++; } SignatureWrapper wrapper1 = wrapperWithStart(start); int end = wrapper1.skipAngleContents(wrapper1.computeEnd()); return end; } @Override public ITypeAnnotationWalker toTypeParameter(boolean isClassTypeParameter, int rank) { if (this.source[0] == '<') { if (this.typeParametersWalker == null) return this.typeParametersWalker = new TypeParametersAnnotationWalker(this.source, this.pos+1, rank, null, this.environment); return this.typeParametersWalker.toTypeParameter(isClassTypeParameter, rank); } return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toTypeParameterBounds(boolean isClassTypeParameter, int parameterRank) { if (this.typeParametersWalker != null) return this.typeParametersWalker.toTypeParameterBounds(isClassTypeParameter, parameterRank); return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toMethodReturn() { int close = CharOperation.indexOf(')', this.source); if (close != -1) { // optimization, see toMethodParameter. this.pos = close+1; return this; } return ITypeAnnotationWalker.EMPTY_ANNOTATION_WALKER; } @Override public ITypeAnnotationWalker toMethodParameter(short index) { if (index == 0) { int start = CharOperation.indexOf('(', this.source) + 1; this.prevParamStart = start; // optimization: normally we should create a new walker with pos=start, // but since we know the order how BTB/LE call us, we can safely use one walker for all parameters: this.pos = start; return this; } int end = typeEnd(this.prevParamStart); // leverage the fact that all parameters are evaluated in order end++; this.prevParamStart = end; // optimization, see above. this.pos = end; return this; } @Override public ITypeAnnotationWalker toThrows(int index) { return this; } @Override public ITypeAnnotationWalker toField() { throw new UnsupportedOperationException("Methods have no fields"); //$NON-NLS-1$ } @Override public int getParameterCount() { int count = 0; int start = CharOperation.indexOf('(', this.source) + 1; while (start < this.source.length && this.source[start] != ')') { start = typeEnd(start) + 1; count++; } return count; } } class FieldAnnotationWalker extends BasicAnnotationWalker { public FieldAnnotationWalker(char[] source, int pos, LookupEnvironment environment) { super(source, pos, environment); } @Override public ITypeAnnotationWalker toField() { return this; } @Override public ITypeAnnotationWalker toMethodReturn() { throw new UnsupportedOperationException("Field has no method return"); //$NON-NLS-1$ } @Override public ITypeAnnotationWalker toMethodParameter(short index) { throw new UnsupportedOperationException("Field has no method parameter"); //$NON-NLS-1$ } @Override public ITypeAnnotationWalker toThrows(int index) { throw new UnsupportedOperationException("Field has no throws"); //$NON-NLS-1$ } } }