Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add patterns index validation #1550

Merged
merged 11 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
run: npm run check:spelling
- name: Check Qiskit bot config
run: npm run check:qiskit-bot
- name: Check Patterns index
run: npm run check:patterns-index
- name: Internal link checker
run: npm run check:internal-links
- name: Check orphaned pages
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
"author": "Qiskit Development Team",
"license": "Apache-2.0",
"scripts": {
"check": "npm run check:qiskit-bot && npm run check:metadata && npm run check:spelling && npm run check:internal-links && npm run check:orphan-pages && npm run check:fmt",
"check": "npm run check:qiskit-bot && npm run check:patterns-index && npm run check:metadata && npm run check:spelling && npm run check:internal-links && npm run check:orphan-pages && npm run check:fmt",
"check:metadata": "node -r esbuild-register scripts/commands/checkMetadata.ts",
"check:spelling": "cspell --relative --no-progress docs/**/*.md* docs/api/**/*.md* --config cspell/cSpell.json",
"check:fmt": "prettier --check .",
"check:internal-links": "node -r esbuild-register scripts/commands/checkInternalLinks.ts",
"check:external-links": "node -r esbuild-register scripts/commands/checkExternalLinks.ts",
"check:pages-render": "node -r esbuild-register scripts/commands/checkPagesRender.ts",
"check:patterns-index": "node -r esbuild-register scripts/commands/checkPatternsIndex.ts",
"check:orphan-pages": "node -r esbuild-register scripts/commands/checkOrphanPages.ts",
"check:qiskit-bot": "node -r esbuild-register scripts/commands/checkQiskitBotFiles.ts",
"check:stale-images": "node -r esbuild-register scripts/commands/checkStaleImages.ts",
Expand Down
211 changes: 211 additions & 0 deletions scripts/commands/checkPatternsIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// This code is a Qiskit project.
//
// (C) Copyright IBM 2024.
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

import { readFile } from "fs/promises";
import { pathExists } from "../lib/fs";
import type { TocEntry } from "../lib/api/generateToc";

const IGNORED_URLS = ["/guides/qiskit-serverless"];

const INDEX_PAGES = [
"docs/guides/map-problem-to-circuits.mdx",
"docs/guides/optimize-for-hardware.mdx",
"docs/guides/execute-on-hardware.mdx",
"docs/guides/postprocess-results.mdx",
];

const TOC_PATH = "docs/guides/_toc.json";

async function getIndexEntries(indexPath: string): Promise<string[]> {
const rawIndex = await readFile(indexPath, "utf-8");
const result: string[] = [];
// The index page has several unordered lists starting with *, each with links to other pages.
// We want to get every slug from those lists. Note that we don't care which list the slugs show
// up in - we only care if the slug shows up at all anywhere.
for (const line of rawIndex.split("\n")) {
if (!line.trimStart().startsWith("* ")) {
continue;
}

const slug = extractPageSlug(line);
if (slug) {
result.push(slug);
}
}

return result;
}

function extractPageSlug(text: string): string | undefined {
const re = /\((.*)\)/gm;
// Ex: '* [Circuit library](./circuit-library)'.
const match = re.exec(text);
if (!match) {
// Nested sections don't have any link
return undefined;
}
const pageSlug = match[1];
if (pageSlug.startsWith("http") || pageSlug.startsWith("/")) {
return pageSlug;
}
const page = pageSlug.split("/").pop();
return `/guides/${page}`;
}

function getTocSectionPageNames(sectionNode: TocEntry): string[] {
let results = [];
if (sectionNode.url) {
results.push(sectionNode.url);
}

if (sectionNode.children) {
for (const child of sectionNode.children) {
results.push(...getTocSectionPageNames(child));
}
}

return results;
}

async function getToolsTocEntriesToCheck(): Promise<string[]> {
const toc = JSON.parse(await readFile(TOC_PATH, "utf-8"));
const toolsNode = toc.children.find(
(child: TocEntry) => child.title == "Tools",
);
const toolsPages = getTocSectionPageNames(toolsNode);
return toolsPages.filter((page) => !IGNORED_URLS.includes(page));
}

async function deduplicateEntries(
filePath: string,
entries: string[],
): Promise<[Set<string>, string[]]> {
const deduplicatedPages: Set<string> = new Set();
const errors: string[] = [];

for (const entry of entries) {
if (deduplicatedPages.has(entry)) {
errors.push(`❌ ${filePath}: The entry ${entry} is duplicated`);
} else {
deduplicatedPages.add(entry);
}
}

return [deduplicatedPages, errors];
}

function getExtraIndexPagesErrors(
indexPage: string,
indexEntries: Set<string>,
toolsEntries: Set<string>,
): string[] {
return [...indexEntries]
.filter((page) => !toolsEntries.has(page))
.map(
(page) =>
`❌ ${indexPage}: The entry ${page} doesn't appear in the \`Tools\` menu.`,
);
}

function getExtraToolsEntriesErrors(
remainingToolsEntries: Set<string>,
): string[] {
return [...remainingToolsEntries].map(
(page) => `❌ The entry ${page} is not present on any index page`,
);
}

function maybePrintErrorsAndFail(
duplicatesErrors: string[],
extraIndexEntriesErrors: string[],
extraToolsEntriesErrors: string[],
): void {
let allGood = true;

if (duplicatesErrors.length > 0) {
duplicatesErrors.forEach((error) => console.error(error));
console.error(
`\nRemove all duplicated entries on the indices and in the Tools menu, which is set in docs/guides/_toc.json.`,
);
console.error("--------\n");
allGood = false;
}

if (extraIndexEntriesErrors.length > 0) {
extraIndexEntriesErrors.forEach((error) => console.error(error));
console.error(
`\nMake sure all pages have an entry in the Tools menu, which is set in docs/guides/_toc.json.`,
);
console.error("--------\n");
allGood = false;
}

if (extraToolsEntriesErrors.length > 0) {
extraToolsEntriesErrors.forEach((error) => console.error(error));
console.error(
"\nAdd the entries in one of the following index pages, or add the URL to the `IGNORED_URLS` list at the beginning of `/scripts/commands/checkPatternsIndex.tsx` if it's not used in Workflow:",
);
INDEX_PAGES.forEach((index) => console.error(`\t➡️ ${index}`));
allGood = false;
}

if (!allGood) {
process.exit(1);
}
}

async function main() {
// Todo: Remove this conditional once the migration is done. This is used only to avoid
// the script crashing if the file's structure doesn't exist.
if (!(await pathExists(TOC_PATH))) {
console.log(
`🚧 Check skipped because the migration hasn't been completed.\n`,
);
process.exit(0);
}

const toolsAllEntries = await getToolsTocEntriesToCheck();
let [toolsEntries, duplicatesErrors] = await deduplicateEntries(
TOC_PATH,
toolsAllEntries,
);

let extraIndexEntriesErrors: string[] = [];
for (const indexPage of INDEX_PAGES) {
const indexAllEntries = await getIndexEntries(indexPage);
let [indexEntries, indexDuplicatedErrors] = await deduplicateEntries(
indexPage,
indexAllEntries,
);
duplicatesErrors.push(...indexDuplicatedErrors);

extraIndexEntriesErrors.push(
...getExtraIndexPagesErrors(indexPage, indexEntries, toolsEntries),
);

// Remove index entries from the tools entries list
toolsEntries = new Set(
[...toolsEntries].filter((page) => !indexEntries.has(page)),
);
arnaucasau marked this conversation as resolved.
Show resolved Hide resolved
}

const extraToolsEntriesErrors = getExtraToolsEntriesErrors(toolsEntries);

maybePrintErrorsAndFail(
duplicatesErrors,
extraIndexEntriesErrors,
extraToolsEntriesErrors,
);
console.log("\n✅ No missing or duplicated pages were found\n");
}

main().then(() => process.exit());
Loading