aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/android/java/com/google/devtools/build/android/desugar/Java7Compatibility.java
blob: 2090d5ccc2f6c88c02abefe6c7894c817d0f34af (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// Copyright 2016 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.android.desugar;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.devtools.build.android.desugar.io.BitFlags;
import javax.annotation.Nullable;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.TypePath;

/**
 * Visitor that tries to ensures bytecode version <= 51 (Java 7) and that throws if it sees default
 * or static interface methods (i.e., non-abstract interface methods), which don't exist in Java 7.
 * <p>The class version will 52 iff static interface method from the bootclasspath is invoked.
 * This is mostly ensure that the generated bytecode is valid.
 */
public class Java7Compatibility extends ClassVisitor {

  @Nullable private final ClassReaderFactory factory;
  @Nullable private final ClassReaderFactory bootclasspathReader;

  private boolean isInterface;
  private String internalName;
  private int access;
  private String signature;
  private String superName;
  private String[] interfaces;

  public Java7Compatibility(
      ClassVisitor cv, ClassReaderFactory factory, ClassReaderFactory bootclasspathReader) {
    super(Opcodes.ASM6, cv);
    this.factory = factory;
    this.bootclasspathReader = bootclasspathReader;
  }

  @Override
  public void visit(
      int version,
      int access,
      String name,
      String signature,
      String superName,
      String[] interfaces) {
    internalName = name;
    this.access = access;
    this.signature = signature;
    this.superName = superName;
    this.interfaces = interfaces;
    isInterface = BitFlags.isSet(access, Opcodes.ACC_INTERFACE);
    super.visit(
        Math.min(version, Opcodes.V1_7),
        access,
        name,
        signature,
        superName,
        interfaces);
  }

  @Override
  public MethodVisitor visitMethod(
      int access, String name, String desc, String signature, String[] exceptions) {
    // Remove bridge default methods in interfaces; javac generates them again for implementing
    // classes anyways.
    if (isInterface
        && (access & (Opcodes.ACC_BRIDGE | Opcodes.ACC_ABSTRACT | Opcodes.ACC_STATIC))
            == Opcodes.ACC_BRIDGE) {
      return null;
    }
    if (isInterface
        && "$jacocoInit".equals(name)
        && BitFlags.isSet(access, Opcodes.ACC_SYNTHETIC | Opcodes.ACC_STATIC)) {
      // Drop static interface method that Jacoco generates--we'll inline it into the static
      // initializer instead
      return null;
    }
    checkArgument(!isInterface
        || BitFlags.isSet(access, Opcodes.ACC_ABSTRACT)
        || "<clinit>".equals(name),
        "Interface %s defines non-abstract method %s%s, which is not supported",
        internalName, name, desc);
    MethodVisitor result =
        new UpdateBytecodeVersionIfNecessary(
            super.visitMethod(access, name, desc, signature, exceptions));

    return (isInterface && "<clinit>".equals(name)) ? new InlineJacocoInit(result) : result;
  }

  @Override
  public void visitInnerClass(String name, String outerName, String innerName, int access) {
    // Drop MethodHandles$Lookup inner class information--it shouldn't be needed anymore.  Proguard
    // complains about this even though the inner class information is never used.
    if (!"java/lang/invoke/MethodHandles$Lookup".equals(name)) {
      super.visitInnerClass(name, outerName, innerName, access);
    }
  }

  /** This will rewrite class version to 52 if it sees invokestatic on an interface. */
  private class UpdateBytecodeVersionIfNecessary extends MethodVisitor {

    boolean updated = false;

    public UpdateBytecodeVersionIfNecessary(MethodVisitor methodVisitor) {
      super(Opcodes.ASM6, methodVisitor);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
      if (itf && opcode == Opcodes.INVOKESTATIC) {
        checkNotNull(bootclasspathReader);
        checkState(
            bootclasspathReader.isKnown(owner),
            "%s contains invocation of static interface method that is "
                + "not in the bootclasspath. Owner: %s, name: %s, desc: %s.",
            Java7Compatibility.this.internalName,
            owner,
            name,
            desc);
        if (!updated) {
          Java7Compatibility.this.cv.visit(
              Opcodes.V1_8,
              Java7Compatibility.this.access,
              Java7Compatibility.this.internalName,
              Java7Compatibility.this.signature,
              Java7Compatibility.this.superName,
              Java7Compatibility.this.interfaces);
          updated = true;
        }
      }
      super.visitMethodInsn(opcode, owner, name, desc, itf);
    }
  }

  private class InlineJacocoInit extends MethodVisitor {
    public InlineJacocoInit(MethodVisitor dest) {
      super(Opcodes.ASM6, dest);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
      if (opcode == Opcodes.INVOKESTATIC
          && "$jacocoInit".equals(name)
          && internalName.equals(owner)) {
        checkNotNull(factory);
        ClassReader bytecode = checkNotNull(factory.readIfKnown(internalName),
            "Couldn't load interface %s to inline $jacocoInit()", internalName);
        InlineOneMethod copier = new InlineOneMethod("$jacocoInit", this);
        bytecode.accept(copier, ClassReader.SKIP_DEBUG /* we're copying generated code anyway */);
      } else {
        super.visitMethodInsn(opcode, owner, name, desc, itf);
      }
    }
  }

  private static class InlineOneMethod extends ClassVisitor {

    private final String methodName;
    private final MethodVisitor dest;
    private int copied = 0;

    public InlineOneMethod(String methodName, MethodVisitor dest) {
      super(Opcodes.ASM6);
      this.methodName = methodName;
      this.dest = dest;
    }

    @Override
    public void visit(
        int version,
        int access,
        String name,
        String signature,
        String superName,
        String[] interfaces) {
      checkArgument(BitFlags.isSet(access, Opcodes.ACC_INTERFACE));
    }

    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {
      if (name.equals(methodName)) {
        checkState(copied == 0, "Found unexpected second method %s with descriptor %s", name, desc);
        ++copied;
        return new InlineMethodBody(dest);
      }
      return null;
    }
  }

  private static class InlineMethodBody extends MethodVisitor {
    private final MethodVisitor dest;

    public InlineMethodBody(MethodVisitor dest) {
      // We'll set the destination visitor in visitCode() to reduce the risk of copying anything
      // we didn't mean to copy
      super(Opcodes.ASM6, (MethodVisitor) null);
      this.dest = dest;
    }

    @Override
    public void visitParameter(String name, int access) {
    }

    @Override
    public AnnotationVisitor visitAnnotationDefault() {
      throw new IllegalStateException("Don't use to copy annotation attributes");
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
      return null;
    }

    @Override
    public AnnotationVisitor visitTypeAnnotation(
        int typeRef, TypePath typePath, String desc, boolean visible) {
      return null;
    }

    @Override
    public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
      return null;
    }

    @Override
    public void visitAttribute(Attribute attr) {
      mv = null; // don't care about anything but the code attribute
    }

    @Override
    public void visitCode() {
      // Start copying instructions but don't call super.visitCode() since dest is already in the
      // middle of visiting another method
      mv = dest;
    }

    @Override
    public void visitInsn(int opcode) {
      switch (opcode) {
        case Opcodes.IRETURN:
        case Opcodes.LRETURN:
        case Opcodes.FRETURN:
        case Opcodes.DRETURN:
        case Opcodes.ARETURN:
        case Opcodes.RETURN:
          checkState(mv != null, "Encountered a second return it would seem: %s", opcode);
          mv = null; // Done: we don't expect anything to follow
          return;
        default:
          super.visitInsn(opcode);
      }
    }

    @Override
    public void visitVarInsn(int opcode, int var) {
      throw new UnsupportedOperationException(
          "We don't support inlining methods with locals: " + opcode + " " + var);
    }

    @Override
    public void visitLocalVariable(
        String name, String desc, String signature, Label start, Label end, int index) {
      throw new UnsupportedOperationException(
          "We don't support inlining methods with locals: " + name + ": " + desc);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
      // Drop this, since dest will get more instructions and will need to recompute stack size.
      // This does indicate the end of visiting bytecode instructions, so defensively reset mv.
      mv = null;
    }
  }
}