/* * 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; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import kotlin.Pair; import kotlin.collections.CollectionsKt; import org.jetbrains.annotations.NotNull; import org.jetbrains.kotlin.backend.common.output.OutputFile; import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles; import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment; import org.jetbrains.kotlin.psi.KtFile; import org.jetbrains.kotlin.test.ConfigurationKind; import org.jetbrains.kotlin.test.KotlinTestUtils; import org.jetbrains.kotlin.test.TestCaseWithTmpdir; import org.jetbrains.kotlin.test.TestJdkKind; import org.jetbrains.kotlin.utils.ExceptionUtilsKt; import org.jetbrains.org.objectweb.asm.*; import java.io.File; import java.io.IOException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public abstract class AbstractLineNumberTest extends TestCaseWithTmpdir { private static final String LINE_NUMBER_FUN = "lineNumber"; private static final Pattern TEST_LINE_NUMBER_PATTERN = Pattern.compile("^.*test." + LINE_NUMBER_FUN + "\\(\\).*$"); @NotNull private KotlinCoreEnvironment createEnvironment() { return KotlinCoreEnvironment.createForTests( myTestRootDisposable, KotlinTestUtils.newConfiguration( ConfigurationKind.JDK_ONLY, TestJdkKind.MOCK_JDK, KotlinTestUtils.getAnnotationsJar(), tmpdir ), EnvironmentConfigFiles.JVM_CONFIG_FILES ); } @Override public void setUp() throws Exception { super.setUp(); KotlinCoreEnvironment environment = createEnvironment(); KtFile psiFile = KotlinTestUtils.createFile( LINE_NUMBER_FUN + ".kt", "package test;\n\npublic fun " + LINE_NUMBER_FUN + "(): Int = 0\n", environment.getProject() ); GenerationUtils.compileFileTo(psiFile, environment, tmpdir); } @NotNull private Pair<KtFile, KotlinCoreEnvironment> createPsiFile(@NotNull String filename) { File file = new File(filename); KotlinCoreEnvironment environment = createEnvironment(); String text; try { text = FileUtil.loadFile(file, true); } catch (IOException e) { throw ExceptionUtilsKt.rethrow(e); } return new Pair<>(KotlinTestUtils.createFile(file.getName(), text, environment.getProject()), environment); } private void doTest(@NotNull String filename, boolean custom) { Pair<KtFile, KotlinCoreEnvironment> fileAndEnv = createPsiFile(filename); KtFile psiFile = fileAndEnv.getFirst(); KotlinCoreEnvironment environment = fileAndEnv.getSecond(); ClassFileFactory classFileFactory = GenerationUtils.compileFile(psiFile, environment); try { if (custom) { List<String> actualLineNumbers = extractActualLineNumbersFromBytecode(classFileFactory, false); String text = psiFile.getText(); String newFileText = text.substring(0, text.indexOf("// ")) + getActualLineNumbersAsString(actualLineNumbers); KotlinTestUtils.assertEqualsToFile(new File(filename), newFileText); } else { List<String> expectedLineNumbers = extractSelectedLineNumbersFromSource(psiFile); List<String> actualLineNumbers = extractActualLineNumbersFromBytecode(classFileFactory, true); assertSameElements(actualLineNumbers, expectedLineNumbers); } } catch (Throwable e) { System.out.println(classFileFactory.createText()); throw ExceptionUtilsKt.rethrow(e); } } private static String getActualLineNumbersAsString(List<String> lines) { return CollectionsKt.joinToString(lines, " ", "// ", "", -1, "...", lineNumber -> lineNumber); } @NotNull private static List<String> extractActualLineNumbersFromBytecode(@NotNull ClassFileFactory factory, boolean testFunInvoke) { List<String> actualLineNumbers = Lists.newArrayList(); for (OutputFile outputFile : ClassFileUtilsKt.getClassFiles(factory)) { ClassReader cr = new ClassReader(outputFile.asByteArray()); try { List<String> lineNumbers = testFunInvoke ? readTestFunLineNumbers(cr) : readAllLineNumbers(cr); actualLineNumbers.addAll(lineNumbers); } catch (Throwable e) { System.out.println(factory.createText()); throw ExceptionUtilsKt.rethrow(e); } } return actualLineNumbers; } protected void doTest(String path) { doTest(path, false); } protected void doTestCustom(String path) { doTest(path, true); } @NotNull private static List<String> extractSelectedLineNumbersFromSource(@NotNull KtFile file) { String fileContent = file.getText(); List<String> lineNumbers = Lists.newArrayList(); String[] lines = StringUtil.convertLineSeparators(fileContent).split("\n"); for (int i = 0; i < lines.length; i++) { Matcher matcher = TEST_LINE_NUMBER_PATTERN.matcher(lines[i]); if (matcher.matches()) { lineNumbers.add(Integer.toString(i + 1)); } } return lineNumbers; } @NotNull private static List<String> readTestFunLineNumbers(@NotNull ClassReader cr) { List<Label> labels = Lists.newArrayList(); Map<Label, String> labels2LineNumbers = Maps.newHashMap(); ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5) { @Override public MethodVisitor visitMethod(int access, @NotNull String name, @NotNull String desc, String signature, String[] exceptions) { return new MethodVisitor(Opcodes.ASM5) { private Label lastLabel; @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if (LINE_NUMBER_FUN.equals(name)) { assert lastLabel != null : "A function call with no preceding label"; labels.add(lastLabel); } lastLabel = null; } @Override public void visitLabel(@NotNull Label label) { lastLabel = label; } @Override public void visitLineNumber(int line, @NotNull Label start) { labels2LineNumbers.put(start, Integer.toString(line)); } }; } }; cr.accept(visitor, ClassReader.SKIP_FRAMES); List<String> lineNumbers = Lists.newArrayList(); for (Label label : labels) { String lineNumber = labels2LineNumbers.get(label); assert lineNumber != null : "No line number found for a label"; lineNumbers.add(lineNumber); } return lineNumbers; } @NotNull private static List<String> readAllLineNumbers(@NotNull ClassReader reader) { List<String> result = new ArrayList<>(); Set<String> visitedLabels = new HashSet<>(); reader.accept(new ClassVisitor(Opcodes.ASM5) { @Override public MethodVisitor visitMethod(int access, @NotNull String name, @NotNull String desc, String signature, String[] exceptions) { return new MethodVisitor(Opcodes.ASM5) { @Override public void visitLineNumber(int line, @NotNull Label label) { boolean overrides = !visitedLabels.add(label.toString()); result.add((overrides ? "+" : "") + line); } }; } }, ClassReader.SKIP_FRAMES); return result; } }