Better error reporting from the compiler to the IDE

This commit is contained in:
Andrey Breslav
2012-02-29 15:11:34 +04:00
parent 461c8d773d
commit 200b6fbd91
10 changed files with 395 additions and 180 deletions
@@ -19,12 +19,8 @@ package org.jetbrains.jet.cli;
import com.sampullara.cli.Args;
import com.sampullara.cli.Argument;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jet.compiler.CompileEnvironment;
import org.jetbrains.jet.compiler.CompileEnvironmentException;
import org.jetbrains.jet.compiler.CompilerPlugin;
import org.jetbrains.jet.compiler.FileNameTransformer;
import org.jetbrains.jet.compiler.*;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
/**
@@ -72,6 +68,9 @@ public class KotlinCompiler {
@Argument(value = "stubs", description = "Compile stubs: ignore function bodies")
public boolean stubs;
@Argument(value = "tags", description = "Demarcate each compilation message (error, warning, etc) with an open and close tag")
public boolean tags;
@Argument(value = "transformNamesToJava", description = "Transform Kotlin file names to *.java. This option is needed for compiling kotlinized Java library headers")
public boolean transformNamesToJava;
}
@@ -108,7 +107,8 @@ public class KotlinCompiler {
return 1;
}
catch (Throwable t) {
t.printStackTrace();
// Always use tags
errStream.println(MessageRenderer.TAGS.renderException(t));
return 1;
}
@@ -117,7 +117,11 @@ public class KotlinCompiler {
return 0;
}
CompileEnvironment environment = new CompileEnvironment(arguments.transformNamesToJava ? ANY_EXTENSION_TO_JAVA : FileNameTransformer.IDENTITY);
FileNameTransformer fileNameTransformer = arguments.transformNamesToJava
? ANY_EXTENSION_TO_JAVA
: FileNameTransformer.IDENTITY;
MessageRenderer messageRenderer = arguments.tags ? MessageRenderer.TAGS : MessageRenderer.PLAIN;
CompileEnvironment environment = new CompileEnvironment(fileNameTransformer, messageRenderer);
try {
environment.setIgnoreErrors(false);
environment.setErrorStream(errStream);
@@ -138,16 +142,18 @@ public class KotlinCompiler {
if (arguments.module != null) {
environment.compileModuleScript(arguments.module, arguments.jar, arguments.outputDir, arguments.includeRuntime);
return 0;
}
else {
if (!environment.compileBunchOfSources(arguments.src, arguments.jar, arguments.outputDir, arguments.includeRuntime)) {
return 1;
}
environment.compileBunchOfSources(arguments.src, arguments.jar, arguments.outputDir, arguments.includeRuntime);
}
return 0;
} finally {
}
catch (Throwable t) {
errStream.println(messageRenderer.renderException(t));
return 1;
}
finally {
environment.dispose();
}
}
@@ -55,7 +55,9 @@ import java.util.jar.*;
public class CompileEnvironment {
private JetCoreEnvironment myEnvironment;
private final Disposable myRootDisposable;
private PrintStream myErrorStream = System.out;
private final MessageRenderer myMessageRenderer;
private PrintStream myErrorStream = System.err;
private final FileNameTransformer myFileNameTransformer;
private URL myStdlib;
@@ -63,10 +65,10 @@ public class CompileEnvironment {
private boolean stubs = false;
public CompileEnvironment() {
this(FileNameTransformer.IDENTITY);
this(FileNameTransformer.IDENTITY, MessageRenderer.PLAIN);
}
public CompileEnvironment(FileNameTransformer fileNameTransformer) {
public CompileEnvironment(FileNameTransformer fileNameTransformer, MessageRenderer messageRenderer) {
myRootDisposable = new Disposable() {
@Override
public void dispose() {
@@ -74,6 +76,7 @@ public class CompileEnvironment {
};
myEnvironment = new JetCoreEnvironment(myRootDisposable);
myFileNameTransformer = fileNameTransformer;
myMessageRenderer = messageRenderer;
}
public void setErrorStream(PrintStream errorStream) {
@@ -145,7 +148,7 @@ public class CompileEnvironment {
File rtJar = findRtJar(javaHome);
if (rtJar == null || !rtJar.exists()) {
throw new CompileEnvironmentException("No JDK rt.jar found under" + javaHome);
throw new CompileEnvironmentException("No JDK rt.jar found under " + javaHome);
}
return rtJar;
@@ -164,8 +167,8 @@ public class CompileEnvironment {
return null;
}
public void compileModuleScript(String moduleFile, @Nullable String jarPath, @Nullable String outputDir, boolean jarRuntime) {
final List<Module> modules = loadModuleScript(moduleFile);
public boolean compileModuleScript(String moduleFile, @Nullable String jarPath, @Nullable String outputDir, boolean jarRuntime) {
List<Module> modules = loadModuleScript(moduleFile);
if (modules == null) {
throw new CompileEnvironmentException("Module script " + moduleFile + " compilation failed");
@@ -178,20 +181,22 @@ public class CompileEnvironment {
final String directory = new File(moduleFile).getParent();
for (Module moduleBuilder : modules) {
ClassFileFactory moduleFactory = compileModule(moduleBuilder, directory);
if (moduleFactory != null) {
if (outputDir != null) {
writeToOutputDirectory(moduleFactory, outputDir);
}
else {
String path = jarPath != null ? jarPath : new File(directory, moduleBuilder.getModuleName() + ".jar").getPath();
try {
writeToJar(moduleFactory, new FileOutputStream(path), null, jarRuntime);
} catch (FileNotFoundException e) {
throw new CompileEnvironmentException("Invalid jar path " + path, e);
}
if (moduleFactory == null) {
return false;
}
if (outputDir != null) {
writeToOutputDirectory(moduleFactory, outputDir);
}
else {
String path = jarPath != null ? jarPath : new File(directory, moduleBuilder.getModuleName() + ".jar").getPath();
try {
writeToJar(moduleFactory, new FileOutputStream(path), null, jarRuntime);
} catch (FileNotFoundException e) {
throw new CompileEnvironmentException("Invalid jar path " + path, e);
}
}
}
return true;
}
public List<Module> loadModuleScript(String moduleFile) {
@@ -199,7 +204,7 @@ public class CompileEnvironment {
scriptCompileSession.addSources(moduleFile);
ensureRuntime();
if (!scriptCompileSession.analyze(myErrorStream)) {
if (!scriptCompileSession.analyze(myErrorStream, myMessageRenderer)) {
return null;
}
final ClassFileFactory factory = scriptCompileSession.generate();
@@ -258,7 +263,7 @@ public class CompileEnvironment {
ensureRuntime();
if (!moduleCompileSession.analyze(myErrorStream) && !ignoreErrors) {
if (!moduleCompileSession.analyze(myErrorStream, myMessageRenderer) && !ignoreErrors) {
return null;
}
return moduleCompileSession.generate();
@@ -345,7 +350,7 @@ public class CompileEnvironment {
CompileSession session = new CompileSession(myEnvironment, myFileNameTransformer);
session.addSources(new LightVirtualFile("script" + LocalTimeCounter.currentTime() + ".kt", JetLanguage.INSTANCE, code));
if (!session.analyze(myErrorStream) && !ignoreErrors) {
if (!session.analyze(myErrorStream, myMessageRenderer) && !ignoreErrors) {
return null;
}
@@ -370,7 +375,7 @@ public class CompileEnvironment {
ensureRuntime();
if (!session.analyze(myErrorStream) && !ignoreErrors) {
if (!session.analyze(myErrorStream, myMessageRenderer) && !ignoreErrors) {
return false;
}
@@ -34,6 +34,7 @@ import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor;
import org.jetbrains.jet.lang.descriptors.ModuleDescriptor;
import org.jetbrains.jet.lang.diagnostics.Diagnostic;
import org.jetbrains.jet.lang.diagnostics.DiagnosticFactory;
import org.jetbrains.jet.lang.diagnostics.DiagnosticUtils;
import org.jetbrains.jet.lang.diagnostics.Severity;
import org.jetbrains.jet.lang.psi.JetFile;
import org.jetbrains.jet.lang.resolve.BindingContext;
@@ -41,8 +42,7 @@ import org.jetbrains.jet.lang.resolve.java.AnalyzerFacade;
import org.jetbrains.jet.lang.resolve.java.JavaDescriptorResolver;
import org.jetbrains.jet.plugin.JetFileType;
import java.io.File;
import java.io.PrintStream;
import java.io.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -84,11 +84,11 @@ public class CompileSession {
VirtualFile vFile = myEnvironment.getLocalFileSystem().findFileByPath(path);
if (vFile == null) {
myErrors.add("ERROR: File/directory not found: " + path);
myErrors.add("File/directory not found: " + path);
return;
}
if (!vFile.isDirectory() && vFile.getFileType() != JetFileType.INSTANCE) {
myErrors.add("ERROR: Not a Kotlin file: " + path);
myErrors.add("Not a Kotlin file: " + path);
return;
}
@@ -135,40 +135,26 @@ public class CompileSession {
return mySourceFiles;
}
public boolean analyze(final PrintStream out) {
if (!myErrors.isEmpty()) {
for (String error : myErrors) {
out.println(error);
}
return false;
public boolean analyze(@NotNull PrintStream out, @NotNull MessageRenderer renderer) {
MessageCollector collector = new MessageCollector(renderer);
for (String error : myErrors) {
collector.report(Severity.ERROR, error, null, -1, -1);
}
final ErrorCollector errorCollector = new ErrorCollector();
reportSyntaxErrors(collector);
analyzeAndReportSemanticErrors(collector);
reportSyntaxErrors(errorCollector);
analyzeAndReportSemanticErrors(errorCollector);
collector.printTo(out);
boolean hasIncompleteHierarchyErrors;
Collection<ClassDescriptor> incompletes = myBindingContext.getKeys(BindingContext.INCOMPLETE_HIERARCHY);
if (!incompletes.isEmpty()) {
out.println(Severity.ERROR + ":: The following classes have incomplete hierarchies:");
for (ClassDescriptor incomplete : incompletes) {
out.println(Severity.ERROR + ":: " + fqName(incomplete));
}
hasIncompleteHierarchyErrors = true;
} else {
hasIncompleteHierarchyErrors = false;
}
errorCollector.flushTo(out);
return !errorCollector.hasErrors() && !hasIncompleteHierarchyErrors;
return !collector.hasErrors();
}
/**
* @see JetTypeMapper#getFQName(DeclarationDescriptor)
* TODO possibly duplicates DescriptorUtils#getFQName(DeclarationDescriptor)
*/
private String fqName(ClassOrNamespaceDescriptor descriptor) {
private static String fqName(ClassOrNamespaceDescriptor descriptor) {
DeclarationDescriptor containingDeclaration = descriptor.getContainingDeclaration();
if (containingDeclaration == null || containingDeclaration instanceof ModuleDescriptor || containingDeclaration.getName().equals(JavaDescriptorResolver.JAVA_ROOT)) {
return descriptor.getName();
@@ -177,18 +163,31 @@ public class CompileSession {
}
}
private void analyzeAndReportSemanticErrors(ErrorCollector errorCollector) {
private void analyzeAndReportSemanticErrors(MessageCollector collector) {
Predicate<PsiFile> filesToAnalyzeCompletely =
stubs ? Predicates.<PsiFile>alwaysFalse() : Predicates.<PsiFile>alwaysTrue();
myBindingContext = AnalyzerFacade.analyzeFilesWithJavaIntegration(
myEnvironment.getProject(), mySourceFiles, filesToAnalyzeCompletely, JetControlFlowDataTraceFactory.EMPTY);
for (Diagnostic diagnostic : myBindingContext.getDiagnostics()) {
errorCollector.report(diagnostic);
reportDiagnostic(collector, diagnostic);
}
reportIncompleteHierarchies(collector);
}
private void reportIncompleteHierarchies(MessageCollector collector) {
Collection<ClassDescriptor> incompletes = myBindingContext.getKeys(BindingContext.INCOMPLETE_HIERARCHY);
if (!incompletes.isEmpty()) {
StringBuilder message = new StringBuilder("The following classes have incomplete hierarchies:\n");
for (ClassDescriptor incomplete : incompletes) {
message.append(" ").append(fqName(incomplete)).append("\n");
}
collector.report(Severity.ERROR, message.toString(), null, -1, -1);
}
}
private void reportSyntaxErrors(final ErrorCollector errorCollector) {
private void reportSyntaxErrors(final MessageCollector messageCollector) {
for (JetFile file : mySourceFiles) {
file.accept(new PsiRecursiveElementWalkingVisitor() {
@Override
@@ -196,12 +195,19 @@ public class CompileSession {
String description = element.getErrorDescription();
String message = StringUtil.isEmpty(description) ? "Syntax error" : description;
Diagnostic diagnostic = DiagnosticFactory.create(Severity.ERROR, message).on(element);
errorCollector.report(diagnostic);
reportDiagnostic(messageCollector, diagnostic);
}
});
}
}
private static void reportDiagnostic(MessageCollector collector, Diagnostic diagnostic) {
DiagnosticUtils.LineAndColumn lineAndColumn = DiagnosticUtils.getLineAndColumn(diagnostic);
VirtualFile virtualFile = diagnostic.getPsiFile().getVirtualFile();
String path = virtualFile == null ? null : virtualFile.getPath();
collector.report(diagnostic.getSeverity(), diagnostic.getMessage(), path, lineAndColumn.getLine(), lineAndColumn.getColumn());
}
@NotNull
public ClassFileFactory generate() {
Project project = myEnvironment.getProject();
@@ -18,9 +18,8 @@ package org.jetbrains.jet.compiler;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.intellij.psi.PsiFile;
import org.jetbrains.jet.lang.diagnostics.Diagnostic;
import org.jetbrains.jet.lang.diagnostics.DiagnosticUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.lang.diagnostics.Severity;
import java.io.PrintStream;
@@ -29,27 +28,28 @@ import java.util.Collection;
/**
* @author alex.tkachman
*/
class ErrorCollector {
private final Multimap<PsiFile, Diagnostic> maps = LinkedHashMultimap.create();
/*package*/ class MessageCollector {
// File path (nullable) -> error message
private final Multimap<String, String> groupedMessages = LinkedHashMultimap.create();
private final MessageRenderer renderer;
private boolean hasErrors;
public ErrorCollector() {
public MessageCollector(@NotNull MessageRenderer renderer) {
this.renderer = renderer;
}
public void report(Diagnostic diagnostic) {
hasErrors |= diagnostic.getSeverity() == Severity.ERROR;
maps.put(diagnostic.getPsiFile(), diagnostic);
public void report(@NotNull Severity severity, @NotNull String message, @Nullable String path, int line, int column) {
hasErrors |= severity == Severity.ERROR;
groupedMessages.put(path, renderer.render(severity, message, path, line, column));
}
public void flushTo(final PrintStream out) {
if(!maps.isEmpty()) {
for (PsiFile psiFile : maps.keySet()) {
String path = psiFile.getVirtualFile().getPath();
Collection<Diagnostic> diagnostics = maps.get(psiFile);
for (Diagnostic diagnostic : diagnostics) {
String position = DiagnosticUtils.formatPosition(diagnostic);
out.println(diagnostic.getSeverity().toString() + ": " + path + ":" + position + " " + diagnostic.getMessage());
public void printTo(@NotNull PrintStream out) {
if (!groupedMessages.isEmpty()) {
for (String path : groupedMessages.keySet()) {
Collection<String> diagnostics = groupedMessages.get(path);
for (String diagnostic : diagnostics) {
out.println(diagnostic);
}
}
}
@@ -0,0 +1,76 @@
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* 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 org.jetbrains.jet.compiler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.lang.diagnostics.Severity;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @author abreslav
*/
public interface MessageRenderer {
MessageRenderer TAGS = new MessageRenderer() {
private String renderWithStringSeverity(String severityString, String message, String path, int line, int column) {
StringBuilder out = new StringBuilder();
out.append("<").append(severityString);
if (path != null) {
out.append(" path=\"").append(path).append("\"");
out.append(" line=\"").append(line).append("\"");
out.append(" column=\"").append(column).append("\"");
}
out.append(">\n");
out.append(message);
out.append("</").append(severityString).append(">\n");
return out.toString();
}
@Override
public String render(@NotNull Severity severity, @NotNull String message, @Nullable String path, int line, int column) {
return renderWithStringSeverity(severity.toString(), message, path, line, column);
}
@Override
public String renderException(@NotNull Throwable e) {
return renderWithStringSeverity("EXCEPTION", PLAIN.renderException(e), null, -1, -1);
}
};
MessageRenderer PLAIN = new MessageRenderer() {
@Override
public String render(@NotNull Severity severity, @NotNull String message, @Nullable String path, int line, int column) {
String position = path == null ? "" : path + ": (" + (line + ", " + column) + ") ";
return severity + ": " + position + message;
}
@Override
public String renderException(@NotNull Throwable e) {
StringWriter out = new StringWriter();
e.printStackTrace(new PrintWriter(out));
return out.toString();
}
};
String render(@NotNull Severity severity, @NotNull String message, @Nullable String path, int line, int column);
String renderException(@NotNull Throwable e);
}