blob: b73c9bc35404ea93ee4150cf2e9f9ae37a2d5261 [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.retrace.internal;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.retrace.StackTraceLineParser;
import com.android.tools.r8.retrace.internal.StackTraceElementStringProxy.ClassNameType;
import com.android.tools.r8.retrace.internal.StackTraceElementStringProxy.StackTraceElementStringProxyBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StackTraceRegularExpressionParser
implements StackTraceLineParser<String, StackTraceElementStringProxy> {
// This is a slight modification of the default regular expression shown for proguard retrace
// that allow for retracing classes in the form <class>: lorem ipsum...
// Seems like Proguard retrace is expecting the form "Caused by: <class>".
public static final String DEFAULT_REGULAR_EXPRESSION =
"(?:.*?\\bat\\s+%c\\.%m\\s*\\(%S\\)\\p{Z}*(?:~\\[.*\\])?)"
+ "|(?:(?:(?:%c|.*)?[:\"]\\s+)?%c(?::.*)?)";
private final Pattern compiledPattern;
private static final int NO_MATCH = -1;
private final SourceFileLineNumberGroup sourceFileLineNumberGroup =
new SourceFileLineNumberGroup();
private final List<RegularExpressionGroupHandler> handlers;
private final TypeNameGroup typeNameGroup = new TypeNameGroup();
private final BinaryNameGroup binaryNameGroup = new BinaryNameGroup();
private final SourceFileGroup sourceFileGroup = new SourceFileGroup();
private final LineNumberGroup lineNumberGroup = new LineNumberGroup();
private final FieldOrReturnTypeGroup fieldOrReturnTypeGroup = new FieldOrReturnTypeGroup();
private final MethodArgumentsGroup methodArgumentsGroup = new MethodArgumentsGroup();
private final MethodNameGroup methodNameGroup = new MethodNameGroup();
private final FieldNameGroup fieldNameGroup = new FieldNameGroup();
private static final String CAPTURE_GROUP_PREFIX = "captureGroup";
private static final int FIRST_CAPTURE_GROUP_INDEX = 0;
public StackTraceRegularExpressionParser() {
this(DEFAULT_REGULAR_EXPRESSION);
}
public StackTraceRegularExpressionParser(String regularExpression) {
handlers = new ArrayList<>();
StringBuilder refinedRegularExpressionBuilder = new StringBuilder();
registerGroups(
regularExpression, refinedRegularExpressionBuilder, handlers, FIRST_CAPTURE_GROUP_INDEX);
compiledPattern = Pattern.compile(refinedRegularExpressionBuilder.toString());
}
@Override
public StackTraceElementStringProxy parse(String stackTraceLine) {
StackTraceElementStringProxyBuilder proxyBuilder =
StackTraceElementStringProxy.builder(stackTraceLine);
Matcher matcher = compiledPattern.matcher(stackTraceLine);
if (matcher.matches()) {
boolean seenMatchedClassHandler = false;
for (RegularExpressionGroupHandler handler : handlers) {
if (seenMatchedClassHandler && handler.isClassHandler()) {
continue;
}
if (handler.matchHandler(proxyBuilder, matcher)) {
seenMatchedClassHandler |= handler.isClassHandler();
}
}
}
return proxyBuilder.build();
}
private int registerGroups(
String regularExpression,
StringBuilder refinedRegularExpression,
List<RegularExpressionGroupHandler> handlers,
int captureGroupIndex) {
int lastCommittedIndex = 0;
boolean seenPercentage = false;
boolean escaped = false;
for (int i = 0; i < regularExpression.length(); i++) {
if (seenPercentage) {
assert !escaped;
final RegularExpressionGroup group = getGroupFromVariable(regularExpression.charAt(i));
refinedRegularExpression.append(regularExpression, lastCommittedIndex, i - 1);
if (group.isSynthetic()) {
captureGroupIndex =
registerGroups(
group.subExpression(), refinedRegularExpression, handlers, captureGroupIndex);
} else {
String captureGroupName = CAPTURE_GROUP_PREFIX + (captureGroupIndex++);
refinedRegularExpression
.append("(?<")
.append(captureGroupName)
.append(">")
.append(group.subExpression())
.append(")");
handlers.add(group.createHandler(captureGroupName));
}
lastCommittedIndex = i + 1;
seenPercentage = false;
} else {
seenPercentage = !escaped && regularExpression.charAt(i) == '%';
escaped = !escaped && regularExpression.charAt(i) == '\\';
}
}
refinedRegularExpression.append(
regularExpression, lastCommittedIndex, regularExpression.length());
return captureGroupIndex;
}
private RegularExpressionGroup getGroupFromVariable(char variable) {
switch (variable) {
case 'c':
return typeNameGroup;
case 'C':
return binaryNameGroup;
case 'm':
return methodNameGroup;
case 'f':
return fieldNameGroup;
case 's':
return sourceFileGroup;
case 'l':
return lineNumberGroup;
case 'S':
return sourceFileLineNumberGroup;
case 't':
return fieldOrReturnTypeGroup;
case 'a':
return methodArgumentsGroup;
default:
throw new Unreachable("Unexpected variable: " + variable);
}
}
private interface RegularExpressionGroupHandler {
boolean matchHandler(StackTraceElementStringProxyBuilder builder, Matcher matcher);
default boolean isClassHandler() {
return false;
}
}
private abstract static class RegularExpressionGroup {
abstract String subExpression();
abstract RegularExpressionGroupHandler createHandler(String captureGroup);
boolean isSynthetic() {
return false;
}
}
// TODO(b/145731185): Extend support for identifiers with strings inside back ticks.
private static final String javaIdentifierSegment =
"\\p{javaJavaIdentifierStart}[-\\p{javaJavaIdentifierPart}]*";
private static final String METHOD_NAME_REGULAR_EXPRESSION =
"(?:(" + javaIdentifierSegment + "|\\<init\\>|\\<clinit\\>))";
abstract static class ClassNameGroup extends RegularExpressionGroup {
abstract ClassNameType getClassNameType();
@Override
RegularExpressionGroupHandler createHandler(String captureGroup) {
return new RegularExpressionGroupHandler() {
@Override
public boolean matchHandler(StackTraceElementStringProxyBuilder builder, Matcher matcher) {
int startOfGroup = matcher.start(captureGroup);
if (startOfGroup == NO_MATCH) {
return false;
}
String typeName = matcher.group(captureGroup);
if (typeName.equals("Suppressed")) {
// Ensure we do not map supressed.
return false;
}
builder.registerClassName(startOfGroup, matcher.end(captureGroup), getClassNameType());
return true;
}
@Override
public boolean isClassHandler() {
return true;
}
};
}
}
private static class TypeNameGroup extends ClassNameGroup {
@Override
String subExpression() {
return "(" + javaIdentifierSegment + "\\.)*" + javaIdentifierSegment;
}
@Override
ClassNameType getClassNameType() {
return ClassNameType.TYPENAME;
}
}
private static class BinaryNameGroup extends ClassNameGroup {
@Override
String subExpression() {
return "(?:" + javaIdentifierSegment + "\\/)*" + javaIdentifierSegment;
}
@Override
ClassNameType getClassNameType() {
return ClassNameType.BINARY;
}
}
private static class MethodNameGroup extends RegularExpressionGroup {
@Override
String subExpression() {
return METHOD_NAME_REGULAR_EXPRESSION;
}
@Override
RegularExpressionGroupHandler createHandler(String captureGroup) {
return (builder, matcher) -> {
int startOfGroup = matcher.start(captureGroup);
if (startOfGroup == NO_MATCH) {
return false;
}
builder.registerMethodName(startOfGroup, matcher.end(captureGroup));
return true;
};
}
}
private static class FieldNameGroup extends RegularExpressionGroup {
@Override
String subExpression() {
return javaIdentifierSegment;
}
@Override
RegularExpressionGroupHandler createHandler(String captureGroup) {
return (builder, matcher) -> {
int startOfGroup = matcher.start(captureGroup);
if (startOfGroup == NO_MATCH) {
return false;
}
builder.registerFieldName(startOfGroup, matcher.end(captureGroup));
return true;
};
}
}
private static class SourceFileGroup extends RegularExpressionGroup {
@Override
String subExpression() {
String anyNonDigitNonColonCharNonWhiteSpace = "[^\\d:\\s]";
String anyNonColonChar = "[^:]";
String colonsWithNonDigitOrWhiteSpaceSuffix = ":+" + anyNonDigitNonColonCharNonWhiteSpace;
return "(?:" + colonsWithNonDigitOrWhiteSpaceSuffix + "|" + anyNonColonChar + "*)*";
}
@Override
RegularExpressionGroupHandler createHandler(String captureGroup) {
return (builder, matcher) -> {
int startOfGroup = matcher.start(captureGroup);
if (startOfGroup == NO_MATCH) {
return false;
}
builder.registerSourceFile(startOfGroup, matcher.end(captureGroup));
return true;
};
}
}
private static class LineNumberGroup extends RegularExpressionGroup {
@Override
String subExpression() {
return "\\d*";
}
@Override
RegularExpressionGroupHandler createHandler(String captureGroup) {
return (builder, matcher) -> {
int startOfGroup = matcher.start(captureGroup);
if (startOfGroup == NO_MATCH) {
return false;
}
boolean insertSeparatorForRetraced = false;
// We need to include ':' in the group since we may want to rewrite '(SourceFile:0)` into
// (SourceFile) and not (SourceFile:)
if (startOfGroup > 0 && builder.getLine().charAt(startOfGroup + -1) == ':') {
startOfGroup = startOfGroup - 1;
insertSeparatorForRetraced = true;
}
int end = matcher.end(captureGroup);
builder.registerLineNumber(startOfGroup, end, insertSeparatorForRetraced);
return true;
};
}
}
private static class SourceFileLineNumberGroup extends RegularExpressionGroup {
@Override
String subExpression() {
return ".*";
}
@Override
RegularExpressionGroupHandler createHandler(String captureGroup) {
return (builder, matcher) -> {
int startOfGroup = matcher.start(captureGroup);
if (startOfGroup == NO_MATCH) {
return false;
}
int endOfSourceFileInGroup = findEndOfSourceFile(matcher.group(captureGroup));
int sourceFileEnd = startOfGroup + endOfSourceFileInGroup;
builder.registerSourceFile(startOfGroup, sourceFileEnd);
int endOfMatch = matcher.end(captureGroup);
// We need to include ':' in the group since we may want to rewrite '(SourceFile:0)` into
// (SourceFile) and not (SourceFile:). We fix this by setting the start of the linenumber
// group to the end of the SourceFile group and then force inserting ':'.
builder.registerLineNumber(Integer.min(sourceFileEnd, endOfMatch), endOfMatch, true);
return true;
};
}
private int findEndOfSourceFile(String group) {
int index = group.length();
while (index > 0) {
char currentChar = group.charAt(index - 1);
if (currentChar == ':' && index < group.length()) {
// Subtract the ':' from the length.
return index - 1;
}
if (!Character.isDigit(currentChar)) {
return group.length();
}
index--;
}
return group.length();
}
}
private static final String JAVA_TYPE_REGULAR_EXPRESSION =
"(" + javaIdentifierSegment + "\\.)*" + javaIdentifierSegment + "[\\[\\]]*";
private static class FieldOrReturnTypeGroup extends RegularExpressionGroup {
@Override
String subExpression() {
return JAVA_TYPE_REGULAR_EXPRESSION;
}
@Override
RegularExpressionGroupHandler createHandler(String captureGroup) {
return (builder, matcher) -> {
int startOfGroup = matcher.start(captureGroup);
if (startOfGroup == NO_MATCH) {
return false;
}
builder.registerFieldOrReturnType(startOfGroup, matcher.end(captureGroup));
return true;
};
}
}
private static class MethodArgumentsGroup extends RegularExpressionGroup {
@Override
String subExpression() {
return "((" + JAVA_TYPE_REGULAR_EXPRESSION + "\\,)*" + JAVA_TYPE_REGULAR_EXPRESSION + ")?";
}
@Override
RegularExpressionGroupHandler createHandler(String captureGroup) {
return (builder, matcher) -> {
int startOfGroup = matcher.start(captureGroup);
if (startOfGroup == NO_MATCH) {
return false;
}
builder.registerMethodArguments(startOfGroup, matcher.end(captureGroup));
return true;
};
}
}
}