UNPKG

oxlint

Version:

Linter for the JavaScript Oxidation Compiler

872 lines (871 loc) 41.1 kB
import { _ as BLOCK_SIZE, b as __commonJSMin, c as allOptions, d as diagnostics, f as replacePlaceholders, g as BLOCK_ALIGN, h as ACTIVE_SIZE, i as resetStateAfterError, l as setOptions, m as getNodeByRangeIndex, o as registerPlugin, p as getLineColumnFromOffset, r as lintFileImpl, s as registeredRules, t as buffers, u as PLACEHOLDER_REGEX, v as BUFFER_SIZE, x as __toESM } from "./lint.js"; import { _ as ObjectValues, a as JSONStringify, d as ObjectEntries, m as ObjectKeys, n as ArrayIsArray, p as ObjectHasOwn, t as ArrayFrom, u as ObjectDefineProperty } from "./globals.js"; import { a as rawTransferSupported$1, i as parseRawSync, n as getBufferOffset, t as applyFixes } from "./bindings.js"; import assert, { AssertionError } from "node:assert"; import { dirname, isAbsolute, join } from "node:path"; import util from "node:util"; //#endregion //#region src-js/package/parse.ts var import_json_stable_stringify_without_jsonify = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => { module.exports = function(obj, opts) { opts ||= {}, typeof opts == "function" && (opts = { cmp: opts }); var space = opts.space || ""; typeof space == "number" && (space = Array(space + 1).join(" ")); var cycles = typeof opts.cycles == "boolean" ? opts.cycles : !1, replacer = opts.replacer || function(key, value) { return value; }, cmp = opts.cmp && (function(f) { return function(node) { return function(a, b) { return f({ key: a, value: node[a] }, { key: b, value: node[b] }); }; }; })(opts.cmp), seen = []; return (function stringify(parent, key, node, level) { var indent = space ? "\n" + Array(level + 1).join(space) : "", colonSeparator = space ? ": " : ":"; if (node && node.toJSON && typeof node.toJSON == "function" && (node = node.toJSON()), node = replacer.call(parent, key, node), node !== void 0) { if (typeof node != "object" || !node) return JSON.stringify(node); if (isArray(node)) { for (var out = [], i = 0; i < node.length; i++) { var item = stringify(node, i, node[i], level + 1) || JSON.stringify(null); out.push(indent + space + item); } return "[" + out.join(",") + indent + "]"; } else { if (seen.indexOf(node) !== -1) { if (cycles) return JSON.stringify("__cycle__"); throw TypeError("Converting circular structure to JSON"); } else seen.push(node); for (var keys = objectKeys(node).sort(cmp && cmp(node)), out = [], i = 0; i < keys.length; i++) { var key = keys[i], value = stringify(node, key, node[key], level + 1); if (value) { var keyValue = JSON.stringify(key) + colonSeparator + value; out.push(indent + space + keyValue); } } return seen.splice(seen.indexOf(node), 1), "{" + out.join(",") + indent + "}"; } } })({ "": obj }, "", obj, 0); }; var isArray = Array.isArray || function(x) { return {}.toString.call(x) === "[object Array]"; }, objectKeys = Object.keys || function(obj) { var has = Object.prototype.hasOwnProperty || function() { return !0; }, keys = []; for (var key in obj) has.call(obj, key) && keys.push(key); return keys; }; })))(), 1); const ARRAY_BUFFER_SIZE = BLOCK_SIZE + BLOCK_ALIGN, textEncoder = new TextEncoder(); let buffer = null, blockBuffer = null, rawTransferIsSupported = null; /** * Parser source text into buffer. * @param path - Path of file to parse * @param sourceText - Source text to parse * @param options - Parsing options * @throws {Error} If raw transfer is not supported on this platform, or parsing failed */ function parse(path, sourceText, options) { if (!rawTransferSupported()) throw Error("`RuleTester` is not supported on 32-bit or big-endian systems, versions of NodeJS prior to v22.0.0, versions of Deno prior to v2.0.0, or other runtimes"); buffer === null && initBuffer(); let maxSourceByteLen = sourceText.length * 3; if (maxSourceByteLen > 1073741824) throw Error("Source text is too long"); let sourceStartPos = ACTIVE_SIZE - maxSourceByteLen, sourceBuffer = new Uint8Array(buffer.buffer, buffer.byteOffset + sourceStartPos, maxSourceByteLen), { read, written: sourceByteLen } = textEncoder.encodeInto(sourceText, sourceBuffer); if (read !== sourceText.length) throw Error("Failed to write source text into buffer"); if (parseRawSync(path, blockBuffer, sourceStartPos, sourceByteLen, options), buffer.int32[536870890] === 0) throw Error("Parsing failed"); } /** * Create a `Uint8Array` which is 2 GiB in size, with its start aligned on 4 GiB. * * Store it in `buffer`, and also in `buffers` array, so it's accessible to `lintFileImpl` by passing `0`as `bufferId`. * * Achieve this by creating a 6 GiB `ArrayBuffer`, getting the offset within it that's aligned to 4 GiB, * chopping off that number of bytes from the start, and shortening to 2 GiB. * * It's always possible to obtain a 2 GiB slice aligned on 4 GiB within a 6 GiB buffer, * no matter how the 6 GiB buffer is aligned. * * `buffer` itself, and `int32` and `float64` views of `buffer`, are `BUFFER_SIZE` bytes, * which excludes `FixedSizeAllocatorMetadata` and `ChunkFooter`. * This ensures this critical data cannot be accidentally overwritten on JS side. * `blockBuffer` is `BLOCK_SIZE` bytes, which includes `FixedSizeAllocatorMetadata` and `ChunkFooter`. * `blockBuffer` is what we pass to Rust, which needs to write them. * * Note: On systems with virtual memory, this only consumes 6 GiB of *virtual* memory. * It does not consume physical memory until data is actually written to the `Uint8Array`. * Physical memory consumed corresponds to the quantity of data actually written. */ function initBuffer() { let arrayBuffer = new ArrayBuffer(ARRAY_BUFFER_SIZE), offset = getBufferOffset(new Uint8Array(arrayBuffer)); buffer = new Uint8Array(arrayBuffer, offset, BUFFER_SIZE), buffer.int32 = new Int32Array(arrayBuffer, offset, BUFFER_SIZE / 4), buffer.float64 = new Float64Array(arrayBuffer, offset, BUFFER_SIZE / 8), blockBuffer = new Uint8Array(arrayBuffer, offset, BLOCK_SIZE), buffers.push(buffer); } /** * Returns `true` if raw transfer is supported. * * Raw transfer is only supported on 64-bit little-endian systems, * and NodeJS >= v22.0.0 or Deno >= v2.0.0. * * Versions of NodeJS prior to v22.0.0 do not support creating an `ArrayBuffer` larger than 4 GiB. * Bun (as at v1.2.4) also does not support creating an `ArrayBuffer` larger than 4 GiB. * Support on Deno v1 is unknown and it's EOL, so treating Deno before v2.0.0 as unsupported. * * No easy way to determining pointer width (64 bit or 32 bit) in JS, * so call a function on Rust side to find out. * * @returns {boolean} - `true` if raw transfer is supported on this platform */ function rawTransferSupported() { return rawTransferIsSupported === null && (rawTransferIsSupported = rawTransferRuntimeSupported() && rawTransferSupported$1()), rawTransferIsSupported; } function rawTransferRuntimeSupported() { let global; try { global = globalThis; } catch { return !1; } if (global.Bun || global.process?.versions?.bun) return !1; if (global.Deno) { let match = Deno.version?.deno?.match(/^(\d+)\./); return !!match && +match[1] >= 2; } if (global.process?.release?.name !== "node") return !1; let match = process.version?.match(/^v(\d+)\./); return !!match && +match[1] >= 22; } //#endregion //#region src-js/package/rule_tester.ts /** * Default `describe` function, if `describe` doesn't exist as a global. * @param text - Description of the test case * @param method - Test case logic * @returns Returned value of `method` */ function defaultDescribe(text, method) { return method.call(this); } const globalObj = globalThis; let describe = typeof globalObj.describe == "function" ? globalObj.describe : defaultDescribe; /** * Default `it` function, if `it` doesn't exist as a global. * @param text - Description of the test case * @param method - Test case logic * @throws {Error} Any error upon execution of `method` * @returns Returned value of `method` */ function defaultIt(text, method) { try { return method.call(this); } catch (err) { throw err instanceof AssertionError && (err.message += ` (${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)})`), err; } } let it = typeof globalObj.it == "function" ? globalObj.it : defaultIt, itOnly = it !== defaultIt && typeof it.only == "function" ? it.only.bind(it) : null; /** * Get `it` function. * @param only - `true` if `it.only` should be used * @throws {Error} If `it.only` is not available * @returns `it` or `it.only` function */ function getIt(only) { return only ? getItOnly() : it; } /** * Get `it.only` function. * @throws {Error} If `it.only` is not available * @returns `it.only` function */ function getItOnly() { if (itOnly === null) throw Error("To use `only`, use `RuleTester` with a test framework that provides `it.only()` like Mocha, or provide a custom `it.only` function by assigning it to `RuleTester.itOnly`"); return itOnly; } const EMPTY_LANGUAGE_OPTIONS = {}; let sharedConfig = {}; const TEST_CASE_PROP_KEYS = new Set([ "code", "name", "only", "filename", "options", "settings", "before", "after", "output", "errors", "__proto__" ]), DEFAULT_CWD = dirname(import.meta.dirname); /** * Utility class for testing rules. */ var RuleTester = class { #config; /** * Creates a new instance of RuleTester. * @param config? - Extra configuration for the tester (optional) */ constructor(config) { if (config === void 0) config = null; else if (config !== null && typeof config != "object") throw TypeError("`config` must be an object if provided"); this.#config = config; } /** * Set the configuration to use for all future tests. * @param config - The configuration to use * @throws {TypeError} If `config` is not an object */ static setDefaultConfig(config) { if (typeof config != "object" || !config) throw TypeError("`config` must be an object"); sharedConfig = config; } /** * Get the current configuration used for all tests. * @returns The current configuration */ static getDefaultConfig() { return sharedConfig; } /** * Reset the configuration to the initial configuration of the tester removing * any changes made until now. * @returns {void} */ static resetDefaultConfig() { sharedConfig = {}; } static get describe() { return describe; } static set describe(value) { describe = value; } static get it() { return it; } static set it(value) { it = value, itOnly = typeof it.only == "function" ? it.only.bind(it) : null; } static get itOnly() { return getItOnly(); } static set itOnly(value) { itOnly = value; } /** * Add the `only` property to a test to run it in isolation. * @param item - A single test to run by itself * @returns The test with `only` set */ static only(item) { return typeof item == "string" ? { code: item, only: !0 } : { ...item, only: !0 }; } /** * Adds a new rule test to execute. * @param ruleName - Name of the rule to run * @param rule - Rule to test * @param tests - Collection of tests to run * @throws {TypeError|Error} If `rule` is not an object with a `create` method, * or if non-object `test`, or if a required scenario of the given type is missing */ run(ruleName, rule, tests) { let plugin = { meta: { name: "rule-to-test" }, rules: { [ruleName]: rule } }, config = createConfigForRun(this.#config); describe(ruleName, () => { tests.valid.length > 0 && describe("valid", () => { let seenTestCases = /* @__PURE__ */ new Set(); for (let test of tests.valid) typeof test == "string" && (test = { code: test }), getIt(test.only)(getTestName(test), () => { runValidTestCase(test, plugin, config, seenTestCases); }); }), tests.invalid.length > 0 && describe("invalid", () => { let seenTestCases = /* @__PURE__ */ new Set(); for (let test of tests.invalid) getIt(test.only)(getTestName(test), () => { runInvalidTestCase(test, plugin, config, seenTestCases); }); }); }); } }; /** * Run valid test case. * @param test - Valid test case * @param plugin - Plugin containing rule being tested * @param config - Config from `RuleTester` instance * @param seenTestCases - Set of serialized test cases to check for duplicates * @throws {AssertionError} If the test case fails */ function runValidTestCase(test, plugin, config, seenTestCases) { try { runBeforeHook(test), assertValidTestCaseIsWellFormed(test, seenTestCases), assertValidTestCasePasses(test, plugin, config); } finally { runAfterHook(test); } } /** * Assert that valid test case passes. * @param test - Valid test case * @param plugin - Plugin containing rule being tested * @param config - Config from `RuleTester` instance * @throws {AssertionError} If the test case fails */ function assertValidTestCasePasses(test, plugin, config) { test = mergeConfigIntoTestCase(test, config), assertErrorCountIsCorrect(lint(test, plugin), 0); } /** * Run invalid test case. * @param test - Invalid test case * @param plugin - Plugin containing rule being tested * @param config - Config from `RuleTester` instance * @param seenTestCases - Set of serialized test cases to check for duplicates * @throws {AssertionError} If the test case fails */ function runInvalidTestCase(test, plugin, config, seenTestCases) { let ruleName = ObjectKeys(plugin.rules)[0]; try { runBeforeHook(test), assertInvalidTestCaseIsWellFormed(test, seenTestCases, ruleName), assertInvalidTestCasePasses(test, plugin, config); } finally { runAfterHook(test); } } /** * Assert that invalid test case passes. * @param test - Invalid test case * @param plugin - Plugin containing rule being tested * @param config - Config from `RuleTester` instance * @throws {AssertionError} If the test case fails */ function assertInvalidTestCasePasses(test, plugin, config) { test = mergeConfigIntoTestCase(test, config); let diagnostics = lint(test, plugin), { errors } = test; if (typeof errors == "number") assertErrorCountIsCorrect(diagnostics, errors); else { assertErrorCountIsCorrect(diagnostics, errors.length), diagnostics.sort((diag1, diag2) => diag1.line - diag2.line || diag1.column - diag2.column); let messages = ObjectValues(plugin.rules)[0].meta?.messages ?? null; for (let errorIndex = 0; errorIndex < errors.length; errorIndex++) { let error = errors[errorIndex], diagnostic = diagnostics[errorIndex]; typeof error == "string" || error instanceof RegExp ? (assertMessageMatches(diagnostic.message, error), assert(diagnostic.suggestions === null, `Error at index ${errorIndex} has suggestions. Please convert the test error into an object and specify \`suggestions\` property on it to test suggestions`)) : (assertInvalidTestCaseMessageIsCorrect(diagnostic, error, messages), assertInvalidTestCaseLocationIsCorrect(diagnostic, error, test), ObjectHasOwn(error, "suggestions") && (error.suggestions == null ? assert(diagnostic.suggestions === null, "Rule produced suggestions") : assertSuggestionsAreCorrect(diagnostic, error, messages, test))); } } let { code } = test, fixedCode = runFixes(diagnostics, code); fixedCode === null && (fixedCode = code); let { recursive } = test, extraPassCount = typeof recursive == "number" ? recursive : recursive === !0 ? 10 : 0; if (extraPassCount > 0 && fixedCode !== code) for (let pass = 0; pass < extraPassCount; pass++) { let newFixedCode = runFixes(lint({ ...test, code: fixedCode }, plugin), fixedCode); if (newFixedCode === null) break; fixedCode = newFixedCode; } if (ObjectHasOwn(test, "output")) { let expectedOutput = test.output; expectedOutput === null ? assert.strictEqual(fixedCode, code, "Expected no autofixes to be suggested") : (assert.strictEqual(fixedCode, expectedOutput, "Output is incorrect"), assert.notStrictEqual(code, expectedOutput, "Test property `output` matches `code`. If no autofix is expected, set output to `null`.")); } else assert.strictEqual(fixedCode, code, "The rule fixed the code. Please add `output` property."); } /** * Run fixes on code and return fixed code. * If no fixes to apply, returns `null`. * * @param diagnostics - Array of `Diagnostic`s returned by `lint` * @param code - Code to run fixes on * @returns Fixed code, or `null` if no fixes to apply * @throws {Error} If error when applying fixes */ function runFixes(diagnostics, code) { let fixGroups = []; for (let diagnostic of diagnostics) diagnostic.fixes !== null && fixGroups.push(diagnostic.fixes); if (fixGroups.length === 0) return null; let fixedCode = applyFixes(code, JSONStringify(fixGroups)); if (fixedCode === null) throw Error("Failed to apply fixes"); return fixedCode; } /** * Assert that message reported by rule under test matches the expected message. * @param diagnostic - Diagnostic emitted by rule under test * @param error - Error object from test case * @param messages - Messages from rule under test * @throws {AssertionError} If `message` / `messageId` is not correct */ function assertInvalidTestCaseMessageIsCorrect(diagnostic, error, messages) { if (ObjectHasOwn(error, "message")) { assert(!ObjectHasOwn(error, "messageId"), "Error should not specify both `message` and a `messageId`"), assert(!ObjectHasOwn(error, "data"), "Error should not specify both `data` and `message`"), assertMessageMatches(diagnostic.message, error.message); return; } assert(ObjectHasOwn(error, "messageId"), "Test error must specify either a `messageId` or `message`"), assertMessageIdIsCorrect(diagnostic.messageId, diagnostic.message, error.messageId, error.data, messages, ""); } /** * Assert that a `messageId` used by the rule under test is correct, and validate `data` (if provided). * * @param reportedMessageId - `messageId` from the diagnostic or suggestion * @param reportedMessage - Message from the diagnostic or suggestion * @param messageId - Expected `messageId` from the test case * @param data - Data from the test case (if provided) * @param messages - Messages from the rule under test * @param prefix - Prefix for assertion error messages (e.g. "" or "Suggestion at index 0: ") * @throws {AssertionError} If messageId is not correct * @throws {AssertionError} If message tenplate with placeholder data inserted does not match reported message */ function assertMessageIdIsCorrect(reportedMessageId, reportedMessage, messageId, data, messages, prefix) { if (assert(messages !== null, `${prefix}Cannot use 'messageId' if rule under test doesn't define 'meta.messages'`), !ObjectHasOwn(messages, messageId)) { let legalMessageIds = `[${ObjectKeys(messages).map((key) => `'${key}'`).join(", ")}]`; assert.fail(`${prefix}Invalid messageId '${messageId}'. Expected one of ${legalMessageIds}.`); } assert.strictEqual(reportedMessageId, messageId, `${prefix}messageId '${reportedMessageId}' does not match expected messageId '${messageId}'`); let ruleMessage = messages[messageId], unsubstitutedPlaceholders = getUnsubstitutedMessagePlaceholders(reportedMessage, ruleMessage, data); if (unsubstitutedPlaceholders.length !== 0 && assert.fail(`${prefix}The reported message has ` + (unsubstitutedPlaceholders.length > 1 ? `unsubstituted placeholders: ${unsubstitutedPlaceholders.map((name) => `'${name}'`).join(", ")}` : `an unsubstituted placeholder '${unsubstitutedPlaceholders[0]}'`) + `. Please provide the missing ${unsubstitutedPlaceholders.length > 1 ? "values" : "value"} via the \`data\` property.`), data !== void 0) { let rehydratedMessage = replacePlaceholders(ruleMessage, data); assert.strictEqual(reportedMessage, rehydratedMessage, `${prefix}Hydrated message "${rehydratedMessage}" does not match "${reportedMessage}"`); } } /** * Assert that location reported by rule under test matches the expected location. * @param diagnostic - Diagnostic emitted by rule under test * @param error - Error object from test case * @param config - Config for this test case * @throws {AssertionError} If diagnostic's location does not match expected location */ function assertInvalidTestCaseLocationIsCorrect(diagnostic, error, test) { let actualLocation = {}, expectedLocation = {}, columnOffset = +(test.eslintCompat === !0); ObjectHasOwn(error, "line") && (actualLocation.line = diagnostic.line, expectedLocation.line = error.line), ObjectHasOwn(error, "column") && (actualLocation.column = diagnostic.column + columnOffset, expectedLocation.column = error.column); let canVoidEndLocation = test.eslintCompat === !0 && diagnostic.endLine === diagnostic.line && diagnostic.endColumn === diagnostic.column; ObjectHasOwn(error, "endLine") && (error.endLine === void 0 && canVoidEndLocation ? actualLocation.endLine = void 0 : actualLocation.endLine = diagnostic.endLine, expectedLocation.endLine = error.endLine), ObjectHasOwn(error, "endColumn") && (error.endColumn === void 0 && canVoidEndLocation ? actualLocation.endColumn = void 0 : actualLocation.endColumn = diagnostic.endColumn + columnOffset, expectedLocation.endColumn = error.endColumn), ObjectKeys(expectedLocation).length > 0 && assert.deepStrictEqual(actualLocation, expectedLocation, "Actual error location does not match expected error location."); } /** * Assert that suggestions reported by the rule under test match expected suggestions. * @param diagnostic - Diagnostic emitted by the rule under test * @param error - Error object from the test case * @param messages - Messages from the rule under test * @param test - Test case * @throws {AssertionError} If suggestions do not match */ function assertSuggestionsAreCorrect(diagnostic, error, messages, test) { let actualSuggestions = diagnostic.suggestions ?? [], expectedSuggestions = error.suggestions; assert.strictEqual(actualSuggestions.length, expectedSuggestions.length, `Error should have ${expectedSuggestions.length} suggestion${expectedSuggestions.length > 1 ? "s" : ""}. Instead found ${actualSuggestions.length} suggestion${actualSuggestions.length > 1 ? "s" : ""}.`); for (let i = 0; i < expectedSuggestions.length; i++) { let actual = actualSuggestions[i], expected = expectedSuggestions[i], prefix = `Suggestion at index ${i}`; assertSuggestionMessageIsCorrect(actual, expected, messages, prefix), assert(ObjectHasOwn(expected, "output"), `${prefix}: \`output\` property is required`); let suggestedCode = applyFixes(test.code, JSONStringify([actual.fixes])); assert(suggestedCode !== null, `${prefix}: Failed to apply suggestion fix`), assert.strictEqual(suggestedCode, expected.output, `${prefix}: Expected the applied suggestion fix to match the test suggestion output`), assert.notStrictEqual(expected.output, test.code, `${prefix}: The output of a suggestion should differ from the original source code`); } } /** * Assert that a suggestion's message matches expectations. * @param actual - Actual suggestion from the diagnostic * @param expected - Expected suggestion from the test case * @param messages - Messages from the rule under test * @param prefix - Prefix for assertion error messages * @throws {AssertionError} If suggestion message does not match */ function assertSuggestionMessageIsCorrect(actual, expected, messages, prefix) { if (ObjectHasOwn(expected, "desc")) { assert(!ObjectHasOwn(expected, "messageId"), `${prefix}: Test should not specify both \`desc\` and \`messageId\``), assert(!ObjectHasOwn(expected, "data"), `${prefix}: Test should not specify both \`desc\` and \`data\``), assert.strictEqual(actual.message, expected.desc, `${prefix}: \`desc\` should be "${expected.desc}" but got "${actual.message}" instead`); return; } if (ObjectHasOwn(expected, "messageId")) { assertMessageIdIsCorrect(actual.messageId, actual.message, expected.messageId, expected.data, messages, `${prefix}: `); return; } ObjectHasOwn(expected, "data") && assert.fail(`${prefix}: Test must specify \`messageId\` if \`data\` is used`), assert.fail(`${prefix}: Test must specify either \`messageId\` or \`desc\``); } /** * Assert that the number of errors reported for test case is as expected. * @param diagnostics - Diagnostics reported by the rule under test * @param expectedErrorCount - Expected number of diagnistics * @throws {AssertionError} If the number of diagnostics is not as expected */ function assertErrorCountIsCorrect(diagnostics, expectedErrorCount) { diagnostics.length !== expectedErrorCount && assert.strictEqual(diagnostics.length, expectedErrorCount, util.format("Should have %s error%s but had %d: %s", expectedErrorCount === 0 ? "no" : expectedErrorCount, expectedErrorCount === 1 ? "" : "s", diagnostics.length, util.inspect(diagnostics))); } /** * Assert that message is matched by matcher. * Matcher can be a string or a regular expression. * @param message - Message * @param matcher - Matcher * @throws {AssertionError} If message does not match */ function assertMessageMatches(message, matcher) { typeof matcher == "string" ? assert.strictEqual(message, matcher) : assert(matcher.test(message), `Expected '${message}' to match ${matcher}`); } /** * Get placeholders in the reported messages but only includes the placeholders available in the raw message * and not in the provided data. * @param message - Reported message * @param raw - Raw message specified in the rule's `meta.messages` * @param data - Data from the test case's error object * @returns Missing placeholder names */ function getUnsubstitutedMessagePlaceholders(message, raw, data) { let unsubstituted = getMessagePlaceholders(message); if (unsubstituted.length === 0) return []; let known = getMessagePlaceholders(raw), provided = data === void 0 ? [] : ObjectKeys(data); return unsubstituted.filter((name) => known.includes(name) && !provided.includes(name)); } /** * Extract names of `{{ name }}` placeholders from a message. * @param message - Message * @returns Array of placeholder names */ function getMessagePlaceholders(message) { return ArrayFrom(message.matchAll(PLACEHOLDER_REGEX), ([, name]) => name.trim()); } /** * Create config for a test run. * Merges config from `RuleTester` instance on top of shared config. * Removes properties which are not allowed in `Config`s, as they can only be properties of `TestCase`. * * @param config - Config from `RuleTester` instance * @returns Merged config */ function createConfigForRun(config) { let merged = {}; return addConfigPropsFrom(sharedConfig, merged), config !== null && addConfigPropsFrom(config, merged), merged; } function addConfigPropsFrom(config, merged) { for (let key of ObjectKeys(config)) TEST_CASE_PROP_KEYS.has(key) || (key === "languageOptions" ? merged.languageOptions = mergeLanguageOptions(config.languageOptions, merged.languageOptions) : merged[key] = config[key]); } /** * Create config for a test case. * Merges properties of test case on top of config from `RuleTester` instance. * * @param test - Test case * @param config - Config from `RuleTester` instance / shared config * @returns Merged config */ function mergeConfigIntoTestCase(test, config) { return { ...config, ...test, languageOptions: mergeLanguageOptions(test.languageOptions, config.languageOptions) }; } /** * Merge language options from test case / config onto language options from base config. * @param localLanguageOptions - Language options from test case / config * @param baseLanguageOptions - Language options from base config * @returns Merged language options, or `undefined` if neither has language options */ function mergeLanguageOptions(localLanguageOptions, baseLanguageOptions) { return localLanguageOptions == null ? baseLanguageOptions ?? void 0 : baseLanguageOptions == null ? localLanguageOptions : { ...baseLanguageOptions, ...localLanguageOptions, parserOptions: mergeParserOptions(localLanguageOptions.parserOptions, baseLanguageOptions.parserOptions), globals: mergeGlobals(localLanguageOptions.globals, baseLanguageOptions.globals) }; } /** * Merge parser options from test case / config onto language options from base config. * @param localParserOptions - Parser options from test case / config * @param baseParserOptions - Parser options from base config * @returns Merged parser options, or `undefined` if neither has parser options */ function mergeParserOptions(localParserOptions, baseParserOptions) { return localParserOptions == null ? baseParserOptions ?? void 0 : baseParserOptions == null ? localParserOptions : { ...baseParserOptions, ...localParserOptions, ecmaFeatures: mergeEcmaFeatures(localParserOptions.ecmaFeatures, baseParserOptions.ecmaFeatures) }; } /** * Merge ecma features from test case / config onto ecma features from base config. * @param localEcmaFeatures - Ecma features from test case / config * @param baseEcmaFeatures - Ecma features from base config * @returns Merged ecma features, or `undefined` if neither has ecma features */ function mergeEcmaFeatures(localEcmaFeatures, baseEcmaFeatures) { return localEcmaFeatures == null ? baseEcmaFeatures ?? void 0 : baseEcmaFeatures == null ? localEcmaFeatures : { ...baseEcmaFeatures, ...localEcmaFeatures }; } /** * Merge globals from test case / config onto globals from base config. * @param localGlobals - Globals from test case / config * @param baseGlobals - Globals from base config * @returns Merged globals */ function mergeGlobals(localGlobals, baseGlobals) { return localGlobals == null ? baseGlobals ?? void 0 : baseGlobals == null ? localGlobals : { ...baseGlobals, ...localGlobals }; } /** * Lint a test case. * @param test - Test case * @param plugin - Plugin containing rule being tested * @returns Array of diagnostics */ function lint(test, plugin) { let parseOptions = getParseOptions(test), path, { filename, cwd } = test; if (filename != null && isAbsolute(filename)) cwd ??= dirname(filename), path = filename; else { if (filename == null) { let ext = parseOptions.lang; ext == null ? ext = "js" : ext === "dts" && (ext = "d.ts"), filename = `file.${ext}`; } cwd ??= DEFAULT_CWD, path = join(cwd, filename); } try { registerPlugin(plugin, null, !1, null); let optionsId = setupOptions(test, cwd); parse(path, test.code, parseOptions); let globalsJSON = getGlobalsJson(test), settingsJSON = JSONStringify(test.settings ?? {}); lintFileImpl(path, 0, null, [0], [optionsId], settingsJSON, globalsJSON, null); let ruleId = `${plugin.meta.name}/${ObjectKeys(plugin.rules)[0]}`; return diagnostics.map((diagnostic) => { let line, column, endLine, endColumn; ({line, column} = getLineColumnFromOffset(diagnostic.start)), {line: endLine, column: endColumn} = getLineColumnFromOffset(diagnostic.end); let node = getNodeByRangeIndex(diagnostic.start); return { ruleId, message: diagnostic.message, messageId: diagnostic.messageId, severity: 1, nodeType: node === null ? null : node.type, line, column, endLine, endColumn, fixes: diagnostic.fixes, suggestions: diagnostic.suggestions }; }); } finally { registeredRules.length = 0, allOptions !== null && (allOptions.length = 1), resetStateAfterError(); } } /** * Get parse options for a test case. * @param test - Test case * @returns Parse options */ function getParseOptions(test) { let parseOptions = {}, languageOptions = test.languageOptions; if (languageOptions ??= EMPTY_LANGUAGE_OPTIONS, languageOptions.parser != null) throw Error("Custom parsers are not supported"); let { sourceType } = languageOptions; if (sourceType != null) { if (test.eslintCompat === !0 && sourceType === "unambiguous") throw Error("'unambiguous' source type is not supported in ESLint compatibility mode.\nDisable ESLint compatibility mode by setting `eslintCompat` to `false` in the config / test case."); parseOptions.sourceType = sourceType; } else test.eslintCompat === !0 && (parseOptions.sourceType = "module"); let { parserOptions } = languageOptions; if (parserOptions != null && (parserOptions.ignoreNonFatalErrors === !0 && (parseOptions.ignoreNonFatalErrors = !0), test.filename == null)) { let { lang } = parserOptions; lang == null ? parserOptions.ecmaFeatures?.jsx === !0 && (parseOptions.lang = "jsx") : parseOptions.lang = lang; } return parseOptions; } /** * Get globals and envs as JSON for test case. * * Normalizes globals values to "readonly", "writable", or "off", same as Rust side does. * `null` is only supported in ESLint compatibility mode. * * Removes envs which are false, same as Rust side does. * * @param test - Test case * @returns Globals and envs as JSON string of form `{ "globals": { ... }, "envs": { ... } }` */ function getGlobalsJson(test) { let globals = { ...test.languageOptions?.globals }, eslintCompat = !!test.eslintCompat; for (let key in globals) { let value = globals[key]; switch (value) { case "readonly": case "writable": case "off": continue; case "writeable": case "true": case !0: value = "writable"; break; case "readable": case "false": case !1: value = "readonly"; break; case null: if (eslintCompat) { value = "readonly"; break; } default: throw Error(`'${value}' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')`); } globals[key] = value; } let originalEnvs = test.languageOptions?.env, envs = {}; if (originalEnvs != null) for (let [key, value] of ObjectEntries(originalEnvs)) value !== !1 && ObjectDefineProperty(envs, key, { value: !0, writable: !0, enumerable: !0, configurable: !0 }); return JSONStringify({ globals, envs }); } /** * Set up options for the test case. * * In linter, all options for all rules are sent over from Rust as a JSON string, * and `setOptions` is called to merge them with the default options for each rule. * The merged options are stored in a global variable `allOptions`. * * This function builds a JSON string in same format as Rust does, and calls `setOptions` with it. * * Returns the options ID to pass to `lintFileImpl` (either 0 for default options, or 1 for user-provided options). * * @param test - Test case * @param cwd - Current working directory for test case * @returns Options ID to pass to `lintFileImpl` */ function setupOptions(test, cwd) { let allOptions = [[]], allRuleIds = [0], optionsId = 0, testOptions = test.options; testOptions != null && (allOptions.push(testOptions), allRuleIds.push(0), optionsId = 1); let allOptionsJson; try { allOptionsJson = JSONStringify({ options: allOptions, ruleIds: allRuleIds, cwd, workspaceUri: null }); } catch (err) { throw Error(`Failed to serialize options: ${err}`); } return setOptions(allOptionsJson), optionsId; } const CONTROL_CHAR_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/gu; /** * Get name of test case. * Control characters in name are replaced with `\u00xx` form. * @param test - Test case * @returns Name of test case */ function getTestName(test) { let name = test.name || test.code; return typeof name == "string" ? name.replace(CONTROL_CHAR_REGEX, (c) => `\\u${c.codePointAt(0).toString(16).padStart(4, "0")}`) : ""; } /** * Runs before hook on the given test case. * @param test - Test to run the hook on * @throws {Error} - If the hook is not a function * @throws {*} - Value thrown by the hook function */ function runBeforeHook(test) { ObjectHasOwn(test, "before") && runHook(test, test.before, "before"); } /** * Runs after hook on the given test case. * @param test - Test to run the hook on * @throws {Error} - If the hook is not a function * @throws {*} - Value thrown by the hook function */ function runAfterHook(test) { ObjectHasOwn(test, "after") && runHook(test, test.after, "after"); } /** * Runs a hook on the given test case. * @param test - Test to run the hook on * @param hook - Hook function * @param name - Name of the hook * @throws {Error} - If the property is not a function * @throws {*} - Value thrown by the hook function */ function runHook(test, hook, name) { assert.strictEqual(typeof hook, "function", `Optional test case property \`${name}\` must be a function`), hook.call(test); } /** * Assert that a valid test case object is valid. * A valid test case must specify a string value for `code`. * Optional properties are checked for correct types. * * @param test - Valid test case object to check * @param seenTestCases - Set of serialized test cases to check for duplicates * @throws {AssertionError} If the test case is not valid */ function assertValidTestCaseIsWellFormed(test, seenTestCases) { assertTestCaseCommonPropertiesAreWellFormed(test), assert(!("errors" in test) || test.errors === void 0, "Valid test case must not have `errors` property"), assert(!("output" in test) || test.output === void 0, "Valid test case must not have `output` property"), assertNotDuplicateTestCase(test, seenTestCases); } /** * Assert that an invalid test case object is valid. * An invalid test case must specify a string value for `code` and must have an `errors` property. * Optional properties are checked for correct types. * * @param test - Invalid test case object to check * @param seenTestCases - Set of serialized test cases to check for duplicates * @param ruleName - Name of the rule being tested * @throws {AssertionError} If the test case is not valid */ function assertInvalidTestCaseIsWellFormed(test, seenTestCases, ruleName) { assertTestCaseCommonPropertiesAreWellFormed(test); let { errors } = test; typeof errors == "number" ? assert(errors > 0, "Invalid cases must have `errors` value greater than 0") : (assert(errors !== void 0, `Did not specify errors for an invalid test of rule \`${ruleName}\``), assert(ArrayIsArray(errors), `Invalid 'errors' property for invalid test of rule \`${ruleName}\`:expected a number or an array but got ${errors === null ? "null" : typeof errors}`), assert(errors.length !== 0, "Invalid cases must have at least one error")), ObjectHasOwn(test, "output") && assert(test.output === null || typeof test.output == "string", "Test property `output`, if specified, must be a string or null. If no autofix is expected, then omit the `output` property or set it to null."), assertNotDuplicateTestCase(test, seenTestCases); } /** * Assert that the common properties of a valid/invalid test case have the correct types. * @param {Object} test - Test case object to check * @throws {AssertionError} If the test case is not valid */ function assertTestCaseCommonPropertiesAreWellFormed(test) { assert(typeof test.code == "string", "Test case must specify a string value for `code`"), test.name && assert(typeof test.name == "string", "Optional test case property `name` must be a string"), ObjectHasOwn(test, "only") && assert(typeof test.only == "boolean", "Optional test case property `only` must be a boolean"), ObjectHasOwn(test, "filename") && assert(typeof test.filename == "string", "Optional test case property `filename` must be a string"), ObjectHasOwn(test, "options") && assert(ArrayIsArray(test.options), "Optional test case property `options` must be an array"); } const DUPLICATION_IGNORED_PROPS = new Set([ "name", "errors", "output" ]); /** * Assert that this test case is not a duplicate of one we have seen before. * @param test - Test case object * @param seenTestCases - Set of serialized test cases we have seen so far (managed by this function) * @throws {AssertionError} If the test case is a duplicate */ function assertNotDuplicateTestCase(test, seenTestCases) { if (!isSerializable(test)) return; let serializedTestCase = (0, import_json_stable_stringify_without_jsonify.default)(test, { replacer(key, value) { return test !== this || !DUPLICATION_IGNORED_PROPS.has(key) ? value : void 0; } }); assert(!seenTestCases.has(serializedTestCase), "Detected duplicate test case"), seenTestCases.add(serializedTestCase); } /** * Check if a value is serializable. * Functions or objects like RegExp cannot be serialized by JSON.stringify(). * Inspired by: https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript * @param value - Value * @param seenObjects - Objects already seen in this path from the root object. * @returns {boolean} `true` if the value is serializable */ function isSerializable(value, seenObjects = /* @__PURE__ */ new Set()) { if (!isSerializablePrimitiveOrPlainObject(value)) return !1; if (typeof value != "object" || !value) return !0; if (seenObjects.has(value)) return !1; for (let property in value) { if (!ObjectHasOwn(value, property)) continue; let prop = value[property]; if (!isSerializablePrimitiveOrPlainObject(prop) || !(typeof prop != "object" || !prop) && !isSerializable(prop, new Set([...seenObjects, value]))) return !1; } return !0; } /** * Check if a value is a primitive or plain object created by the `Object` constructor. * @param value - Value to check * @returns `true` if `value` is a primitive or plain object */ function isSerializablePrimitiveOrPlainObject(value) { return value === null || typeof value == "string" || typeof value == "boolean" || typeof value == "number" || typeof value == "object" && (value.constructor === Object || ArrayIsArray(value)); } //#endregion export { RuleTester };