Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved diagnostic rendering #42236

Draft
wants to merge 55 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
49316b7
Improve diagnostic rendering for simple cases
RadCod3 Jan 30, 2024
0e214c3
Handling tab chars in user code
RadCod3 Jan 31, 2024
61bc9fc
Add caret underline for multi lined diagnostics
RadCod3 Feb 1, 2024
affe957
Filter duplicate diagnostics
RadCod3 Feb 2, 2024
5585a3b
Change tree traversal approach
RadCod3 Feb 9, 2024
6399787
Move classes to shell-cli to access jansi
RadCod3 Feb 9, 2024
a43a4ff
Truncate singleline diagnostics by terminal width
RadCod3 Feb 12, 2024
f5477c0
Change tree traversal to direct line access
RadCod3 Feb 12, 2024
c79a1c8
Handle extreme terminal window sizes
RadCod3 Feb 13, 2024
662b8df
Use existing syntaxtree, handle bal build projects
RadCod3 Feb 15, 2024
bd77867
Bump jline version to access jansi from within it
RadCod3 Feb 16, 2024
ac7c15f
Pass missing tokens through diagnosticproperties
RadCod3 Feb 21, 2024
05373ed
Clean up
RadCod3 Feb 21, 2024
837a200
Move back to ballerina-cli, show error code
RadCod3 Feb 27, 2024
5cf9400
Annotate errors in module tests
RadCod3 Feb 27, 2024
5b43f9c
Fix padding before colon when single digit
RadCod3 Feb 27, 2024
ea57515
Add license header and class comment
RadCod3 Feb 28, 2024
3d3a6f0
Fix checkstyle fail and add review suggestions
RadCod3 Feb 28, 2024
ec2b328
Update jline version for test compatibility
RadCod3 Mar 3, 2024
163df5f
Move usage of jansi to AnnotateDiagnostics class
RadCod3 Mar 3, 2024
e3dfaaf
Add option to disable color for tests
RadCod3 Mar 3, 2024
8eb0560
Update ballerina-cli tests for new diagnostics
RadCod3 Mar 4, 2024
c6d4865
Fix to pass jballerina-integration-test
RadCod3 Mar 5, 2024
710f66d
Update testerina-tests for new diagnostics
RadCod3 Mar 5, 2024
151f957
Add tests for AnnotateDiagnostics
RadCod3 Mar 8, 2024
b061f95
Remove unused imports
RadCod3 Mar 8, 2024
0f86532
Add newline
RadCod3 Mar 8, 2024
4fc62c6
Fix testerina windows test
RadCod3 Mar 9, 2024
bb5bc5e
Handle forward slashes in document names
RadCod3 Mar 10, 2024
b3ac8ab
Add more tests to improve codecov score
RadCod3 Mar 11, 2024
95f0cd2
Add tests for DiagnosticAnnotation
RadCod3 Mar 12, 2024
4c4d8fb
Improve truncate function
RadCod3 Mar 12, 2024
89ab5b8
Add a workaround for tests not running
RadCod3 Mar 20, 2024
36a762e
Fix cannot resolve module error
RadCod3 Mar 21, 2024
0fc64f0
Change back to regular compile method
RadCod3 Mar 21, 2024
03d4b31
Update tests
RadCod3 Mar 21, 2024
3a9a080
Optimize logic and add more tests
RadCod3 Mar 22, 2024
8d5cfb1
Add newlines and more tests
RadCod3 Mar 22, 2024
32e2373
Address code review comments
RadCod3 Mar 28, 2024
c67a40d
Fix tests for new diagnostics
RadCod3 Mar 29, 2024
bb72797
Use string builder for truncation
RadCod3 Mar 29, 2024
a81b064
Refactor truncation logic, rename function name
RadCod3 Apr 9, 2024
a4ece31
Pull reusable method calls into variables
RadCod3 Apr 10, 2024
7cf46e5
Address code review comments
RadCod3 Apr 17, 2024
ce37ed7
Move logic from CompileTask to AnnotateDiagnostics
RadCod3 Apr 17, 2024
998bfd3
Address suggestions made at code review
RadCod3 Apr 30, 2024
b903b1b
Format test bal files, update toml files
RadCod3 May 6, 2024
85d3c98
Integrate dataprovider for resilient tests
RadCod3 May 6, 2024
a66495b
Use listener class instead of http library
RadCod3 May 6, 2024
7f4f3ed
Update color annotation test with new hint color
RadCod3 May 6, 2024
2ede880
Fix bug found in code review
RadCod3 May 7, 2024
4129911
Refactor method visibility for cleaner code
RadCod3 May 7, 2024
fda2128
Use string builder for efficiency
RadCod3 May 9, 2024
12e70f6
WIP diagnostic area text wrap
RadCod3 May 24, 2024
ca8c192
Apply suggestions from code review
gimantha Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/ballerina-cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ configurations {

dependencies {

implementation project(':ballerina-parser')
implementation project(':ballerina-lang')
implementation project(':ballerina-runtime')
implementation project(':ballerina-tools-api')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
/*
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. 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 io.ballerina.cli.diagnostics;

import io.ballerina.compiler.internal.diagnostics.StringDiagnosticProperty;
import io.ballerina.projects.Document;
import io.ballerina.projects.DocumentId;
import io.ballerina.projects.Module;
import io.ballerina.projects.ModuleName;
import io.ballerina.projects.Package;
import io.ballerina.projects.internal.PackageDiagnostic;
import io.ballerina.tools.diagnostics.Diagnostic;
import io.ballerina.tools.diagnostics.DiagnosticInfo;
import io.ballerina.tools.diagnostics.DiagnosticSeverity;
import io.ballerina.tools.diagnostics.Location;
import io.ballerina.tools.text.LinePosition;
import io.ballerina.tools.text.TextDocument;
import org.jline.jansi.Ansi;
import org.jline.terminal.TerminalBuilder;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static io.ballerina.cli.diagnostics.DiagnosticAnnotation.NEW_LINE;
import static io.ballerina.cli.diagnostics.DiagnosticAnnotation.SEVERITY_COLORS;
import static io.ballerina.cli.diagnostics.DiagnosticAnnotation.getColoredString;
import static io.ballerina.cli.utils.OsUtils.isWindows;

/**
* This class is used to generate diagnostic annotated messages from diagnostics.
*
* @since 2201.9.0
RadCod3 marked this conversation as resolved.
Show resolved Hide resolved
*/
public class AnnotateDiagnostics {

private static final String COMPILER_ERROR_PREFIX = "BCE";
private static final int SYNTAX_ERROR_CODE_THRESHOLD = 1000;
private static final int MISSING_TOKEN_KEYWORD_CODE_THRESHOLD = 400;
private static final int INVALID_TOKEN_CODE = 600;
private static final int NO_TRUNCATE_WIDTH = 999;

private final Map<String, Document> documentMap;
private int terminalWidth;
RadCod3 marked this conversation as resolved.
Show resolved Hide resolved
private final boolean colorEnabled;

RadCod3 marked this conversation as resolved.
Show resolved Hide resolved
public AnnotateDiagnostics(Package currentPackage) {
this.documentMap = getDocumentMap(currentPackage);
this.terminalWidth = getTerminalWidth();
this.colorEnabled = this.terminalWidth != 0;
}

/**
* Returns an annotated diagnostic that is ready to be printed to the console.
*
* @param diagnostic The diagnostic to be annotated.
* @return The annotated diagnostic.
*/
public Ansi renderDiagnostic(Diagnostic diagnostic) {
Location diagnosticLocation = diagnostic.location();
Document document = documentMap.get(diagnosticLocation.lineRange().fileName());
if (document == null) {
return renderAnsi(diagnosticToString(diagnostic, colorEnabled));
}

DiagnosticInfo diagnosticInfo = diagnostic.diagnosticInfo();
String diagnosticCode = diagnosticInfo.code();
this.terminalWidth = this.terminalWidth == 0 ? NO_TRUNCATE_WIDTH : this.terminalWidth;
if (diagnostic instanceof PackageDiagnostic packageDiagnostic && diagnosticCode != null &&
diagnosticCode.startsWith(COMPILER_ERROR_PREFIX)) {
int diagnosticCodeNumber = Integer.parseInt(diagnosticCode.substring(3));
RadCod3 marked this conversation as resolved.
Show resolved Hide resolved
if (diagnosticCodeNumber < SYNTAX_ERROR_CODE_THRESHOLD) {
return renderAnsi(
diagnosticToString(diagnostic, colorEnabled) + NEW_LINE + getSyntaxDiagnosticAnnotation(
document, packageDiagnostic, diagnosticCodeNumber, this.terminalWidth, colorEnabled));
}
}

DiagnosticAnnotation diagnosticAnnotation =
getDiagnosticAnnotation(document, diagnosticLocation, diagnosticInfo.severity(), this.terminalWidth,
colorEnabled);
return renderAnsi(diagnosticToString(diagnostic, colorEnabled) + NEW_LINE + diagnosticAnnotation);

}

private static int getTerminalWidth() {
try {
return TerminalBuilder.builder().dumb(true).build().getWidth();
} catch (IOException e) {
return NO_TRUNCATE_WIDTH;
}
}

private static void processDocuments(Module module, DocumentId documentId, Map<String, Document> documentMap) {
Document document = module.document(documentId);
documentMap.put(getDocumentPath(module.moduleName(), document.name()), document);
}

/**
* Returns a map of documents in the given package.
*
* @param currentPackage The package to get the documents from.
* @return A map of document paths to documents.
*/
private static Map<String, Document> getDocumentMap(Package currentPackage) {
Map<String, Document> documentMap = new HashMap<>();
currentPackage.moduleIds().forEach(moduleId -> {
Module module = currentPackage.module(moduleId);
module.documentIds().forEach(documentId -> processDocuments(module, documentId, documentMap));
module.testDocumentIds().forEach(documentId -> processDocuments(module, documentId, documentMap));
});

return documentMap;
}

private static String getDocumentPath(ModuleName moduleName, String documentName) {
String documentNameFixed = isWindows() ? documentName.replace("/", "\\") : documentName;
if (moduleName.isDefaultModuleName()) {
return documentNameFixed;
}
return Paths.get("modules", moduleName.moduleNamePart(), documentNameFixed).toString();
}

private static String diagnosticToString(Diagnostic diagnostic, boolean colorEnabled) {
DiagnosticInfo diagnosticInfo = diagnostic.diagnosticInfo();
DiagnosticSeverity severity = diagnosticInfo.severity();
String severityString = severity.toString();
String color = SEVERITY_COLORS.get(severity);
String message = diagnostic.toString().substring(severityString.length());
String code = diagnosticInfo.code();
boolean isMultiline = diagnostic.message().contains(NEW_LINE);
boolean isCodeNotNull = code != null;
String formatString = getColoredString("%s", color, colorEnabled) + "%s" +
(isCodeNotNull ? (isMultiline ? NEW_LINE + "(%s)" : " (%s)") : "");

return String.format(formatString, severityString, message, isCodeNotNull ? code : "");
}

private static DiagnosticAnnotation getDiagnosticAnnotation(Document document, Location location,
DiagnosticSeverity severity, int terminalWidth,
boolean colorEnabled) {
TextDocument textDocument = document.textDocument();
LocationDetails locationDetails = getLocationDetails(location);
boolean isMultiline = locationDetails.startLine != locationDetails.endLine;
RadCod3 marked this conversation as resolved.
Show resolved Hide resolved
int length = isMultiline ? textDocument.line(locationDetails.startLine).length() - locationDetails.startOffset :
locationDetails.endOffset - locationDetails.startOffset;

return new DiagnosticAnnotation(
getLines(textDocument, locationDetails.startLine, locationDetails.endLine),
locationDetails.startOffset,
length == 0 ? 1 : length,
isMultiline,
locationDetails.endOffset,
locationDetails.startLine + 1,
severity,
DiagnosticAnnotation.DiagnosticAnnotationType.REGULAR,
terminalWidth, colorEnabled);
}

private static DiagnosticAnnotation getSyntaxDiagnosticAnnotation(Document document,
RadCod3 marked this conversation as resolved.
Show resolved Hide resolved
PackageDiagnostic packageDiagnostic,
int diagnosticCode, int terminalWidth,
boolean colorEnabled) {
TextDocument textDocument = document.textDocument();
Location location = packageDiagnostic.location();
LocationDetails locationDetails = getLocationDetails(location);
int padding = 0;
String color = SEVERITY_COLORS.get(DiagnosticSeverity.ERROR);

if (diagnosticCode < MISSING_TOKEN_KEYWORD_CODE_THRESHOLD) {
RadCod3 marked this conversation as resolved.
Show resolved Hide resolved
return getMissingTokenAnnotation(packageDiagnostic, textDocument, locationDetails, color, colorEnabled,
terminalWidth, padding);
}

if (diagnosticCode == INVALID_TOKEN_CODE) {
return getInvalidTokenAnnotation(textDocument, locationDetails, color, colorEnabled, terminalWidth);
}
return getDiagnosticAnnotation(document, location, DiagnosticSeverity.ERROR, terminalWidth,
colorEnabled);
}

private static DiagnosticAnnotation getMissingTokenAnnotation(PackageDiagnostic packageDiagnostic,
RadCod3 marked this conversation as resolved.
Show resolved Hide resolved
TextDocument textDocument,
LocationDetails locationDetails, String color,
boolean colorEnabled, int terminalWidth,
int padding) {
StringDiagnosticProperty strProperty = (StringDiagnosticProperty) packageDiagnostic.properties().get(0);
String lineString = textDocument.line(locationDetails.startLine).text();
String missingTokenString = getColoredString(strProperty.value(), color, colorEnabled);
if (locationDetails.startOffset < lineString.length() &&
lineString.charAt(locationDetails.startOffset) != ' ') {
missingTokenString = missingTokenString + " ";
}
if (locationDetails.startOffset > 0 && lineString.charAt(locationDetails.startOffset - 1) != ' ') {
missingTokenString = " " + missingTokenString;
padding++;
}

String lineWithMissingToken = lineString.substring(0, locationDetails.startOffset) + missingTokenString +
lineString.substring(locationDetails.startOffset);
List<String> lines = new ArrayList<>();
lines.add(lineWithMissingToken);
return new DiagnosticAnnotation(
lines,
padding + locationDetails.startOffset,
strProperty.value().length(),
false,
0,
locationDetails.startLine + 1,
DiagnosticSeverity.ERROR,
DiagnosticAnnotation.DiagnosticAnnotationType.MISSING,
terminalWidth, colorEnabled);
}

private static DiagnosticAnnotation getInvalidTokenAnnotation(TextDocument textDocument,
LocationDetails locationDetails, String color,
boolean colorEnabled, int terminalWidth) {
List<String> lines = getLines(textDocument, locationDetails.startLine, locationDetails.endLine);
String line = lines.get(0);
String annotatedLine = line.substring(0, locationDetails.startOffset) +
getColoredString(line.substring(locationDetails.startOffset, locationDetails.endOffset), color,
colorEnabled) +
line.substring(locationDetails.endOffset);
lines.set(0, annotatedLine);
return new DiagnosticAnnotation(
lines,
locationDetails.startOffset,
locationDetails.endOffset - locationDetails.startOffset,
false,
0,
locationDetails.startLine + 1,
DiagnosticSeverity.ERROR,
DiagnosticAnnotation.DiagnosticAnnotationType.INVALID,
terminalWidth, colorEnabled);
}

private static List<String> getLines(TextDocument textDocument, int start, int end) {
List<String> lines = new ArrayList<>();
for (int i = start; i <= end; i++) {
lines.add(textDocument.line(i).text());
}
return lines;
}

private static LocationDetails getLocationDetails(Location location) {
LinePosition startLine = location.lineRange().startLine();
LinePosition endLine = location.lineRange().endLine();
return new LocationDetails(startLine.line(), startLine.offset(), endLine.line(), endLine.offset());
}

private static Ansi renderAnsi(String message) {
return Ansi.ansi().render(message);
}

/**
* Represents the location details of a diagnostic.
*
* @param startLine The start line of the diagnostic.
* @param startOffset The start offset of the diagnostic.
* @param endLine The end line of the diagnostic.
* @param endOffset The end offset of the diagnostic.
*/
private record LocationDetails(int startLine, int startOffset, int endLine, int endOffset) {

}

}
Loading
Loading