@flint.fyi/rule-tester 0.16.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,81 @@
1
+ import { AnyOptionalSchema, AnyRule, InferredInputObject, RuleAbout } from "@flint.fyi/core";
2
+
3
+ //#region src/types.d.ts
4
+ interface InvalidTestCase<Options extends object | undefined = object | undefined> extends TestCase<Options> {
5
+ output?: string;
6
+ snapshot: string;
7
+ suggestions?: TestSuggestion[];
8
+ }
9
+ interface TestCase<Options extends object | undefined = object | undefined> {
10
+ code: string;
11
+ fileName?: string | undefined;
12
+ files?: Record<string, string> | undefined;
13
+ name?: string | undefined;
14
+ /**
15
+ * Run only this test case. Useful for debugging.
16
+ *
17
+ * Do not commit code with this flag set.
18
+ */
19
+ only?: boolean;
20
+ options?: Options | undefined;
21
+ /**
22
+ * Skip running this test case. Useful for work-in-progress tests.
23
+ *
24
+ * Do not commit code with this flag set.
25
+ */
26
+ skip?: boolean;
27
+ }
28
+ type TestSuggestion = TestSuggestionForFile | TestSuggestionForFiles;
29
+ interface TestSuggestionFileCase {
30
+ original: string;
31
+ updated: string;
32
+ }
33
+ interface TestSuggestionForFile {
34
+ id: string;
35
+ updated: string;
36
+ }
37
+ interface TestSuggestionForFiles {
38
+ files: Record<string, TestSuggestionFileCase[]>;
39
+ id: string;
40
+ }
41
+ type ValidTestCase<Options extends object | undefined> = string | ValidTestCaseObject<Options>;
42
+ type ValidTestCaseObject<Options extends object | undefined> = TestCase<Options>;
43
+ //#endregion
44
+ //#region src/RuleTester.d.ts
45
+ interface RuleTesterDefaults {
46
+ fileName?: string;
47
+ files?: Record<string, string>;
48
+ }
49
+ interface RuleTesterOptions {
50
+ defaults?: RuleTesterDefaults;
51
+ describe?: TesterSetupDescribe;
52
+ diskBackedFSRoot?: string;
53
+ it?: TesterSetupIt;
54
+ only?: TesterSetupIt;
55
+ scope?: Record<string, unknown>;
56
+ skip?: TesterSetupIt;
57
+ }
58
+ interface TestCases<Options extends object | undefined> {
59
+ invalid: InvalidTestCase<Options>[];
60
+ valid: ValidTestCase<Options>[];
61
+ }
62
+ type TesterSetupDescribe = (description: string, setup: () => void) => void;
63
+ type TesterSetupIt = (description: string, setup: () => Promise<void>) => void;
64
+ declare class RuleTester {
65
+ #private;
66
+ constructor({
67
+ defaults,
68
+ describe,
69
+ diskBackedFSRoot,
70
+ it,
71
+ only,
72
+ scope,
73
+ skip
74
+ }?: RuleTesterOptions);
75
+ describe<OptionsSchema extends AnyOptionalSchema | undefined>(rule: AnyRule<RuleAbout, OptionsSchema>, {
76
+ invalid,
77
+ valid
78
+ }: TestCases<InferredInputObject<OptionsSchema>>): void;
79
+ }
80
+ //#endregion
81
+ export { type InvalidTestCase, RuleTester, type TestCase, type TestSuggestion, type TestSuggestionFileCase, type TestSuggestionForFile, type TestSuggestionForFiles, type ValidTestCase, type ValidTestCaseObject };
package/dist/index.mjs ADDED
@@ -0,0 +1,245 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import { CachedFactory } from "cached-factory";
4
+ import { applyChangesToText, createDiskBackedLinterHost, createEphemeralLinterHost, createVFSLinterHost, formatReport, getPositionOfColumnAndLine, isSuggestionForFiles, parseOptions, processRuleReport } from "@flint.fyi/core";
5
+ import { isTruthy, normalizePath, nullThrows, pathKey } from "@flint.fyi/utils";
6
+ //#region src/createOutput.ts
7
+ function createOutput(reports, testCaseNormalized) {
8
+ const changes = reports.filter((report) => report.fix !== void 0).flatMap((report) => report.fix);
9
+ return changes.length ? applyChangesToText(changes, testCaseNormalized.code) : void 0;
10
+ }
11
+ //#endregion
12
+ //#region src/createReportSnapshot.ts
13
+ function createReportSnapshot(sourceText, reports) {
14
+ let result = sourceText;
15
+ for (const report of reports.toReversed()) result = createReportSnapshotAt(result, report);
16
+ return result;
17
+ }
18
+ function createReportSnapshotAt(sourceText, report) {
19
+ const { begin, end } = getDisplayedRange(sourceText, report.range);
20
+ const lineStartIndex = begin.raw - begin.column;
21
+ const lineEndIndex = getLineBounds(sourceText, end.line).end;
22
+ const lines = sourceText.slice(lineStartIndex, lineEndIndex).split("\n");
23
+ const output = [];
24
+ for (let i = begin.line; i <= end.line; i++) {
25
+ const line = nullThrows(lines[i - begin.line], "Line is expected to be present by the loop condition");
26
+ output.push(line);
27
+ const prevLineIndent = /^[\t ]*/.exec(line)?.[0] ?? "";
28
+ if (i === begin.line) {
29
+ const indent = prevLineIndent.padEnd(begin.column, " ");
30
+ const squiggleEnd = begin.line === end.line ? end.column : line.length;
31
+ output.push(indent.padEnd(squiggleEnd, "~"));
32
+ for (const errorMessageLine of formatReport(report.data, report.message.primary).split("\n")) output.push(indent + errorMessageLine);
33
+ } else {
34
+ const squiggleEnd = i === end.line ? end.column : line.length;
35
+ output.push(prevLineIndent.padEnd(squiggleEnd, "~"));
36
+ }
37
+ }
38
+ return sourceText.slice(0, lineStartIndex) + output.join("\n") + sourceText.slice(lineEndIndex);
39
+ }
40
+ function getDisplayedRange(sourceText, { begin, end }) {
41
+ if (end.column > 0 || end.line === begin.line) return {
42
+ begin,
43
+ end
44
+ };
45
+ const previousLine = getLineBounds(sourceText, end.line - 1);
46
+ if (begin.line === previousLine.line && previousLine.text === "") {
47
+ const currentLine = getLineBounds(sourceText, end.line);
48
+ if (currentLine.text !== "") return {
49
+ begin: {
50
+ column: 0,
51
+ line: currentLine.line,
52
+ raw: currentLine.start
53
+ },
54
+ end: {
55
+ column: 1,
56
+ line: currentLine.line,
57
+ raw: currentLine.start + 1
58
+ }
59
+ };
60
+ }
61
+ return {
62
+ begin,
63
+ end: {
64
+ column: previousLine.text.length,
65
+ line: previousLine.line,
66
+ raw: previousLine.end
67
+ }
68
+ };
69
+ }
70
+ function getLineBounds(sourceText, line) {
71
+ const start = getPositionOfColumnAndLine(sourceText, {
72
+ column: 0,
73
+ line
74
+ });
75
+ let end = sourceText.indexOf("\n", start);
76
+ if (end < 0) end = sourceText.length;
77
+ return {
78
+ end,
79
+ line,
80
+ start,
81
+ text: sourceText.slice(start, end)
82
+ };
83
+ }
84
+ //#endregion
85
+ //#region src/normalizeTestCase.ts
86
+ function normalizeTestCase(testCase, fileName) {
87
+ const rv = { ...testCase };
88
+ rv.fileName ??= fileName ?? "file.ts";
89
+ return rv;
90
+ }
91
+ //#endregion
92
+ //#region src/predicates.ts
93
+ function isTestSuggestionForFiles(suggestion) {
94
+ return "files" in suggestion;
95
+ }
96
+ //#endregion
97
+ //#region src/resolveReportedSuggestions.ts
98
+ function resolveReportedSuggestions(reports, testCaseNormalized) {
99
+ const suggestionsReported = reports.flatMap((report) => report.suggestions).filter(isTruthy);
100
+ if (!suggestionsReported.length) return;
101
+ return suggestionsReported.map((suggestionReported) => isSuggestionForFiles(suggestionReported) ? {
102
+ files: resolveReportedSuggestionForFiles(suggestionReported, testCaseNormalized),
103
+ id: suggestionReported.id
104
+ } : {
105
+ id: suggestionReported.id,
106
+ updated: applyChangesToText([suggestionReported], testCaseNormalized.code)
107
+ });
108
+ }
109
+ function resolveReportedSuggestionForFiles(suggestionReported, testCaseNormalized) {
110
+ if (!testCaseNormalized.suggestions) return {};
111
+ if (!testCaseNormalized.suggestions.every(isTestSuggestionForFiles)) throw new Error("This test case describes suggestions across files, but the rule is only reporting changes to its own file.");
112
+ return Object.fromEntries(testCaseNormalized.suggestions.flatMap((suggestionExpected) => {
113
+ return Object.entries(suggestionExpected.files).map(([filePath, suggestionCasesExpected]) => {
114
+ return [filePath, suggestionCasesExpected.map((suggestionCaseExpected) => {
115
+ const changes = suggestionReported.files[filePath];
116
+ return {
117
+ original: suggestionCaseExpected.original,
118
+ updated: changes ? applyChangesToText(changes, suggestionCaseExpected.original) : suggestionCaseExpected.original
119
+ };
120
+ })];
121
+ });
122
+ }));
123
+ }
124
+ //#endregion
125
+ //#region src/runTestCaseRule.ts
126
+ async function runTestCaseRule(fileFactories, linterHost, { options, rule }, { code, fileName, files }) {
127
+ const filePathAbsolute = normalizePath(path.resolve(linterHost.getCurrentDirectory(), fileName));
128
+ const caseSensitive = linterHost.isCaseSensitiveFS();
129
+ const targetKey = pathKey(filePathAbsolute, caseSensitive);
130
+ for (const oldFile of linterHost.vfsListFiles().keys()) if (pathKey(oldFile, caseSensitive) !== targetKey) linterHost.vfsDeleteFile(oldFile);
131
+ for (const [name, content] of Object.entries(files ?? {})) {
132
+ const filePath = normalizePath(path.resolve(linterHost.getCurrentDirectory(), name));
133
+ assert.notEqual(filePath, filePathAbsolute, `Expected 'files' not to shadow '${fileName}'`);
134
+ linterHost.vfsUpsertFile(filePath, content);
135
+ }
136
+ linterHost.vfsUpsertFile(filePathAbsolute, code);
137
+ using file = fileFactories.get(rule.language).createFile({
138
+ filePath: fileName,
139
+ filePathAbsolute,
140
+ sourceText: code
141
+ });
142
+ const reports = [];
143
+ const ruleRuntime = await rule.setup({
144
+ host: linterHost,
145
+ report(ruleReport) {
146
+ const processedReport = processRuleReport(file, rule, ruleReport);
147
+ if (processedReport == null) return;
148
+ reports.push(processedReport);
149
+ }
150
+ });
151
+ if (ruleRuntime) {
152
+ rule.language.runFileVisitors(file, options, ruleRuntime);
153
+ await ruleRuntime.teardown?.();
154
+ }
155
+ return reports.toSorted((a, b) => a.range.begin.raw - b.range.begin.raw || a.range.end.raw - b.range.end.raw);
156
+ }
157
+ //#endregion
158
+ //#region src/RuleTester.ts
159
+ var RuleTester = class {
160
+ #fileFactories;
161
+ #linterHost;
162
+ #testerOptions;
163
+ constructor({ defaults = {}, describe, diskBackedFSRoot, it, only, scope = globalThis, skip } = {}) {
164
+ let baseHost = diskBackedFSRoot != null ? createEphemeralLinterHost(createDiskBackedLinterHost(path.resolve(process.cwd(), diskBackedFSRoot, "_flint-rule-tester-virtual"))) : void 0;
165
+ const { files: defaultFiles = {} } = defaults;
166
+ if (Object.keys(defaultFiles).length) {
167
+ const vfs = createVFSLinterHost(baseHost == null ? { cwd: process.cwd() } : { baseHost });
168
+ for (const [name, content] of Object.entries(defaultFiles)) {
169
+ const filePath = path.resolve(vfs.getCurrentDirectory(), name);
170
+ vfs.vfsUpsertFile(filePath, content);
171
+ }
172
+ baseHost = vfs;
173
+ }
174
+ this.#linterHost = createVFSLinterHost(baseHost == null ? { cwd: process.cwd() } : { baseHost });
175
+ this.#fileFactories = new CachedFactory((language) => language.createFileFactory(this.#linterHost));
176
+ it = defaultTo(it, scope, "it");
177
+ if (!skip && "skip" in it && typeof it.skip === "function") skip = it.skip;
178
+ if (!only && "only" in it && typeof it.only === "function") only = it.only;
179
+ if (!skip) throw new TypeError("RuleTester needs a `skip` function");
180
+ if (!only) throw new TypeError("RuleTester needs a `only` function");
181
+ this.#testerOptions = {
182
+ defaults,
183
+ describe: defaultTo(describe, scope, "describe"),
184
+ it,
185
+ only,
186
+ scope,
187
+ skip
188
+ };
189
+ }
190
+ describe(rule, { invalid, valid }) {
191
+ this.#testerOptions.describe(rule.about.id, () => {
192
+ this.#testerOptions.describe("invalid", () => {
193
+ for (const testCase of invalid) this.#itInvalidCase(rule, testCase);
194
+ });
195
+ this.#testerOptions.describe("valid", () => {
196
+ for (const testCase of valid) this.#itValidCase(rule, testCase);
197
+ });
198
+ });
199
+ }
200
+ #itInvalidCase(rule, testCase) {
201
+ const testCaseNormalized = normalizeTestCase(testCase, this.#testerOptions.defaults.fileName);
202
+ this.#itTestCase(testCaseNormalized, async () => {
203
+ const reports = await runTestCaseRule(this.#fileFactories, this.#linterHost, {
204
+ options: parseOptions(rule.options, testCase.options),
205
+ rule
206
+ }, testCaseNormalized);
207
+ const actualSnapshot = createReportSnapshot(testCase.code, reports);
208
+ assert.equal(actualSnapshot, testCase.snapshot);
209
+ const actualOutput = createOutput(reports, testCaseNormalized);
210
+ assert.equal(testCase.output, actualOutput, "Expected `output` property to equal:");
211
+ const actualSuggestions = resolveReportedSuggestions(reports, testCaseNormalized);
212
+ assert.deepStrictEqual(actualSuggestions, testCase.suggestions);
213
+ });
214
+ }
215
+ #itTestCase(testCase, setup) {
216
+ let test = testCase.only ? this.#testerOptions.only : this.#testerOptions.it;
217
+ if (testCase.skip) if ("skip" in test && typeof test.skip === "function") test = test.skip;
218
+ else test = this.#testerOptions.skip;
219
+ test(testCase.name ?? ("files" in testCase ? JSON.stringify({
220
+ [testCase.fileName]: testCase.code,
221
+ ...testCase.files
222
+ }, null, 2) : testCase.code), () => {
223
+ if (testCase.files != null) assert.notEqual(Object.keys(testCase.files).length, 0, `'files' must have at least one file`);
224
+ return setup();
225
+ });
226
+ }
227
+ #itValidCase(rule, testCaseRaw) {
228
+ const testCase = typeof testCaseRaw === "string" ? { code: testCaseRaw } : testCaseRaw;
229
+ const testCaseNormalized = normalizeTestCase(testCase, this.#testerOptions.defaults.fileName);
230
+ this.#itTestCase(testCaseNormalized, async () => {
231
+ const reports = await runTestCaseRule(this.#fileFactories, this.#linterHost, {
232
+ options: parseOptions(rule.options, testCase.options),
233
+ rule
234
+ }, testCaseNormalized);
235
+ if (reports.length) assert.deepStrictEqual(createReportSnapshot(testCaseNormalized.code, reports), testCaseNormalized.code);
236
+ });
237
+ }
238
+ };
239
+ function defaultTo(provided, scope, scopeKey) {
240
+ if (provided) return provided;
241
+ if (scopeKey in scope && typeof scope[scopeKey] === "function") return scope[scopeKey];
242
+ throw new Error(`No ${scopeKey} function found`);
243
+ }
244
+ //#endregion
245
+ export { RuleTester };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flint.fyi/rule-tester",
3
- "version": "0.16.4",
3
+ "version": "0.17.0",
4
4
  "description": "[Experimental] Rule unit tests for Flint.",
5
5
  "homepage": "https://flint.fyi",
6
6
  "repository": {
@@ -16,20 +16,20 @@
16
16
  "sideEffects": false,
17
17
  "type": "module",
18
18
  "exports": {
19
- ".": "./lib/index.js"
19
+ ".": "./dist/index.mjs"
20
20
  },
21
21
  "files": [
22
- "lib/",
23
- "!lib/**/*.map"
22
+ "dist/"
24
23
  ],
25
24
  "dependencies": {
26
- "cached-factory": "^0.2.0",
27
- "@flint.fyi/core": "^0.23.0",
28
- "@flint.fyi/utils": "^0.14.1"
25
+ "cached-factory": "^0.3.0",
26
+ "@flint.fyi/core": "^0.24.0",
27
+ "@flint.fyi/utils": "^0.15.0"
29
28
  },
30
29
  "devDependencies": {
31
- "tsdown": "0.21.0",
32
- "vitest": "4.1.0"
30
+ "tsdown": "0.22.3",
31
+ "vitest": "4.1.0",
32
+ "@flint.fyi/build": "^0.1.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=24.0.0"
@@ -1,42 +0,0 @@
1
- import { InvalidTestCase, ValidTestCase } from "./types.js";
2
- import { AnyOptionalSchema, AnyRule, InferredInputObject, RuleAbout } from "@flint.fyi/core";
3
-
4
- //#region src/RuleTester.d.ts
5
- interface RuleTesterDefaults {
6
- fileName?: string;
7
- files?: Record<string, string>;
8
- }
9
- interface RuleTesterOptions {
10
- defaults?: RuleTesterDefaults;
11
- describe?: TesterSetupDescribe;
12
- diskBackedFSRoot?: string;
13
- it?: TesterSetupIt;
14
- only?: TesterSetupIt;
15
- scope?: Record<string, unknown>;
16
- skip?: TesterSetupIt;
17
- }
18
- interface TestCases<Options extends object | undefined> {
19
- invalid: InvalidTestCase<Options>[];
20
- valid: ValidTestCase<Options>[];
21
- }
22
- type TesterSetupDescribe = (description: string, setup: () => void) => void;
23
- type TesterSetupIt = (description: string, setup: () => Promise<void>) => void;
24
- declare class RuleTester {
25
- #private;
26
- constructor({
27
- defaults,
28
- describe,
29
- diskBackedFSRoot,
30
- it,
31
- only,
32
- scope,
33
- skip
34
- }?: RuleTesterOptions);
35
- describe<OptionsSchema extends AnyOptionalSchema | undefined>(rule: AnyRule<RuleAbout, OptionsSchema>, {
36
- invalid,
37
- valid
38
- }: TestCases<InferredInputObject<OptionsSchema>>): void;
39
- }
40
- //#endregion
41
- export { RuleTester };
42
- //# sourceMappingURL=RuleTester.d.ts.map
package/lib/RuleTester.js DELETED
@@ -1,99 +0,0 @@
1
- import { createOutput } from "./createOutput.js";
2
- import { createReportSnapshot } from "./createReportSnapshot.js";
3
- import { normalizeTestCase } from "./normalizeTestCase.js";
4
- import { resolveReportedSuggestions } from "./resolveReportedSuggestions.js";
5
- import { runTestCaseRule } from "./runTestCaseRule.js";
6
- import assert from "node:assert/strict";
7
- import path from "node:path";
8
- import { CachedFactory } from "cached-factory";
9
- import { createDiskBackedLinterHost, createEphemeralLinterHost, createVFSLinterHost, parseOptions } from "@flint.fyi/core";
10
- //#region src/RuleTester.ts
11
- var RuleTester = class {
12
- #fileFactories;
13
- #linterHost;
14
- #testerOptions;
15
- constructor({ defaults = {}, describe, diskBackedFSRoot, it, only, scope = globalThis, skip } = {}) {
16
- let baseHost = diskBackedFSRoot != null ? createEphemeralLinterHost(createDiskBackedLinterHost(path.resolve(process.cwd(), diskBackedFSRoot, "_flint-rule-tester-virtual"))) : void 0;
17
- const { files: defaultFiles = {} } = defaults;
18
- if (Object.keys(defaultFiles).length) {
19
- const vfs = createVFSLinterHost(baseHost == null ? { cwd: process.cwd() } : { baseHost });
20
- for (const [name, content] of Object.entries(defaultFiles)) {
21
- const filePath = path.resolve(vfs.getCurrentDirectory(), name);
22
- vfs.vfsUpsertFile(filePath, content);
23
- }
24
- baseHost = vfs;
25
- }
26
- this.#linterHost = createVFSLinterHost(baseHost == null ? { cwd: process.cwd() } : { baseHost });
27
- this.#fileFactories = new CachedFactory((language) => language.createFileFactory(this.#linterHost));
28
- it = defaultTo(it, scope, "it");
29
- if (!skip && "skip" in it && typeof it.skip === "function") skip = it.skip;
30
- if (!only && "only" in it && typeof it.only === "function") only = it.only;
31
- if (!skip) throw new TypeError("RuleTester needs a `skip` function");
32
- if (!only) throw new TypeError("RuleTester needs a `only` function");
33
- this.#testerOptions = {
34
- defaults,
35
- describe: defaultTo(describe, scope, "describe"),
36
- it,
37
- only,
38
- scope,
39
- skip
40
- };
41
- }
42
- describe(rule, { invalid, valid }) {
43
- this.#testerOptions.describe(rule.about.id, () => {
44
- this.#testerOptions.describe("invalid", () => {
45
- for (const testCase of invalid) this.#itInvalidCase(rule, testCase);
46
- });
47
- this.#testerOptions.describe("valid", () => {
48
- for (const testCase of valid) this.#itValidCase(rule, testCase);
49
- });
50
- });
51
- }
52
- #itInvalidCase(rule, testCase) {
53
- const testCaseNormalized = normalizeTestCase(testCase, this.#testerOptions.defaults.fileName);
54
- this.#itTestCase(testCaseNormalized, async () => {
55
- const reports = await runTestCaseRule(this.#fileFactories, this.#linterHost, {
56
- options: parseOptions(rule.options, testCase.options),
57
- rule
58
- }, testCaseNormalized);
59
- const actualSnapshot = createReportSnapshot(testCase.code, reports);
60
- assert.equal(actualSnapshot, testCase.snapshot);
61
- const actualOutput = createOutput(reports, testCaseNormalized);
62
- assert.equal(testCase.output, actualOutput, "Expected `output` property to equal:");
63
- const actualSuggestions = resolveReportedSuggestions(reports, testCaseNormalized);
64
- assert.deepStrictEqual(actualSuggestions, testCase.suggestions);
65
- });
66
- }
67
- #itTestCase(testCase, setup) {
68
- let test = testCase.only ? this.#testerOptions.only : this.#testerOptions.it;
69
- if (testCase.skip) if ("skip" in test && typeof test.skip === "function") test = test.skip;
70
- else test = this.#testerOptions.skip;
71
- test(testCase.name ?? ("files" in testCase ? JSON.stringify({
72
- [testCase.fileName]: testCase.code,
73
- ...testCase.files
74
- }, null, 2) : testCase.code), () => {
75
- if (testCase.files != null) assert.notEqual(Object.keys(testCase.files), 0, `'files' must have at least one file`);
76
- return setup();
77
- });
78
- }
79
- #itValidCase(rule, testCaseRaw) {
80
- const testCase = typeof testCaseRaw === "string" ? { code: testCaseRaw } : testCaseRaw;
81
- const testCaseNormalized = normalizeTestCase(testCase, this.#testerOptions.defaults.fileName);
82
- this.#itTestCase(testCaseNormalized, async () => {
83
- const reports = await runTestCaseRule(this.#fileFactories, this.#linterHost, {
84
- options: parseOptions(rule.options, testCase.options),
85
- rule
86
- }, testCaseNormalized);
87
- if (reports.length) assert.deepStrictEqual(createReportSnapshot(testCaseNormalized.code, reports), testCaseNormalized.code);
88
- });
89
- }
90
- };
91
- function defaultTo(provided, scope, scopeKey) {
92
- if (provided) return provided;
93
- if (scopeKey in scope && typeof scope[scopeKey] === "function") return scope[scopeKey];
94
- throw new Error(`No ${scopeKey} function found`);
95
- }
96
- //#endregion
97
- export { RuleTester };
98
-
99
- //# sourceMappingURL=RuleTester.js.map
@@ -1,5 +0,0 @@
1
- import { type NormalizedReport } from "@flint.fyi/core";
2
- import type { TestCaseNormalized } from "./normalizeTestCase.ts";
3
- import type { InvalidTestCase } from "./types.ts";
4
- export declare function createOutput(reports: NormalizedReport[], testCaseNormalized: InvalidTestCase & TestCaseNormalized): string | undefined;
5
- //# sourceMappingURL=createOutput.d.ts.map
@@ -1,10 +0,0 @@
1
- import { applyChangesToText } from "@flint.fyi/core";
2
- //#region src/createOutput.ts
3
- function createOutput(reports, testCaseNormalized) {
4
- const changes = reports.filter((report) => report.fix !== void 0).flatMap((report) => report.fix);
5
- return changes.length ? applyChangesToText(changes, testCaseNormalized.code) : void 0;
6
- }
7
- //#endregion
8
- export { createOutput };
9
-
10
- //# sourceMappingURL=createOutput.js.map
@@ -1,3 +0,0 @@
1
- import { type NormalizedReport } from "@flint.fyi/core";
2
- export declare function createReportSnapshot(sourceText: string, reports: NormalizedReport[]): string;
3
- //# sourceMappingURL=createReportSnapshot.d.ts.map
@@ -1,78 +0,0 @@
1
- import { formatReport, getPositionOfColumnAndLine } from "@flint.fyi/core";
2
- import { nullThrows } from "@flint.fyi/utils";
3
- //#region src/createReportSnapshot.ts
4
- function createReportSnapshot(sourceText, reports) {
5
- let result = sourceText;
6
- for (const report of reports.toReversed()) result = createReportSnapshotAt(result, report);
7
- return result;
8
- }
9
- function createReportSnapshotAt(sourceText, report) {
10
- const { begin, end } = getDisplayedRange(sourceText, report.range);
11
- const lineStartIndex = begin.raw - begin.column;
12
- const lineEndIndex = getLineBounds(sourceText, end.line).end;
13
- const lines = sourceText.slice(lineStartIndex, lineEndIndex).split("\n");
14
- const output = [];
15
- for (let i = begin.line; i <= end.line; i++) {
16
- const line = nullThrows(lines[i - begin.line], "Line is expected to be present by the loop condition");
17
- output.push(line);
18
- const prevLineIndent = /^[\t ]*/.exec(line)?.[0] ?? "";
19
- if (i === begin.line) {
20
- const indent = prevLineIndent.padEnd(begin.column, " ");
21
- const squiggleEnd = begin.line === end.line ? end.column : line.length;
22
- output.push(indent.padEnd(squiggleEnd, "~"));
23
- for (const errorMessageLine of formatReport(report.data, report.message.primary).split("\n")) output.push(indent + errorMessageLine);
24
- } else {
25
- const squiggleEnd = i === end.line ? end.column : line.length;
26
- output.push(prevLineIndent.padEnd(squiggleEnd, "~"));
27
- }
28
- }
29
- return sourceText.slice(0, lineStartIndex) + output.join("\n") + sourceText.slice(lineEndIndex);
30
- }
31
- function getDisplayedRange(sourceText, { begin, end }) {
32
- if (end.column > 0 || end.line === begin.line) return {
33
- begin,
34
- end
35
- };
36
- const previousLine = getLineBounds(sourceText, end.line - 1);
37
- if (begin.line === previousLine.line && previousLine.text === "") {
38
- const currentLine = getLineBounds(sourceText, end.line);
39
- if (currentLine.text !== "") return {
40
- begin: {
41
- column: 0,
42
- line: currentLine.line,
43
- raw: currentLine.start
44
- },
45
- end: {
46
- column: 1,
47
- line: currentLine.line,
48
- raw: currentLine.start + 1
49
- }
50
- };
51
- }
52
- return {
53
- begin,
54
- end: {
55
- column: previousLine.text.length,
56
- line: previousLine.line,
57
- raw: previousLine.end
58
- }
59
- };
60
- }
61
- function getLineBounds(sourceText, line) {
62
- const start = getPositionOfColumnAndLine(sourceText, {
63
- column: 0,
64
- line
65
- });
66
- let end = sourceText.indexOf("\n", start);
67
- if (end < 0) end = sourceText.length;
68
- return {
69
- end,
70
- line,
71
- start,
72
- text: sourceText.slice(start, end)
73
- };
74
- }
75
- //#endregion
76
- export { createReportSnapshot };
77
-
78
- //# sourceMappingURL=createReportSnapshot.js.map
package/lib/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import { InvalidTestCase, TestCase, TestSuggestion, TestSuggestionFileCase, TestSuggestionForFile, TestSuggestionForFiles, ValidTestCase, ValidTestCaseObject } from "./types.js";
2
- import { RuleTester } from "./RuleTester.js";
3
- export { type InvalidTestCase, RuleTester, type TestCase, type TestSuggestion, type TestSuggestionFileCase, type TestSuggestionForFile, type TestSuggestionForFiles, type ValidTestCase, type ValidTestCaseObject };
package/lib/index.js DELETED
@@ -1,2 +0,0 @@
1
- import { RuleTester } from "./RuleTester.js";
2
- export { RuleTester };
@@ -1,6 +0,0 @@
1
- import type { TestCase } from "./types.ts";
2
- export interface TestCaseNormalized extends TestCase {
3
- fileName: string;
4
- }
5
- export declare function normalizeTestCase<T extends TestCase>(testCase: T, fileName: string | undefined): T & TestCaseNormalized;
6
- //# sourceMappingURL=normalizeTestCase.d.ts.map
@@ -1,10 +0,0 @@
1
- //#region src/normalizeTestCase.ts
2
- function normalizeTestCase(testCase, fileName) {
3
- const rv = { ...testCase };
4
- rv.fileName ??= fileName ?? "file.ts";
5
- return rv;
6
- }
7
- //#endregion
8
- export { normalizeTestCase };
9
-
10
- //# sourceMappingURL=normalizeTestCase.js.map
@@ -1,3 +0,0 @@
1
- import type { TestSuggestion, TestSuggestionForFiles } from "./types.ts";
2
- export declare function isTestSuggestionForFiles(suggestion: TestSuggestion): suggestion is TestSuggestionForFiles;
3
- //# sourceMappingURL=predicates.d.ts.map
package/lib/predicates.js DELETED
@@ -1,8 +0,0 @@
1
- //#region src/predicates.ts
2
- function isTestSuggestionForFiles(suggestion) {
3
- return "files" in suggestion;
4
- }
5
- //#endregion
6
- export { isTestSuggestionForFiles };
7
-
8
- //# sourceMappingURL=predicates.js.map
@@ -1,15 +0,0 @@
1
- import { type NormalizedReport } from "@flint.fyi/core";
2
- import type { TestCaseNormalized } from "./normalizeTestCase.ts";
3
- import type { InvalidTestCase, TestSuggestionFileCase } from "./types.ts";
4
- export declare function resolveReportedSuggestions(reports: NormalizedReport[], testCaseNormalized: InvalidTestCase & TestCaseNormalized): ({
5
- files: {
6
- [k: string]: TestSuggestionFileCase[];
7
- };
8
- id: string;
9
- updated?: never;
10
- } | {
11
- id: string;
12
- updated: string;
13
- files?: never;
14
- })[] | undefined;
15
- //# sourceMappingURL=resolveReportedSuggestions.d.ts.map
@@ -1,34 +0,0 @@
1
- import { isTestSuggestionForFiles } from "./predicates.js";
2
- import { applyChangesToText, isSuggestionForFiles } from "@flint.fyi/core";
3
- import { isTruthy } from "@flint.fyi/utils";
4
- //#region src/resolveReportedSuggestions.ts
5
- function resolveReportedSuggestions(reports, testCaseNormalized) {
6
- const suggestionsReported = reports.flatMap((report) => report.suggestions).filter(isTruthy);
7
- if (!suggestionsReported.length) return;
8
- return suggestionsReported.map((suggestionReported) => isSuggestionForFiles(suggestionReported) ? {
9
- files: resolveReportedSuggestionForFiles(suggestionReported, testCaseNormalized),
10
- id: suggestionReported.id
11
- } : {
12
- id: suggestionReported.id,
13
- updated: applyChangesToText([suggestionReported], testCaseNormalized.code)
14
- });
15
- }
16
- function resolveReportedSuggestionForFiles(suggestionReported, testCaseNormalized) {
17
- if (!testCaseNormalized.suggestions) return {};
18
- if (!testCaseNormalized.suggestions.every(isTestSuggestionForFiles)) throw new Error("This test case describes suggestions across files, but the rule is only reporting changes to its own file.");
19
- return Object.fromEntries(testCaseNormalized.suggestions.flatMap((suggestionExpected) => {
20
- return Object.entries(suggestionExpected.files).map(([filePath, suggestionCasesExpected]) => {
21
- return [filePath, suggestionCasesExpected.map((suggestionCaseExpected) => {
22
- const changes = suggestionReported.files[filePath];
23
- return {
24
- original: suggestionCaseExpected.original,
25
- updated: changes ? applyChangesToText(changes, suggestionCaseExpected.original) : suggestionCaseExpected.original
26
- };
27
- })];
28
- });
29
- }));
30
- }
31
- //#endregion
32
- export { resolveReportedSuggestions };
33
-
34
- //# sourceMappingURL=resolveReportedSuggestions.js.map
@@ -1,9 +0,0 @@
1
- import type { CachedFactory } from "cached-factory";
2
- import { type AnyLanguage, type AnyLanguageFileFactory, type AnyOptionalSchema, type AnyRule, type InferredOutputObject, type NormalizedReport, type RuleAbout, type VFSLinterHost } from "@flint.fyi/core";
3
- import type { TestCaseNormalized } from "./normalizeTestCase.ts";
4
- export interface TestCaseRuleConfiguration<OptionsSchema extends AnyOptionalSchema | undefined> {
5
- options?: InferredOutputObject<OptionsSchema | undefined>;
6
- rule: AnyRule<RuleAbout, OptionsSchema>;
7
- }
8
- export declare function runTestCaseRule<OptionsSchema extends AnyOptionalSchema | undefined>(fileFactories: CachedFactory<AnyLanguage, AnyLanguageFileFactory>, linterHost: VFSLinterHost, { options, rule }: Required<TestCaseRuleConfiguration<OptionsSchema>>, { code, fileName, files }: TestCaseNormalized): Promise<NormalizedReport[]>;
9
- //# sourceMappingURL=runTestCaseRule.d.ts.map
@@ -1,40 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import path from "node:path";
3
- import { processRuleReport } from "@flint.fyi/core";
4
- import { normalizePath, pathKey } from "@flint.fyi/utils";
5
- //#region src/runTestCaseRule.ts
6
- async function runTestCaseRule(fileFactories, linterHost, { options, rule }, { code, fileName, files }) {
7
- const filePathAbsolute = normalizePath(path.resolve(linterHost.getCurrentDirectory(), fileName));
8
- const caseSensitive = linterHost.isCaseSensitiveFS();
9
- const targetKey = pathKey(filePathAbsolute, caseSensitive);
10
- for (const oldFile of linterHost.vfsListFiles().keys()) if (pathKey(oldFile, caseSensitive) !== targetKey) linterHost.vfsDeleteFile(oldFile);
11
- for (const [name, content] of Object.entries(files ?? {})) {
12
- const filePath = normalizePath(path.resolve(linterHost.getCurrentDirectory(), name));
13
- assert.notEqual(filePath, filePathAbsolute, `Expected 'files' not to shadow '${fileName}'`);
14
- linterHost.vfsUpsertFile(filePath, content);
15
- }
16
- linterHost.vfsUpsertFile(filePathAbsolute, code);
17
- using file = fileFactories.get(rule.language).createFile({
18
- filePath: fileName,
19
- filePathAbsolute,
20
- sourceText: code
21
- });
22
- const reports = [];
23
- const ruleRuntime = await rule.setup({
24
- host: linterHost,
25
- report(ruleReport) {
26
- const processedReport = processRuleReport(file, rule, ruleReport);
27
- if (processedReport == null) return;
28
- reports.push(processedReport);
29
- }
30
- });
31
- if (ruleRuntime) {
32
- rule.language.runFileVisitors(file, options, ruleRuntime);
33
- await ruleRuntime.teardown?.();
34
- }
35
- return reports.toSorted((a, b) => a.range.begin.raw - b.range.begin.raw || a.range.end.raw - b.range.end.raw);
36
- }
37
- //#endregion
38
- export { runTestCaseRule };
39
-
40
- //# sourceMappingURL=runTestCaseRule.js.map
package/lib/types.d.ts DELETED
@@ -1,43 +0,0 @@
1
- //#region src/types.d.ts
2
- interface InvalidTestCase<Options extends object | undefined = object | undefined> extends TestCase<Options> {
3
- output?: string;
4
- snapshot: string;
5
- suggestions?: TestSuggestion[];
6
- }
7
- interface TestCase<Options extends object | undefined = object | undefined> {
8
- code: string;
9
- fileName?: string | undefined;
10
- files?: Record<string, string> | undefined;
11
- name?: string | undefined;
12
- /**
13
- * Run only this test case. Useful for debugging.
14
- *
15
- * Do not commit code with this flag set.
16
- */
17
- only?: boolean;
18
- options?: Options | undefined;
19
- /**
20
- * Skip running this test case. Useful for work-in-progress tests.
21
- *
22
- * Do not commit code with this flag set.
23
- */
24
- skip?: boolean;
25
- }
26
- type TestSuggestion = TestSuggestionForFile | TestSuggestionForFiles;
27
- interface TestSuggestionFileCase {
28
- original: string;
29
- updated: string;
30
- }
31
- interface TestSuggestionForFile {
32
- id: string;
33
- updated: string;
34
- }
35
- interface TestSuggestionForFiles {
36
- files: Record<string, TestSuggestionFileCase[]>;
37
- id: string;
38
- }
39
- type ValidTestCase<Options extends object | undefined> = string | ValidTestCaseObject<Options>;
40
- type ValidTestCaseObject<Options extends object | undefined> = TestCase<Options>;
41
- //#endregion
42
- export { InvalidTestCase, TestCase, TestSuggestion, TestSuggestionFileCase, TestSuggestionForFile, TestSuggestionForFiles, ValidTestCase, ValidTestCaseObject };
43
- //# sourceMappingURL=types.d.ts.map