@typespec/compiler
Version:
TypeSpec compiler and standard library
124 lines • 6 kB
JavaScript
import { ok, strictEqual } from "assert";
import { applyCodeFix as applyCodeFixReal } from "../core/code-fixes.js";
import { createDiagnosticCollector } from "../core/diagnostics.js";
import { createLinterRuleContext } from "../core/linter.js";
import { navigateProgram } from "../core/semantic-walker.js";
import { expectDiagnosticEmpty, expectDiagnostics } from "./expect.js";
import { resolveVirtualPath, trimBlankLines } from "./test-utils.js";
export function createLinterRuleTester(
// eslint-disable-next-line @typescript-eslint/no-deprecated
runner, ruleDef, libraryName) {
return {
expect,
};
function expect(code) {
return {
toBeValid,
toEmitDiagnostics,
applyCodeFix,
};
async function toBeValid() {
const [_, diagnostics] = await compileAndDiagnose(code);
expectDiagnosticEmpty(diagnostics);
}
async function toEmitDiagnostics(match) {
const [result, diagnostics] = await compileAndDiagnose(code);
let expected;
if (typeof match === "function") {
if ("autoCodeOffset" in runner) {
throw new Error(".toEmitDiagnostics with a function match can only be used with a TesterInstance");
}
expected = match(result);
}
else {
expected = match;
}
expectDiagnostics(diagnostics, expected);
}
function applyCodeFix(fixId) {
return { toEqual };
async function toEqual(expectedCode) {
const [_, diagnostics] = await compileAndDiagnose(code);
const codefix = diagnostics[0].codefixes?.find((x) => x.id === fixId);
ok(codefix, `Codefix with id "${fixId}" not found.`);
if (typeof expectedCode === "string") {
await assertSingleFileCodeFix(codefix, expectedCode);
}
else {
await assertMultiFileCodeFix(codefix, expectedCode);
}
}
async function assertSingleFileCodeFix(codefix, expectedCode) {
let content;
const host = {
...runner.program.host,
writeFile: (name, newContent) => {
content = newContent;
return Promise.resolve();
},
};
await applyCodeFixReal(host, codefix);
ok(content, "No content was written to the host.");
const fs = "keys" in runner.fs ? runner.fs : runner.fs.fs;
const offset = fs.get(resolveVirtualPath("./main.tsp"))?.indexOf(code);
strictEqual(trimBlankLines(content.slice(offset)), trimBlankLines(expectedCode));
}
async function assertMultiFileCodeFix(codefix, expectedFiles) {
const writtenFiles = new Map();
const host = {
...runner.program.host,
writeFile: (name, newContent) => {
writtenFiles.set(name, newContent);
return Promise.resolve();
},
};
await applyCodeFixReal(host, codefix);
ok(writtenFiles.size > 0, "No content was written to the host.");
const fs = "keys" in runner.fs ? runner.fs : runner.fs.fs;
for (const [filename, expected] of Object.entries(expectedFiles)) {
const virtualPath = resolveVirtualPath(filename);
const written = writtenFiles.get(virtualPath);
ok(written !== undefined, `Expected file "${filename}" to be written by the code fix but it was not. Written files: ${[...writtenFiles.keys()].join(", ")}`);
const inputCode = typeof code === "string" ? code : code[filename];
const originalContent = fs.get(virtualPath);
const offset = typeof inputCode === "string" && originalContent
? originalContent.indexOf(inputCode)
: undefined;
const actual = offset !== undefined && offset >= 0 ? written.slice(offset) : written;
strictEqual(trimBlankLines(actual), trimBlankLines(expected));
}
}
}
}
async function compileAndDiagnose(code) {
const compilerOptions = { parseOptions: { comments: true } };
let res;
let codeDiagnostics;
if (isLegacyTestRunner(runner)) {
if (typeof code !== "string") {
throw new Error("Only string code is supported with BasicTestRunner. Use Tester.createInstance()");
}
codeDiagnostics = await runner.diagnose(code, compilerOptions);
}
else {
[res, codeDiagnostics] = await runner.compileAndDiagnose(code, { compilerOptions });
}
expectDiagnosticEmpty(codeDiagnostics);
const diagnostics = createDiagnosticCollector();
const rule = { ...ruleDef, id: `${libraryName}/${ruleDef.name}` };
const context = createLinterRuleContext(runner.program, rule, rule.defaultOptions ?? {}, diagnostics);
const listener = ruleDef.create(context);
navigateProgram(runner.program, listener);
if (listener.exit) {
await listener.exit(runner.program);
}
// No diagnostics should have been reported to the program. If it happened the rule is calling reportDiagnostic directly and should NOT be doing that.
expectDiagnosticEmpty(runner.program.diagnostics);
return [res, diagnostics.diagnostics];
}
}
// eslint-disable-next-line @typescript-eslint/no-deprecated
function isLegacyTestRunner(tester) {
return "autoCodeOffset" in tester;
}
//# sourceMappingURL=rule-tester.js.map