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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to the "prettier-vscode" extension will be documented in thi

## [Unreleased]

- Fixed raw ANSI escape codes appearing in the Prettier output channel for syntax errors (#4002)

## [12.4.0]

- **Security**: Fixed config resolution in untrusted workspaces to prevent JavaScript config files (`.prettierrc.js`, `prettier.config.js`, etc.) from being executed. Previously, even when workspace trust was enforced for module resolution, Prettier's config resolution could still `require()`/`import()` JS config files, allowing arbitrary code execution. Reported by Hector Ruiz Ruiz.
Expand Down
27 changes: 23 additions & 4 deletions src/LoggingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import { window } from "vscode";

type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "NONE";

// The VS Code Output panel does not interpret ANSI escape sequences, so strip
// them to keep the log readable (e.g. Prettier's colorized code frames).
// See https://github.com/prettier/prettier-vscode/issues/4002
const ansiEscapeSequence =
// eslint-disable-next-line no-control-regex
/[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;

function stripAnsiCodes(text: string): string {
return text.replace(ansiEscapeSequence, "");
}

export class LoggingService {
private outputChannel = window.createOutputChannel("Prettier");

Expand Down Expand Up @@ -73,13 +84,13 @@ export class LoggingService {
if (typeof error === "string") {
// Errors as a string usually only happen with
// plugins that don't return the expected error.
this.outputChannel.appendLine(error);
this.appendLine(error);
} else if (error instanceof Error) {
if (error?.message) {
this.logMessage(error.message, "ERROR");
}
if (error?.stack) {
this.outputChannel.appendLine(error.stack);
this.appendLine(error.stack);
}
} else if (error) {
this.logObject(error);
Expand All @@ -90,6 +101,14 @@ export class LoggingService {
this.outputChannel.show();
}

/**
* Append a line to the output channel, stripping ANSI escape sequences
* that the Output panel cannot render.
*/
private appendLine(message: string): void {
this.outputChannel.appendLine(stripAnsiCodes(message));
}

private logObject(data: unknown): void {
// const message = JSON.parser
// .format(JSON.stringify(data, null, 2), {
Expand All @@ -98,7 +117,7 @@ export class LoggingService {
// .trim();
const message = JSON.stringify(data, null, 2); // dont use prettier to keep it simple

this.outputChannel.appendLine(message);
this.appendLine(message);
}

/**
Expand All @@ -108,6 +127,6 @@ export class LoggingService {
*/
private logMessage(message: string, logLevel: LogLevel): void {
const title = new Date().toLocaleTimeString();
this.outputChannel.appendLine(`["${logLevel}" - ${title}] ${message}`);
this.appendLine(`["${logLevel}" - ${title}] ${message}`);
}
}
62 changes: 62 additions & 0 deletions src/test/suite/LoggingService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as assert from "assert";
import * as vscode from "vscode";

import { LoggingService } from "../../LoggingService.js";

describe("LoggingService ANSI stripping", () => {
let lines: string[];
let originalCreateOutputChannel: typeof vscode.window.createOutputChannel;

beforeEach(() => {
lines = [];
originalCreateOutputChannel = vscode.window.createOutputChannel;
(vscode.window as unknown as Record<string, unknown>).createOutputChannel =
() => ({
appendLine: (line: string) => lines.push(line),
show: () => undefined,
dispose: () => undefined,
});
});

afterEach(() => {
(vscode.window as unknown as Record<string, unknown>).createOutputChannel =
originalCreateOutputChannel;
});

it("strips ANSI escape codes from error output", () => {
const service = new LoggingService();
// Sample taken from https://github.com/prettier/prettier-vscode/issues/4002
const error = new Error(
'Opening tag "liPunto" not terminated. (23:11)\n' +
"\u001b[0m \u001b[90m 21 |\u001b[39m \u001b[33m<\u001b[39m\u001b[33mli\u001b[39m\u001b[33m>\u001b[39mPunto 1",
);
error.stack =
"\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 23 |\u001b[39m \u001b[33m<\u001b[39m\u001b[33mliPunto\u001b[39m \u001b[37m\u001b[41m\u001b[1m2\u001b[22m\u001b[49m\u001b[39m";
service.logError("Format failed", error);

assert.ok(lines.length > 0, "expected output lines");
for (const line of lines) {
assert.ok(
!line.includes("\u001b"),
`line still contains ANSI escapes: ${JSON.stringify(line)}`,
);
}
assert.ok(
lines.some((line) =>
line.includes('Opening tag "liPunto" not terminated'),
),
"error message text preserved",
);
assert.ok(
lines.some((line) => line.includes("liPunto")),
"code frame text preserved",
);
});

it("leaves plain text untouched", () => {
const service = new LoggingService();
service.setOutputLevel("DEBUG");
service.logDebug("plain message, no escapes");
assert.ok(lines.some((line) => line.includes("plain message, no escapes")));
});
});