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

[meta] Create Workflow to validate WASM Grammar Changes #740

Merged
merged 11 commits into from
Oct 26, 2023
30 changes: 30 additions & 0 deletions .github/workflows/validate-wasm-grammar-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Validate WASM Grammar PR Changes
# Since we now want to enforce the rule that any changes to a WASM grammar binary
# file, is accompanied by a change to the `parserSource` key within the
# `grammar.cson` file. This GHA will preform this check for us.

on:
pull_request:
paths:
- '**.wasm'

jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout the Latest Code
uses: actions/checkout@v3
with:
fetch-depth: 0
# Make sure we get all commits, so that we can compare to early commits

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16

- name: Install dependencies
run: yarn install
confused-Techie marked this conversation as resolved.
Show resolved Hide resolved

- name: Run Validation Script
run: node ./script/validate-wasm-grammar-prs.js
148 changes: 148 additions & 0 deletions script/validate-wasm-grammar-prs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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) {
const wasmPath = path.dirname(wasmFile);

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) {
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 interation)");
confused-Techie marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
}
Loading