Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions packages/typescript/src/api/async/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import {
unescapeLeadingUnderscores,
} from "../../ast/index.ts";
import { assertNever } from "../../internal/utils.ts";
import {
formatDiagnostics,
formatDiagnosticsWithColorAndContext,
} from "../diagnosticFormatter.ts";
import {
encodeNode,
uint8ArrayToBase64,
Expand Down Expand Up @@ -102,6 +106,7 @@ import type {
EmitOutput,
EmitOutputFile,
EmitResult,
FormatDiagnosticsHost,
FreshableType,
GetImportEditsForSymbolsOptions,
IdentifierTypePredicate,
Expand Down Expand Up @@ -154,6 +159,7 @@ export type {
EmitOutput,
EmitOutputFile,
EmitResult,
FormatDiagnosticsHost,
FreshableType,
GetImportEditsForSymbolsOptions,
IdentifierTypePredicate,
Expand Down Expand Up @@ -1205,6 +1211,26 @@ export class Program {
return data ?? [];
}

/**
* Formats diagnostics produced by this program.
*/
async formatDiagnostics(
diagnostics: readonly Diagnostic[],
host: FormatDiagnosticsHost,
): Promise<string> {
return formatDiagnostics(diagnostics, host);
}

/**
* Formats diagnostics produced by this program with color and context.
*/
async formatDiagnosticsWithColorAndContext(
diagnostics: readonly Diagnostic[],
host: FormatDiagnosticsHost,
): Promise<string> {
return formatDiagnosticsWithColorAndContext(diagnostics, host);
}

/**
* Emits files to the configured filesystem.
*
Expand Down
6 changes: 6 additions & 0 deletions packages/typescript/src/api/async/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,12 @@ export interface CompletionInfo {
readonly entries: readonly CompletionEntry[];
}

export interface FormatDiagnosticsHost {
getCurrentDirectory(): string;
getCanonicalFileName(fileName: string): string;
getNewLine(): string;
}

export interface EmitOutputFile {
readonly text: string;
readonly sourceFileName?: string | undefined;
Expand Down
196 changes: 196 additions & 0 deletions packages/typescript/src/api/diagnosticFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { convertToRelativePath } from "./path.ts";
import type { DiagnosticResponse as Diagnostic } from "./proto.generated.ts";

export interface FormatDiagnosticsHost {
getCurrentDirectory(): string;
getCanonicalFileName(fileName: string): string;
getNewLine(): string;
}

const foregroundColorEscapeGrey = "\x1b[90m";
const foregroundColorEscapeRed = "\x1b[91m";
const foregroundColorEscapeYellow = "\x1b[93m";
const foregroundColorEscapeBlue = "\x1b[94m";
const foregroundColorEscapeCyan = "\x1b[96m";
const gutterStyleSequence = "\x1b[7m";
const gutterSeparator = " ";
const resetEscapeSequence = "\x1b[0m";
const ellipsis = "...";
const halfIndent = " ";
const indent = " ";
const fileAppearsToBeBinaryCode = 1490;

function diagnosticCategoryName(category: number): string {
switch (category) {
case 0:
return "warning";
case 1:
return "error";
case 2:
return "suggestion";
case 3:
return "message";
default:
throw new Error(`Unknown diagnostic category: ${category}`);
}
}

function getCategoryFormat(category: number): string {
switch (category) {
case 0:
return foregroundColorEscapeYellow;
case 1:
return foregroundColorEscapeRed;
case 2:
return foregroundColorEscapeGrey;
case 3:
return foregroundColorEscapeBlue;
default:
throw new Error(`Unknown diagnostic category: ${category}`);
}
}

function formatColorAndReset(text: string, formatStyle: string): string {
return formatStyle + text + resetEscapeSequence;
}

function diagnosticPrefix(diagnostic: Diagnostic): string {
return diagnostic.source || "TS";
}

function flattenDiagnosticMessage(diagnostic: Diagnostic, newLine: string, indentLevel = 0): string {
let result = "";
if (indentLevel) {
result += newLine + " ".repeat(indentLevel);
}
result += diagnostic.text;
for (const child of diagnostic.messageChain ?? []) {
result += flattenDiagnosticMessage(child, newLine, indentLevel + 1);
}
return result;
}

function relativeFileName(fileName: string, host: FormatDiagnosticsHost): string {
return convertToRelativePath(
fileName,
host.getCurrentDirectory(),
name => host.getCanonicalFileName(name),
);
}

function formatLocation(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string {
if (!diagnostic.fileName || !diagnostic.startPosition) return "";
const fileName = relativeFileName(diagnostic.fileName, host);
const { line, character } = diagnostic.startPosition;
return formatColorAndReset(fileName, foregroundColorEscapeCyan) +
":" +
formatColorAndReset(`${line + 1}`, foregroundColorEscapeYellow) +
":" +
formatColorAndReset(`${character + 1}`, foregroundColorEscapeYellow);
}

function formatCodeSpan(
diagnostic: Diagnostic,
lineIndent: string,
squiggleColor: string,
host: FormatDiagnosticsHost,
): string {
const { startPosition, endPosition, sourceLines } = diagnostic;
if (!startPosition || !endPosition || !sourceLines?.length) return "";

const hasMoreThanFiveLines = endPosition.line - startPosition.line >= 4;
const gutterWidth = hasMoreThanFiveLines
? Math.max(ellipsis.length, `${endPosition.line + 1}`.length)
: `${endPosition.line + 1}`.length;
let context = "";
let previousLine: number | undefined;

for (const sourceLine of sourceLines) {
if (previousLine !== undefined && sourceLine.line > previousLine + 1) {
context += host.getNewLine();
context += lineIndent +
formatColorAndReset(ellipsis.padStart(gutterWidth), gutterStyleSequence) +
gutterSeparator;
}

const lineContent = sourceLine.text.trimEnd().replace(/\t/g, " ");
context += host.getNewLine();
context += lineIndent +
formatColorAndReset(`${sourceLine.line + 1}`.padStart(gutterWidth), gutterStyleSequence) +
gutterSeparator +
lineContent +
host.getNewLine();
context += lineIndent +
formatColorAndReset("".padStart(gutterWidth), gutterStyleSequence) +
gutterSeparator +
squiggleColor;

if (sourceLine.line === startPosition.line) {
const lastCharacter = sourceLine.line === endPosition.line
? endPosition.character
: lineContent.length;
context += " ".repeat(startPosition.character);
context += "~".repeat(Math.max(0, lastCharacter - startPosition.character));
Comment on lines +129 to +133
}
else if (sourceLine.line === endPosition.line) {
context += "~".repeat(endPosition.character);
}
else {
context += "~".repeat(lineContent.length);
}
context += resetEscapeSequence;
previousLine = sourceLine.line;
}

return context;
}

export function formatDiagnostics(diagnostics: readonly Diagnostic[], host: FormatDiagnosticsHost): string {
let output = "";
for (const diagnostic of diagnostics) {
const errorMessage = `${diagnosticCategoryName(diagnostic.category)} ${diagnosticPrefix(diagnostic)}${diagnostic.code}: ${flattenDiagnosticMessage(diagnostic, host.getNewLine())}${host.getNewLine()}`;
if (diagnostic.fileName && diagnostic.startPosition) {
const { line, character } = diagnostic.startPosition;
output += `${relativeFileName(diagnostic.fileName, host)}(${line + 1},${character + 1}): ${errorMessage}`;
}
else {
output += errorMessage;
}
}
return output;
}

export function formatDiagnosticsWithColorAndContext(
diagnostics: readonly Diagnostic[],
host: FormatDiagnosticsHost,
): string {
let output = "";
for (const diagnostic of diagnostics) {
if (diagnostic.fileName && diagnostic.startPosition) {
output += formatLocation(diagnostic, host) + " - ";
}
output += formatColorAndReset(diagnosticCategoryName(diagnostic.category), getCategoryFormat(diagnostic.category));
output += formatColorAndReset(` ${diagnosticPrefix(diagnostic)}${diagnostic.code}: `, foregroundColorEscapeGrey);
output += flattenDiagnosticMessage(diagnostic, host.getNewLine());

if (diagnostic.fileName && diagnostic.code !== fileAppearsToBeBinaryCode) {
output += host.getNewLine();
output += formatCodeSpan(diagnostic, "", getCategoryFormat(diagnostic.category), host);
}

if (diagnostic.relatedInformation?.length) {
output += host.getNewLine();
for (const related of diagnostic.relatedInformation) {
if (related.fileName && related.startPosition) {
output += host.getNewLine();
output += halfIndent + formatLocation(related, host);
output += formatCodeSpan(related, indent, foregroundColorEscapeCyan, host);
}
output += host.getNewLine();
output += indent + flattenDiagnosticMessage(related, host.getNewLine());
}
}
output += host.getNewLine();
Comment on lines +176 to +193
}
return output;
}
39 changes: 39 additions & 0 deletions packages/typescript/src/api/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,45 @@ export function isRootedDiskPath(path: string): boolean {
return getEncodedRootLength(path) > 0;
}

export function convertToRelativePath(
absoluteOrRelativePath: string,
basePath: string,
getCanonicalFileName: (path: string) => string,
): string {
if (!isRootedDiskPath(absoluteOrRelativePath)) {
return absoluteOrRelativePath;
}

const fromComponents = getPathComponents(getNormalizedAbsolutePath(basePath, ""));
const toComponents = getPathComponents(getNormalizedAbsolutePath(absoluteOrRelativePath, ""));
let start = 0;
for (; start < fromComponents.length && start < toComponents.length; start++) {
const fromComponent = getCanonicalFileName(fromComponents[start]);
const toComponent = getCanonicalFileName(toComponents[start]);
const equal = start === 0
? fromComponent.toLowerCase() === toComponent.toLowerCase()
: fromComponent === toComponent;
if (!equal) {
break;
}
}

if (start === 0) {
return pathFromComponents(toComponents);
}

const relative = Array(fromComponents.length - start).fill("..");
return pathFromComponents(["", ...relative, ...toComponents.slice(start)]);
}

function pathFromComponents(components: readonly string[]): string {
if (components.length === 0) {
return "";
}
const root = components[0] && ensureTrailingDirectorySeparator(components[0]);
return root + components.slice(1).join(directorySeparator);
}

/**
* Converts a file name to a normalized path.
*
Expand Down
18 changes: 18 additions & 0 deletions packages/typescript/src/api/proto.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,10 +763,18 @@ export interface DiagnosticResponse {
pos: number;
/** End is the end position of the diagnostic in the source file. */
end: number;
/** StartPosition is the zero-based line and UTF-16 character position of Pos. */
startPosition?: DiagnosticPositionResponse;
/** EndPosition is the zero-based line and UTF-16 character position of End. */
endPosition?: DiagnosticPositionResponse;
/** SourceLines contains the source lines needed to render this diagnostic with context. */
sourceLines?: DiagnosticSourceLineResponse[];
/** Code is the diagnostic error code. */
code: number;
/** Category is the diagnostic category (error, warning, suggestion, message). */
category: number;
/** Source is a custom diagnostic-code prefix. An empty value uses the default "TS". */
source?: string;
/** Text is the localized diagnostic message text. */
text: string;
/** ReportsUnnecessary indicates this diagnostic highlights unnecessary code. */
Expand Down Expand Up @@ -1034,6 +1042,16 @@ export interface CompletionEntryResponse {
symbol?: SymbolResponse;
}

export interface DiagnosticPositionResponse {
line: number;
character: number;
}

export interface DiagnosticSourceLineResponse {
line: number;
text: string;
}

export interface EmitOutputFile {
fileName: string;
text: string;
Expand Down
26 changes: 26 additions & 0 deletions packages/typescript/src/api/sync/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import {
unescapeLeadingUnderscores,
} from "../../ast/index.ts";
import { assertNever } from "../../internal/utils.ts";
import {
formatDiagnostics,
formatDiagnosticsWithColorAndContext,
} from "../diagnosticFormatter.ts";
import {
encodeNode,
uint8ArrayToBase64,
Expand Down Expand Up @@ -110,6 +114,7 @@ import type {
EmitOutput,
EmitOutputFile,
EmitResult,
FormatDiagnosticsHost,
FreshableType,
GetImportEditsForSymbolsOptions,
IdentifierTypePredicate,
Expand Down Expand Up @@ -162,6 +167,7 @@ export type {
EmitOutput,
EmitOutputFile,
EmitResult,
FormatDiagnosticsHost,
FreshableType,
GetImportEditsForSymbolsOptions,
IdentifierTypePredicate,
Expand Down Expand Up @@ -1213,6 +1219,26 @@ export class Program {
return data ?? [];
}

/**
* Formats diagnostics produced by this program.
*/
formatDiagnostics(
diagnostics: readonly Diagnostic[],
host: FormatDiagnosticsHost,
): string {
return formatDiagnostics(diagnostics, host);
}

/**
* Formats diagnostics produced by this program with color and context.
*/
formatDiagnosticsWithColorAndContext(
diagnostics: readonly Diagnostic[],
host: FormatDiagnosticsHost,
): string {
return formatDiagnosticsWithColorAndContext(diagnostics, host);
}

/**
* Emits files to the configured filesystem.
*
Expand Down
Loading