Use strict equality (===) to prevent type coercion bugs
return (index % 2 === 1) ? ("`" + value + "`") : value;
1/* global globalThis */23const fs = require("fs");4const path = require("path");5const { isGeneratorObject } = require("util/types");67const CHANNEL_REGEX = new RegExp("/nightly/|/beta/|/stable/|/1\\.[0-9]+\\.[0-9]+/");89function arrayToCode(array) {10 return array.map((value, index) => {11 value = value.split(" ").join(" ");12 return (index % 2 === 1) ? ("`" + value + "`") : value;13 }).join("");14}1516function loadContent(content) {17 const Module = module.constructor;18 const m = new Module();19 m._compile(content, "tmp.js");20 m.exports.ignore_order = content.indexOf("\n// ignore-order\n") !== -1 ||21 content.startsWith("// ignore-order\n");22 m.exports.exact_check = content.indexOf("\n// exact-check\n") !== -1 ||23 content.startsWith("// exact-check\n");24 m.exports.should_fail = content.indexOf("\n// should-fail\n") !== -1 ||25 content.startsWith("// should-fail\n");26 return m.exports;27}2829function readFile(filePath) {30 return fs.readFileSync(filePath, "utf8");31}3233function contentToDiffLine(key, value) {34 if (typeof value === "object" && !Array.isArray(value) && value !== null) {35 const out = Object.entries(value)36 .filter(([subKey, _]) => ["path", "name"].includes(subKey))37 .map(([subKey, subValue]) => `"${subKey}": ${JSON.stringify(subValue)}`)38 .join(", ");39 return `"${key}": ${out},`;40 }41 return `"${key}": ${JSON.stringify(value)},`;42}4344function shouldIgnoreField(fieldName) {45 return fieldName === "query" || fieldName === "correction" ||46 fieldName === "proposeCorrectionFrom" ||47 fieldName === "proposeCorrectionTo";48}4950function valueMapper(key, testOutput) {51 let value = testOutput[key];52 // To make our life easier, if there is a "parent" type, we add it to the path.53 if (key === "path") {54 if (testOutput["parent"]) {55 if (value.length > 0) {56 value += "::" + testOutput["parent"]["name"];57 } else {58 value = testOutput["parent"]["name"];59 }60 }61 } else if (key === "href") {62 value = value.replace(CHANNEL_REGEX, "/$CHANNEL/");63 }64 return value;65}6667// This function is only called when no matching result was found and therefore will only display68// the diff between the two items.69function betterLookingDiff(expected, testOutput) {70 let output = " {\n";71 const spaces = " ";72 for (const key in expected) {73 if (!Object.prototype.hasOwnProperty.call(expected, key)) {74 continue;75 }76 const expectedValue = expected[key];77 if (!testOutput || !Object.prototype.hasOwnProperty.call(testOutput, key)) {78 output += "-" + spaces + contentToDiffLine(key, expectedValue) + "\n";79 continue;80 }81 const value = valueMapper(key, testOutput);82 if (value !== expectedValue) {83 output += "-" + spaces + contentToDiffLine(key, expectedValue) + "\n";84 output += "+" + spaces + contentToDiffLine(key, value) + "\n";85 } else {86 output += spaces + " " + contentToDiffLine(key, value) + "\n";87 }88 }89 return output + " }";90}9192function lookForEntry(expected, testOutput) {93 return testOutput.findIndex(testOutputEntry => {94 let allGood = true;95 for (const key in expected) {96 if (!Object.prototype.hasOwnProperty.call(expected, key)) {97 continue;98 }99 const value = valueMapper(key, testOutputEntry);100 let expectedValue = expected[key];101 if (key === "href") {102 expectedValue = expectedValue.replace(CHANNEL_REGEX, "/$CHANNEL/");103 }104 if (value !== expectedValue) {105 allGood = false;106 break;107 }108 }109 return allGood === true;110 });111}112113// This function checks if `expected` has all the required fields needed for the checks.114function checkNeededFields(fullPath, expected, error_text, queryName, position) {115 let fieldsToCheck;116 if (fullPath.length === 0) {117 fieldsToCheck = [118 "foundElems",119 "returned",120 "userQuery",121 "error",122 ];123 } else if (fullPath.endsWith("elems") || fullPath.endsWith("returned")) {124 fieldsToCheck = [125 "name",126 "fullPath",127 "pathWithoutLast",128 "pathLast",129 "generics",130 ];131 } else if (fullPath.endsWith("generics")) {132 fieldsToCheck = [133 "name",134 "fullPath",135 "pathWithoutLast",136 "pathLast",137 "generics",138 ];139 } else {140 fieldsToCheck = [];141 }142 for (const field of fieldsToCheck) {143 if (!Object.prototype.hasOwnProperty.call(expected, field)) {144 let text = `${queryName}==> Mandatory key \`${field}\` is not present`;145 if (fullPath.length > 0) {146 text += ` in field \`${fullPath}\``;147 if (position !== null) {148 text += ` (position ${position})`;149 }150 }151 error_text.push(text);152 }153 }154}155156function valueCheck(fullPath, expected, result, error_text, queryName) {157 if (Array.isArray(expected) && result instanceof Map) {158 const expected_set = new Set();159 for (const [key, expected_value] of expected) {160 expected_set.add(key);161 checkNeededFields(fullPath, expected_value, error_text, queryName, key);162 if (result.has(key)) {163 valueCheck(164 fullPath + "[" + key + "]",165 expected_value,166 result.get(key),167 error_text,168 queryName,169 );170 } else {171 error_text.push(`${queryName}==> EXPECTED has extra key in map from field ` +172 `\`${fullPath}\` (key ${key}): \`${JSON.stringify(expected_value)}\``);173 }174 }175 for (const [key, result_value] of result.entries()) {176 if (!expected_set.has(key)) {177 error_text.push(`${queryName}==> EXPECTED missing key in map from field ` +178 `\`${fullPath}\` (key ${key}): \`${JSON.stringify(result_value)}\``);179 }180 }181 } else if (Array.isArray(expected)) {182 let i;183 for (i = 0; i < expected.length; ++i) {184 checkNeededFields(fullPath, expected[i], error_text, queryName, i);185 if (i >= result.length) {186 error_text.push(`${queryName}==> EXPECTED has extra value in array from field ` +187 `\`${fullPath}\` (position ${i}): \`${JSON.stringify(expected[i])}\``);188 } else {189 valueCheck(fullPath + "[" + i + "]", expected[i], result[i], error_text, queryName);190 }191 }192 for (; i < result.length; ++i) {193 error_text.push(`${queryName}==> RESULT has extra value in array from field ` +194 `\`${fullPath}\` (position ${i}): \`${JSON.stringify(result[i])}\` ` +195 "compared to EXPECTED");196 }197 } else if (expected !== null && typeof expected !== "undefined" &&198 expected.constructor == Object) { // eslint-disable-line eqeqeq199 for (const key in expected) {200 if (shouldIgnoreField(key)) {201 continue;202 }203 if (!Object.prototype.hasOwnProperty.call(expected, key)) {204 continue;205 }206 if (!Object.prototype.hasOwnProperty.call(result, key)) {207 error_text.push("==> Unknown key \"" + key + "\"");208 break;209 }210 let result_v = result[key];211 if (result_v !== null && key === "error") {212 if (!result_v.forEach) {213 throw result_v;214 }215 result_v = arrayToCode(result_v);216 }217 const obj_path = fullPath + (fullPath.length > 0 ? "." : "") + key;218 valueCheck(obj_path, expected[key], result_v, error_text, queryName);219 }220 } else {221 const expectedValue = JSON.stringify(expected);222 const resultValue = JSON.stringify(result);223 if (expectedValue !== resultValue) {224 error_text.push(`${queryName}==> Different values for field \`${fullPath}\`:\n` +225 `EXPECTED: \`${expectedValue}\`\nRESULT: \`${resultValue}\``);226 }227 }228}229230function runParser(query, expected, parseQuery, queryName) {231 const error_text = [];232 checkNeededFields("", expected, error_text, queryName, null);233 if (error_text.length === 0) {234 valueCheck("", expected, parseQuery(query), error_text, queryName);235 }236 return error_text;237}238239async function runSearch(query, expected, doSearch, loadedFile, queryName) {240 const ignore_order = loadedFile.ignore_order;241 const exact_check = loadedFile.exact_check;242243 const { resultsTable } = await doSearch(query, loadedFile.FILTER_CRATE);244 const error_text = [];245246 for (const key in expected) {247 if (shouldIgnoreField(key)) {248 continue;249 }250 if (!Object.prototype.hasOwnProperty.call(expected, key)) {251 continue;252 }253 if (!Object.prototype.hasOwnProperty.call(resultsTable, key)) {254 error_text.push("==> Unknown key \"" + key + "\"");255 break;256 }257 const entry = expected[key];258259 if (exact_check && entry.length !== resultsTable[key].length) {260 error_text.push(queryName + "==> Expected exactly " + entry.length +261 " results but found " + resultsTable[key].length + " in '" + key + "'");262 }263264 let prev_pos = -1;265 for (const [index, elem] of entry.entries()) {266 const entry_pos = lookForEntry(elem, resultsTable[key]);267 if (entry_pos === -1) {268 error_text.push(queryName + "==> Result not found in '" + key + "': '" +269 JSON.stringify(elem) + "'");270 // By default, we just compare the two first items.271 let item_to_diff = 0;272 if ((!ignore_order || exact_check) && index < resultsTable[key].length) {273 item_to_diff = index;274 }275 error_text.push("Diff of first error:\n" +276 betterLookingDiff(elem, resultsTable[key][item_to_diff]));277 } else if (exact_check === true && prev_pos + 1 !== entry_pos) {278 error_text.push(queryName + "==> Exact check failed at position " + (prev_pos + 1) +279 ": expected '" + JSON.stringify(elem) + "' but found '" +280 JSON.stringify(resultsTable[key][index]) + "'");281 } else if (ignore_order === false && entry_pos < prev_pos) {282 error_text.push(queryName + "==> '" +283 JSON.stringify(elem) + "' was supposed to be before '" +284 JSON.stringify(resultsTable[key][prev_pos]) + "'");285 } else {286 prev_pos = entry_pos;287 }288 }289 }290 return error_text;291}292293async function runCorrections(query, corrections, doSearch, loadedFile) {294 const { parsedQuery } = await doSearch(query, loadedFile.FILTER_CRATE);295 const qc = parsedQuery.correction;296 const error_text = [];297298 if (corrections === null) {299 if (qc !== null) {300 error_text.push(`==> [correction] expected = null, found = ${qc}`);301 }302 return error_text;303 }304305 if (qc.toLowerCase() !== corrections.toLowerCase()) {306 error_text.push(`==> [correction] expected = ${corrections}, found = ${qc}`);307 }308309 return error_text;310}311312function checkResult(error_text, loadedFile, displaySuccess) {313 if (error_text.length === 0 && loadedFile.should_fail === true) {314 console.log("FAILED");315 console.log("==> Test was supposed to fail but all items were found...");316 } else if (error_text.length !== 0 && loadedFile.should_fail === false) {317 console.log("FAILED");318 console.log(error_text.join("\n"));319 } else {320 if (displaySuccess) {321 console.log("OK");322 }323 return 0;324 }325 return 1;326}327328async function runCheckInner(callback, loadedFile, entry, extra, doSearch) {329 if (typeof entry.query !== "string") {330 console.log("FAILED");331 console.log("==> Missing `query` field");332 return false;333 }334 let error_text = await callback(335 entry.query,336 entry,337 extra ? "[ query `" + entry.query + "`]" : "",338 );339 if (checkResult(error_text, loadedFile, false) !== 0) {340 return false;341 }342 if (entry.correction !== undefined) {343 error_text = await runCorrections(344 entry.query,345 entry.correction,346 doSearch,347 loadedFile,348 );349 if (checkResult(error_text, loadedFile, false) !== 0) {350 return false;351 }352 }353 return true;354}355356async function runCheck(loadedFile, key, doSearch, callback) {357 const expected = loadedFile[key];358359 if (Array.isArray(expected)) {360 for (const entry of expected) {361 if (!await runCheckInner(callback, loadedFile, entry, true, doSearch)) {362 return 1;363 }364 }365 } else if (!await runCheckInner(callback, loadedFile, expected, false, doSearch)) {366 return 1;367 }368 console.log("OK");369 return 0;370}371372function hasCheck(content, checkName) {373 return content.startsWith(`const ${checkName}`) || content.includes(`\nconst ${checkName}`);374}375376async function runChecks(testFile, doSearch, parseQuery, revision) {377 let checkExpected = false;378 let checkParsed = false;379 let testFileContent = `const REVISION = "${revision}";\n${readFile(testFile)}`;380381 if (testFileContent.indexOf("FILTER_CRATE") !== -1) {382 testFileContent += "exports.FILTER_CRATE = FILTER_CRATE;";383 } else {384 testFileContent += "exports.FILTER_CRATE = null;";385 }386387 if (hasCheck(testFileContent, "EXPECTED")) {388 testFileContent += "exports.EXPECTED = EXPECTED;";389 checkExpected = true;390 }391 if (hasCheck(testFileContent, "PARSED")) {392 testFileContent += "exports.PARSED = PARSED;";393 checkParsed = true;394 }395 if (!checkParsed && !checkExpected) {396 console.log("FAILED");397 console.log("==> At least `PARSED` or `EXPECTED` is needed!");398 return 1;399 }400401 const loadedFile = loadContent(testFileContent);402 let res = 0;403404 if (checkExpected) {405 res += await runCheck(loadedFile, "EXPECTED", doSearch, (query, expected, text) => {406 return runSearch(query, expected, doSearch, loadedFile, text);407 });408 }409 if (checkParsed) {410 res += await runCheck(loadedFile, "PARSED", doSearch, (query, expected, text) => {411 return runParser(query, expected, parseQuery, text);412 });413 }414 return res;415}416417function mostRecentMatch(staticFiles, regex) {418 const matchingEntries = fs.readdirSync(staticFiles)419 .filter(f => f.match(regex))420 .map(f => {421 const stats = fs.statSync(path.join(staticFiles, f));422 return {423 path: f,424 time: stats.mtimeMs,425 };426 });427 if (matchingEntries.length === 0) {428 throw "No static file matching regex";429 }430 // We sort entries in descending order.431 matchingEntries.sort((a, b) => b.time - a.time);432 return matchingEntries[0].path;433}434435/**436 * Load searchNNN.js and search-indexNNN.js.437 *438 * @param {string} doc_folder - Path to a folder generated by running rustdoc439 * @param {string} resource_suffix - Version number between filename and .js, e.g. "1.59.0"440 * @returns {Object} - Object containing keys: `doSearch`, which runs a search441 * with the loaded index and returns a table of results; `parseQuery`, which is the442 * `parseQuery` function exported from the search module, which runs443 * a search but returns type name corrections instead of results.444 */445async function loadSearchJS(doc_folder, resource_suffix) {446 const staticFiles = path.join(doc_folder, "static.files");447 const stringdexJs = mostRecentMatch(staticFiles, /stringdex.*\.js$/);448 const stringdexModule = require(path.join(staticFiles, stringdexJs));449 const searchJs = mostRecentMatch(staticFiles, /search-[0-9a-f]{8}.*\.js$/);450 const searchModule = require(path.join(staticFiles, searchJs));451 globalThis.nonnull = (x, msg) => {452 if (x === null) {453 throw (msg || "unexpected null value!");454 } else {455 return x;456 }457 };458 const { docSearch, DocSearch } = await searchModule.initSearch(459 stringdexModule.Stringdex,460 stringdexModule.RoaringBitmap,461 {462 loadRoot: callbacks => {463 for (const key in callbacks) {464 if (Object.hasOwn(callbacks, key)) {465 globalThis[key] = callbacks[key];466 }467 }468 const rootJs = readFile(path.join(doc_folder, "search.index/root" +469 resource_suffix + ".js"));470 eval(rootJs);471 },472 loadTreeByHash: hashHex => {473 const shardJs = readFile(path.join(doc_folder, "search.index/" + hashHex + ".js"));474 eval(shardJs);475 },476 loadDataByNameAndHash: (name, hashHex) => {477 const shardJs = readFile(path.join(doc_folder, "search.index/" + name + "/" +478 hashHex + ".js"));479 eval(shardJs);480 },481 },482 );483 return {484 doSearch: async function(queryStr, filterCrate, currentCrate) {485 const parsedQuery = DocSearch.parseQuery(queryStr);486 const result = await docSearch.execQuery(parsedQuery, filterCrate, currentCrate);487 const resultsTable = {};488 for (const tab in result) {489 if (!Object.prototype.hasOwnProperty.call(result, tab)) {490 continue;491 }492 if (!isGeneratorObject(result[tab])) {493 continue;494 }495 resultsTable[tab] = [];496 for await (const entry of result[tab]) {497 const flatEntry = Object.assign({498 crate: entry.item.crate,499 name: entry.item.name,500 path: entry.item.modulePath,501 exactPath: entry.item.exactModulePath,502 ty: entry.item.ty,503 }, entry);504 for (const key in entry) {505 if (!Object.prototype.hasOwnProperty.call(entry, key)) {506 continue;507 }508 if (key === "desc" && entry.desc !== null) {509 flatEntry.desc = await entry.desc;510 } else if (key === "displayTypeSignature" &&511 entry.displayTypeSignature !== null512 ) {513 flatEntry.displayTypeSignature = await entry.displayTypeSignature;514 const {515 type,516 mappedNames,517 whereClause,518 } = flatEntry.displayTypeSignature;519 flatEntry.displayType = arrayToCode(type);520 flatEntry.displayMappedNames = [...mappedNames.entries()]521 .map(([name, qname]) => {522 return `${name} = ${qname}`;523 }).join(", ");524 flatEntry.displayWhereClause = [...whereClause.entries()]525 .flatMap(([name, value]) => {526 if (value.length === 0) {527 return [];528 }529 return [`${name}: ${arrayToCode(value)}`];530 }).join(", ");531 }532 }533 resultsTable[tab].push(flatEntry);534 }535 }536 return { resultsTable, parsedQuery };537 },538 parseQuery: DocSearch.parseQuery,539 };540}541542function showHelp() {543 console.log("rustdoc-js options:");544 console.log(" --doc-folder [PATH] : location of the generated doc folder");545 console.log(" --help : show this message then quit");546 console.log(" --crate-name [STRING] : crate name to be used");547 console.log(" --test-file [PATHs] : location of the JS test files (can be called " +548 "multiple times)");549 console.log(" --test-folder [PATH] : location of the JS tests folder");550 console.log(" --resource-suffix [STRING] : suffix to refer to the correct files");551}552553function parseOptions(args) {554 const opts = {555 "crate_name": "",556 "resource_suffix": "",557 "doc_folder": "",558 "test_folder": "",559 "test_file": [],560 "revision": "",561 };562 const correspondences = {563 "--resource-suffix": "resource_suffix",564 "--doc-folder": "doc_folder",565 "--test-folder": "test_folder",566 "--test-file": "test_file",567 "--crate-name": "crate_name",568 "--revision": "revision",569 };570571 for (let i = 0; i < args.length; ++i) {572 const arg = args[i];573 if (Object.prototype.hasOwnProperty.call(correspondences, arg)) {574 i += 1;575 if (i >= args.length) {576 console.log("Missing argument after `" + arg + "` option.");577 return null;578 }579 const arg_value = args[i];580 if (arg !== "--test-file") {581 opts[correspondences[arg]] = arg_value;582 } else {583 opts[correspondences[arg]].push(arg_value);584 }585 } else if (arg === "--help") {586 showHelp();587 process.exit(0);588 } else {589 console.log("Unknown option `" + arg + "`.");590 console.log("Use `--help` to see the list of options");591 return null;592 }593 }594 if (opts["doc_folder"].length < 1) {595 console.log("Missing `--doc-folder` option.");596 } else if (opts["crate_name"].length < 1) {597 console.log("Missing `--crate-name` option.");598 } else if (opts["test_folder"].length < 1 && opts["test_file"].length < 1) {599 console.log("At least one of `--test-folder` or `--test-file` option is required.");600 } else {601 return opts;602 }603 return null;604}605606async function main(argv) {607 const opts = parseOptions(argv.slice(2));608 if (opts === null) {609 return 1;610 }611612 const parseAndSearch = await loadSearchJS(613 opts["doc_folder"],614 opts["resource_suffix"],615 );616 let errors = 0;617618 const doSearch = function(queryStr, filterCrate) {619 return parseAndSearch.doSearch(queryStr, filterCrate, opts["crate_name"]);620 };621622 if (opts["test_file"].length !== 0) {623 for (const file of opts["test_file"]) {624 process.stdout.write(`Testing ${file} ... `);625 errors += await runChecks(file, doSearch, parseAndSearch.parseQuery, opts.revision);626 }627 } else if (opts["test_folder"].length !== 0) {628 for (const file of fs.readdirSync(opts["test_folder"])) {629 if (!file.endsWith(".js")) {630 continue;631 }632 process.stdout.write(`Testing ${file} ... `);633 errors += await runChecks(path.join(opts["test_folder"], file, ""), doSearch,634 parseAndSearch.parseQuery);635 }636 }637 return errors > 0 ? 1 : 0;638}639640main(process.argv).catch(e => {641 console.log(e);642 process.exit(1);643}).then(x => process.exit(x));644645process.on("beforeExit", () => {646 console.log("process did not complete");647 process.exit(1);648});
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.