blob: 0890bf537560b834e29590ee1059052009e5ef62 [file] [log] [blame]
/*
* Copyright 2016 The Bazel Authors. All rights reserved.
*
* 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.google.idea.blaze.base.issueparser;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.idea.blaze.base.model.primitives.TargetExpression;
import com.google.idea.blaze.base.model.primitives.WorkspacePath;
import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
import com.google.idea.blaze.base.projectview.ProjectViewManager;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.projectview.section.ListSection;
import com.google.idea.blaze.base.projectview.section.Section;
import com.google.idea.blaze.base.projectview.section.SectionKey;
import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
import com.google.idea.blaze.base.scope.output.IssueOutput;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkState;
/**
* Parses blaze output for compile errors.
*/
public class BlazeIssueParser {
private static class ParseResult {
public static final ParseResult NEEDS_MORE_INPUT = new ParseResult(true, null);
public static final ParseResult NO_RESULT = new ParseResult(false, null);
private boolean needsMoreInput;
@Nullable private IssueOutput output;
private ParseResult(boolean needsMoreInput, IssueOutput output) {
this.needsMoreInput = needsMoreInput;
this.output = output;
}
public static ParseResult needsMoreInput() {
return NEEDS_MORE_INPUT;
}
public static ParseResult output(IssueOutput output) {
return new ParseResult(false, output);
}
public static ParseResult noResult() {
return NO_RESULT;
}
}
interface Parser {
@NotNull
ParseResult parse(@NotNull String currentLine, @NotNull List<String> previousLines);
}
static abstract class SingleLineParser implements Parser {
@NotNull
Pattern pattern;
SingleLineParser(@NotNull String regex) {
pattern = Pattern.compile(regex);
}
@Override
public ParseResult parse(@NotNull String currentLine, @NotNull List<String> multilineMatchResult) {
checkState(multilineMatchResult.isEmpty(), "SingleLineParser recieved multiple lines of input");
return parse(currentLine);
}
ParseResult parse(@NotNull String line) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return ParseResult.output(createIssue(matcher));
}
return ParseResult.noResult();
}
@Nullable
protected abstract IssueOutput createIssue(@NotNull Matcher matcher);
}
static class CompileParser extends SingleLineParser {
@NotNull
private final WorkspaceRoot workspaceRoot;
public CompileParser(@NotNull WorkspaceRoot workspaceRoot) {
super("(.*?):([0-9]+):([0-9]+:)? (error|warning): (.*)");
this.workspaceRoot = workspaceRoot;
}
@Override
protected IssueOutput createIssue(@NotNull Matcher matcher) {
final File file;
try {
String fileName = matcher.group(1);
final WorkspacePath workspacePath;
if (fileName.startsWith("//depot/google3/")) {
workspacePath = new WorkspacePath(fileName.substring("//depot/google3/".length()));
} else if (fileName.startsWith("/")) {
workspacePath = workspaceRoot.workspacePathFor(new File(fileName));
} else {
workspacePath = new WorkspacePath(fileName);
}
file = workspaceRoot.fileForPath(workspacePath);
}
catch (IllegalArgumentException e) {
// Ignore -- malformed error message
return null;
}
IssueOutput.Category type = matcher.group(4).equals("error")
? IssueOutput.Category.ERROR
: IssueOutput.Category.WARNING;
return IssueOutput.issue(type, matcher.group(5))
.inFile(file)
.onLine(Integer.parseInt(matcher.group(2)))
.build();
}
}
static class TracebackParser implements Parser {
private static final Pattern PATTERN = Pattern.compile("(ERROR): (.*?):([0-9]+):([0-9]+): (Traceback \\(most recent call last\\):)");
@NotNull
@Override
public ParseResult parse(@NotNull String currentLine, @NotNull List<String> previousLines) {
if (previousLines.isEmpty()) {
if (PATTERN.matcher(currentLine).find()) {
return ParseResult.needsMoreInput();
}
else {
return ParseResult.noResult();
}
}
if (currentLine.startsWith("\t")) {
return ParseResult.needsMoreInput();
}
else {
Matcher matcher = PATTERN.matcher(previousLines.get(0));
checkState(matcher.find(), "Found a match in the first line previously, but now it isn't there.");
StringBuilder message = new StringBuilder(matcher.group(5));
for (int i = 1; i < previousLines.size(); ++i) {
message.append(System.lineSeparator())
.append(previousLines.get(i));
}
message.append(System.lineSeparator())
.append(currentLine);
return ParseResult.output(IssueOutput.error(message.toString())
.inFile(new File(matcher.group(2)))
.onLine(Integer.parseInt(matcher.group(3)))
.build());
}
}
}
static class BuildParser extends SingleLineParser {
BuildParser() {
super("(ERROR): (.*?):([0-9]+):([0-9]+): (.*)");
}
@Override
protected IssueOutput createIssue(@NotNull Matcher matcher) {
return IssueOutput.error(matcher.group(5))
.inFile(new File(matcher.group(2)))
.onLine(Integer.parseInt(matcher.group(3)))
.build();
}
}
static class LinelessBuildParser extends SingleLineParser {
LinelessBuildParser() {
super("(ERROR): (.*?):char offsets [0-9]+--[0-9]+: (.*)");
}
@Override
protected IssueOutput createIssue(@NotNull Matcher matcher) {
return IssueOutput.error(matcher.group(3))
.inFile(new File(matcher.group(2)))
.build();
}
}
static class ProjectViewLabelParser extends SingleLineParser {
@Nullable private final ProjectViewSet projectViewSet;
ProjectViewLabelParser(
@Nullable ProjectViewSet projectViewSet) {
super("no such target '(.*)': target .*? not declared in package .*? defined by");
this.projectViewSet = projectViewSet;
}
@Override
protected IssueOutput createIssue(@NotNull Matcher matcher) {
File file = null;
if (projectViewSet != null) {
String targetString = matcher.group(1);
final TargetExpression targetExpression = TargetExpression.fromString(targetString);
file = projectViewFileWithSection(projectViewSet, TargetSection.KEY, new Predicate<ListSection<TargetExpression>>() {
@Override
public boolean apply(@NotNull ListSection<TargetExpression> targetSection) {
return targetSection.items().contains(targetExpression);
}
});
}
return IssueOutput.error(matcher.group(0))
.inFile(file)
.build();
}
}
static class InvalidTargetProjectViewPackageParser extends SingleLineParser {
@Nullable private final ProjectViewSet projectViewSet;
InvalidTargetProjectViewPackageParser(
@Nullable ProjectViewSet projectViewSet,
String regex) {
super(regex);
this.projectViewSet = projectViewSet;
}
@Override
protected IssueOutput createIssue(@NotNull Matcher matcher) {
File file = null;
if (projectViewSet != null) {
final String packageString = matcher.group(1);
file = projectViewFileWithSection(projectViewSet, TargetSection.KEY, targetSection -> {
for (TargetExpression targetExpression : targetSection.items()) {
if (targetExpression.toString().startsWith("//" + packageString + ":")) {
return true;
}
}
return false;
});
}
return IssueOutput.error(matcher.group(0))
.inFile(file)
.build();
}
}
@Nullable
private static <T, SectionType extends Section<T>> File projectViewFileWithSection(
@NotNull ProjectViewSet projectViewSet,
@NotNull SectionKey<T, SectionType> key,
@NotNull Predicate<SectionType> predicate) {
for (ProjectViewSet.ProjectViewFile projectViewFile : projectViewSet.getProjectViewFiles()) {
SectionType section = projectViewFile.projectView.getSectionOfType(key);
if (section != null && predicate.apply(section)) {
return projectViewFile.projectViewFile;
}
}
return null;
}
@NotNull private List<Parser> parsers = Lists.newArrayList();
/** The parser that requested more lines of input during the last call to {@link #parseIssue(String)}. */
@Nullable private Parser multilineMatchingParser;
@NotNull private List<String> multilineMatchResult = new ArrayList<>();
public BlazeIssueParser(
@Nullable Project project,
@NotNull WorkspaceRoot workspaceRoot) {
ProjectViewSet projectViewSet = project != null ? ProjectViewManager.getInstance(project).getProjectViewSet() : null;
parsers.add(new CompileParser(workspaceRoot));
parsers.add(new TracebackParser());
parsers.add(new BuildParser());
parsers.add(new LinelessBuildParser());
parsers.add(new ProjectViewLabelParser(projectViewSet));
parsers.add(new InvalidTargetProjectViewPackageParser(projectViewSet, "no such package '(.*)': BUILD file not found on package path"));
parsers.add(new InvalidTargetProjectViewPackageParser(projectViewSet, "no targets found beneath '(.*)'"));
parsers.add(new InvalidTargetProjectViewPackageParser(projectViewSet, "ERROR: invalid target format '(.*)'"));
}
@Nullable
public IssueOutput parseIssue(String line) {
List<Parser> parsers = this.parsers;
if (multilineMatchingParser != null) {
parsers = Lists.newArrayList(multilineMatchingParser);
}
for (Parser parser : parsers) {
ParseResult issue = parser.parse(line, multilineMatchResult);
if (issue.needsMoreInput) {
multilineMatchingParser = parser;
multilineMatchResult.add(line);
return null;
}
else {
multilineMatchingParser = null;
multilineMatchResult = new ArrayList<>();
}
if (issue.output != null) {
return issue.output;
}
}
return null;
}
}