-
Notifications
You must be signed in to change notification settings - Fork 255
👷(CLDSRV-860) Monitor async await migration #6088
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
Changes from all commits
08265a7
9623ae1
f7e599c
1899244
9fca0ae
380abb7
5ac3a2a
c4be965
429ded3
d134d53
84461c0
4682684
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| /** | ||
| * @name Callback-style function (async migration) | ||
| * @description These functions use callback parameters. They should be refactored to use async/await. | ||
| * @kind problem | ||
| * @problem.severity recommendation | ||
| * @id js/callback-style-function | ||
| * @tags maintainability | ||
| * async-migration | ||
| */ | ||
|
|
||
| import javascript | ||
|
|
||
| from Function f, Parameter p | ||
| where | ||
| p = f.getParameter(f.getNumParameter() - 1) and | ||
| p.getName().regexpMatch("(?i)^(cb|callback|next|done)$") and | ||
| not f.isAsync() and | ||
| // Exclude test files and node_modules | ||
| not f.getFile().getAbsolutePath().matches("%/tests/%") and | ||
| not f.getFile().getAbsolutePath().matches("%/node_modules/%") | ||
| select f, "This function uses a callback parameter ('" + p.getName() + "'). Refactor to async/await." |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| - description: Scality Cloudserver Async Migration Suite | ||
| - queries: . |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| /** | ||
| * @name Promise .then() usage (async migration) | ||
| * @description These calls use .then() instead of async/await. They should be refactored to use async/await. | ||
| * @kind problem | ||
| * @problem.severity recommendation | ||
| * @id js/promise-then-usage | ||
| * @tags maintainability | ||
| * async-migration | ||
| */ | ||
|
|
||
| import javascript | ||
|
|
||
| from MethodCallExpr m | ||
| where | ||
| m.getMethodName() = "then" and | ||
| // Exclude test files and node_modules | ||
| not m.getFile().getAbsolutePath().matches("%/tests/%") and | ||
| not m.getFile().getAbsolutePath().matches("%/node_modules/%") | ||
| select m, "This call uses .then(). Refactor to async/await." |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| name: scality/cloudserver-async-migration | ||
| version: 0.0.1 | ||
| dependencies: | ||
| codeql/javascript-all: "*" | ||
|
DarkIsDude marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| /** | ||
| * Check that all new/modified functions in the current git diff use async/await. | ||
| * Fails with exit code 1 if any additions introduce callback-style functions. | ||
| * | ||
| * Usage: node scripts/check-diff-async.mjs | ||
| * In CI: runs against the current PR diff (files changed vs base branch) | ||
| */ | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { Project, SyntaxKind } from 'ts-morph'; | ||
|
|
||
| const CALLBACK_PARAM_PATTERN = /^(cb|callback|next|done)$/i; | ||
|
DarkIsDude marked this conversation as resolved.
DarkIsDude marked this conversation as resolved.
DarkIsDude marked this conversation as resolved.
|
||
|
|
||
| function getChangedJsFiles() { | ||
| const base = process.env.GITHUB_BASE_REF | ||
| ? `origin/${process.env.GITHUB_BASE_REF}` | ||
| : 'HEAD'; | ||
| const output = execFileSync('git', [ | ||
| 'diff', | ||
| '--name-only', | ||
| '--diff-filter=ACMR', | ||
| base, | ||
| '--', | ||
|
DarkIsDude marked this conversation as resolved.
|
||
| '**/*.js', | ||
| ], { encoding: 'utf8' }).trim(); | ||
|
|
||
| return output ? output.split('\n').filter(f => f.endsWith('.js')) : []; | ||
| } | ||
|
|
||
| /** | ||
| * Get added line numbers for a file in the current diff. | ||
| */ | ||
| function getAddedLineNumbers(filePath) { | ||
| const base = process.env.GITHUB_BASE_REF | ||
| ? `origin/${process.env.GITHUB_BASE_REF}` | ||
| : 'HEAD'; | ||
| const diff = execFileSync('git', ['diff', base, '--', filePath], { encoding: 'utf8' }); | ||
| const addedLines = new Set(); | ||
| let currentLine = 0; | ||
|
|
||
| for (const line of diff.split('\n')) { | ||
| const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/); | ||
|
|
||
| if (hunkMatch) { | ||
| currentLine = parseInt(hunkMatch[1], 10) - 1; | ||
| continue; | ||
| } | ||
|
|
||
| if (line.startsWith('+') && !line.startsWith('+++')) { | ||
| currentLine++; | ||
| addedLines.add(currentLine); | ||
| } else if (!line.startsWith('-')) { | ||
| currentLine++; | ||
| } | ||
| } | ||
|
|
||
| return addedLines; | ||
| } | ||
|
|
||
| const changedFiles = getChangedJsFiles(); | ||
| if (changedFiles.length === 0) { | ||
| console.log('No changed JS files to check.'); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| console.log(`Checking ${changedFiles.length} changed JS file(s) for async/await compliance...\n`); | ||
|
|
||
| const project = new Project({ | ||
| compilerOptions: { allowJs: true, noEmit: true }, | ||
| skipAddingFilesFromTsConfig: true, | ||
| }); | ||
|
|
||
| const filesToCheck = changedFiles.filter(f => | ||
| !f.startsWith('tests/') && | ||
| !f.startsWith('node_modules/') && | ||
| ( | ||
| f.startsWith('lib/') || | ||
| f.startsWith('bin/') || | ||
| !f.includes('/') | ||
| ) | ||
| ); | ||
| if (filesToCheck.length === 0) { | ||
| console.log('No source JS files in diff (tests and node_modules excluded).'); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| project.addSourceFilesAtPaths(filesToCheck); | ||
|
|
||
| const violations = []; | ||
|
|
||
| for (const sourceFile of project.getSourceFiles()) { | ||
| const filePath = sourceFile.getFilePath().replace(process.cwd() + '/', ''); | ||
| const addedLines = getAddedLineNumbers(filePath); | ||
|
|
||
| if (addedLines.size === 0) continue; | ||
|
|
||
| const functions = [ | ||
| ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration), | ||
| ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression), | ||
| ...sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction), | ||
| ...sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration), | ||
| ]; | ||
|
|
||
| for (const fn of functions) { | ||
| if (fn.isAsync()) continue; | ||
|
|
||
| const startLine = fn.getStartLineNumber(); | ||
| if (!addedLines.has(startLine)) continue; | ||
|
DarkIsDude marked this conversation as resolved.
|
||
|
|
||
| const params = fn.getParameters(); | ||
| const lastParam = params[params.length - 1]; | ||
| if (lastParam && CALLBACK_PARAM_PATTERN.test(lastParam.getName())) { | ||
| violations.push({ | ||
| file: filePath, | ||
| line: startLine, | ||
| type: 'callback', | ||
| detail: `function has callback parameter '${lastParam.getName()}'`, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (violations.length === 0) { | ||
| console.log('✓ All new code in the diff uses async/await.'); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| console.error(`✗ Found ${violations.length} async/await violation(s) in the diff:\n`); | ||
| for (const v of violations) { | ||
| console.error(` ${v.file}:${v.line} [${v.type}] ${v.detail}`); | ||
| } | ||
| console.error('\nNew code must use async/await instead of callbacks.'); | ||
| console.error('See the async/await migration guide in CONTRIBUTING.md for help.'); | ||
| process.exit(1); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| /** | ||
| * Count async vs callback-style functions across the codebase using ts-morph. | ||
| * Used in CI to track async/await migration progress over time. | ||
| * | ||
| * Usage: node scripts/count-async-functions.mjs | ||
| */ | ||
| import { readFileSync, appendFileSync, writeFileSync } from 'node:fs'; | ||
| import { Project, SyntaxKind } from 'ts-morph'; | ||
|
|
||
| function getSourcePathsFromPackageJson() { | ||
| const packageJsonPath = new URL('../../package.json', import.meta.url); | ||
| const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); | ||
| const paths = packageJson.countAsyncSourcePaths; | ||
|
|
||
| if (Array.isArray(paths) && paths.length > 0 && paths.every(p => typeof p === 'string')) { | ||
| return paths; | ||
| } | ||
|
|
||
| throw new Error('package.json must define a non-empty string array "countAsyncSourcePaths"'); | ||
| } | ||
|
|
||
| const project = new Project({ | ||
| compilerOptions: { | ||
| allowJs: true, | ||
| noEmit: true, | ||
| }, | ||
| skipAddingFilesFromTsConfig: true, | ||
| }); | ||
|
|
||
| project.addSourceFilesAtPaths(getSourcePathsFromPackageJson()); | ||
|
|
||
| let asyncFunctions = 0; | ||
| let totalFunctions = 0; | ||
| let callbackFunctions = 0; | ||
| let thenChains = 0; | ||
|
|
||
| const CALLBACK_PARAM_PATTERN = /^(cb|callback|next|done)$/i; | ||
|
DarkIsDude marked this conversation as resolved.
|
||
|
|
||
| for (const sourceFile of project.getSourceFiles()) { | ||
| const functions = [ | ||
| ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration), | ||
| ...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression), | ||
| ...sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction), | ||
| ...sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration), | ||
| ]; | ||
|
|
||
| for (const fn of functions) { | ||
| totalFunctions++; | ||
|
|
||
| if (fn.isAsync()) { | ||
| asyncFunctions++; | ||
| continue; | ||
| } | ||
|
|
||
| const params = fn.getParameters(); | ||
| const lastParam = params[params.length - 1]; | ||
| if (lastParam && CALLBACK_PARAM_PATTERN.test(lastParam.getName())) { | ||
| callbackFunctions++; | ||
| } | ||
| } | ||
|
|
||
| const propertyAccesses = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression); | ||
| for (const access of propertyAccesses) { | ||
| if (access.getName() === 'then') { | ||
|
DarkIsDude marked this conversation as resolved.
DarkIsDude marked this conversation as resolved.
DarkIsDude marked this conversation as resolved.
DarkIsDude marked this conversation as resolved.
DarkIsDude marked this conversation as resolved.
DarkIsDude marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| thenChains++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const asyncFunctionPercent = totalFunctions > 0 | ||
| ? ((asyncFunctions / totalFunctions) * 100).toFixed(1) | ||
| : '0.0'; | ||
|
|
||
| const migrationPercent = (asyncFunctions + callbackFunctions) > 0 | ||
| ? ((asyncFunctions / (asyncFunctions + callbackFunctions)) * 100).toFixed(1) | ||
| : '0.0'; | ||
|
|
||
| console.log('=== Async/Await Migration Progress ==='); | ||
| console.log(`Total functions: ${totalFunctions}`); | ||
| console.log(`Async functions: ${asyncFunctions} (${asyncFunctionPercent}%)`); | ||
| console.log(`Callback functions: ${callbackFunctions}`); | ||
| console.log(`Remaining .then(): ${thenChains}`); | ||
|
DarkIsDude marked this conversation as resolved.
|
||
| console.log(''); | ||
| console.log(`Migration (trend): ${asyncFunctions}/${asyncFunctions + callbackFunctions} (${migrationPercent}%)`); | ||
|
|
||
| if (process.env.GITHUB_STEP_SUMMARY) { | ||
| appendFileSync(process.env.GITHUB_STEP_SUMMARY, [ | ||
|
francoisferrand marked this conversation as resolved.
|
||
| '## Async/Await Migration Progress', | ||
| '', | ||
| `| Metric | Count |`, | ||
| `|--------|-------|`, | ||
| `| Total functions | ${totalFunctions} |`, | ||
| `| Async functions | ${asyncFunctions} (${asyncFunctionPercent}%) |`, | ||
| `| Callback-style functions | ${callbackFunctions} |`, | ||
| `| Remaining \`.then()\` chains | ${thenChains} |`, | ||
| `| Migration trend (async / (async + callback)) | ${asyncFunctions}/${asyncFunctions + callbackFunctions} (${migrationPercent}%) |`, | ||
| '', | ||
| ].join('\n')); | ||
|
|
||
| // Output benchmark JSON for visualization | ||
| const benchmarkData = [ | ||
| { | ||
| name: 'Async Migration Progress', | ||
| unit: '%', | ||
| value: parseFloat(migrationPercent), | ||
| }, | ||
| { | ||
| name: 'Async Functions Percentage', | ||
| unit: '%', | ||
| value: parseFloat(asyncFunctionPercent), | ||
| }, | ||
| { | ||
| name: 'Total callback functions', | ||
| unit: 'count', | ||
| value: callbackFunctions, | ||
| } | ||
| ]; | ||
| writeFileSync('async-migration-benchmark.json', JSON.stringify(benchmarkData, null, 2)); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.