| /* |
| * 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; |
| } |
| } |