/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* 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 org.jetbrains.kotlin.codegen.flags;
import com.intellij.openapi.util.io.FileUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.backend.common.output.OutputFile;
import org.jetbrains.kotlin.codegen.CodegenTestCase;
import org.jetbrains.org.objectweb.asm.*;
import java.io.File;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import static org.jetbrains.kotlin.test.InTextDirectivesUtils.findListWithPrefixes;
import static org.jetbrains.kotlin.test.InTextDirectivesUtils.findStringWithPrefixes;
/*
* Test correctness of written flags in class file
*
* TESTED_OBJECT_KIND - maybe class, function or property
* TESTED_OBJECTS - className, [function/property name]
* FLAGS - only flags which must be true (could be skipped if ABSENT is TRUE)
* ABSENT - true or false, optional (false by default)
*
* There is could be specified several tested objects separated by empty line, e.g:
* TESTED_OBJECT_KIND: property
* TESTED_OBJECTS: Test$object, prop
* ABSENT: TRUE
*
* TESTED_OBJECT_KIND: property
* TESTED_OBJECTS: Test, prop$delegate
* FLAGS: ACC_STATIC, ACC_FINAL, ACC_PRIVATE
*/
public abstract class AbstractWriteFlagsTest extends CodegenTestCase {
@Override
protected void doMultiFileTest(
@NotNull File wholeFile, @NotNull List<TestFile> files, @Nullable File javaFilesDir
) throws Exception {
compile(files, null);
String fileText = FileUtil.loadFile(wholeFile, true);
List<TestedObject> testedObjects = parseExpectedTestedObject(fileText);
for (TestedObject testedObject : testedObjects) {
String className = null;
for (OutputFile outputFile : classFileFactory.asList()) {
String filePath = outputFile.getRelativePath();
if (testedObject.isFullContainingClassName && filePath.equals(testedObject.containingClass + ".class") ||
!testedObject.isFullContainingClassName && filePath.startsWith(testedObject.containingClass)) {
className = filePath;
}
}
assertNotNull("Couldn't find a class file with name " + testedObject.containingClass, className);
OutputFile outputFile = classFileFactory.get(className);
assertNotNull(outputFile);
ClassReader cr = new ClassReader(outputFile.asByteArray());
TestClassVisitor classVisitor = getClassVisitor(testedObject.kind, testedObject.name, false);
cr.accept(classVisitor, ClassReader.SKIP_CODE);
if (!classVisitor.isExists()) {
classVisitor = getClassVisitor(testedObject.kind, testedObject.name, true);
cr.accept(classVisitor, ClassReader.SKIP_CODE);
}
boolean isObjectExists = !Boolean.valueOf(findStringWithPrefixes(testedObject.textData, "// ABSENT: "));
assertEquals("Wrong object existence state: " + testedObject, isObjectExists, classVisitor.isExists());
if (isObjectExists) {
assertEquals("Wrong access flag for " + testedObject + " \n" + outputFile.asText(),
getExpectedFlags(testedObject.textData), classVisitor.getAccess());
}
}
}
private static List<TestedObject> parseExpectedTestedObject(String testDescription) {
String[] testObjectData = testDescription.substring(testDescription.indexOf("// TESTED_OBJECT_KIND")).split("\n\n");
List<TestedObject> objects = new ArrayList<>();
for (String testData : testObjectData) {
if (testData.isEmpty()) continue;
TestedObject testObject = new TestedObject();
testObject.textData = testData;
List<String> testedObjects = findListWithPrefixes(testData, "// TESTED_OBJECTS: ");
assertTrue("Cannot find TESTED_OBJECTS instruction", !testedObjects.isEmpty());
testObject.containingClass = testedObjects.get(0);
if (testedObjects.size() == 1) {
testObject.name = testedObjects.get(0);
}
else if (testedObjects.size() == 2) {
testObject.name = testedObjects.get(1);
}
else {
throw new IllegalArgumentException(
"TESTED_OBJECTS instruction must contain one (for class) or two (for function and property) values");
}
testObject.kind = findStringWithPrefixes(testData, "// TESTED_OBJECT_KIND: ");
List<String> isFullName = findListWithPrefixes(testData, "// IS_FULL_CONTAINING_CLASS_NAME: ");
if (isFullName.size() == 1) {
testObject.isFullContainingClassName = Boolean.parseBoolean(isFullName.get(0));
}
objects.add(testObject);
}
assertTrue("Test description not present!", !objects.isEmpty());
return objects;
}
private static class TestedObject {
public String name;
public String containingClass = "";
public boolean isFullContainingClassName = true;
public String kind;
public String textData;
@Override
public String toString() {
return "Class = " + containingClass + ", name = " + name + ", kind = " + kind;
}
}
private static TestClassVisitor getClassVisitor(String visitorKind, String testedObjectName, boolean allowSynthetic) {
switch (visitorKind) {
case "class":
return new ClassFlagsVisitor();
case "function":
return new FunctionFlagsVisitor(testedObjectName, allowSynthetic);
case "property":
return new PropertyFlagsVisitor(testedObjectName);
case "innerClass":
return new InnerClassFlagsVisitor(testedObjectName);
default:
}
throw new IllegalArgumentException("Value of TESTED_OBJECT_KIND is incorrect: " + visitorKind);
}
protected static abstract class TestClassVisitor extends ClassVisitor {
protected boolean isExists;
public TestClassVisitor() {
super(Opcodes.ASM5);
}
abstract public int getAccess();
public boolean isExists() {
return isExists;
}
}
private static int getExpectedFlags(String text) {
int expectedAccess = 0;
Class klass = Opcodes.class;
List<String> flags = findListWithPrefixes(text, "// FLAGS: ");
for (String flag : flags) {
try {
Field field = klass.getDeclaredField(flag);
expectedAccess |= field.getInt(klass);
}
catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalArgumentException("Cannot find " + flag + " field in Opcodes class", e);
}
}
return expectedAccess;
}
private static class ClassFlagsVisitor extends TestClassVisitor {
private int access = 0;
@Override
public void visit(int version, int access, @NotNull String name, String signature, String superName, String[] interfaces) {
this.access = access;
isExists = true;
}
@Override
public int getAccess() {
return access;
}
}
private static class FunctionFlagsVisitor extends TestClassVisitor {
private int access = 0;
private final String funName;
private final boolean allowSynthetic;
public FunctionFlagsVisitor(String name, boolean allowSynthetic) {
funName = name;
this.allowSynthetic = allowSynthetic;
}
@Override
public MethodVisitor visitMethod(int access, @NotNull String name, @NotNull String desc, String signature, String[] exceptions) {
if (name.equals(funName)) {
if (!allowSynthetic && (access & Opcodes.ACC_SYNTHETIC) != 0) return null;
this.access = access;
isExists = true;
}
return null;
}
@Override
public int getAccess() {
return access;
}
}
private static class PropertyFlagsVisitor extends TestClassVisitor {
private int access = 0;
private final String propertyName;
public PropertyFlagsVisitor(String name) {
propertyName = name;
}
@Override
public FieldVisitor visitField(int access, @NotNull String name, @NotNull String desc, String signature, Object value) {
if (name.equals(propertyName)) {
this.access = access;
isExists = true;
}
return null;
}
@Override
public int getAccess() {
return access;
}
}
private static class InnerClassFlagsVisitor extends TestClassVisitor {
private int access = 0;
private final String innerClassName;
public InnerClassFlagsVisitor(String name) {
innerClassName = name;
}
@Override
public void visitInnerClass(@NotNull String innerClassInternalName, String outerClassInternalName, String name, int access) {
if (name.equals(innerClassName)) {
this.access = access;
isExists = true;
}
}
@Override
public int getAccess() {
return access;
}
}
}