blob: 8c6c2b89a027b77abca3d683443d6fb66258a0ce [file] [log] [blame]
// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.kotlin;
import com.android.tools.r8.naming.Range;
import com.android.tools.r8.utils.SegmentTree;
import com.android.tools.r8.utils.ThrowingConsumer;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// This is a parser for the Kotlin-generated source debug extensions, which is a stratified map.
// The kotlin-parser for this data is can be found here in the kotlin compiler:
// compiler/backend/src/org/jetbrains/kotlin/codegen/inline/SMAPParser.kt
public class KotlinSourceDebugExtensionParser {
private static final String SMAP_IDENTIFIER = "SMAP";
private static final String SMAP_SECTION_START = "*S";
private static final String SMAP_SECTION_KOTLIN_START = SMAP_SECTION_START + " Kotlin";
private static final String SMAP_FILES_IDENTIFIER = "*F";
private static final String SMAP_LINES_IDENTIFIER = "*L";
private static final String SMAP_END_IDENTIFIER = "*E";
public static class KotlinSourceDebugExtensionParserException extends Exception {
KotlinSourceDebugExtensionParserException(String message) {
super(message);
}
}
public static class BufferedStringReader implements Closeable {
private final BufferedReader reader;
private String readLine;
BufferedStringReader(String data) {
reader = new BufferedReader(new StringReader(data));
}
String readNextLine() throws IOException {
return readLine = reader.readLine();
}
boolean readExpectedLine(String expected) throws IOException {
return readNextLine().equals(expected);
}
void readExpectedLineOrThrow(String expected)
throws KotlinSourceDebugExtensionParserException, IOException {
if (!readExpectedLine(expected)) {
throw new KotlinSourceDebugExtensionParserException(
"The string " + readLine + " does not match the expected string " + expected);
}
}
boolean isEOF() {
return readLine == null;
}
BufferedStringReader readUntil(String terminator) throws IOException {
while (!terminator.equals(readLine) && !isEOF()) {
readNextLine();
}
if (isEOF()) {
return this;
}
return this;
}
void readUntil(
String terminator,
int linesInBlock,
ThrowingConsumer<List<String>, KotlinSourceDebugExtensionParserException> callback)
throws IOException, KotlinSourceDebugExtensionParserException {
if (terminator.equals(readLine)) {
return;
}
List<String> readStrings = new ArrayList<>();
readStrings.add(readNextLine());
int linesLeft = linesInBlock;
while (!terminator.equals(readLine) && !isEOF()) {
if (linesLeft == 1) {
assert readStrings.size() == linesInBlock;
callback.accept(readStrings);
linesLeft = linesInBlock;
readStrings = new ArrayList<>();
} else {
linesLeft -= 1;
}
readStrings.add(readNextLine());
}
if (readStrings.size() > 0 && !readStrings.get(0).equals(terminator)) {
throw new KotlinSourceDebugExtensionParserException(
"Block size does not match linesInBlock = " + linesInBlock);
}
}
@Override
public void close() throws IOException {
reader.close();
}
}
public static Result parse(String annotationData) {
if (annotationData == null || annotationData.isEmpty()) {
return null;
}
// The source debug smap data is on the following form:
// SMAP
// <filename>
// Kotlin <-- why is this even here?
// *S <section>
// *F
// + <file_id_i> <file_name_i>
// <path_i>
// *L
// <from>#<file>,<to>:<debug-line-position>
// <from>#<file>:<debug-line-position>
// *E
// *S KotlinDebug
// ***
// *E
try (BufferedStringReader reader = new BufferedStringReader(annotationData)) {
ResultBuilder builder = new ResultBuilder();
// Check for SMAP
if (!reader.readExpectedLine(SMAP_IDENTIFIER)) {
return null;
}
if (reader.readUntil(SMAP_SECTION_KOTLIN_START).isEOF()) {
return null;
}
// At this point we should be parsing a kotlin source debug extension, so we will throw if we
// read an unexpected line.
reader.readExpectedLineOrThrow(SMAP_FILES_IDENTIFIER);
// Iterate over the files section with the format:
// + <file_number_i> <file_name_i>
// <file_path_i>
reader.readUntil(
SMAP_LINES_IDENTIFIER, 2, block -> addFileToBuilder(block.get(0), block.get(1), builder));
// Ensure that we read *L.
if (reader.isEOF()) {
throw new KotlinSourceDebugExtensionParserException(
"Unexpected EOF - no debug line positions");
}
// Iterate over the debug line number positions:
// <from>#<file>,<range>:<debug-line-position>
// or
// <from>#<file>:<debug-line-position>
reader.readUntil(
SMAP_END_IDENTIFIER, 1, block -> addDebugEntryToBuilder(block.get(0), builder));
// Ensure that we read the end section *E.
if (reader.isEOF()) {
throw new KotlinSourceDebugExtensionParserException(
"Unexpected EOF when parsing SMAP debug entries");
}
return builder.build();
} catch (IOException | KotlinSourceDebugExtensionParserException e) {
return null;
}
}
private static void addFileToBuilder(String entryLine, String filePath, ResultBuilder builder)
throws KotlinSourceDebugExtensionParserException {
// + <file_number_i> <file_name_i>
// <file_path_i>
String[] entries = entryLine.trim().split(" ");
if (entries.length != 3 || !entries[0].equals("+")) {
throw new KotlinSourceDebugExtensionParserException(
"Wrong number of entries on line " + entryLine);
}
String fileName = entries[2];
if (fileName.isEmpty()) {
throw new KotlinSourceDebugExtensionParserException(
"Did not expect file name to be empty for line " + entryLine);
}
if (filePath == null || filePath.isEmpty()) {
throw new KotlinSourceDebugExtensionParserException(
"Did not expect file path to be null or empty for " + entryLine);
}
int index = asInteger(entries[1]);
Source source = new Source(fileName, filePath);
Source existingSource = builder.files.put(index, source);
if (existingSource != null) {
throw new KotlinSourceDebugExtensionParserException(
"File index " + index + " was already mapped to an existing source: " + source);
}
}
private static int asInteger(String numberAsString)
throws KotlinSourceDebugExtensionParserException {
int number = -1;
try {
number = Integer.parseInt(numberAsString);
} catch (NumberFormatException e) {
throw new KotlinSourceDebugExtensionParserException(
"Could not parse number " + numberAsString);
}
return number;
}
private static void addDebugEntryToBuilder(String debugEntry, ResultBuilder builder)
throws KotlinSourceDebugExtensionParserException {
// <from>#<file>,<size>:<debug-line-position>
// or
// <from>#<file>:<debug-line-position>
// All positions should define intervals for mappings.
try {
int targetSplit = debugEntry.indexOf(':');
int target = Integer.parseInt(debugEntry.substring(targetSplit + 1));
String original = debugEntry.substring(0, targetSplit);
int fileIndexSplit = original.indexOf('#');
int originalStart = Integer.parseInt(original.substring(0, fileIndexSplit));
// The range may have a different end than start.
String fileAndEndRange = original.substring(fileIndexSplit + 1);
int endRangeCharPosition = fileAndEndRange.indexOf(',');
int size = 1;
if (endRangeCharPosition > -1) {
// The file should be at least one number wide.
assert endRangeCharPosition > 0;
size = Integer.parseInt(fileAndEndRange.substring(endRangeCharPosition + 1));
} else {
endRangeCharPosition = fileAndEndRange.length();
}
int fileIndex = Integer.parseInt(fileAndEndRange.substring(0, endRangeCharPosition));
Source thisFileSource = builder.files.get(fileIndex);
if (thisFileSource == null) {
throw new KotlinSourceDebugExtensionParserException(
"Could not find file with index " + fileIndex);
}
Range range = new Range(originalStart, originalStart + (size - 1));
Position position = new Position(thisFileSource, range);
builder.segmentTree.add(target, target + (size - 1), position);
} catch (NumberFormatException e) {
throw new KotlinSourceDebugExtensionParserException("Could not convert position to number");
}
}
public static class Result {
private final SegmentTree<Position> segmentTree;
public Result(SegmentTree<Position> segmentTree) {
this.segmentTree = segmentTree;
}
public Map.Entry<Integer, Position> lookup(int point) {
return segmentTree.findEntry(point);
}
public int size() {
return segmentTree.size();
}
}
public static class ResultBuilder {
SegmentTree<Position> segmentTree = new SegmentTree<>(false);
Map<Integer, Source> files = new HashMap<>();
public Result build() {
return new Result(segmentTree);
}
}
public static class Source {
private final String fileName;
private final String path;
private Source(String fileName, String path) {
this.fileName = fileName;
this.path = path;
}
public String getFileName() {
return fileName;
}
public String getPath() {
return path;
}
@Override
public String toString() {
return path + "(" + fileName + ")";
}
}
public static class Position {
private final Source source;
private final Range range;
public Position(Source source, Range range) {
this.source = source;
this.range = range;
}
public Source getSource() {
return source;
}
public Range getRange() {
return range;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(range.from);
sb.append("#");
sb.append(source);
if (range.to != range.from) {
sb.append(",");
sb.append(range.to);
}
sb.append(":");
return sb.toString();
}
}
}