blob: ed4429207b4620c2156df2751ee1a7f9c0e1b094 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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.android.tools.idea.gradle.output.parser;
import com.android.ide.common.blame.Message;
import com.android.ide.common.blame.SourceFile;
import com.android.ide.common.blame.SourceFilePosition;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.blame.parser.ParsingFailedException;
import com.android.ide.common.blame.parser.PatternAwareOutputParser;
import com.android.ide.common.blame.parser.aapt.AaptOutputParser;
import com.android.ide.common.blame.parser.aapt.AbstractAaptOutputParser;
import com.android.ide.common.blame.parser.util.OutputLineReader;
import com.android.utils.ILogger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A parser for Gradle's final error message on failed builds. It is of the form:
* <p/>
* <pre>
* FAILURE: Build failed with an exception.
*
* * What went wrong:
* Execution failed for task 'TASK_PATH'.
*
* * Where:
* Build file 'PATHNAME' line: LINE_NUM
* > ERROR_MESSAGE
*
* * Try:
* Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
*
* BUILD FAILED
* </pre>
* <p/>
* The Where section may not appear (it usually only shows up if there's a problem in the build.gradle file itself). We parse this
* out to get the failure message and module, and the where output if it appears.
*/
public class BuildFailureParser implements PatternAwareOutputParser {
private static final Pattern[] BEGINNING_PATTERNS =
{Pattern.compile("^FAILURE: Build failed with an exception."), Pattern.compile("^\\* What went wrong:")};
private static final Pattern WHERE_LINE_1 = Pattern.compile("^\\* Where:");
private static final Pattern WHERE_LINE_2 = Pattern.compile("^Build file '(.+)' line: (\\d+)");
private static final Pattern[] ENDING_PATTERNS = {Pattern.compile("^\\* Try:"),
Pattern.compile("^Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.")};
// If there's a failure executing a command-line tool, Gradle will output the complete command line of the tool and will embed the
// output from that tool. We catch the command line, pull out the tool being invoked, and then take the output and run it through a
// sub-parser to generate parsed error messages.
private static final Pattern COMMAND_FAILURE_MESSAGE = Pattern.compile("^> Failed to run command:");
private static final Pattern COMMAND_LINE_PARSER = Pattern.compile("^\\s+/([^/ ]+/)+([^/ ]+) (.*)");
private static final Pattern COMMAND_LINE_ERROR_OUTPUT = Pattern.compile("^ Output:$");
private enum State {
BEGINNING,
WHERE,
MESSAGE,
COMMAND_FAILURE_COMMAND_LINE,
COMMAND_FAILURE_OUTPUT,
ENDING
}
private AaptOutputParser myAaptParser = new AaptOutputParser();
@Override
public boolean parse(@NotNull String line, @NotNull OutputLineReader reader, @NotNull List<Message> messages, @NotNull ILogger logger)
throws ParsingFailedException {
State state = State.BEGINNING;
int pos = 0;
String currentLine = line;
SourceFile file = SourceFile.UNKNOWN;
SourcePosition position = SourcePosition.UNKNOWN;
String lastQuotedLine = null;
StringBuilder errorMessage = new StringBuilder();
Matcher matcher;
// TODO: If the output isn't quite matching this format (for example, the "Try" statement is missing) this will eat
// some of the output. We should fall back to emitting all the output in that case.
while (true) {
switch (state) {
case BEGINNING:
if (WHERE_LINE_1.matcher(currentLine).matches()) {
state = State.WHERE;
}
else if (!BEGINNING_PATTERNS[pos].matcher(currentLine).matches()) {
return false;
}
else if (++pos >= BEGINNING_PATTERNS.length) {
state = State.MESSAGE;
}
break;
case WHERE:
matcher = WHERE_LINE_2.matcher(currentLine);
if (!matcher.matches()) {
return false;
}
file = new SourceFile(new File(matcher.group(1)));
position = new SourcePosition(Integer.parseInt(matcher.group(2))-1, 0, -1);
state = State.BEGINNING;
break;
case MESSAGE:
if (ENDING_PATTERNS[0].matcher(currentLine).matches()) {
state = State.ENDING;
pos = 1;
}
else if (COMMAND_FAILURE_MESSAGE.matcher(currentLine).matches()) {
state = State.COMMAND_FAILURE_COMMAND_LINE;
}
else {
// Determine whether the string starts with ">" (possibly indented by whitespace), and if so, where
int quoted = -1;
for (int i = 0, n = currentLine.length(); i < n; i++) {
char c = currentLine.charAt(i);
if (c == '>') {
quoted = i;
break;
}
else if (!Character.isWhitespace(c)) {
break;
}
}
if (quoted != -1) {
if (currentLine.startsWith("> In DataSet ", quoted) && currentLine.contains("no data file for changedFile")) {
matcher = Pattern.compile("\\s*> In DataSet '.+', no data file for changedFile '(.+)'").matcher(currentLine);
if (matcher.find()) {
file = new SourceFile(new File(matcher.group(1)));
}
}
else if (currentLine.startsWith("> Duplicate resources: ", quoted)) {
// For exact format, see com.android.ide.common.res2.DuplicateDataException
matcher = Pattern.compile("\\s*> Duplicate resources: (.+):(.+), (.+):(.+)\\s*").matcher(currentLine);
if (matcher.matches()) {
file = new SourceFile(new File(matcher.group(1)));
position = AbstractAaptOutputParser.findResourceLine(file.getSourceFile(), matcher.group(2), logger);
File other = new File(matcher.group(3));
SourcePosition otherPos = AbstractAaptOutputParser.findResourceLine(other, matcher.group(4), logger);
messages.add(new Message(
Message.Kind.ERROR, currentLine, new SourceFilePosition(file, position), new SourceFilePosition(other, otherPos)));
// Skip appending to the errorMessage buffer; we've already added both locations to the message
break;
}
}
else if (currentLine.startsWith("> Problems pinging owner of lock ", quoted)) {
String text = "Possibly unstable network connection: Failed to connect to lock owner. Try to rebuild.";
messages.add(new Message(Message.Kind.ERROR, text, SourceFilePosition.UNKNOWN));
}
}
if (errorMessage.length() > 0) {
errorMessage.append("\n");
}
if (isGradleQuotedLine(currentLine)) {
lastQuotedLine = currentLine;
}
errorMessage.append(currentLine);
}
break;
case COMMAND_FAILURE_COMMAND_LINE:
// Gradle can put an unescaped "Android Studio" in its command-line output. (It doesn't care because this doesn't have to be
// a perfectly valid command line; it's just an error message). To keep it from messing up our parsing, let's convert those
// to "Android_Studio". If there are other spaces in the command-line path, though, it will mess up our parsing. Oh, well.
currentLine = currentLine.replaceAll("Android Studio", "Android_Studio");
matcher = COMMAND_LINE_PARSER.matcher(currentLine);
if (matcher.matches()) {
String message = String.format("Error while executing %s command", matcher.group(2));
messages.add(new Message(Message.Kind.ERROR, message, SourceFilePosition.UNKNOWN));
}
else if (COMMAND_LINE_ERROR_OUTPUT.matcher(currentLine).matches()) {
state = State.COMMAND_FAILURE_OUTPUT;
}
else if (ENDING_PATTERNS[0].matcher(currentLine).matches()) {
state = State.ENDING;
pos = 1;
}
break;
case COMMAND_FAILURE_OUTPUT:
if (ENDING_PATTERNS[0].matcher(currentLine).matches()) {
state = State.ENDING;
pos = 1;
}
else {
currentLine = currentLine.trim();
if (!myAaptParser.parse(currentLine, reader, messages, logger)) {
// The AAPT parser punted on it. Just create a message with the unparsed error.
messages.add(new Message(Message.Kind.ERROR, currentLine, SourceFilePosition.UNKNOWN));
}
}
break;
case ENDING:
if (!ENDING_PATTERNS[pos].matcher(currentLine).matches()) {
return false;
}
else if (++pos >= ENDING_PATTERNS.length) {
if (errorMessage.length() > 0) {
String text = errorMessage.toString();
// Sometimes Gradle exits with an error message that doesn't have an associated
// file. This will show up first in the output, for errors without file associations.
// However, in some cases we can guess what the error is by looking at the other error
// messages, for example from the XML Validation parser, where the same error message is
// provided along with an error message. See for example the parser unit test for
// duplicate resources.
if (SourceFile.UNKNOWN.equals(file) && lastQuotedLine != null) {
String msg = unquoteGradleLine(lastQuotedLine);
Message rootCause = findRootCause(msg, messages);
if (rootCause == null) {
// For AAPT execution errors, the real cause is the last line (the AAPT output).
// Try searching there instead.
if (msg.endsWith("Failed to run command:")) {
String[] lines = text.split("\n");
if (lines.length > 2 && lines[lines.length - 2].contains("Output:")) {
String lastLine = lines[lines.length - 1];
if (!lastLine.isEmpty()) {
rootCause = findRootCause(lastLine.trim(), messages);
}
}
}
}
if (rootCause != null) {
if (!rootCause.getSourceFilePositions().isEmpty()) {
SourceFilePosition sourceFilePosition = rootCause.getSourceFilePositions().get(0);
file = sourceFilePosition.getFile();
position = sourceFilePosition.getPosition();
}
}
}
if (!SourceFile.UNKNOWN.equals(file)) {
messages.add(new Message(Message.Kind.ERROR, text, new SourceFilePosition(file, position)));
}
else if (text.contains("Build cancelled")) {
// Gradle throws an exception (BuildCancelledException) when we cancel task processing
// (org.gradle.tooling.CancellationTokenSource.cancel()). We don't want to report that as an error though.
messages.add(new Message(Message.Kind.INFO, text, SourceFilePosition.UNKNOWN));
}
else {
messages.add(new Message(Message.Kind.ERROR, text, SourceFilePosition.UNKNOWN));
}
}
return true;
}
break;
}
while (true) {
currentLine = reader.readLine();
if (currentLine == null) {
return false;
}
if (!currentLine.trim().isEmpty()) {
break;
}
}
}
}
/**
* Looks through the existing errors and attempts to find one that has the same root cause
*/
@Nullable
private static Message findRootCause(@NotNull String text, @NotNull Collection<Message> messages) {
for (Message message : messages) {
if (message.getKind() != Message.Kind.INFO && message.getText().contains(text)) {
if (message.getSourceFilePositions().isEmpty()) {
return message;
}
}
}
// We sometimes strip out the exception name prefix in the error messages;
// e.g. the gradle output may be "> java.io.IOException: My error message" whereas
// the XML validation error message was "My error message", so look for these
// scenarios too
int index = text.indexOf(':');
if (index != -1 && index < text.length() - 1) {
return findRootCause(text.substring(index + 1).trim(), messages);
}
return null;
}
private static boolean isGradleQuotedLine(@NotNull String line) {
for (int i = 0, n = line.length() - 1; i < n; i++) {
char c = line.charAt(i);
if (c == '>') {
return line.charAt(i + 1) == ' ';
}
else if (c != ' ') {
break;
}
}
return false;
}
private static String unquoteGradleLine(@NotNull String line) {
assert isGradleQuotedLine(line);
return line.substring(line.indexOf('>') + 2);
}
}