/* * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.flex.compiler.clients.problems; import java.io.FileNotFoundException; import java.io.IOException; import java.io.LineNumberReader; import java.io.Reader; import org.apache.commons.io.input.NullReader; import org.apache.flex.compiler.filespecs.IFileSpecification; import org.apache.flex.compiler.internal.workspaces.Workspace; import org.apache.flex.compiler.problems.CompilerProblemClassification; import org.apache.flex.compiler.problems.CompilerProblemSeverity; import org.apache.flex.compiler.problems.ICompilerProblem; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; /** * Problem formatter class that reports more detailed readable description of a * problem. In addition to the problem message, this class reports the text * where the problem occurred if any. This class requires a Workspace to access * problem's file and the necessary line information. * * All the command line tools such as MXMLC, ASC should use this class for * problem reporting as we would like to give as much detail as possible to * the users in command line. */ public final class WorkspaceProblemFormatter extends ProblemFormatter { private static final String NEW_LINE = System.getProperty("line.separator"); private static final int MAX_CACHED_LINES_PER_FILE = 10; // The ProblemLocalizer appends these id and strings after the // problem descriptions in a generated properties file. private static String ERROR_FORMAT_ID = "ErrorFormat"; private static String WARNING_FORMAT_ID = "WarningFormat"; private static String SYNTAXERROR_FORMAT_ID = "SyntaxErrorFormat"; private static String INTERNALERROR_FORMAT_ID = "InternalErrorFormat"; private static String LOCATION_FORMAT_ID = "LocationFormat"; private static String LOCATION_FORMAT_STRING = "%s(%d): col: %d"; private final Workspace workspace; private final LoadingCache<String, FileLineInfo> readers; private final CompilerProblemCategorizer problemCategorizer; /** * Constructor. * * @param workspace workspace that contains files associated with specified * problems. */ public WorkspaceProblemFormatter(final Workspace workspace) { this(workspace, null); } /** * Constructor. * * @param workspace workspace that contains files associated with specified * problems. * @param problemCategorizer a class the categorizes problems into one of the * values in {@link CompilerProblemSeverity}. Adds "Error:" and "Warning:" into * the format of a problem. If null, the problem categories will not be added * to the output. */ public WorkspaceProblemFormatter(final Workspace workspace, CompilerProblemCategorizer problemCategorizer) { super(); this.workspace = workspace; this.problemCategorizer = problemCategorizer; readers = CacheBuilder.newBuilder() .concurrencyLevel(1) .softValues() .build( new CacheLoader<String, FileLineInfo>() { @Override public FileLineInfo load(String fileName) { return new FileLineInfo(fileName); } }); } @Override public String format(ICompilerProblem problem) { StringBuffer buffer = new StringBuffer(); final String locationString = getLocationString(problem.getSourcePath(), problem.getLine(), problem.getColumn()); if (!locationString.isEmpty()) { buffer.append(locationString); buffer.append(" "); } String description = super.format(problem); if (problemCategorizer != null) { // Prepend the classification on the front (ex: "Error: ") description = String.format(getProblemFormat(problem), description); } assert description != null; buffer.append(description); buffer.append(NEW_LINE); buffer.append(NEW_LINE); final String lineText = getLineText(problem); if (lineText != null) { buffer.append(lineText); buffer.append(NEW_LINE); buffer.append(getLinePointer(lineText, problem.getColumn())); buffer.append(NEW_LINE); } return buffer.toString(); } /** * * @param problem the problem whose formatter we want * @return an "sprints format string", like "Error: %s" */ private String getProblemFormat(ICompilerProblem problem) { CompilerProblemSeverity severity = problemCategorizer.getProblemSeverity(problem); CompilerProblemClassification classification = problemCategorizer.getProblemClassification(problem); String formatString=null; if (classification == CompilerProblemClassification.DEFAULT) { switch(severity) { case ERROR: formatString = getErrorFormat(); break; case WARNING: formatString = getWarningFormat(); break; default: assert false; formatString = getErrorFormat(); } } else { switch (classification) { case SYNTAX_ERROR: formatString = getSyntaxErrorFormat(); break; case INTERNAL_ERROR: formatString = getInternalErrorFormat(); break; default: assert false; } } assert formatString != null; return formatString; } /** * Gets the text of the line specified problem occured or * <code>null</code> if there is no line information. * * @param problem problem to process * @return the text of the line or <code>null</code> if there * is no line information */ protected String getLineText(ICompilerProblem problem) { String filePath = problem.getSourcePath(); if (filePath == null) return null; int lineNumber = problem.getLine(); if (lineNumber < 0) return null; FileLineInfo fileLineInfo = readers.getUnchecked(filePath); return fileLineInfo.getLineText(lineNumber); } /* * Helper method to display a caret marking a single character * within the line of source text. */ private String getLinePointer(final String lineText, int column) { if (lineText == null || column == -1) return ""; final StringBuilder b = new StringBuilder(column); final int len = lineText.length(); for (int i = 0; i < column; i++) { if (i < len && lineText.charAt(i) == '\t') b.append('\t'); else b.append(' '); } b.append('^'); return b.toString(); } /* * Helper method to display the file and line number. * <p> * Never returns null. */ private String getLocationString(String filePath, int line, int col) { if (filePath == null) return ""; String location = filePath; if (line != -1) location = String.format(getLocationFormat(), location, (line + 1), (col + 1)); assert location != null; return location; } private class FileLineInfo { FileLineInfo(String fileName) { this.fileName = fileName; this.reader = createReader(); cachedLines = CacheBuilder.newBuilder() .concurrencyLevel(1) .softValues() .maximumSize(MAX_CACHED_LINES_PER_FILE) .build(); } private LineNumberReader createReader() { IFileSpecification fileSpec = workspace.getFileSpecification(fileName); Reader reader; try { reader = fileSpec.createReader(); } catch (FileNotFoundException e) { reader = new NullReader(0); } return new LineNumberReader(reader); } final String fileName; LineNumberReader reader; final Cache<Integer, String> cachedLines; String getLineText(int lineNumber) { String result = cachedLines.getIfPresent(lineNumber); if (result != null) return result; if (reader.getLineNumber() > lineNumber) reader = createReader(); assert reader.getLineNumber() <= lineNumber; try { while (reader.getLineNumber() < lineNumber) { final String lineText = reader.readLine(); if (lineText == null) return null; } result = reader.readLine(); if (result == null) return null; cachedLines.put(lineNumber, result); return result; } catch (IOException e) { return null; } } } /** * @return the string used to format messages categorized as errors. */ private String getErrorFormat() { return getMessage(ERROR_FORMAT_ID); } /** * @return the string used to format messages categorized as warnings. */ private String getWarningFormat() { return getMessage(WARNING_FORMAT_ID); } /** * @return the string used to format messages categorized as syntax errors. */ private String getSyntaxErrorFormat() { return getMessage(SYNTAXERROR_FORMAT_ID); } /** * @return the string used to format messages categorized as internal errors. */ private String getInternalErrorFormat() { return getMessage(INTERNALERROR_FORMAT_ID); } /** * Format of the line number the problem was found. * Example) foo:7 * The problem was found on line 7 of foo. * * @return the string used to format line number of the problem in a file. */ private String getLocationFormat() { String format = getMessage(LOCATION_FORMAT_ID); if (format != null) return format; return LOCATION_FORMAT_STRING; } }