Skip to content

Commit

Permalink
FEAT: automate update_core_deprecations (#23)
Browse files Browse the repository at this point in the history
* DEV: add workflow and update_discourse_core_deprecations script
  • Loading branch information
tyb-talks authored Jul 24, 2024
1 parent 6229a3d commit 0309869
Show file tree
Hide file tree
Showing 6 changed files with 1,234 additions and 2 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/update-discourse-core-deprecations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Update Discourse Core Deprecations
on:
schedule:
- cron: '0 0 * * 1' # run every Monday 00:00 UTC
workflow_dispatch:

jobs:
update-discourse-core-deprecations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/checkout@v4
with:
repository: discourse/discourse
path: 'discourse'

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 21
cache: yarn

- name: Yarn install
run: yarn install

- name: Run deprecation script
id: run-script
run: |
node ./scripts/update_discourse_core_deprecations.mjs ./discourse
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
commit-message: Update discourse core deprecations
title: Update Discourse Core Deprecations
body: |
This PR updates the list of Discourse deprecations in the `deprecation-ids.yml` file.
The `files_to_debug.txt` file contains paths of files that should be checked for deprecation IDs that
were not found by the script. Please remove those entries (**but not the file**) before merging.
branch: update-discourse-core-deprecations
delete-branch: true
add-paths: |
./lib/deprecation_collector/deprecation-ids.yml
./scripts/files_to_debug.txt
6 changes: 6 additions & 0 deletions babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"presets": ["@babel/preset-env"],
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "2023-11" }],
]
}
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
{
"private": true,
"devDependencies": {
"@babel/parser": "^7.24.8",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/preset-env": "^7.24.8",
"@babel/traverse": "^7.24.8",
"@babel/types": "^7.24.8",
"@discourse/lint-configs": "1.3.9",
"content-tag": "^2.0.1",
"ember-template-lint": "6.0.0",
"eslint": "8.57.0",
"prettier": "2.8.8"
Expand Down
Empty file added scripts/files_to_debug.txt
Empty file.
235 changes: 235 additions & 0 deletions scripts/update_discourse_core_deprecations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import fs from "fs";
import path from "path";
import yaml from "js-yaml";
import parser from "@babel/parser";
import _traverse from "@babel/traverse";
import * as t from "@babel/types";
import { promisify } from "util";
import { Preprocessor } from "content-tag";

const traverse = _traverse.default;
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
const stat = promisify(fs.stat);
const GJSPreprocessor = new Preprocessor();

const EXCLUDED_DIR_PATTERNS = [
"/app/assets/javascripts/discourse/tests/unit/",
"/discourse/tmp/",
"node_modules",
"/discourse/dist/",
"/discourse/vendor/",
"/discourse/public/",
"/discourse/spec/",
"/discourse/plugins/",
];
const filesToDebug = [];

async function isExcludedDir(filePath) {
return EXCLUDED_DIR_PATTERNS.some((pattern) => filePath.includes(pattern));
}

function extractId(node, scope, ast = null) {
if (t.isObjectExpression(node)) {
for (const prop of node.properties) {
if (
t.isObjectProperty(prop) &&
t.isIdentifier(prop.key, { name: "id" })
) {
if (t.isStringLiteral(prop.value)) {
return [prop.value.value];
} else if (t.isIdentifier(prop.value)) {
return resolveIdentifier(prop.value.name, scope, ast);
}
}
}
}
return [];
}

function resolveIdentifier(name, scope, ast = null) {
const binding = scope.getBinding(name);
if (!binding) {
return [];
}

if (t.isVariableDeclarator(binding.path.node)) {
const init = binding.path.node.init;

if (t.isIdentifier(init)) {
return resolveIdentifier(init.name, binding.path.scope);
} else if (t.isObjectExpression(init)) {
return extractId(init, binding.path.scope);
} else if (t.isArrayExpression(init)) {
for (const currNode of init.elements) {
if (t.isObjectExpression(currNode)) {
return extractId(currNode, binding.path.scope);
}
}
}
}
// discovery-controller-shim case
if (t.isIdentifier(binding.path.node) && binding.kind === "param") {
let argIndex;
const calleeFunctionName = binding.path.findParent((p) => {
if (p.isFunctionDeclaration()) {
const matchingArg = p.node.params.find((p) => p.name === name);
if (matchingArg) {
argIndex = p.node.params.findIndex((p) => p.name === name);
return true;
}
}
})?.node?.id?.name;

return traverseForDeprecationId(ast, name, calleeFunctionName, argIndex);
}

return [];
}

function traverseForDeprecationId(
ast,
deprecationIdName,
calleeFunctionName,
argIndex
) {
const ids = [];
if (!ast) {
return null;
}

traverse(ast, {
CallExpression(path) {
if (t.isIdentifier(path.node.callee, { name: calleeFunctionName })) {
const id = path.node.arguments[argIndex].value;
if (id) {
ids.push(id);
}
}
},
});

return ids;
}

async function parseFile(filePath) {
let hasDeprecatedFunction = false;
let code = await readFile(filePath, "utf8");

try {
if (filePath.endsWith(".gjs")) {
code = GJSPreprocessor.process(code);
}

const ast = parser.parse(code, {
sourceType: "module",
plugins: [["decorators", { version: "2023-11" }]],
errorRecovery: true,
});
const ids = [];

traverse(ast, {
CallExpression(path) {
if (t.isIdentifier(path.node.callee, { name: "deprecated" })) {
hasDeprecatedFunction = true;
for (const arg of path.node.arguments) {
if (t.isObjectExpression(arg)) {
const extractedIds = extractId(arg, path.scope, ast);
if (extractedIds) {
ids.push(...extractedIds);
}
} else if (t.isIdentifier(arg)) {
const resolvedIds = resolveIdentifier(arg.name, path.scope);
if (resolvedIds) {
ids.push(...resolvedIds);
}
} else if (t.isSpreadElement(arg)) {
const resolvedIds = resolveIdentifier(
arg.argument.name,
path.scope
);
if (resolvedIds) {
ids.push(...resolvedIds);
}
}
}
}
},
});
return [ids, hasDeprecatedFunction];
} catch (error) {
console.error(`Error parsing file: ${filePath}`);
console.error(error);
return [[], false];
}
}

async function parseDirectory(directoryPath) {
const ids = [];
const files = await readdir(directoryPath);

for (const file of files) {
const filePath = path.join(directoryPath, file);

if (await isExcludedDir(filePath)) {
continue;
}

const fileStat = await stat(filePath);

if (
fileStat.isFile() &&
(filePath.endsWith(".js") || filePath.endsWith(".gjs"))
) {
const [parsedIds, hasDeprecatedFunction] = await parseFile(filePath);

if (hasDeprecatedFunction && parsedIds.length === 0) {
console.log(`DEBUG THE FILE: ${filePath}`);
filesToDebug.push(filePath);
}

ids.push(...parsedIds);
} else if (fileStat.isDirectory()) {
ids.push(...(await parseDirectory(filePath)));
}
}

return ids;
}

// Main script
(async () => {
if (process.argv.length < 3) {
console.log("Usage: node update_discourse_deprecations.js <CODEBASE_DIR>");
process.exit(1);
}

const directoryPath = process.argv[2];
const ids = [...new Set(await parseDirectory(directoryPath))].sort();

if (filesToDebug.length > 0) {
const filesToDebugFilePath = path.join(
".",
"scripts",
"files_to_debug.txt"
)
fs.writeFileSync(filesToDebugFilePath, filesToDebug.join("\n"));
}

const deprecationIdsFilePath = path.join(
".",
"lib",
"deprecation_collector",
"deprecation-ids.yml"
);
const deprecationIds = yaml.load(
fs.readFileSync(deprecationIdsFilePath, "utf8")
);
deprecationIds["discourse_deprecation_ids"] = ids;
fs.writeFileSync(
deprecationIdsFilePath,
"---\n" + yaml.dump(deprecationIds, { noArrayIndent: true }),
"utf8"
);
console.log(`${ids.length} Extracted IDs saved to ${deprecationIdsFilePath}`);
})();
Loading

0 comments on commit 0309869

Please sign in to comment.