mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-24 10:38:59 +01:00
255 lines
6.8 KiB
TypeScript
255 lines
6.8 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as ts from 'typescript';
|
|
import {
|
|
commands,
|
|
Disposable,
|
|
languages,
|
|
Position,
|
|
Range,
|
|
TestMessage,
|
|
TestResultSnapshot,
|
|
TestRunResult,
|
|
tests,
|
|
TextDocument,
|
|
Uri,
|
|
workspace,
|
|
WorkspaceEdit,
|
|
} from 'vscode';
|
|
import { memoizeLast } from './memoize';
|
|
import { getTestMessageMetadata } from './metadata';
|
|
|
|
const enum Constants {
|
|
FixCommandId = 'selfhost-test.fix-test',
|
|
}
|
|
|
|
export class FailingDeepStrictEqualAssertFixer {
|
|
private disposables: Disposable[] = [];
|
|
|
|
constructor() {
|
|
this.disposables.push(
|
|
commands.registerCommand(Constants.FixCommandId, async (uri: Uri, position: Position) => {
|
|
const document = await workspace.openTextDocument(uri);
|
|
|
|
const failingAssertion = detectFailingDeepStrictEqualAssertion(document, position);
|
|
if (!failingAssertion) {
|
|
return;
|
|
}
|
|
|
|
const expectedValueNode = failingAssertion.assertion.expectedValue;
|
|
if (!expectedValueNode) {
|
|
return;
|
|
}
|
|
|
|
const start = document.positionAt(expectedValueNode.getStart());
|
|
const end = document.positionAt(expectedValueNode.getEnd());
|
|
|
|
const edit = new WorkspaceEdit();
|
|
edit.replace(uri, new Range(start, end), formatJsonValue(failingAssertion.actualJSONValue));
|
|
await workspace.applyEdit(edit);
|
|
})
|
|
);
|
|
|
|
this.disposables.push(
|
|
languages.registerCodeActionsProvider('typescript', {
|
|
provideCodeActions: (document, range) => {
|
|
const failingAssertion = detectFailingDeepStrictEqualAssertion(document, range.start);
|
|
if (!failingAssertion) {
|
|
return undefined;
|
|
}
|
|
|
|
return [
|
|
{
|
|
title: 'Fix Expected Value',
|
|
command: Constants.FixCommandId,
|
|
arguments: [document.uri, range.start],
|
|
},
|
|
];
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
dispose() {
|
|
for (const d of this.disposables) {
|
|
d.dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
const identifierLikeRe = /^[$a-z_][a-z0-9_$]*$/i;
|
|
|
|
const tsPrinter = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
|
|
const formatJsonValue = (value: unknown) => {
|
|
if (typeof value !== 'object') {
|
|
return JSON.stringify(value, undefined, '\t');
|
|
}
|
|
|
|
const src = ts.createSourceFile('', `(${JSON.stringify(value, undefined, '\t')})`, ts.ScriptTarget.ES5, true);
|
|
const outerExpression = src.statements[0] as ts.ExpressionStatement;
|
|
const parenExpression = outerExpression.expression as ts.ParenthesizedExpression;
|
|
|
|
const unquoted = ts.transform(parenExpression, [
|
|
context => (node: ts.Node) => {
|
|
const visitor = (node: ts.Node): ts.Node =>
|
|
ts.isPropertyAssignment(node) &&
|
|
ts.isStringLiteralLike(node.name) &&
|
|
identifierLikeRe.test(node.name.text)
|
|
? ts.factory.createPropertyAssignment(
|
|
ts.factory.createIdentifier(node.name.text),
|
|
ts.visitNode(node.initializer, visitor) as ts.Expression
|
|
)
|
|
: ts.isStringLiteralLike(node) && node.text === '[undefined]'
|
|
? ts.factory.createIdentifier('undefined')
|
|
: ts.visitEachChild(node, visitor, context);
|
|
|
|
return ts.visitNode(node, visitor);
|
|
},
|
|
]);
|
|
|
|
return tsPrinter.printNode(ts.EmitHint.Expression, unquoted.transformed[0], src);
|
|
};
|
|
|
|
/** Parses the source file, memoizing the last document so cursor moves are efficient */
|
|
const parseSourceFile = memoizeLast((text: string) =>
|
|
ts.createSourceFile('', text, ts.ScriptTarget.ES5, true)
|
|
);
|
|
|
|
const assertionFailureMessageRe = /^Expected values to be strictly (deep-)?equal:/;
|
|
|
|
/** Gets information about the failing assertion at the poisition, if any. */
|
|
function detectFailingDeepStrictEqualAssertion(
|
|
document: TextDocument,
|
|
position: Position
|
|
): { assertion: StrictEqualAssertion; actualJSONValue: unknown } | undefined {
|
|
const sf = parseSourceFile(document.getText());
|
|
const offset = document.offsetAt(position);
|
|
const assertion = StrictEqualAssertion.atPosition(sf, offset);
|
|
if (!assertion) {
|
|
return undefined;
|
|
}
|
|
|
|
const startLine = document.positionAt(assertion.offsetStart).line;
|
|
const messages = getAllTestStatusMessagesAt(document.uri, startLine);
|
|
const strictDeepEqualMessage = messages.find(m =>
|
|
assertionFailureMessageRe.test(typeof m.message === 'string' ? m.message : m.message.value)
|
|
);
|
|
|
|
if (!strictDeepEqualMessage) {
|
|
return undefined;
|
|
}
|
|
|
|
const metadata = getTestMessageMetadata(strictDeepEqualMessage);
|
|
if (!metadata) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
assertion: assertion,
|
|
actualJSONValue: metadata.actualValue,
|
|
};
|
|
}
|
|
|
|
class StrictEqualAssertion {
|
|
/**
|
|
* Extracts the assertion at the current node, if it is one.
|
|
*/
|
|
public static fromNode(node: ts.Node): StrictEqualAssertion | undefined {
|
|
if (!ts.isCallExpression(node)) {
|
|
return undefined;
|
|
}
|
|
|
|
const expr = node.expression.getText();
|
|
if (expr !== 'assert.deepStrictEqual' && expr !== 'assert.strictEqual') {
|
|
return undefined;
|
|
}
|
|
|
|
return new StrictEqualAssertion(node);
|
|
}
|
|
|
|
/**
|
|
* Gets the equals assertion at the given offset in the file.
|
|
*/
|
|
public static atPosition(sf: ts.SourceFile, offset: number): StrictEqualAssertion | undefined {
|
|
let node = findNodeAt(sf, offset);
|
|
|
|
while (node.parent) {
|
|
const obj = StrictEqualAssertion.fromNode(node);
|
|
if (obj) {
|
|
return obj;
|
|
}
|
|
node = node.parent;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
constructor(private readonly expression: ts.CallExpression) { }
|
|
|
|
/** Gets the expected value */
|
|
public get expectedValue(): ts.Expression | undefined {
|
|
return this.expression.arguments[1];
|
|
}
|
|
|
|
/** Gets the position of the assertion expression. */
|
|
public get offsetStart(): number {
|
|
return this.expression.getStart();
|
|
}
|
|
}
|
|
|
|
function findNodeAt(parent: ts.Node, offset: number): ts.Node {
|
|
for (const child of parent.getChildren()) {
|
|
if (child.getStart() <= offset && offset <= child.getEnd()) {
|
|
return findNodeAt(child, offset);
|
|
}
|
|
}
|
|
return parent;
|
|
}
|
|
|
|
function getAllTestStatusMessagesAt(uri: Uri, lineNumber: number): TestMessage[] {
|
|
if (tests.testResults.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const run = tests.testResults[0];
|
|
const snapshots = getTestResultsWithUri(run, uri);
|
|
const result: TestMessage[] = [];
|
|
|
|
for (const snapshot of snapshots) {
|
|
for (const m of snapshot.taskStates[0].messages) {
|
|
if (
|
|
m.location &&
|
|
m.location.range.start.line <= lineNumber &&
|
|
lineNumber <= m.location.range.end.line
|
|
) {
|
|
result.push(m);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getTestResultsWithUri(testRun: TestRunResult, uri: Uri): TestResultSnapshot[] {
|
|
const results: TestResultSnapshot[] = [];
|
|
|
|
const walk = (r: TestResultSnapshot) => {
|
|
for (const c of r.children) {
|
|
walk(c);
|
|
}
|
|
if (r.uri?.toString() === uri.toString()) {
|
|
results.push(r);
|
|
}
|
|
};
|
|
|
|
for (const r of testRun.results) {
|
|
walk(r);
|
|
}
|
|
|
|
return results;
|
|
}
|