pulsar/script/validate-wasm-grammar-prs.js

160 lines
6.1 KiB
JavaScript

/*
* This script is called via `validate-wasm-grammar-prs.yml`
* It's purpose is to ensure that everytime a `.wasm` file is changed in a PR
* That the `parserSource` key of the grammar that uses that specific `.wasm`
* file is also updated.
* This way we can ensure that the `parserSource` is always accurate, and is
* never forgotten about.
*/
const cp = require("node:child_process");
const path = require("node:path");
const fs = require("node:fs");
const CSON = require("season");
// Change this if you want more logs
let verbose = true;
// Lets first find our common ancestor commit
// This lets us determine the commit where the branch or fork departed from
const commonAncestorCmd = cp.spawnSync("git", [ "merge-base", "origin/master", "HEAD^" ]);
if (commonAncestorCmd.status !== 0 || commonAncestorCmd.stderr.toString().length > 0) {
console.error("Git Command has failed!");
console.error("'git merge-base origin/master HEAD^'");
console.error(commonAncestorCmd.stderr.toString());
process.exit(1);
}
const commit = commonAncestorCmd.stdout.toString().trim();
if (verbose) {
console.log(`Common Ancestor Commit: '${commit}'`);
}
const cmd = cp.spawnSync("git", [ "diff", "--name-only", "-r", "HEAD", commit])
if (cmd.status !== 0 || cmd.stderr.toString().length > 0) {
console.error("Git Command has failed!");
console.error(`'git diff --name-only -r HEAD ${commit}'`);
console.error(cmd.stderr.toString());
process.exit(1);
}
const changedFiles = cmd.stdout.toString().split("\n");
// This gives us an array of the name and path of every single changed file from the last two commits
// Now to check if there's any changes we care about.
if (verbose) {
console.log("Array of changed files between commits:");
console.log(changedFiles);
}
const wasmFilesChanged = changedFiles.filter(element => element.endsWith(".wasm"));
if (wasmFilesChanged.length === 0) {
// No WASM files have been modified. Return success
console.log("No WASM files have been changed.");
process.exit(0);
}
// Now for every single wasm file that's been changed, we must validate those changes
// are also accompanied by a change in the `parserSource` key
for (const wasmFile of wasmFilesChanged) {
// Ignore files that have been deleted or moved.
if (!fs.existsSync(wasmFile)) {
console.log(`Skipping file that no longer exists: ${wasmFile}`);
continue;
}
const wasmPath = path.dirname(wasmFile);
// Don't check the base `tree-sitter.wasm` file.
if (wasmFile.includes('vendor/web-tree-sitter')) continue;
const files = fs.readdirSync(path.join(wasmPath, ".."));
console.log(`Detected changes to: ${wasmFile}`);
if (verbose) {
console.log("Verbose file check details:");
console.log(wasmFile);
console.log(wasmPath);
console.log(files);
console.log("\n");
}
for (const file of files) {
// Only check `cson` files.
if (!file.endsWith('.cson')) continue;
const filePath = path.join(wasmPath, "..", file);
console.log(`Checking: ${filePath}`);
if (fs.lstatSync(filePath).isFile()) {
const contents = CSON.readFileSync(filePath);
// We now have the contents of one of the grammar files for this specific grammar.
// Since each grammar may contain multiple grammar files, we need to ensure
// that this particular one is using the tree-sitter wasm file that was
// actually changed.
const grammarFile = contents.treeSitter?.grammar ?? "";
if (path.basename(grammarFile) === path.basename(wasmFile)) {
// This grammar uses the WASM file that's changed. So we must ensure our key has also changed
// Sidenote we use `basename` here, since the `wasmFile` will be
// a path relative from the root of the repo, meanwhile `grammarFile`
// will be relative from the file itself
// In order to check the previous state of what the key is, we first must retreive the file prior to this PR
const getPrevFile = cp.spawnSync("git", [ "show", `${commit}:./${filePath}` ]);
if (getPrevFile.status !== 0 || getPrevFile.stderr.toString().length > 0) {
// This can fail for two major reasons
// 1. The `git show` command has returned an error code other than `0`, failing.
// 2. This is a new file, and it failed to find an earlier copy (which didn't exist)
// So that we don't fail brand new TreeSitter grammars, we manually check for number 2
if (getPrevFile.stderr.toString().includes("exists on disk, but not in")) {
// Looks like this file is new. Skip this check
if (verbose) {
console.log("Looks like this file is new. Skipping...");
}
continue;
}
console.error("Git command failed!");
console.error(`'git show ${commit}:./${filePath}'`);
console.error(getPrevFile.stderr.toString());
process.exit(1);
}
fs.writeFileSync(path.join(wasmPath, "..", `OLD-${file}`), getPrevFile.stdout.toString());
const oldContents = CSON.readFileSync(path.join(wasmPath, "..", `OLD-${file}`));
const oldParserSource = oldContents.treeSitter?.parserSource ?? "";
const newParserSource = contents.treeSitter?.parserSource ?? "";
if (newParserSource.length === 0) {
console.error(`Failed to find the new \`parserSource\` within: '${filePath}'`);
console.error(contents.treeSitter);
process.exit(1);
}
if (oldParserSource == newParserSource) {
// The repo and commit is identical! This means it hasn't been updated
console.error(`The \`parserSource\` key of '${filePath}' has not been updated!`);
console.error(`Current key: ${newParserSource} - Old key: ${oldParserSource}`);
process.exit(1);
}
// Else it looks like it has been updated properly
console.log(`Validated \`parserSource\` has been updated within '${filePath}' properly.`);
} else {
if (verbose) {
console.log("This grammar file doesn't use a WASM file that's changed (On the current iteration)");
}
}
}
}
}