/* * Copyright (C) 2011 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.lint; import static com.android.tools.lint.detector.api.TextFormat.RAW; import static com.android.tools.lint.detector.api.TextFormat.TEXT; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.lint.client.api.IssueRegistry; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Position; import com.android.tools.lint.detector.api.Severity; import com.android.tools.lint.detector.api.TextFormat; import com.android.utils.SdkUtils; import com.google.common.annotations.Beta; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import java.io.File; import java.io.IOException; import java.io.Writer; import java.util.List; /** * A reporter which emits lint warnings as plain text strings * <p> * <b>NOTE: This is not a public or final API; if you rely on this be prepared * to adjust your code for the next tools release.</b> */ @Beta public class TextReporter extends Reporter { private final Writer mWriter; private final boolean mClose; private final LintCliFlags mFlags; /** * Constructs a new {@link TextReporter} * * @param client the client * @param flags the flags * @param writer the writer to write into * @param close whether the writer should be closed when done */ public TextReporter(@NonNull LintCliClient client, @NonNull LintCliFlags flags, @NonNull Writer writer, boolean close) { this(client, flags, null, writer, close); } /** * Constructs a new {@link TextReporter} * * @param client the client * @param flags the flags * @param file the file corresponding to the writer, if any * @param writer the writer to write into * @param close whether the writer should be closed when done */ public TextReporter(@NonNull LintCliClient client, @NonNull LintCliFlags flags, @Nullable File file, @NonNull Writer writer, boolean close) { super(client, file); mFlags = flags; mWriter = writer; mClose = close; } @Override public void write(int errorCount, int warningCount, List<Warning> issues) throws IOException { boolean abbreviate = !mFlags.isShowEverything(); StringBuilder output = new StringBuilder(issues.size() * 200); if (issues.isEmpty()) { if (mDisplayEmpty) { mWriter.write("No issues found."); mWriter.write('\n'); mWriter.flush(); } } else { Issue lastIssue = null; for (Warning warning : issues) { if (warning.issue != lastIssue) { explainIssue(output, lastIssue); lastIssue = warning.issue; } int startLength = output.length(); if (warning.path != null) { output.append(warning.path); output.append(':'); if (warning.line >= 0) { output.append(Integer.toString(warning.line + 1)); output.append(':'); } if (startLength < output.length()) { output.append(' '); } } Severity severity = warning.severity; if (severity == Severity.FATAL) { // Treat the fatal error as an error such that we don't display // both "Fatal:" and "Error:" etc in the error output. severity = Severity.ERROR; } output.append(severity.getDescription()); output.append(':'); output.append(' '); output.append(RAW.convertTo(warning.message, TEXT)); if (warning.issue != null) { output.append(' ').append('['); output.append(warning.issue.getId()); output.append(']'); } output.append('\n'); if (warning.errorLine != null && !warning.errorLine.isEmpty()) { output.append(warning.errorLine); } if (warning.location != null && warning.location.getSecondary() != null) { Location location = warning.location.getSecondary(); boolean omitted = false; while (location != null) { if (location.getMessage() != null && !location.getMessage().isEmpty()) { output.append(" "); //$NON-NLS-1$ String path = mClient.getDisplayPath(warning.project, location.getFile()); output.append(path); Position start = location.getStart(); if (start != null) { int line = start.getLine(); if (line >= 0) { output.append(':'); output.append(Integer.toString(line + 1)); } } if (location.getMessage() != null && !location.getMessage().isEmpty()) { output.append(':'); output.append(' '); output.append(RAW.convertTo(location.getMessage(), TEXT)); } output.append('\n'); } else { omitted = true; } location = location.getSecondary(); } if (!abbreviate && omitted) { location = warning.location.getSecondary(); StringBuilder sb = new StringBuilder(100); sb.append("Also affects: "); int begin = sb.length(); while (location != null) { if (location.getMessage() == null || location.getMessage().isEmpty()) { if (sb.length() > begin) { sb.append(", "); } String path = mClient.getDisplayPath(warning.project, location.getFile()); sb.append(path); Position start = location.getStart(); if (start != null) { int line = start.getLine(); if (line >= 0) { sb.append(':'); sb.append(Integer.toString(line + 1)); } } } location = location.getSecondary(); } String wrapped = Main.wrap(sb.toString(), Main.MAX_LINE_WIDTH, " "); //$NON-NLS-1$ output.append(wrapped); } } if (warning.isVariantSpecific()) { List<String> names; if (warning.includesMoreThanExcludes()) { output.append("Applies to variants: "); names = warning.getIncludedVariantNames(); } else { output.append("Does not apply to variants: "); names = warning.getExcludedVariantNames(); } output.append(Joiner.on(", ").join(names)); output.append('\n'); } } explainIssue(output, lastIssue); mWriter.write(output.toString()); mWriter.write(String.format("%1$d errors, %2$d warnings", errorCount, warningCount)); mWriter.write('\n'); mWriter.flush(); if (mClose) { mWriter.close(); if (!mClient.getFlags().isQuiet() && mOutput != null) { String path = mOutput.getAbsolutePath(); System.out.println(String.format("Wrote text report to %1$s", path)); } } } } private void explainIssue(@NonNull StringBuilder output, @Nullable Issue issue) throws IOException { if (issue == null || !mFlags.isExplainIssues() || issue == IssueRegistry.LINT_ERROR) { return; } String explanation = issue.getExplanation(TextFormat.TEXT); if (explanation.trim().isEmpty()) { return; } String indent = " "; String formatted = SdkUtils.wrap(explanation, Main.MAX_LINE_WIDTH - indent.length(), null); output.append('\n'); output.append(indent); output.append("Explanation for issues of type \"").append(issue.getId()).append("\":\n"); for (String line : Splitter.on('\n').split(formatted)) { if (!line.isEmpty()) { output.append(indent); output.append(line); } output.append('\n'); } List<String> moreInfo = issue.getMoreInfo(); if (!moreInfo.isEmpty()) { for (String url : moreInfo) { if (formatted.contains(url)) { continue; } output.append(indent); output.append(url); output.append('\n'); } output.append('\n'); } } boolean isWriteToConsole() { return mOutput == null; } }