blob: 55755ecea6c646d684b819d7fa03086af4187038 [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 static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.idea.blaze.base.model.primitives.Label;
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.ProjectViewSet;
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 java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/** Parses blaze output for compile errors. */
public class BlazeIssueParser {
/** Result from parsing the current line */
public 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;
}
}
/** Used by BlazeIssueParser. Generally implemented by subclassing SingleLineParser */
public interface Parser {
ParseResult parse(String currentLine, List<String> previousLines);
}
/** Base for a Parser that consumes a single contextless line at a time, matched via regex */
public abstract static class SingleLineParser implements Parser {
Pattern pattern;
public SingleLineParser(String regex) {
pattern = Pattern.compile(regex);
}
@Override
public ParseResult parse(String currentLine, List<String> multilineMatchResult) {
checkState(
multilineMatchResult.isEmpty(), "SingleLineParser recieved multiple lines of input");
return parse(currentLine);
}
ParseResult parse(String line) {
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
return ParseResult.output(createIssue(matcher));
}
return ParseResult.noResult();
}
@Nullable
protected abstract IssueOutput createIssue(Matcher matcher);
}
@Nullable
public static File fileFromAbsolutePath(String absolutePath) {
return new File(absolutePath);
}
@Nullable
public static File fileFromRelativePath(WorkspaceRoot workspaceRoot, String relativePath) {
try {
final WorkspacePath workspacePath = new WorkspacePath(relativePath);
return workspaceRoot.fileForPath(workspacePath);
} catch (IllegalArgumentException e) {
// Ignore -- malformed error message
return null;
}
}
/** Returns the file referenced by the target */
@Nullable
private static File fileFromTarget(WorkspaceRoot workspaceRoot, String targetString) {
Label label = Label.createIfValid(targetString);
if (label == null || label.isExternal()) {
return null;
}
try {
final WorkspacePath combined =
new WorkspacePath(label.blazePackage(), label.targetName().toString());
return workspaceRoot.fileForPath(combined);
} catch (IllegalArgumentException e) {
return null;
}
}
/** Falls back to returning -1 if no integer can be parsed. */
public static int parseOptionalInt(@Nullable String intString) {
if (intString == null) {
return -1;
}
try {
return Integer.parseInt(intString);
} catch (NumberFormatException e) {
return -1;
}
}
static class CompileParser extends SingleLineParser {
private final WorkspaceRoot workspaceRoot;
CompileParser(WorkspaceRoot workspaceRoot) {
super("^([^/].*?):([0-9]+):(?:([0-9]+):)? (error|warning): (.*)$");
this.workspaceRoot = workspaceRoot;
}
@Override
protected IssueOutput createIssue(Matcher matcher) {
final File file = fileFromRelativePath(workspaceRoot, matcher.group(1));
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)))
.inColumn(parseOptionalInt(matcher.group(3)))
.build();
}
}
static class TracebackParser implements Parser {
private static final Pattern PATTERN =
Pattern.compile(
"(ERROR): (.*?):([0-9]+):([0-9]+): (Traceback \\(most recent call last\\):)");
@Override
public ParseResult parse(String currentLine, 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)))
.inColumn(parseOptionalInt(matcher.group(4)))
.build());
}
}
}
static class BuildParser extends SingleLineParser {
BuildParser() {
super("^ERROR: (/.*?BUILD):([0-9]+):([0-9]+): (.*)$");
}
@Override
protected IssueOutput createIssue(Matcher matcher) {
File file = fileFromAbsolutePath(matcher.group(1));
return IssueOutput.error(matcher.group(4))
.inFile(file)
.onLine(Integer.parseInt(matcher.group(2)))
.inColumn(parseOptionalInt(matcher.group(3)))
.build();
}
}
static class SkylarkErrorParser extends SingleLineParser {
SkylarkErrorParser() {
super("^ERROR: (/.*?\\.bzl):([0-9]+):([0-9]+): (.*)$");
}
@Override
protected IssueOutput createIssue(Matcher matcher) {
File file = fileFromAbsolutePath(matcher.group(1));
return IssueOutput.error(matcher.group(4))
.inFile(file)
.onLine(Integer.parseInt(matcher.group(2)))
.inColumn(parseOptionalInt(matcher.group(3)))
.build();
}
}
static class LinelessBuildParser extends SingleLineParser {
LinelessBuildParser() {
super("^ERROR: (.*?):char offsets [0-9]+--[0-9]+: (.*)$");
}
@Override
protected IssueOutput createIssue(Matcher matcher) {
return IssueOutput.error(matcher.group(2)).inFile(new File(matcher.group(1))).build();
}
}
static class FileNotFoundBuildParser extends SingleLineParser {
private final WorkspaceRoot workspaceRoot;
FileNotFoundBuildParser(WorkspaceRoot workspaceRoot) {
super("^ERROR: .*? Unable to load file '(.*?)': (.*)$");
this.workspaceRoot = workspaceRoot;
}
@Override
protected IssueOutput createIssue(Matcher matcher) {
File file = fileFromTarget(workspaceRoot, matcher.group(1));
return IssueOutput.error(matcher.group(2)).inFile(file).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(Matcher matcher) {
File file = null;
if (projectViewSet != null) {
String targetString = matcher.group(1);
final TargetExpression targetExpression = TargetExpression.fromString(targetString);
file =
projectViewFileWithSection(
projectViewSet,
TargetSection.KEY,
targetSection -> 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(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(
ProjectViewSet projectViewSet,
SectionKey<T, SectionType> key,
Predicate<SectionType> predicate) {
for (ProjectViewSet.ProjectViewFile projectViewFile : projectViewSet.getProjectViewFiles()) {
ImmutableList<SectionType> sections = projectViewFile.projectView.getSectionsOfType(key);
for (SectionType section : sections) {
if (predicate.test(section)) {
return projectViewFile.projectViewFile;
}
}
}
return null;
}
private ImmutableList<Parser> parsers;
/**
* The parser that requested more lines of input during the last call to {@link
* #parseIssue(String)}.
*/
@Nullable private Parser multilineMatchingParser;
private List<String> multilineMatchResult = new ArrayList<>();
public BlazeIssueParser(ImmutableList<Parser> parsers) {
this.parsers = parsers;
}
@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;
}
}