diff --git a/functions/createReview/app.js b/functions/createReview/app.js index 1a94ccd..c51d17a 100644 --- a/functions/createReview/app.js +++ b/functions/createReview/app.js @@ -2,8 +2,11 @@ import * as dotenv from 'dotenv' import { createLLMPRComments } from './llm/index.js' import { createASTPRComments } from './astParsing/index.js' import { createRegexComments } from './regex/index.js' +import { REVIEW_TYPE, filterOutInvalidComments } from './utils.js' import getOctokit from './oktokit/index.js' -import { filterOutInvalidComments } from './utils.js' +import pino from 'pino' + +const logger = pino({ name: 'reviewbot' }) dotenv.config() /** @@ -12,36 +15,45 @@ dotenv.config() * @return {void} */ export default async function app(message) { - console.log('[reviewbot] - createReview') + logger.info('createReview') const messageContext = JSON.parse( Buffer.from(message.data, 'base64').toString() ) - console.log('[reviewbot] - creating suggestions') + let comments = [] - const llmComments = await createLLMPRComments( - messageContext.files, - messageContext.diff - ) + if (messageContext.reviewType === REVIEW_TYPE.LLM) { + logger.info('Creating LLM review comments') + comments = await createLLMPRComments( + messageContext.files, + messageContext.diff + ) + } else if (messageContext.reviewType === REVIEW_TYPE.RuleBased) { + logger.info('Creating rule based review comments') + const astComments = createASTPRComments( + messageContext.files, + messageContext.fullFiles, + messageContext.diff + ) - const astComments = createASTPRComments( - messageContext.files, - messageContext.fullFiles, - messageContext.diff - ) + const regexpComments = createRegexComments( + messageContext.files, + messageContext.diff + ) - const regexpComments = createRegexComments( - messageContext.files, - messageContext.diff - ) + comments = astComments.concat(regexpComments) + } else { + logger.warn( + 'Received a message but could not determine the type of review requested. Ignoring the message.' + ) + return + } - const comments = filterOutInvalidComments( - llmComments.concat(astComments, regexpComments) - ) + comments = filterOutInvalidComments(comments) - console.log( - `[reviewbot] - creating review with ${comments.length} comments for commit ${messageContext.latestCommit}` + logger.info( + `creating review with ${comments.length} comments for commit ${messageContext.latestCommit}` ) const octokit = await getOctokit(messageContext.installationId) @@ -56,5 +68,5 @@ export default async function app(message) { comments: comments }) - console.log('[reviewbot] - review finished') + logger.info('review finished') } diff --git a/functions/createReview/astParsing/index.js b/functions/createReview/astParsing/index.js index 4661063..60d8123 100644 --- a/functions/createReview/astParsing/index.js +++ b/functions/createReview/astParsing/index.js @@ -7,6 +7,10 @@ import jsxPlugin from 'acorn-jsx' import validators from './rules/index.js' import { parseAndAggregate } from './parser.js' import { mapLineToDiff } from 'map-line-to-diff' +import pino from 'pino' +import { filterAcceptedFiles, filterOnlyModified } from '../utils.js' + +const logger = pino({ name: 'reviewbot' }) function getParser(fileName) { const fileExtension = path.extname(fileName) @@ -38,8 +42,8 @@ export function generateAST(fileContent, fileDiff) { locations: true }) } catch (err) { - console.log( - `[reviewbot] - Failed to parse the file content for [${fileDiff.afterName}] into AST. Falling back to the loose parser. Error: ${err.message}` + logger.info( + `Failed to parse the file content for [${fileDiff.afterName}] into AST. Falling back to the loose parser. Error: ${err.message}` ) ast = LooseParser.parse(fileContent, { locations: true }) } @@ -70,20 +74,28 @@ export function parseForIssues(astDocument, gitDiff) { export function createASTPRComments(gitDiff, filesContents, rawDiff) { if (!filesContents) { - console.log( - '[reviewbot] - Skipping AST comment generation because raw files contents are missing' + logger.info( + 'Skipping AST comment generation because raw files contents are missing' ) return [] } + const acceptedFiles = filterAcceptedFiles(gitDiff) + const filesWithModifiedLines = filterOnlyModified(acceptedFiles) + let comments = [] - for (let fileDiff of gitDiff) { + logger.info( + `Evaluating ${filesWithModifiedLines.length} files for AST rules violations` + ) + for (let fileDiff of filesWithModifiedLines) { const fileContent = filesContents.find( fileContent => fileContent.filename === fileDiff.afterName ) if (fileContent) { const ast = generateAST(fileContent.content, fileDiff) const fileIssues = parseForIssues(ast, fileDiff) - + logger.info( + `Found ${fileIssues.length} violations in file ${fileContent.filename}` + ) for (let issue of fileIssues) { comments.push({ path: fileContent.filename, diff --git a/functions/createReview/astParsing/rules/P10.example.js_10_P10.diff b/functions/createReview/astParsing/rules/P10.example.js_10_P10.diff deleted file mode 100644 index f3186cd..0000000 --- a/functions/createReview/astParsing/rules/P10.example.js_10_P10.diff +++ /dev/null @@ -1,2232 +0,0 @@ -From 7f658f7ec0af7ff545ebafe1418dd010f70cca79 Mon Sep 17 00:00:00 2001 -From: Nicolas Herment -Date: Wed, 25 Oct 2023 09:21:56 +0200 -Subject: [PATCH 1/4] feat: add AST parsing code (WIP) - ---- - .eslintrc | 2 +- - functions/createReview/astParsing/index.js | 34 +++++++ - functions/createReview/astParsing/parser.js | 97 +++++++++++++++++++ - .../astParsing/rules/J3_no_parse_stringify.js | 17 ++++ - .../astParsing/rules/J8_no_var.js | 15 +++ - .../astParsing/rules/P10.example.js | 22 +++++ - .../astParsing/rules/P10_loop_await.js | 24 +++++ - .../astParsing/rules/P11.example.js | 4 + - .../astParsing/rules/P11_unbound_promise.js | 12 +++ - .../createReview/astParsing/rules/index.js | 11 +++ - package-lock.json | 26 ++++- - package.json | 1 + - tests/astParsing.test.js | 71 ++++++++++++++ - 13 files changed, 331 insertions(+), 5 deletions(-) - create mode 100644 functions/createReview/astParsing/index.js - create mode 100644 functions/createReview/astParsing/parser.js - create mode 100644 functions/createReview/astParsing/rules/J3_no_parse_stringify.js - create mode 100644 functions/createReview/astParsing/rules/J8_no_var.js - create mode 100644 functions/createReview/astParsing/rules/P10.example.js - create mode 100644 functions/createReview/astParsing/rules/P10_loop_await.js - create mode 100644 functions/createReview/astParsing/rules/P11.example.js - create mode 100644 functions/createReview/astParsing/rules/P11_unbound_promise.js - create mode 100644 functions/createReview/astParsing/rules/index.js - create mode 100644 tests/astParsing.test.js - -diff --git a/.eslintrc b/.eslintrc -index a4f2a22..e71fb27 100644 ---- a/.eslintrc -+++ b/.eslintrc -@@ -8,5 +8,5 @@ - "ecmaVersion": 2022, - "sourceType": "module" - }, -- "ignorePatterns": ["dist/**"] -+ "ignorePatterns": ["dist/**", "functions/createReview/astParsing/rules/*.example.js"] - } -diff --git a/functions/createReview/astParsing/index.js b/functions/createReview/astParsing/index.js -new file mode 100644 -index 0000000..3b3ccbc ---- /dev/null -+++ b/functions/createReview/astParsing/index.js -@@ -0,0 +1,34 @@ -+import { Parser } from 'acorn' -+// import tsPlugin from 'acorn-typescript' -+// import { LooseParser } from 'acorn-loose' -+// import jsxPlugin from 'acorn-jsx' -+ -+import validators from './rules/index.js' -+import { parseAndAggregate } from './parser.js' -+ -+const parser = Parser -+ .extend -+ // jsxPlugin() -+ () -+ -+export function generateAST(fileContent) { -+ return parser.parse(fileContent, { -+ sourceType: 'module', -+ locations: true -+ }) -+} -+ -+function aggregateComments(astNode) { -+ const comments = [] -+ for (let validator of validators) { -+ const comment = validator(astNode) -+ if (comment) { -+ comments.push(comment) -+ } -+ } -+ return comments -+} -+ -+export function parseForIssues(astDocument) { -+ return parseAndAggregate(astDocument, aggregateComments) -+} -diff --git a/functions/createReview/astParsing/parser.js b/functions/createReview/astParsing/parser.js -new file mode 100644 -index 0000000..e5c81cd ---- /dev/null -+++ b/functions/createReview/astParsing/parser.js -@@ -0,0 +1,97 @@ -+// The properties in the AST tree that the parsing algorithm should walk through to recurse through the whole tree -+export const AST_PARSING_PROPS = [ -+ 'body', -+ 'declarations', -+ 'init', -+ 'expression', -+ 'argument', -+ 'arguments', -+ 'children' -+] -+ -+export function parseAndAggregate(astDocument, aggrFunc) { -+ let aggregatedData = aggrFunc(astDocument) -+ -+ if (typeof astDocument === 'object' && astDocument !== null) { -+ for (let prop of AST_PARSING_PROPS) { -+ if (prop in astDocument && Array.isArray(astDocument[prop])) { -+ for (let el of astDocument[prop]) { -+ const newAggrData = parseAndAggregate(el, aggrFunc) -+ aggregatedData = aggregatedData.concat(newAggrData) -+ } -+ } else if (prop in astDocument) { -+ const newAggrData = parseAndAggregate(astDocument[prop], aggrFunc) -+ aggregatedData = aggregatedData.concat(newAggrData) -+ } -+ } -+ } -+ return aggregatedData -+} -+ -+export function findChildNode(astDocument, boolFunction) { -+ if (typeof astDocument === 'object' && astDocument !== null) { -+ for (let prop of AST_PARSING_PROPS) { -+ if (prop in astDocument && Array.isArray(astDocument[prop])) { -+ for (let el of astDocument[prop]) { -+ console.log( -+ 'Evaluating child 1', -+ prop, -+ '->', -+ el.type, -+ boolFunction(el) -+ ) -+ if (boolFunction(el)) { -+ return el -+ } -+ const matchingChildNode = findChildNode(el, boolFunction) -+ if (matchingChildNode) { -+ return matchingChildNode -+ } -+ } -+ } else if (prop in astDocument) { -+ console.log( -+ 'Evaluating child 2', -+ prop, -+ '->', -+ astDocument[prop].type, -+ boolFunction(astDocument[prop]) -+ ) -+ if (boolFunction(astDocument[prop])) { -+ return astDocument[prop] -+ } -+ const matchingChildNode = findChildNode(astDocument[prop], boolFunction) -+ if (matchingChildNode) { -+ return matchingChildNode -+ } -+ } -+ } -+ } -+} -+ -+export function isCall(node, objectName, propertyName) { -+ if ( -+ typeof node === 'object' && -+ node !== null && -+ node.type == 'CallExpression' && -+ node.callee && -+ node.callee.object && -+ node.callee.property -+ ) { -+ const obj = node.callee.object -+ const prop = node.callee.property -+ if ( -+ obj.type == 'Identifier' && -+ obj.name == objectName && -+ prop.type == 'Identifier' && -+ prop.name == propertyName -+ ) { -+ return true -+ } -+ } -+} -+ -+export function isType(node, type) { -+ if (typeof node === 'object' && node !== null && node.type == type) { -+ return true -+ } -+} -diff --git a/functions/createReview/astParsing/rules/J3_no_parse_stringify.js b/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -new file mode 100644 -index 0000000..03db35f ---- /dev/null -+++ b/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -@@ -0,0 +1,17 @@ -+import { findChildNode, isCall } from '../parser.js' -+ -+export default function validator(node) { -+ if (isCall(node, 'JSON', 'parse')) { -+ const jsonStringifyChildNode = findChildNode(node, childNode => -+ isCall(childNode, 'JSON', 'stringify') -+ ) -+ if (jsonStringifyChildNode) { -+ return { -+ line: node.loc.start.line, -+ code: 'J3', -+ description: -+ 'Do not use JSON.parse(JSON.stringify(obj)) to clone objects because it is slow and resource intensive. Prefer a specialised library like [rfdc](https://github.com/davidmarkclements/rfdc).' -+ } -+ } -+ } -+} -diff --git a/functions/createReview/astParsing/rules/J8_no_var.js b/functions/createReview/astParsing/rules/J8_no_var.js -new file mode 100644 -index 0000000..e2e5fa4 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/J8_no_var.js -@@ -0,0 +1,15 @@ -+export default function validator(node) { -+ if ( -+ typeof node === 'object' && -+ node !== null && -+ node.type == 'VariableDeclaration' && -+ node.kind == 'var' -+ ) { -+ return { -+ line: node.loc.start.line, -+ code: 'J8', -+ description: -+ 'Use let or const instead of var because the scope for let and const is smaller. [Learn more about the difference](https://stackoverflow.com/questions/762011/what-is-the-difference-between-let-and-var).' -+ } -+ } -+} -diff --git a/functions/createReview/astParsing/rules/P10.example.js b/functions/createReview/astParsing/rules/P10.example.js -new file mode 100644 -index 0000000..4c36f82 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/P10.example.js -@@ -0,0 +1,22 @@ -+const myPromise = new Promise((resolve, reject) => { -+ setTimeout(() => { -+ resolve("foo") -+ }, 300) -+}) -+ -+async function test1() { -+ for(let i = 0 ; i < 10 ; i++) { -+ // expect:P10 -+ await myPromise() -+ } -+} -+ -+// function test2() { -+// [1,2,3].map(async () => await myPromise()) -+// } -+ -+// function test3() { -+ -+// [1,2,3].map(async () => await myPromise()) -+ -+// } -\ No newline at end of file -diff --git a/functions/createReview/astParsing/rules/P10_loop_await.js b/functions/createReview/astParsing/rules/P10_loop_await.js -new file mode 100644 -index 0000000..73f5576 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/P10_loop_await.js -@@ -0,0 +1,24 @@ -+import { findChildNode, isType } from '../parser.js' -+ -+export default function validator(node) { -+ if (isType(node, 'ForStatement')) { -+ const nestedAwaitExpression = findChildNode(node, childNode => -+ isType(childNode, 'AwaitExpression') -+ ) -+ if (nestedAwaitExpression) { -+ return { -+ line: nestedAwaitExpression.loc.start.line, -+ code: 'P10', -+ description: `Review instances of awaiting promises in imperative loops (for, while, do/while) or array native methods (forEach, map). -+ -+ In many cases they are unnecessary as parallel execution is preferred. -+ -+ Prefer using Promise native methods like Promise.all, or Promise.race, or Promise.allSettled or libraries like p-map as recommended next. -+ -+ If serial execution is needed, consider using a library like sindresorhus/p-series. -+ -+ There is also an eslint rule to enforce this which we recommend using.` -+ } -+ } -+ } -+} -diff --git a/functions/createReview/astParsing/rules/P11.example.js b/functions/createReview/astParsing/rules/P11.example.js -new file mode 100644 -index 0000000..be9ef02 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/P11.example.js -@@ -0,0 +1,4 @@ -+ -+ -+// expect:P11 -+Promise.all(myPromises) -\ No newline at end of file -diff --git a/functions/createReview/astParsing/rules/P11_unbound_promise.js b/functions/createReview/astParsing/rules/P11_unbound_promise.js -new file mode 100644 -index 0000000..f232bbb ---- /dev/null -+++ b/functions/createReview/astParsing/rules/P11_unbound_promise.js -@@ -0,0 +1,12 @@ -+import { isCall } from '../parser.js' -+ -+export default function validator(node) { -+ if (isCall(node, 'Promise', 'all')) { -+ return { -+ line: node.loc.start.line, -+ code: 'P11', -+ description: -+ 'Executing Promise.all(items.map(async => { … })) leads to the creation of an undefined number of Promises, each executing something asynchronous and possibly saturating the event loop and consuming much memory. Recommend using a library like sindresorhus/p-map or sindresorhus/p-all instead.' -+ } -+ } -+} -diff --git a/functions/createReview/astParsing/rules/index.js b/functions/createReview/astParsing/rules/index.js -new file mode 100644 -index 0000000..abaefd9 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/index.js -@@ -0,0 +1,11 @@ -+// import J3 from './J3_no_parse_stringify.js' -+// import J8 from './J8_no_var.js' -+import P10 from './P10_loop_await.js' -+import P11 from './P11_unbound_promise.js' -+ -+export default [ -+ // J3, -+ // J8, -+ P10, -+ P11 -+] -diff --git a/package-lock.json b/package-lock.json -index 1bc4c2d..68b74da 100644 ---- a/package-lock.json -+++ b/package-lock.json -@@ -10,9 +10,11 @@ - "dependencies": { - "@google-cloud/pubsub": "^4.0.6", - "@octokit/auth-app": "^6.0.1", -+ "acorn": "^8.10.0", - "octokit": "^3.1.1", - "parse-git-patch": "^1.2.2", -- "probot": "^12.3.1" -+ "probot": "^12.3.1", -+ "yarn": "^1.22.19" - }, - "devDependencies": { - "@commitlint/cli": "^18.0.0", -@@ -2541,7 +2543,6 @@ - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", -- "dev": true, - "bin": { - "acorn": "bin/acorn" - }, -@@ -8392,6 +8393,19 @@ - "node": ">=12" - } - }, -+ "node_modules/yarn": { -+ "version": "1.22.19", -+ "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", -+ "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==", -+ "hasInstallScript": true, -+ "bin": { -+ "yarn": "bin/yarn.js", -+ "yarnpkg": "bin/yarn.js" -+ }, -+ "engines": { -+ "node": ">=4.0.0" -+ } -+ }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", -@@ -10556,8 +10570,7 @@ - "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", -- "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", -- "dev": true -+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" - }, - "acorn-jsx": { - "version": "5.3.2", -@@ -14950,6 +14963,11 @@ - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - }, -+ "yarn": { -+ "version": "1.22.19", -+ "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", -+ "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==" -+ }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", -diff --git a/package.json b/package.json -index a6f50e8..0996218 100644 ---- a/package.json -+++ b/package.json -@@ -25,6 +25,7 @@ - "dependencies": { - "@google-cloud/pubsub": "^4.0.6", - "@octokit/auth-app": "^6.0.1", -+ "acorn": "^8.10.0", - "octokit": "^3.1.1", - "parse-git-patch": "^1.2.2", - "probot": "^12.3.1" -diff --git a/tests/astParsing.test.js b/tests/astParsing.test.js -new file mode 100644 -index 0000000..c5b0e0a ---- /dev/null -+++ b/tests/astParsing.test.js -@@ -0,0 +1,71 @@ -+import { describe, test } from 'node:test' -+import assert from 'node:assert' -+import { readFileSync } from 'fs' -+ -+import path from 'path' -+import url from 'url' -+ -+import { -+ parseForIssues, -+ generateAST -+} from '../functions/createReview/astParsing/index.js' -+ -+const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) -+ -+function parseForExpectedViolations(fileContent) { -+ return fileContent -+ .split('\n') -+ .map((line, index) => { -+ const match = /\/\/\s*expect\s*:\s*(\w+)/.exec(line) -+ if (match && match[1]) { -+ return { -+ line: index + 2, // assumes the relevant line is +1 of current line. -+ code: match[1] -+ } -+ } else { -+ return null -+ } -+ }) -+ .filter(match => match !== null) -+} -+ -+function removeDescription(violation) { -+ return { -+ line: violation.line, -+ code: violation.code -+ } -+} -+ -+describe('AST parsing unit testing', () => { -+ test('P10', () => { -+ const jsFile = readFileSync( -+ path.join( -+ __dirname, -+ '../functions/createReview/astParsing/rules/P10.example.js' -+ ), -+ 'utf8' -+ ) -+ const ast = generateAST(jsFile) -+ const violations = parseForIssues(ast).map(removeDescription) -+ -+ const expectedViolations = parseForExpectedViolations(jsFile) -+ assert.notEqual(violations.length, 0) -+ assert.deepEqual(violations, expectedViolations) -+ }) -+ -+ test('P11', () => { -+ const jsFile = readFileSync( -+ path.join( -+ __dirname, -+ '../functions/createReview/astParsing/rules/P11.example.js' -+ ), -+ 'utf8' -+ ) -+ const ast = generateAST(jsFile) -+ const violations = parseForIssues(ast).map(removeDescription) -+ -+ const expectedViolations = parseForExpectedViolations(jsFile) -+ assert.notEqual(violations.length, 0) -+ assert.deepEqual(violations, expectedViolations) -+ }) -+}) - -From 46b8ea66a053ac4c54cb61ba6978b12f56078c9c Mon Sep 17 00:00:00 2001 -From: Nicolas Herment -Date: Thu, 26 Oct 2023 09:50:27 +0200 -Subject: [PATCH 2/4] feat: ast, add more examples - ---- - functions/createReview/astParsing/rules/J3.example.js | 6 ++++++ - functions/createReview/astParsing/rules/J8.example.js | 8 ++++++++ - 2 files changed, 14 insertions(+) - create mode 100644 functions/createReview/astParsing/rules/J3.example.js - create mode 100644 functions/createReview/astParsing/rules/J8.example.js - -diff --git a/functions/createReview/astParsing/rules/J3.example.js b/functions/createReview/astParsing/rules/J3.example.js -new file mode 100644 -index 0000000..7deb04c ---- /dev/null -+++ b/functions/createReview/astParsing/rules/J3.example.js -@@ -0,0 +1,6 @@ -+export function example() { -+ const obj = {'foo': 'bar'} -+ -+ // expect: J3 -+ JSON.parse(JSON.stringify(obj)) -+} -\ No newline at end of file -diff --git a/functions/createReview/astParsing/rules/J8.example.js b/functions/createReview/astParsing/rules/J8.example.js -new file mode 100644 -index 0000000..de63bb3 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/J8.example.js -@@ -0,0 +1,8 @@ -+ -+export function example() { -+ const foo = 'bar' -+ let bar = 'foo' -+ // expect: J8 -+ var foobar = bar + foo -+ bar = foobar -+} -\ No newline at end of file - -From e13649b1d326d67cd2abc4b1424717700664e1b0 Mon Sep 17 00:00:00 2001 -From: Nicolas Herment -Date: Fri, 27 Oct 2023 16:44:10 +0200 -Subject: [PATCH 3/4] AST, WIP - ---- - functions/createReview/astParsing/index.js | 49 +++++++++++++------ - .../createReview/createSuggestions/index.js | 9 +++- - .../createSuggestions/prompt-engine.js | 4 +- - functions/createReview/oktokit/index.js | 38 ++++++++++++++ - functions/webhook/app.js | 13 ++++- - 5 files changed, 95 insertions(+), 18 deletions(-) - -diff --git a/functions/createReview/astParsing/index.js b/functions/createReview/astParsing/index.js -index 3b3ccbc..25ac1b5 100644 ---- a/functions/createReview/astParsing/index.js -+++ b/functions/createReview/astParsing/index.js -@@ -1,21 +1,41 @@ - import { Parser } from 'acorn' --// import tsPlugin from 'acorn-typescript' --// import { LooseParser } from 'acorn-loose' --// import jsxPlugin from 'acorn-jsx' -+import tsPlugin from 'acorn-typescript' -+import path from 'node:path' -+import { LooseParser } from 'acorn-loose' -+import jsxPlugin from 'acorn-jsx' - - import validators from './rules/index.js' - import { parseAndAggregate } from './parser.js' - --const parser = Parser -- .extend -- // jsxPlugin() -- () -+function getParser(fileName) { -+ const fileExtension = path.extname(fileName) -+ let parser; -+ if(fileExtension === '.ts' || fileExtension === '.tsx') { -+ parser = Parser.extend(tsPlugin()) -+ } else if(fileExtension === '.jsx') { -+ parser = Parser.extend(jsxPlugin()) -+ } else if(fileExtension === '.js') { -+ parser = Parser -+ } -+ return parser -+} -+ -+export function generateAST(fileDiff, fileContent) { -+ const parser = getParser(fileDiff.afterName) -+ if(!parser) return -+ let ast; -+ try { -+ ast = parser.parse(fileContent, { -+ sourceType: /^import\s+/.test(fileContent) ? 'module' : 'commonjs', -+ locations: true -+ }) -+ } catch(err) { -+ console.log('[reviewbot] - Failed to parse the file content into AST. Falling back to the loose parser.') -+ ast = LooseParser.parse(fileContent, {locations: true}) -+ } - --export function generateAST(fileContent) { -- return parser.parse(fileContent, { -- sourceType: 'module', -- locations: true -- }) -+ ast.diff = fileDiff -+ return ast - } - - function aggregateComments(astNode) { -@@ -29,6 +49,7 @@ function aggregateComments(astNode) { - return comments - } - --export function parseForIssues(astDocument) { -- return parseAndAggregate(astDocument, aggregateComments) -+export function parseForIssues(astDocument, gitDiff) { -+ // TODO: only report issues on added lines -+ return parseAndAggregate(astDocument, gitDiff, aggregateComments) - } -diff --git a/functions/createReview/createSuggestions/index.js b/functions/createReview/createSuggestions/index.js -index 9169e6f..a8d7514 100644 ---- a/functions/createReview/createSuggestions/index.js -+++ b/functions/createReview/createSuggestions/index.js -@@ -1,5 +1,7 @@ - import buildPrompt from './prompt-engine.js' - import generateSuggestions from './suggestions.js' -+import { generateAST } from '../astParsing/index.js' -+ - - /** - Creates suggestions for each file in a git diff using the ChatGPT transformer API. -@@ -7,12 +9,17 @@ import generateSuggestions from './suggestions.js' - @returns {Promise} - A promise that resolves to an array of objects containing suggestions for each file. - @throws {Error} If an error occurs while creating suggestions. - */ --async function createSuggestions(gitDiff) { -+async function createSuggestions(gitDiff, files) { - const prompts = buildPrompt(gitDiff) -+ - const response = await Promise.all( - prompts.map(async file => { - let suggestionsForFile = {} - -+ const ast = generateAST(file, files.find()) -+ // TODO run AST analysis -+ -+ - const transformerResponse = await generateSuggestions({ - transformerType: 'chatGPT', - payload: file.changes -diff --git a/functions/createReview/createSuggestions/prompt-engine.js b/functions/createReview/createSuggestions/prompt-engine.js -index 49f308f..5b4f65d 100644 ---- a/functions/createReview/createSuggestions/prompt-engine.js -+++ b/functions/createReview/createSuggestions/prompt-engine.js -@@ -10,7 +10,7 @@ function filterOnlyModified(files) { - function filterAcceptedFiles(files) { - const filteredFiles = files.filter( - f => -- path.extname(f.afterName) === '.js' || path.extname(f.afterName) === '.ts' -+ /\.[tj]sx?$/g.test(path.extname(f.afterName)) - ) - return filteredFiles - } -@@ -40,7 +40,7 @@ function enhanceWithPromptContext(change) { - based on analyzing the git diff in order to see whats changed. - The language in the snippet is JavaScript. - Feel free to provide any examples as markdown code snippets in your answer. -- -+ - ${change} - ` - return [ -diff --git a/functions/createReview/oktokit/index.js b/functions/createReview/oktokit/index.js -index b73270e..a2cfd36 100644 ---- a/functions/createReview/oktokit/index.js -+++ b/functions/createReview/oktokit/index.js -@@ -27,4 +27,42 @@ async function getOctokit(installationId) { - }) - } - -+export async function getPRContent(context) { -+ -+ const { owner, repo } = context.issue() -+ const { pull_number } = context.pullRequest() -+ -+ const response = await context.octokit.pulls.listFiles({ -+ owner, -+ repo, -+ pull_number, -+ }); -+ -+ let fullFiles = [] -+ if(response && response.data) { -+ fullFiles = response.data -+ } -+ const fetchFilesPromises = [] -+ for (const file of fullFiles) { -+ if (file.status === 'modified' || file.status === 'added') { -+ fetchFilesPromises.push( -+ context.octokit.request('GET /repos/:owner/:repo/contents/:path', { -+ owner, -+ repo, -+ path: file.filename, -+ ref: `pull/${pull_number}/head`, -+ }).then(response => { -+ const content = Buffer.from(response.data.content, 'base64').toString(); -+ console.log(content) -+ file.content = content -+ }) -+ ) -+ -+ } -+ } -+ await Promise.all(fetchFilesPromises); -+ -+ return fullFiles -+} -+ - export default getOctokit -diff --git a/functions/webhook/app.js b/functions/webhook/app.js -index 6992426..66f244d 100644 ---- a/functions/webhook/app.js -+++ b/functions/webhook/app.js -@@ -1,5 +1,6 @@ - import { PubSub } from '@google-cloud/pubsub' - import parseGitPatch from 'parse-git-patch' -+import { getPRContent } from '../createReview/oktokit/index.js' - - /** - * This is the main entrypoint to the Probot app -@@ -70,13 +71,23 @@ export default async app => { - return - } - -+ -+ const response = await context.octokit.pulls.listFiles({ -+ ...common, -+ pull_number: pullRequest.pull_number, -+ }); -+ -+ let fullFiles = await getPRContent(context) -+ -+ - const messageContext = { - ...common, - pull_number: pullRequest.pull_number, - diff, - latestCommit, - files, -- installationId: context.payload.installation.id -+ installationId: context.payload.installation.id, -+ fullFiles - } - - const pubsub = new PubSub({ - -From b8537f5a61dae7be7a216c417efd0f1b98cd7efb Mon Sep 17 00:00:00 2001 -From: Nicolas Herment -Date: Wed, 1 Nov 2023 08:58:13 +0100 -Subject: [PATCH 4/4] feat: integrate AST with LLM suggestion - ---- - functions/createReview/app.js | 31 +- - functions/createReview/astParsing/index.js | 70 ++- - functions/createReview/astParsing/parser.js | 4 + - .../astParsing/rules/J3_no_parse_stringify.js | 3 +- - .../astParsing/rules/J8_no_var.js | 3 +- - .../astParsing/rules/P10_loop_await.js | 15 +- - .../astParsing/rules/P11_unbound_promise.js | 9 +- - .../{createSuggestions => llm}/index.js | 36 +- - .../prompt-engine.js | 5 +- - .../{createSuggestions => llm}/suggestions.js | 0 - functions/createReview/oktokit/index.js | 34 +- - functions/webhook/app.js | 8 - - package-lock.json | 471 ++++++++++++------ - package.json | 2 + - tests/astParsing.test.js | 189 ++++++- - tests/create-suggestions/index.test.js | 18 +- - 16 files changed, 625 insertions(+), 273 deletions(-) - rename functions/createReview/{createSuggestions => llm}/index.js (52%) - rename functions/createReview/{createSuggestions => llm}/prompt-engine.js (95%) - rename functions/createReview/{createSuggestions => llm}/suggestions.js (100%) - -diff --git a/functions/createReview/app.js b/functions/createReview/app.js -index 65523cd..74d9806 100644 ---- a/functions/createReview/app.js -+++ b/functions/createReview/app.js -@@ -1,6 +1,6 @@ - import * as dotenv from 'dotenv' --import createSuggestions from './createSuggestions/index.js' --import { findLinePositionInDiff } from '../utils.js' -+import { createLLMPRComments } from './llm/index.js' -+import { createASTPRComments } from './astParsing/index.js' - import getOctokit from './oktokit/index.js' - - dotenv.config() -@@ -17,19 +17,20 @@ export default async function app(message) { - ) - - console.log('[reviewbot] - creating suggestions', messageContext) -- const filesWithSuggestions = await createSuggestions(messageContext.files) -- -- const comments = filesWithSuggestions.map(f => ({ -- path: f.filename, -- position: findLinePositionInDiff( -- messageContext.diff, -- f.filename, -- f.lineRange.start -- ), -- body: f.suggestions -- })) -- -- console.log('filesWithSuggestions', filesWithSuggestions) -+ const llmComments = await createLLMPRComments( -+ messageContext.files, -+ messageContext.diff -+ ) -+ const astComments = await createASTPRComments( -+ messageContext.files, -+ message.fullFiles, -+ messageContext.diff -+ ) -+ -+ const comments = llmComments -+ .concat(astComments) -+ .filter(({ position }) => position > -1) -+ - console.log( - `[reviewbot] - creating review for commit ${messageContext.latestCommit}` - ) -diff --git a/functions/createReview/astParsing/index.js b/functions/createReview/astParsing/index.js -index 25ac1b5..af432d5 100644 ---- a/functions/createReview/astParsing/index.js -+++ b/functions/createReview/astParsing/index.js -@@ -6,50 +6,82 @@ import jsxPlugin from 'acorn-jsx' - - import validators from './rules/index.js' - import { parseAndAggregate } from './parser.js' -+import { findLinePositionInDiff } from '../../utils.js' - - function getParser(fileName) { - const fileExtension = path.extname(fileName) -- let parser; -- if(fileExtension === '.ts' || fileExtension === '.tsx') { -+ let parser -+ if (fileExtension === '.ts' || fileExtension === '.tsx') { - parser = Parser.extend(tsPlugin()) -- } else if(fileExtension === '.jsx') { -+ } else if (fileExtension === '.jsx') { - parser = Parser.extend(jsxPlugin()) -- } else if(fileExtension === '.js') { -+ } else if (fileExtension === '.js') { - parser = Parser - } - return parser - } - --export function generateAST(fileDiff, fileContent) { -+export function generateAST(fileContent, fileDiff) { -+ console.log(fileDiff) - const parser = getParser(fileDiff.afterName) -- if(!parser) return -- let ast; -+ if (!parser) return -+ let ast - try { - ast = parser.parse(fileContent, { - sourceType: /^import\s+/.test(fileContent) ? 'module' : 'commonjs', - locations: true - }) -- } catch(err) { -- console.log('[reviewbot] - Failed to parse the file content into AST. Falling back to the loose parser.') -- ast = LooseParser.parse(fileContent, {locations: true}) -+ } catch (err) { -+ console.log( -+ '[reviewbot] - Failed to parse the file content into AST. Falling back to the loose parser.' -+ ) -+ ast = LooseParser.parse(fileContent, { locations: true }) - } - - ast.diff = fileDiff - return ast - } - --function aggregateComments(astNode) { -- const comments = [] -- for (let validator of validators) { -- const comment = validator(astNode) -- if (comment) { -- comments.push(comment) -+function aggregateComments(linesWhiteList) { -+ return astNode => { -+ const comments = [] -+ for (let validator of validators) { -+ const comment = validator(astNode, linesWhiteList) -+ if (comment) { -+ comments.push(comment) -+ } - } -+ return comments - } -- return comments - } - - export function parseForIssues(astDocument, gitDiff) { -- // TODO: only report issues on added lines -- return parseAndAggregate(astDocument, gitDiff, aggregateComments) -+ const modifiedLines = gitDiff.modifiedLines -+ .filter(lineData => lineData.added === true) -+ .map(lineData => lineData.lineNumber) -+ return parseAndAggregate(astDocument, aggregateComments(modifiedLines)) -+} -+ -+export function createASTPRComments(gitDiff, filesContents, rawDiff) { -+ let allIssues = [] -+ for (let fileDiff of gitDiff) { -+ const fileContent = filesContents.find( -+ fileContent => fileContent.filename === fileDiff.afterName -+ ) -+ if (fileContent) { -+ const ast = generateAST(fileContent.content, fileDiff) -+ const fileIssues = parseForIssues(ast, fileDiff) -+ allIssues = allIssues.concat(fileIssues) -+ } -+ } -+ -+ return allIssues.map(issue => ({ -+ path: issue.filename, -+ position: findLinePositionInDiff( -+ rawDiff, -+ issue.filename, -+ issue.lineContent -+ ), -+ body: issue.description -+ })) - } -diff --git a/functions/createReview/astParsing/parser.js b/functions/createReview/astParsing/parser.js -index e5c81cd..3532325 100644 ---- a/functions/createReview/astParsing/parser.js -+++ b/functions/createReview/astParsing/parser.js -@@ -95,3 +95,7 @@ export function isType(node, type) { - return true - } - } -+ -+export function isLineEdited(node, editedLineNumbers) { -+ return editedLineNumbers.indexOf(node.loc.start.line) > -1 -+} -diff --git a/functions/createReview/astParsing/rules/J3_no_parse_stringify.js b/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -index 03db35f..9b4bbd5 100644 ---- a/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -+++ b/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -@@ -7,7 +7,8 @@ export default function validator(node) { - ) - if (jsonStringifyChildNode) { - return { -- line: node.loc.start.line, -+ lineNumber: node.loc.start.line, -+ lineContent: node.line, - code: 'J3', - description: - 'Do not use JSON.parse(JSON.stringify(obj)) to clone objects because it is slow and resource intensive. Prefer a specialised library like [rfdc](https://github.com/davidmarkclements/rfdc).' -diff --git a/functions/createReview/astParsing/rules/J8_no_var.js b/functions/createReview/astParsing/rules/J8_no_var.js -index e2e5fa4..59aea79 100644 ---- a/functions/createReview/astParsing/rules/J8_no_var.js -+++ b/functions/createReview/astParsing/rules/J8_no_var.js -@@ -6,7 +6,8 @@ export default function validator(node) { - node.kind == 'var' - ) { - return { -- line: node.loc.start.line, -+ lineNumber: node.loc.start.line, -+ lineContent: node.line, - code: 'J8', - description: - 'Use let or const instead of var because the scope for let and const is smaller. [Learn more about the difference](https://stackoverflow.com/questions/762011/what-is-the-difference-between-let-and-var).' -diff --git a/functions/createReview/astParsing/rules/P10_loop_await.js b/functions/createReview/astParsing/rules/P10_loop_await.js -index 73f5576..2eada56 100644 ---- a/functions/createReview/astParsing/rules/P10_loop_await.js -+++ b/functions/createReview/astParsing/rules/P10_loop_await.js -@@ -1,13 +1,20 @@ --import { findChildNode, isType } from '../parser.js' -+import { findChildNode, isType, isLineEdited } from '../parser.js' - --export default function validator(node) { -+export default function validator(node, editedLineNumbers) { - if (isType(node, 'ForStatement')) { - const nestedAwaitExpression = findChildNode(node, childNode => - isType(childNode, 'AwaitExpression') - ) -- if (nestedAwaitExpression) { -+ if ( -+ nestedAwaitExpression && -+ (isLineEdited(node, editedLineNumbers) || -+ isLineEdited(nestedAwaitExpression, editedLineNumbers)) -+ ) { - return { -- line: nestedAwaitExpression.loc.start.line, -+ lineNumber: isLineEdited(nestedAwaitExpression, editedLineNumbers) -+ ? nestedAwaitExpression.loc.start.line -+ : node.loc.start.line, -+ lineContent: node.line, - code: 'P10', - description: `Review instances of awaiting promises in imperative loops (for, while, do/while) or array native methods (forEach, map). - -diff --git a/functions/createReview/astParsing/rules/P11_unbound_promise.js b/functions/createReview/astParsing/rules/P11_unbound_promise.js -index f232bbb..494d205 100644 ---- a/functions/createReview/astParsing/rules/P11_unbound_promise.js -+++ b/functions/createReview/astParsing/rules/P11_unbound_promise.js -@@ -1,9 +1,10 @@ --import { isCall } from '../parser.js' -+import { isCall, isLineEdited } from '../parser.js' - --export default function validator(node) { -- if (isCall(node, 'Promise', 'all')) { -+export default function validator(node, editedLineNumbers) { -+ if (isCall(node, 'Promise', 'all') && isLineEdited(node, editedLineNumbers)) { - return { -- line: node.loc.start.line, -+ lineNumber: node.loc.start.line, -+ lineContent: node.line, - code: 'P11', - description: - 'Executing Promise.all(items.map(async => { … })) leads to the creation of an undefined number of Promises, each executing something asynchronous and possibly saturating the event loop and consuming much memory. Recommend using a library like sindresorhus/p-map or sindresorhus/p-all instead.' -diff --git a/functions/createReview/createSuggestions/index.js b/functions/createReview/llm/index.js -similarity index 52% -rename from functions/createReview/createSuggestions/index.js -rename to functions/createReview/llm/index.js -index a8d7514..f59d619 100644 ---- a/functions/createReview/createSuggestions/index.js -+++ b/functions/createReview/llm/index.js -@@ -1,7 +1,6 @@ - import buildPrompt from './prompt-engine.js' - import generateSuggestions from './suggestions.js' --import { generateAST } from '../astParsing/index.js' -- -+import { findLinePositionInDiff } from '../../utils.js' - - /** - Creates suggestions for each file in a git diff using the ChatGPT transformer API. -@@ -9,27 +8,23 @@ import { generateAST } from '../astParsing/index.js' - @returns {Promise} - A promise that resolves to an array of objects containing suggestions for each file. - @throws {Error} If an error occurs while creating suggestions. - */ --async function createSuggestions(gitDiff, files) { -+async function createLLMSuggestions(gitDiff) { - const prompts = buildPrompt(gitDiff) - - const response = await Promise.all( -- prompts.map(async file => { -+ prompts.map(async filePrompt => { - let suggestionsForFile = {} - -- const ast = generateAST(file, files.find()) -- // TODO run AST analysis -- -- - const transformerResponse = await generateSuggestions({ - transformerType: 'chatGPT', -- payload: file.changes -+ payload: filePrompt.changes - }) - - transformerResponse.forEach((suggestion, index) => { - suggestionsForFile = { -- filename: file.fileName, -- lineRange: file.changes[index].range, -- diff: file.changes[index].diff, -+ filename: filePrompt.fileName, -+ lineRange: filePrompt.changes[index].range, -+ diff: filePrompt.changes[index].diff, - suggestions: suggestion - } - }) -@@ -40,4 +35,19 @@ async function createSuggestions(gitDiff, files) { - return response - } - --export default createSuggestions -+function transformSuggestionsIntoComments(suggestions, rawDiff) { -+ const comments = suggestions.map(f => ({ -+ path: f.filename, -+ position: findLinePositionInDiff(rawDiff, f.filename, f.lineRange.start), -+ body: f.suggestions -+ })) -+ return comments -+} -+ -+export function createLLMPRComments(gitDiff, rawDiff) { -+ const suggestions = createLLMSuggestions(gitDiff) -+ const comments = transformSuggestionsIntoComments(suggestions, rawDiff) -+ return comments -+} -+ -+export default createLLMSuggestions -diff --git a/functions/createReview/createSuggestions/prompt-engine.js b/functions/createReview/llm/prompt-engine.js -similarity index 95% -rename from functions/createReview/createSuggestions/prompt-engine.js -rename to functions/createReview/llm/prompt-engine.js -index 5b4f65d..fba757e 100644 ---- a/functions/createReview/createSuggestions/prompt-engine.js -+++ b/functions/createReview/llm/prompt-engine.js -@@ -8,9 +8,8 @@ function filterOnlyModified(files) { - } - - function filterAcceptedFiles(files) { -- const filteredFiles = files.filter( -- f => -- /\.[tj]sx?$/g.test(path.extname(f.afterName)) -+ const filteredFiles = files.filter(f => -+ /\.[tj]sx?$/g.test(path.extname(f.afterName)) - ) - return filteredFiles - } -diff --git a/functions/createReview/createSuggestions/suggestions.js b/functions/createReview/llm/suggestions.js -similarity index 100% -rename from functions/createReview/createSuggestions/suggestions.js -rename to functions/createReview/llm/suggestions.js -diff --git a/functions/createReview/oktokit/index.js b/functions/createReview/oktokit/index.js -index a2cfd36..8adae26 100644 ---- a/functions/createReview/oktokit/index.js -+++ b/functions/createReview/oktokit/index.js -@@ -28,39 +28,41 @@ async function getOctokit(installationId) { - } - - export async function getPRContent(context) { -- - const { owner, repo } = context.issue() - const { pull_number } = context.pullRequest() - - const response = await context.octokit.pulls.listFiles({ - owner, - repo, -- pull_number, -- }); -+ pull_number -+ }) - - let fullFiles = [] -- if(response && response.data) { -+ if (response && response.data) { - fullFiles = response.data - } - const fetchFilesPromises = [] - for (const file of fullFiles) { - if (file.status === 'modified' || file.status === 'added') { - fetchFilesPromises.push( -- context.octokit.request('GET /repos/:owner/:repo/contents/:path', { -- owner, -- repo, -- path: file.filename, -- ref: `pull/${pull_number}/head`, -- }).then(response => { -- const content = Buffer.from(response.data.content, 'base64').toString(); -- console.log(content) -- file.content = content -- }) -+ context.octokit -+ .request('GET /repos/:owner/:repo/contents/:path', { -+ owner, -+ repo, -+ path: file.filename, -+ ref: `pull/${pull_number}/head` -+ }) -+ .then(response => { -+ const content = Buffer.from( -+ response.data.content, -+ 'base64' -+ ).toString() -+ file.content = content -+ }) - ) -- - } - } -- await Promise.all(fetchFilesPromises); -+ await Promise.all(fetchFilesPromises) - - return fullFiles - } -diff --git a/functions/webhook/app.js b/functions/webhook/app.js -index 66f244d..2560074 100644 ---- a/functions/webhook/app.js -+++ b/functions/webhook/app.js -@@ -46,7 +46,6 @@ export default async app => { - }) - - const { files } = parseGitPatch.default(diff) -- - const { data: commits } = await context.octokit.pulls.listCommits({ - ...common, - pull_number: pullRequest.pull_number -@@ -71,15 +70,8 @@ export default async app => { - return - } - -- -- const response = await context.octokit.pulls.listFiles({ -- ...common, -- pull_number: pullRequest.pull_number, -- }); -- - let fullFiles = await getPRContent(context) - -- - const messageContext = { - ...common, - pull_number: pullRequest.pull_number, -diff --git a/package-lock.json b/package-lock.json -index 68b74da..1006b27 100644 ---- a/package-lock.json -+++ b/package-lock.json -@@ -11,10 +11,11 @@ - "@google-cloud/pubsub": "^4.0.6", - "@octokit/auth-app": "^6.0.1", - "acorn": "^8.10.0", -+ "acorn-loose": "^8.4.0", -+ "acorn-typescript": "^1.4.10", - "octokit": "^3.1.1", - "parse-git-patch": "^1.2.2", -- "probot": "^12.3.1", -- "yarn": "^1.22.19" -+ "probot": "^12.3.1" - }, - "devDependencies": { - "@commitlint/cli": "^18.0.0", -@@ -56,17 +57,89 @@ - } - }, - "node_modules/@babel/code-frame": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", -- "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", -+ "version": "7.22.13", -+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", -+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "dependencies": { -- "@babel/highlight": "^7.18.6" -+ "@babel/highlight": "^7.22.13", -+ "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, -+ "node_modules/@babel/code-frame/node_modules/ansi-styles": { -+ "version": "3.2.1", -+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", -+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", -+ "dev": true, -+ "dependencies": { -+ "color-convert": "^1.9.0" -+ }, -+ "engines": { -+ "node": ">=4" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/chalk": { -+ "version": "2.4.2", -+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", -+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", -+ "dev": true, -+ "dependencies": { -+ "ansi-styles": "^3.2.1", -+ "escape-string-regexp": "^1.0.5", -+ "supports-color": "^5.3.0" -+ }, -+ "engines": { -+ "node": ">=4" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/color-convert": { -+ "version": "1.9.3", -+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", -+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", -+ "dev": true, -+ "dependencies": { -+ "color-name": "1.1.3" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/color-name": { -+ "version": "1.1.3", -+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", -+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", -+ "dev": true -+ }, -+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { -+ "version": "1.0.5", -+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", -+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", -+ "dev": true, -+ "engines": { -+ "node": ">=0.8.0" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/has-flag": { -+ "version": "3.0.0", -+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", -+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", -+ "dev": true, -+ "engines": { -+ "node": ">=4" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/supports-color": { -+ "version": "5.5.0", -+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", -+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", -+ "dev": true, -+ "dependencies": { -+ "has-flag": "^3.0.0" -+ }, -+ "engines": { -+ "node": ">=4" -+ } -+ }, - "node_modules/@babel/compat-data": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz", -@@ -107,21 +180,21 @@ - } - }, - "node_modules/@babel/core/node_modules/semver": { -- "version": "6.3.0", -- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", -- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", -+ "version": "6.3.1", -+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", -+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz", -- "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", -+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "dependencies": { -- "@babel/types": "^7.21.3", -+ "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" -@@ -183,9 +256,9 @@ - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { -- "version": "6.3.0", -- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", -- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", -+ "version": "6.3.1", -+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", -+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" -@@ -198,34 +271,34 @@ - "dev": true - }, - "node_modules/@babel/helper-environment-visitor": { -- "version": "7.18.9", -- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", -- "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", -+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { -- "version": "7.21.0", -- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", -- "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", -+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { -- "@babel/template": "^7.20.7", -- "@babel/types": "^7.21.0" -+ "@babel/template": "^7.22.15", -+ "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", -- "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", -+ "version": "7.22.5", -+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", -+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { -- "@babel/types": "^7.18.6" -+ "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" -@@ -275,30 +348,30 @@ - } - }, - "node_modules/@babel/helper-split-export-declaration": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", -- "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", -+ "version": "7.22.6", -+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", -+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { -- "@babel/types": "^7.18.6" -+ "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { -- "version": "7.19.4", -- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", -- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", -+ "version": "7.22.5", -+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", -+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { -- "version": "7.19.1", -- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", -- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", -+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" -@@ -328,13 +401,13 @@ - } - }, - "node_modules/@babel/highlight": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", -- "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", -+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { -- "@babel/helper-validator-identifier": "^7.18.6", -- "chalk": "^2.0.0", -+ "@babel/helper-validator-identifier": "^7.22.20", -+ "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { -@@ -413,9 +486,9 @@ - } - }, - "node_modules/@babel/parser": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", -- "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", -+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" -@@ -437,33 +510,33 @@ - } - }, - "node_modules/@babel/template": { -- "version": "7.20.7", -- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", -- "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", -+ "version": "7.22.15", -+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", -+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { -- "@babel/code-frame": "^7.18.6", -- "@babel/parser": "^7.20.7", -- "@babel/types": "^7.20.7" -+ "@babel/code-frame": "^7.22.13", -+ "@babel/parser": "^7.22.15", -+ "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz", -- "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==", -- "dev": true, -- "dependencies": { -- "@babel/code-frame": "^7.18.6", -- "@babel/generator": "^7.21.3", -- "@babel/helper-environment-visitor": "^7.18.9", -- "@babel/helper-function-name": "^7.21.0", -- "@babel/helper-hoist-variables": "^7.18.6", -- "@babel/helper-split-export-declaration": "^7.18.6", -- "@babel/parser": "^7.21.3", -- "@babel/types": "^7.21.3", -+ "version": "7.23.2", -+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", -+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", -+ "dev": true, -+ "dependencies": { -+ "@babel/code-frame": "^7.22.13", -+ "@babel/generator": "^7.23.0", -+ "@babel/helper-environment-visitor": "^7.22.20", -+ "@babel/helper-function-name": "^7.23.0", -+ "@babel/helper-hoist-variables": "^7.22.5", -+ "@babel/helper-split-export-declaration": "^7.22.6", -+ "@babel/parser": "^7.23.0", -+ "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, -@@ -481,13 +554,13 @@ - } - }, - "node_modules/@babel/types": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", -- "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", -+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { -- "@babel/helper-string-parser": "^7.19.4", -- "@babel/helper-validator-identifier": "^7.19.1", -+ "@babel/helper-string-parser": "^7.22.5", -+ "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { -@@ -2540,9 +2613,9 @@ - } - }, - "node_modules/acorn": { -- "version": "8.10.0", -- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", -- "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", -+ "version": "8.11.2", -+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", -+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "bin": { - "acorn": "bin/acorn" - }, -@@ -2559,6 +2632,25 @@ - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, -+ "node_modules/acorn-loose": { -+ "version": "8.4.0", -+ "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", -+ "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", -+ "dependencies": { -+ "acorn": "^8.11.0" -+ }, -+ "engines": { -+ "node": ">=0.4.0" -+ } -+ }, -+ "node_modules/acorn-typescript": { -+ "version": "1.4.10", -+ "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.10.tgz", -+ "integrity": "sha512-f+gNIxCaTc9WVe5W6Fgu35UnTmtEOCZlsYf/ZADFIUAS2LL1UnBa1gmS/EYPvT4bo8R2ikw7VG62VGP/CFLQ5Q==", -+ "peerDependencies": { -+ "acorn": ">=8.9.0" -+ } -+ }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", -@@ -8393,19 +8485,6 @@ - "node": ">=12" - } - }, -- "node_modules/yarn": { -- "version": "1.22.19", -- "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", -- "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==", -- "hasInstallScript": true, -- "bin": { -- "yarn": "bin/yarn.js", -- "yarnpkg": "bin/yarn.js" -- }, -- "engines": { -- "node": ">=4.0.0" -- } -- }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", -@@ -8446,12 +8525,71 @@ - } - }, - "@babel/code-frame": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", -- "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", -+ "version": "7.22.13", -+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", -+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "requires": { -- "@babel/highlight": "^7.18.6" -+ "@babel/highlight": "^7.22.13", -+ "chalk": "^2.4.2" -+ }, -+ "dependencies": { -+ "ansi-styles": { -+ "version": "3.2.1", -+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", -+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", -+ "dev": true, -+ "requires": { -+ "color-convert": "^1.9.0" -+ } -+ }, -+ "chalk": { -+ "version": "2.4.2", -+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", -+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", -+ "dev": true, -+ "requires": { -+ "ansi-styles": "^3.2.1", -+ "escape-string-regexp": "^1.0.5", -+ "supports-color": "^5.3.0" -+ } -+ }, -+ "color-convert": { -+ "version": "1.9.3", -+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", -+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", -+ "dev": true, -+ "requires": { -+ "color-name": "1.1.3" -+ } -+ }, -+ "color-name": { -+ "version": "1.1.3", -+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", -+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", -+ "dev": true -+ }, -+ "escape-string-regexp": { -+ "version": "1.0.5", -+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", -+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", -+ "dev": true -+ }, -+ "has-flag": { -+ "version": "3.0.0", -+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", -+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", -+ "dev": true -+ }, -+ "supports-color": { -+ "version": "5.5.0", -+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", -+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", -+ "dev": true, -+ "requires": { -+ "has-flag": "^3.0.0" -+ } -+ } - } - }, - "@babel/compat-data": { -@@ -8484,20 +8622,20 @@ - }, - "dependencies": { - "semver": { -- "version": "6.3.0", -- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", -- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", -+ "version": "6.3.1", -+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", -+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "@babel/generator": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz", -- "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", -+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "requires": { -- "@babel/types": "^7.21.3", -+ "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" -@@ -8549,9 +8687,9 @@ - } - }, - "semver": { -- "version": "6.3.0", -- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", -- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", -+ "version": "6.3.1", -+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", -+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - }, - "yallist": { -@@ -8563,28 +8701,28 @@ - } - }, - "@babel/helper-environment-visitor": { -- "version": "7.18.9", -- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", -- "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", -+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true - }, - "@babel/helper-function-name": { -- "version": "7.21.0", -- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", -- "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", -+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "requires": { -- "@babel/template": "^7.20.7", -- "@babel/types": "^7.21.0" -+ "@babel/template": "^7.22.15", -+ "@babel/types": "^7.23.0" - } - }, - "@babel/helper-hoist-variables": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", -- "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", -+ "version": "7.22.5", -+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", -+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "requires": { -- "@babel/types": "^7.18.6" -+ "@babel/types": "^7.22.5" - } - }, - "@babel/helper-module-imports": { -@@ -8622,24 +8760,24 @@ - } - }, - "@babel/helper-split-export-declaration": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", -- "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", -+ "version": "7.22.6", -+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", -+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "requires": { -- "@babel/types": "^7.18.6" -+ "@babel/types": "^7.22.5" - } - }, - "@babel/helper-string-parser": { -- "version": "7.19.4", -- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", -- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", -+ "version": "7.22.5", -+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", -+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true - }, - "@babel/helper-validator-identifier": { -- "version": "7.19.1", -- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", -- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", -+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true - }, - "@babel/helper-validator-option": { -@@ -8660,13 +8798,13 @@ - } - }, - "@babel/highlight": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", -- "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", -+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "requires": { -- "@babel/helper-validator-identifier": "^7.18.6", -- "chalk": "^2.0.0", -+ "@babel/helper-validator-identifier": "^7.22.20", -+ "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { -@@ -8729,9 +8867,9 @@ - } - }, - "@babel/parser": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", -- "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", -+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, - "@babel/runtime": { -@@ -8744,30 +8882,30 @@ - } - }, - "@babel/template": { -- "version": "7.20.7", -- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", -- "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", -+ "version": "7.22.15", -+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", -+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "requires": { -- "@babel/code-frame": "^7.18.6", -- "@babel/parser": "^7.20.7", -- "@babel/types": "^7.20.7" -+ "@babel/code-frame": "^7.22.13", -+ "@babel/parser": "^7.22.15", -+ "@babel/types": "^7.22.15" - } - }, - "@babel/traverse": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz", -- "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==", -- "dev": true, -- "requires": { -- "@babel/code-frame": "^7.18.6", -- "@babel/generator": "^7.21.3", -- "@babel/helper-environment-visitor": "^7.18.9", -- "@babel/helper-function-name": "^7.21.0", -- "@babel/helper-hoist-variables": "^7.18.6", -- "@babel/helper-split-export-declaration": "^7.18.6", -- "@babel/parser": "^7.21.3", -- "@babel/types": "^7.21.3", -+ "version": "7.23.2", -+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", -+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", -+ "dev": true, -+ "requires": { -+ "@babel/code-frame": "^7.22.13", -+ "@babel/generator": "^7.23.0", -+ "@babel/helper-environment-visitor": "^7.22.20", -+ "@babel/helper-function-name": "^7.23.0", -+ "@babel/helper-hoist-variables": "^7.22.5", -+ "@babel/helper-split-export-declaration": "^7.22.6", -+ "@babel/parser": "^7.23.0", -+ "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, -@@ -8781,13 +8919,13 @@ - } - }, - "@babel/types": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", -- "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", -+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "requires": { -- "@babel/helper-string-parser": "^7.19.4", -- "@babel/helper-validator-identifier": "^7.19.1", -+ "@babel/helper-string-parser": "^7.22.5", -+ "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - }, -@@ -10568,9 +10706,9 @@ - } - }, - "acorn": { -- "version": "8.10.0", -- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", -- "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" -+ "version": "8.11.2", -+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", -+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==" - }, - "acorn-jsx": { - "version": "5.3.2", -@@ -10579,6 +10717,20 @@ - "dev": true, - "requires": {} - }, -+ "acorn-loose": { -+ "version": "8.4.0", -+ "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", -+ "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", -+ "requires": { -+ "acorn": "^8.11.0" -+ } -+ }, -+ "acorn-typescript": { -+ "version": "1.4.10", -+ "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.10.tgz", -+ "integrity": "sha512-f+gNIxCaTc9WVe5W6Fgu35UnTmtEOCZlsYf/ZADFIUAS2LL1UnBa1gmS/EYPvT4bo8R2ikw7VG62VGP/CFLQ5Q==", -+ "requires": {} -+ }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", -@@ -14963,11 +15115,6 @@ - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - }, -- "yarn": { -- "version": "1.22.19", -- "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", -- "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==" -- }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", -diff --git a/package.json b/package.json -index 0996218..51bd66e 100644 ---- a/package.json -+++ b/package.json -@@ -26,6 +26,8 @@ - "@google-cloud/pubsub": "^4.0.6", - "@octokit/auth-app": "^6.0.1", - "acorn": "^8.10.0", -+ "acorn-loose": "^8.4.0", -+ "acorn-typescript": "^1.4.10", - "octokit": "^3.1.1", - "parse-git-patch": "^1.2.2", - "probot": "^12.3.1" -diff --git a/tests/astParsing.test.js b/tests/astParsing.test.js -index c5b0e0a..ccfa3f3 100644 ---- a/tests/astParsing.test.js -+++ b/tests/astParsing.test.js -@@ -19,7 +19,7 @@ function parseForExpectedViolations(fileContent) { - const match = /\/\/\s*expect\s*:\s*(\w+)/.exec(line) - if (match && match[1]) { - return { -- line: index + 2, // assumes the relevant line is +1 of current line. -+ lineNumber: index + 2, // assumes the relevant line is +1 of current line. - code: match[1] - } - } else { -@@ -31,41 +31,186 @@ function parseForExpectedViolations(fileContent) { - - function removeDescription(violation) { - return { -- line: violation.line, -+ lineNumber: violation.lineNumber, - code: violation.code - } - } - -+function loadExampleFile(relativeFilePath) { -+ const exampleFileContent = readFileSync( -+ path.join(__dirname, relativeFilePath), -+ 'utf8' -+ ) -+ const lines = exampleFileContent.split('\n').map((l, idx) => { -+ return { -+ added: true, -+ lineNumber: idx + 1, -+ line: l -+ } -+ }) -+ const gitDiff = { -+ added: true, -+ deleted: false, -+ beforeName: relativeFilePath, -+ afterName: relativeFilePath, -+ modifiedLines: lines -+ } -+ return { -+ diff: gitDiff, -+ fileContent: exampleFileContent -+ } -+} -+ -+describe('AST parsing intergration testing', () => { -+ test('P10 - nominal', () => { -+ const { fileContent, diff } = loadExampleFile( -+ '../functions/createReview/astParsing/rules/P10.example.js' -+ ) -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ -+ const expectedViolations = parseForExpectedViolations(fileContent) -+ assert.notEqual(violations.length, 0) -+ assert.deepEqual(violations, expectedViolations) -+ }) -+}) -+ - describe('AST parsing unit testing', () => { -- test('P10', () => { -- const jsFile = readFileSync( -- path.join( -- __dirname, -- '../functions/createReview/astParsing/rules/P10.example.js' -- ), -- 'utf8' -+ test('P10 - nominal', () => { -+ const { fileContent, diff } = loadExampleFile( -+ '../functions/createReview/astParsing/rules/P10.example.js' - ) -- const ast = generateAST(jsFile) -- const violations = parseForIssues(ast).map(removeDescription) -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) - -- const expectedViolations = parseForExpectedViolations(jsFile) -+ const expectedViolations = parseForExpectedViolations(fileContent) - assert.notEqual(violations.length, 0) - assert.deepEqual(violations, expectedViolations) - }) - -- test('P11', () => { -- const jsFile = readFileSync( -- path.join( -- __dirname, -- '../functions/createReview/astParsing/rules/P11.example.js' -- ), -- 'utf8' -+ test('P10 - parse if line has been updated in the PR', () => { -+ const fileContent = `async function test1() { -+ for(let i = 0 ; i < 10 ; i++) { -+ await myPromise() -+ } -+ }` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: true, -+ lineNumber: 3 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ assert.deepEqual(violations, [{ lineNumber: 3, code: 'P10' }]) -+ }) -+ -+ test('P10 - ignore if line has been deleted from the PR', () => { -+ const fileContent = `async function test1() { -+ for(let i = 0 ; i < 10 ; i++) { -+ await myPromise() -+ } -+ }` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: false, -+ lineNumber: 3 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ assert.deepEqual(violations, []) -+ }) -+ -+ test('P10 - report the loop if it has been added around an existing await call`', () => { -+ const fileContent = `async function test1() { -+ for(let i = 0 ; i < 10 ; i++) { -+ await myPromise() -+ } -+ }` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: true, -+ lineNumber: 1 -+ }, -+ { -+ added: true, -+ lineNumber: 2 -+ }, -+ { -+ added: true, -+ lineNumber: 4 -+ }, -+ { -+ added: true, -+ lineNumber: 5 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ assert.deepEqual(violations, [{ lineNumber: 2, code: 'P10' }]) -+ }) -+ -+ test('P11 - nominal', () => { -+ const { fileContent, diff } = loadExampleFile( -+ '../functions/createReview/astParsing/rules/P11.example.js' - ) -- const ast = generateAST(jsFile) -- const violations = parseForIssues(ast).map(removeDescription) -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) - -- const expectedViolations = parseForExpectedViolations(jsFile) -+ const expectedViolations = parseForExpectedViolations(fileContent) - assert.notEqual(violations.length, 0) - assert.deepEqual(violations, expectedViolations) - }) -+ -+ test('P11 unbound promise - parse if line has been updated in the PR', () => { -+ const fileContent = `const promises = [] -+ Promise.all(promises) -+ ` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: true, -+ lineNumber: 2 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ -+ assert.deepEqual(violations, [{ lineNumber: 2, code: 'P11' }]) -+ }) -+ -+ test('P11 unbound promise - ignore if line has NOT been updated in the PR', () => { -+ const fileContent = `const promises = [] -+ Promise.all(promises) -+ ` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: true, -+ lineNumber: 1 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ assert.deepEqual(violations, []) -+ }) - }) -diff --git a/tests/create-suggestions/index.test.js b/tests/create-suggestions/index.test.js -index f397d80..cd908df 100644 ---- a/tests/create-suggestions/index.test.js -+++ b/tests/create-suggestions/index.test.js -@@ -1,9 +1,9 @@ - import { describe, test } from 'node:test' - import assert from 'node:assert' - import fetchMock from 'fetch-mock' --import createSuggestions from '../../functions/createReview/createSuggestions/index.js' -+import createLLMSuggestions from '../../functions/createReview/llm/index.js' - --describe('createSuggestions()', () => { -+describe('createLLMSuggestions()', () => { - test('should generate a a sugggestion for a valid git diff with one file', async t => { - t.after(() => { - fetchMock.restore() -@@ -46,7 +46,15 @@ describe('createSuggestions()', () => { - ] - } - ] -- const response = await createSuggestions(gitDiff) -+ const files = [ -+ { -+ filename: 'test.js', -+ status: 'added', -+ content: `const a = 1;\n- const a = 2;\n -+ const a = 1;` -+ } -+ ] -+ const response = await createLLMSuggestions(gitDiff, files) - const expectedResponse = [ - { - diff: 'const a = 1;', -@@ -122,7 +130,7 @@ describe('createSuggestions()', () => { - ] - } - ] -- const response = await createSuggestions(gitDiff) -+ const response = await createLLMSuggestions(gitDiff) - const expectedResponse = [ - { - diff: 'const a = 1;', -@@ -189,7 +197,7 @@ describe('createSuggestions()', () => { - ] - } - ] -- const response = await createSuggestions(gitDiff) -+ const response = await createLLMSuggestions(gitDiff) - const expectedResponse = [] - assert.deepStrictEqual(response, expectedResponse) - }) diff --git a/functions/createReview/astParsing/rules/P11.example.js_4_P11.diff b/functions/createReview/astParsing/rules/P11.example.js_4_P11.diff deleted file mode 100644 index f3186cd..0000000 --- a/functions/createReview/astParsing/rules/P11.example.js_4_P11.diff +++ /dev/null @@ -1,2232 +0,0 @@ -From 7f658f7ec0af7ff545ebafe1418dd010f70cca79 Mon Sep 17 00:00:00 2001 -From: Nicolas Herment -Date: Wed, 25 Oct 2023 09:21:56 +0200 -Subject: [PATCH 1/4] feat: add AST parsing code (WIP) - ---- - .eslintrc | 2 +- - functions/createReview/astParsing/index.js | 34 +++++++ - functions/createReview/astParsing/parser.js | 97 +++++++++++++++++++ - .../astParsing/rules/J3_no_parse_stringify.js | 17 ++++ - .../astParsing/rules/J8_no_var.js | 15 +++ - .../astParsing/rules/P10.example.js | 22 +++++ - .../astParsing/rules/P10_loop_await.js | 24 +++++ - .../astParsing/rules/P11.example.js | 4 + - .../astParsing/rules/P11_unbound_promise.js | 12 +++ - .../createReview/astParsing/rules/index.js | 11 +++ - package-lock.json | 26 ++++- - package.json | 1 + - tests/astParsing.test.js | 71 ++++++++++++++ - 13 files changed, 331 insertions(+), 5 deletions(-) - create mode 100644 functions/createReview/astParsing/index.js - create mode 100644 functions/createReview/astParsing/parser.js - create mode 100644 functions/createReview/astParsing/rules/J3_no_parse_stringify.js - create mode 100644 functions/createReview/astParsing/rules/J8_no_var.js - create mode 100644 functions/createReview/astParsing/rules/P10.example.js - create mode 100644 functions/createReview/astParsing/rules/P10_loop_await.js - create mode 100644 functions/createReview/astParsing/rules/P11.example.js - create mode 100644 functions/createReview/astParsing/rules/P11_unbound_promise.js - create mode 100644 functions/createReview/astParsing/rules/index.js - create mode 100644 tests/astParsing.test.js - -diff --git a/.eslintrc b/.eslintrc -index a4f2a22..e71fb27 100644 ---- a/.eslintrc -+++ b/.eslintrc -@@ -8,5 +8,5 @@ - "ecmaVersion": 2022, - "sourceType": "module" - }, -- "ignorePatterns": ["dist/**"] -+ "ignorePatterns": ["dist/**", "functions/createReview/astParsing/rules/*.example.js"] - } -diff --git a/functions/createReview/astParsing/index.js b/functions/createReview/astParsing/index.js -new file mode 100644 -index 0000000..3b3ccbc ---- /dev/null -+++ b/functions/createReview/astParsing/index.js -@@ -0,0 +1,34 @@ -+import { Parser } from 'acorn' -+// import tsPlugin from 'acorn-typescript' -+// import { LooseParser } from 'acorn-loose' -+// import jsxPlugin from 'acorn-jsx' -+ -+import validators from './rules/index.js' -+import { parseAndAggregate } from './parser.js' -+ -+const parser = Parser -+ .extend -+ // jsxPlugin() -+ () -+ -+export function generateAST(fileContent) { -+ return parser.parse(fileContent, { -+ sourceType: 'module', -+ locations: true -+ }) -+} -+ -+function aggregateComments(astNode) { -+ const comments = [] -+ for (let validator of validators) { -+ const comment = validator(astNode) -+ if (comment) { -+ comments.push(comment) -+ } -+ } -+ return comments -+} -+ -+export function parseForIssues(astDocument) { -+ return parseAndAggregate(astDocument, aggregateComments) -+} -diff --git a/functions/createReview/astParsing/parser.js b/functions/createReview/astParsing/parser.js -new file mode 100644 -index 0000000..e5c81cd ---- /dev/null -+++ b/functions/createReview/astParsing/parser.js -@@ -0,0 +1,97 @@ -+// The properties in the AST tree that the parsing algorithm should walk through to recurse through the whole tree -+export const AST_PARSING_PROPS = [ -+ 'body', -+ 'declarations', -+ 'init', -+ 'expression', -+ 'argument', -+ 'arguments', -+ 'children' -+] -+ -+export function parseAndAggregate(astDocument, aggrFunc) { -+ let aggregatedData = aggrFunc(astDocument) -+ -+ if (typeof astDocument === 'object' && astDocument !== null) { -+ for (let prop of AST_PARSING_PROPS) { -+ if (prop in astDocument && Array.isArray(astDocument[prop])) { -+ for (let el of astDocument[prop]) { -+ const newAggrData = parseAndAggregate(el, aggrFunc) -+ aggregatedData = aggregatedData.concat(newAggrData) -+ } -+ } else if (prop in astDocument) { -+ const newAggrData = parseAndAggregate(astDocument[prop], aggrFunc) -+ aggregatedData = aggregatedData.concat(newAggrData) -+ } -+ } -+ } -+ return aggregatedData -+} -+ -+export function findChildNode(astDocument, boolFunction) { -+ if (typeof astDocument === 'object' && astDocument !== null) { -+ for (let prop of AST_PARSING_PROPS) { -+ if (prop in astDocument && Array.isArray(astDocument[prop])) { -+ for (let el of astDocument[prop]) { -+ console.log( -+ 'Evaluating child 1', -+ prop, -+ '->', -+ el.type, -+ boolFunction(el) -+ ) -+ if (boolFunction(el)) { -+ return el -+ } -+ const matchingChildNode = findChildNode(el, boolFunction) -+ if (matchingChildNode) { -+ return matchingChildNode -+ } -+ } -+ } else if (prop in astDocument) { -+ console.log( -+ 'Evaluating child 2', -+ prop, -+ '->', -+ astDocument[prop].type, -+ boolFunction(astDocument[prop]) -+ ) -+ if (boolFunction(astDocument[prop])) { -+ return astDocument[prop] -+ } -+ const matchingChildNode = findChildNode(astDocument[prop], boolFunction) -+ if (matchingChildNode) { -+ return matchingChildNode -+ } -+ } -+ } -+ } -+} -+ -+export function isCall(node, objectName, propertyName) { -+ if ( -+ typeof node === 'object' && -+ node !== null && -+ node.type == 'CallExpression' && -+ node.callee && -+ node.callee.object && -+ node.callee.property -+ ) { -+ const obj = node.callee.object -+ const prop = node.callee.property -+ if ( -+ obj.type == 'Identifier' && -+ obj.name == objectName && -+ prop.type == 'Identifier' && -+ prop.name == propertyName -+ ) { -+ return true -+ } -+ } -+} -+ -+export function isType(node, type) { -+ if (typeof node === 'object' && node !== null && node.type == type) { -+ return true -+ } -+} -diff --git a/functions/createReview/astParsing/rules/J3_no_parse_stringify.js b/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -new file mode 100644 -index 0000000..03db35f ---- /dev/null -+++ b/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -@@ -0,0 +1,17 @@ -+import { findChildNode, isCall } from '../parser.js' -+ -+export default function validator(node) { -+ if (isCall(node, 'JSON', 'parse')) { -+ const jsonStringifyChildNode = findChildNode(node, childNode => -+ isCall(childNode, 'JSON', 'stringify') -+ ) -+ if (jsonStringifyChildNode) { -+ return { -+ line: node.loc.start.line, -+ code: 'J3', -+ description: -+ 'Do not use JSON.parse(JSON.stringify(obj)) to clone objects because it is slow and resource intensive. Prefer a specialised library like [rfdc](https://github.com/davidmarkclements/rfdc).' -+ } -+ } -+ } -+} -diff --git a/functions/createReview/astParsing/rules/J8_no_var.js b/functions/createReview/astParsing/rules/J8_no_var.js -new file mode 100644 -index 0000000..e2e5fa4 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/J8_no_var.js -@@ -0,0 +1,15 @@ -+export default function validator(node) { -+ if ( -+ typeof node === 'object' && -+ node !== null && -+ node.type == 'VariableDeclaration' && -+ node.kind == 'var' -+ ) { -+ return { -+ line: node.loc.start.line, -+ code: 'J8', -+ description: -+ 'Use let or const instead of var because the scope for let and const is smaller. [Learn more about the difference](https://stackoverflow.com/questions/762011/what-is-the-difference-between-let-and-var).' -+ } -+ } -+} -diff --git a/functions/createReview/astParsing/rules/P10.example.js b/functions/createReview/astParsing/rules/P10.example.js -new file mode 100644 -index 0000000..4c36f82 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/P10.example.js -@@ -0,0 +1,22 @@ -+const myPromise = new Promise((resolve, reject) => { -+ setTimeout(() => { -+ resolve("foo") -+ }, 300) -+}) -+ -+async function test1() { -+ for(let i = 0 ; i < 10 ; i++) { -+ // expect:P10 -+ await myPromise() -+ } -+} -+ -+// function test2() { -+// [1,2,3].map(async () => await myPromise()) -+// } -+ -+// function test3() { -+ -+// [1,2,3].map(async () => await myPromise()) -+ -+// } -\ No newline at end of file -diff --git a/functions/createReview/astParsing/rules/P10_loop_await.js b/functions/createReview/astParsing/rules/P10_loop_await.js -new file mode 100644 -index 0000000..73f5576 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/P10_loop_await.js -@@ -0,0 +1,24 @@ -+import { findChildNode, isType } from '../parser.js' -+ -+export default function validator(node) { -+ if (isType(node, 'ForStatement')) { -+ const nestedAwaitExpression = findChildNode(node, childNode => -+ isType(childNode, 'AwaitExpression') -+ ) -+ if (nestedAwaitExpression) { -+ return { -+ line: nestedAwaitExpression.loc.start.line, -+ code: 'P10', -+ description: `Review instances of awaiting promises in imperative loops (for, while, do/while) or array native methods (forEach, map). -+ -+ In many cases they are unnecessary as parallel execution is preferred. -+ -+ Prefer using Promise native methods like Promise.all, or Promise.race, or Promise.allSettled or libraries like p-map as recommended next. -+ -+ If serial execution is needed, consider using a library like sindresorhus/p-series. -+ -+ There is also an eslint rule to enforce this which we recommend using.` -+ } -+ } -+ } -+} -diff --git a/functions/createReview/astParsing/rules/P11.example.js b/functions/createReview/astParsing/rules/P11.example.js -new file mode 100644 -index 0000000..be9ef02 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/P11.example.js -@@ -0,0 +1,4 @@ -+ -+ -+// expect:P11 -+Promise.all(myPromises) -\ No newline at end of file -diff --git a/functions/createReview/astParsing/rules/P11_unbound_promise.js b/functions/createReview/astParsing/rules/P11_unbound_promise.js -new file mode 100644 -index 0000000..f232bbb ---- /dev/null -+++ b/functions/createReview/astParsing/rules/P11_unbound_promise.js -@@ -0,0 +1,12 @@ -+import { isCall } from '../parser.js' -+ -+export default function validator(node) { -+ if (isCall(node, 'Promise', 'all')) { -+ return { -+ line: node.loc.start.line, -+ code: 'P11', -+ description: -+ 'Executing Promise.all(items.map(async => { … })) leads to the creation of an undefined number of Promises, each executing something asynchronous and possibly saturating the event loop and consuming much memory. Recommend using a library like sindresorhus/p-map or sindresorhus/p-all instead.' -+ } -+ } -+} -diff --git a/functions/createReview/astParsing/rules/index.js b/functions/createReview/astParsing/rules/index.js -new file mode 100644 -index 0000000..abaefd9 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/index.js -@@ -0,0 +1,11 @@ -+// import J3 from './J3_no_parse_stringify.js' -+// import J8 from './J8_no_var.js' -+import P10 from './P10_loop_await.js' -+import P11 from './P11_unbound_promise.js' -+ -+export default [ -+ // J3, -+ // J8, -+ P10, -+ P11 -+] -diff --git a/package-lock.json b/package-lock.json -index 1bc4c2d..68b74da 100644 ---- a/package-lock.json -+++ b/package-lock.json -@@ -10,9 +10,11 @@ - "dependencies": { - "@google-cloud/pubsub": "^4.0.6", - "@octokit/auth-app": "^6.0.1", -+ "acorn": "^8.10.0", - "octokit": "^3.1.1", - "parse-git-patch": "^1.2.2", -- "probot": "^12.3.1" -+ "probot": "^12.3.1", -+ "yarn": "^1.22.19" - }, - "devDependencies": { - "@commitlint/cli": "^18.0.0", -@@ -2541,7 +2543,6 @@ - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", -- "dev": true, - "bin": { - "acorn": "bin/acorn" - }, -@@ -8392,6 +8393,19 @@ - "node": ">=12" - } - }, -+ "node_modules/yarn": { -+ "version": "1.22.19", -+ "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", -+ "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==", -+ "hasInstallScript": true, -+ "bin": { -+ "yarn": "bin/yarn.js", -+ "yarnpkg": "bin/yarn.js" -+ }, -+ "engines": { -+ "node": ">=4.0.0" -+ } -+ }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", -@@ -10556,8 +10570,7 @@ - "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", -- "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", -- "dev": true -+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" - }, - "acorn-jsx": { - "version": "5.3.2", -@@ -14950,6 +14963,11 @@ - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - }, -+ "yarn": { -+ "version": "1.22.19", -+ "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", -+ "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==" -+ }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", -diff --git a/package.json b/package.json -index a6f50e8..0996218 100644 ---- a/package.json -+++ b/package.json -@@ -25,6 +25,7 @@ - "dependencies": { - "@google-cloud/pubsub": "^4.0.6", - "@octokit/auth-app": "^6.0.1", -+ "acorn": "^8.10.0", - "octokit": "^3.1.1", - "parse-git-patch": "^1.2.2", - "probot": "^12.3.1" -diff --git a/tests/astParsing.test.js b/tests/astParsing.test.js -new file mode 100644 -index 0000000..c5b0e0a ---- /dev/null -+++ b/tests/astParsing.test.js -@@ -0,0 +1,71 @@ -+import { describe, test } from 'node:test' -+import assert from 'node:assert' -+import { readFileSync } from 'fs' -+ -+import path from 'path' -+import url from 'url' -+ -+import { -+ parseForIssues, -+ generateAST -+} from '../functions/createReview/astParsing/index.js' -+ -+const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) -+ -+function parseForExpectedViolations(fileContent) { -+ return fileContent -+ .split('\n') -+ .map((line, index) => { -+ const match = /\/\/\s*expect\s*:\s*(\w+)/.exec(line) -+ if (match && match[1]) { -+ return { -+ line: index + 2, // assumes the relevant line is +1 of current line. -+ code: match[1] -+ } -+ } else { -+ return null -+ } -+ }) -+ .filter(match => match !== null) -+} -+ -+function removeDescription(violation) { -+ return { -+ line: violation.line, -+ code: violation.code -+ } -+} -+ -+describe('AST parsing unit testing', () => { -+ test('P10', () => { -+ const jsFile = readFileSync( -+ path.join( -+ __dirname, -+ '../functions/createReview/astParsing/rules/P10.example.js' -+ ), -+ 'utf8' -+ ) -+ const ast = generateAST(jsFile) -+ const violations = parseForIssues(ast).map(removeDescription) -+ -+ const expectedViolations = parseForExpectedViolations(jsFile) -+ assert.notEqual(violations.length, 0) -+ assert.deepEqual(violations, expectedViolations) -+ }) -+ -+ test('P11', () => { -+ const jsFile = readFileSync( -+ path.join( -+ __dirname, -+ '../functions/createReview/astParsing/rules/P11.example.js' -+ ), -+ 'utf8' -+ ) -+ const ast = generateAST(jsFile) -+ const violations = parseForIssues(ast).map(removeDescription) -+ -+ const expectedViolations = parseForExpectedViolations(jsFile) -+ assert.notEqual(violations.length, 0) -+ assert.deepEqual(violations, expectedViolations) -+ }) -+}) - -From 46b8ea66a053ac4c54cb61ba6978b12f56078c9c Mon Sep 17 00:00:00 2001 -From: Nicolas Herment -Date: Thu, 26 Oct 2023 09:50:27 +0200 -Subject: [PATCH 2/4] feat: ast, add more examples - ---- - functions/createReview/astParsing/rules/J3.example.js | 6 ++++++ - functions/createReview/astParsing/rules/J8.example.js | 8 ++++++++ - 2 files changed, 14 insertions(+) - create mode 100644 functions/createReview/astParsing/rules/J3.example.js - create mode 100644 functions/createReview/astParsing/rules/J8.example.js - -diff --git a/functions/createReview/astParsing/rules/J3.example.js b/functions/createReview/astParsing/rules/J3.example.js -new file mode 100644 -index 0000000..7deb04c ---- /dev/null -+++ b/functions/createReview/astParsing/rules/J3.example.js -@@ -0,0 +1,6 @@ -+export function example() { -+ const obj = {'foo': 'bar'} -+ -+ // expect: J3 -+ JSON.parse(JSON.stringify(obj)) -+} -\ No newline at end of file -diff --git a/functions/createReview/astParsing/rules/J8.example.js b/functions/createReview/astParsing/rules/J8.example.js -new file mode 100644 -index 0000000..de63bb3 ---- /dev/null -+++ b/functions/createReview/astParsing/rules/J8.example.js -@@ -0,0 +1,8 @@ -+ -+export function example() { -+ const foo = 'bar' -+ let bar = 'foo' -+ // expect: J8 -+ var foobar = bar + foo -+ bar = foobar -+} -\ No newline at end of file - -From e13649b1d326d67cd2abc4b1424717700664e1b0 Mon Sep 17 00:00:00 2001 -From: Nicolas Herment -Date: Fri, 27 Oct 2023 16:44:10 +0200 -Subject: [PATCH 3/4] AST, WIP - ---- - functions/createReview/astParsing/index.js | 49 +++++++++++++------ - .../createReview/createSuggestions/index.js | 9 +++- - .../createSuggestions/prompt-engine.js | 4 +- - functions/createReview/oktokit/index.js | 38 ++++++++++++++ - functions/webhook/app.js | 13 ++++- - 5 files changed, 95 insertions(+), 18 deletions(-) - -diff --git a/functions/createReview/astParsing/index.js b/functions/createReview/astParsing/index.js -index 3b3ccbc..25ac1b5 100644 ---- a/functions/createReview/astParsing/index.js -+++ b/functions/createReview/astParsing/index.js -@@ -1,21 +1,41 @@ - import { Parser } from 'acorn' --// import tsPlugin from 'acorn-typescript' --// import { LooseParser } from 'acorn-loose' --// import jsxPlugin from 'acorn-jsx' -+import tsPlugin from 'acorn-typescript' -+import path from 'node:path' -+import { LooseParser } from 'acorn-loose' -+import jsxPlugin from 'acorn-jsx' - - import validators from './rules/index.js' - import { parseAndAggregate } from './parser.js' - --const parser = Parser -- .extend -- // jsxPlugin() -- () -+function getParser(fileName) { -+ const fileExtension = path.extname(fileName) -+ let parser; -+ if(fileExtension === '.ts' || fileExtension === '.tsx') { -+ parser = Parser.extend(tsPlugin()) -+ } else if(fileExtension === '.jsx') { -+ parser = Parser.extend(jsxPlugin()) -+ } else if(fileExtension === '.js') { -+ parser = Parser -+ } -+ return parser -+} -+ -+export function generateAST(fileDiff, fileContent) { -+ const parser = getParser(fileDiff.afterName) -+ if(!parser) return -+ let ast; -+ try { -+ ast = parser.parse(fileContent, { -+ sourceType: /^import\s+/.test(fileContent) ? 'module' : 'commonjs', -+ locations: true -+ }) -+ } catch(err) { -+ console.log('[reviewbot] - Failed to parse the file content into AST. Falling back to the loose parser.') -+ ast = LooseParser.parse(fileContent, {locations: true}) -+ } - --export function generateAST(fileContent) { -- return parser.parse(fileContent, { -- sourceType: 'module', -- locations: true -- }) -+ ast.diff = fileDiff -+ return ast - } - - function aggregateComments(astNode) { -@@ -29,6 +49,7 @@ function aggregateComments(astNode) { - return comments - } - --export function parseForIssues(astDocument) { -- return parseAndAggregate(astDocument, aggregateComments) -+export function parseForIssues(astDocument, gitDiff) { -+ // TODO: only report issues on added lines -+ return parseAndAggregate(astDocument, gitDiff, aggregateComments) - } -diff --git a/functions/createReview/createSuggestions/index.js b/functions/createReview/createSuggestions/index.js -index 9169e6f..a8d7514 100644 ---- a/functions/createReview/createSuggestions/index.js -+++ b/functions/createReview/createSuggestions/index.js -@@ -1,5 +1,7 @@ - import buildPrompt from './prompt-engine.js' - import generateSuggestions from './suggestions.js' -+import { generateAST } from '../astParsing/index.js' -+ - - /** - Creates suggestions for each file in a git diff using the ChatGPT transformer API. -@@ -7,12 +9,17 @@ import generateSuggestions from './suggestions.js' - @returns {Promise} - A promise that resolves to an array of objects containing suggestions for each file. - @throws {Error} If an error occurs while creating suggestions. - */ --async function createSuggestions(gitDiff) { -+async function createSuggestions(gitDiff, files) { - const prompts = buildPrompt(gitDiff) -+ - const response = await Promise.all( - prompts.map(async file => { - let suggestionsForFile = {} - -+ const ast = generateAST(file, files.find()) -+ // TODO run AST analysis -+ -+ - const transformerResponse = await generateSuggestions({ - transformerType: 'chatGPT', - payload: file.changes -diff --git a/functions/createReview/createSuggestions/prompt-engine.js b/functions/createReview/createSuggestions/prompt-engine.js -index 49f308f..5b4f65d 100644 ---- a/functions/createReview/createSuggestions/prompt-engine.js -+++ b/functions/createReview/createSuggestions/prompt-engine.js -@@ -10,7 +10,7 @@ function filterOnlyModified(files) { - function filterAcceptedFiles(files) { - const filteredFiles = files.filter( - f => -- path.extname(f.afterName) === '.js' || path.extname(f.afterName) === '.ts' -+ /\.[tj]sx?$/g.test(path.extname(f.afterName)) - ) - return filteredFiles - } -@@ -40,7 +40,7 @@ function enhanceWithPromptContext(change) { - based on analyzing the git diff in order to see whats changed. - The language in the snippet is JavaScript. - Feel free to provide any examples as markdown code snippets in your answer. -- -+ - ${change} - ` - return [ -diff --git a/functions/createReview/oktokit/index.js b/functions/createReview/oktokit/index.js -index b73270e..a2cfd36 100644 ---- a/functions/createReview/oktokit/index.js -+++ b/functions/createReview/oktokit/index.js -@@ -27,4 +27,42 @@ async function getOctokit(installationId) { - }) - } - -+export async function getPRContent(context) { -+ -+ const { owner, repo } = context.issue() -+ const { pull_number } = context.pullRequest() -+ -+ const response = await context.octokit.pulls.listFiles({ -+ owner, -+ repo, -+ pull_number, -+ }); -+ -+ let fullFiles = [] -+ if(response && response.data) { -+ fullFiles = response.data -+ } -+ const fetchFilesPromises = [] -+ for (const file of fullFiles) { -+ if (file.status === 'modified' || file.status === 'added') { -+ fetchFilesPromises.push( -+ context.octokit.request('GET /repos/:owner/:repo/contents/:path', { -+ owner, -+ repo, -+ path: file.filename, -+ ref: `pull/${pull_number}/head`, -+ }).then(response => { -+ const content = Buffer.from(response.data.content, 'base64').toString(); -+ console.log(content) -+ file.content = content -+ }) -+ ) -+ -+ } -+ } -+ await Promise.all(fetchFilesPromises); -+ -+ return fullFiles -+} -+ - export default getOctokit -diff --git a/functions/webhook/app.js b/functions/webhook/app.js -index 6992426..66f244d 100644 ---- a/functions/webhook/app.js -+++ b/functions/webhook/app.js -@@ -1,5 +1,6 @@ - import { PubSub } from '@google-cloud/pubsub' - import parseGitPatch from 'parse-git-patch' -+import { getPRContent } from '../createReview/oktokit/index.js' - - /** - * This is the main entrypoint to the Probot app -@@ -70,13 +71,23 @@ export default async app => { - return - } - -+ -+ const response = await context.octokit.pulls.listFiles({ -+ ...common, -+ pull_number: pullRequest.pull_number, -+ }); -+ -+ let fullFiles = await getPRContent(context) -+ -+ - const messageContext = { - ...common, - pull_number: pullRequest.pull_number, - diff, - latestCommit, - files, -- installationId: context.payload.installation.id -+ installationId: context.payload.installation.id, -+ fullFiles - } - - const pubsub = new PubSub({ - -From b8537f5a61dae7be7a216c417efd0f1b98cd7efb Mon Sep 17 00:00:00 2001 -From: Nicolas Herment -Date: Wed, 1 Nov 2023 08:58:13 +0100 -Subject: [PATCH 4/4] feat: integrate AST with LLM suggestion - ---- - functions/createReview/app.js | 31 +- - functions/createReview/astParsing/index.js | 70 ++- - functions/createReview/astParsing/parser.js | 4 + - .../astParsing/rules/J3_no_parse_stringify.js | 3 +- - .../astParsing/rules/J8_no_var.js | 3 +- - .../astParsing/rules/P10_loop_await.js | 15 +- - .../astParsing/rules/P11_unbound_promise.js | 9 +- - .../{createSuggestions => llm}/index.js | 36 +- - .../prompt-engine.js | 5 +- - .../{createSuggestions => llm}/suggestions.js | 0 - functions/createReview/oktokit/index.js | 34 +- - functions/webhook/app.js | 8 - - package-lock.json | 471 ++++++++++++------ - package.json | 2 + - tests/astParsing.test.js | 189 ++++++- - tests/create-suggestions/index.test.js | 18 +- - 16 files changed, 625 insertions(+), 273 deletions(-) - rename functions/createReview/{createSuggestions => llm}/index.js (52%) - rename functions/createReview/{createSuggestions => llm}/prompt-engine.js (95%) - rename functions/createReview/{createSuggestions => llm}/suggestions.js (100%) - -diff --git a/functions/createReview/app.js b/functions/createReview/app.js -index 65523cd..74d9806 100644 ---- a/functions/createReview/app.js -+++ b/functions/createReview/app.js -@@ -1,6 +1,6 @@ - import * as dotenv from 'dotenv' --import createSuggestions from './createSuggestions/index.js' --import { findLinePositionInDiff } from '../utils.js' -+import { createLLMPRComments } from './llm/index.js' -+import { createASTPRComments } from './astParsing/index.js' - import getOctokit from './oktokit/index.js' - - dotenv.config() -@@ -17,19 +17,20 @@ export default async function app(message) { - ) - - console.log('[reviewbot] - creating suggestions', messageContext) -- const filesWithSuggestions = await createSuggestions(messageContext.files) -- -- const comments = filesWithSuggestions.map(f => ({ -- path: f.filename, -- position: findLinePositionInDiff( -- messageContext.diff, -- f.filename, -- f.lineRange.start -- ), -- body: f.suggestions -- })) -- -- console.log('filesWithSuggestions', filesWithSuggestions) -+ const llmComments = await createLLMPRComments( -+ messageContext.files, -+ messageContext.diff -+ ) -+ const astComments = await createASTPRComments( -+ messageContext.files, -+ message.fullFiles, -+ messageContext.diff -+ ) -+ -+ const comments = llmComments -+ .concat(astComments) -+ .filter(({ position }) => position > -1) -+ - console.log( - `[reviewbot] - creating review for commit ${messageContext.latestCommit}` - ) -diff --git a/functions/createReview/astParsing/index.js b/functions/createReview/astParsing/index.js -index 25ac1b5..af432d5 100644 ---- a/functions/createReview/astParsing/index.js -+++ b/functions/createReview/astParsing/index.js -@@ -6,50 +6,82 @@ import jsxPlugin from 'acorn-jsx' - - import validators from './rules/index.js' - import { parseAndAggregate } from './parser.js' -+import { findLinePositionInDiff } from '../../utils.js' - - function getParser(fileName) { - const fileExtension = path.extname(fileName) -- let parser; -- if(fileExtension === '.ts' || fileExtension === '.tsx') { -+ let parser -+ if (fileExtension === '.ts' || fileExtension === '.tsx') { - parser = Parser.extend(tsPlugin()) -- } else if(fileExtension === '.jsx') { -+ } else if (fileExtension === '.jsx') { - parser = Parser.extend(jsxPlugin()) -- } else if(fileExtension === '.js') { -+ } else if (fileExtension === '.js') { - parser = Parser - } - return parser - } - --export function generateAST(fileDiff, fileContent) { -+export function generateAST(fileContent, fileDiff) { -+ console.log(fileDiff) - const parser = getParser(fileDiff.afterName) -- if(!parser) return -- let ast; -+ if (!parser) return -+ let ast - try { - ast = parser.parse(fileContent, { - sourceType: /^import\s+/.test(fileContent) ? 'module' : 'commonjs', - locations: true - }) -- } catch(err) { -- console.log('[reviewbot] - Failed to parse the file content into AST. Falling back to the loose parser.') -- ast = LooseParser.parse(fileContent, {locations: true}) -+ } catch (err) { -+ console.log( -+ '[reviewbot] - Failed to parse the file content into AST. Falling back to the loose parser.' -+ ) -+ ast = LooseParser.parse(fileContent, { locations: true }) - } - - ast.diff = fileDiff - return ast - } - --function aggregateComments(astNode) { -- const comments = [] -- for (let validator of validators) { -- const comment = validator(astNode) -- if (comment) { -- comments.push(comment) -+function aggregateComments(linesWhiteList) { -+ return astNode => { -+ const comments = [] -+ for (let validator of validators) { -+ const comment = validator(astNode, linesWhiteList) -+ if (comment) { -+ comments.push(comment) -+ } - } -+ return comments - } -- return comments - } - - export function parseForIssues(astDocument, gitDiff) { -- // TODO: only report issues on added lines -- return parseAndAggregate(astDocument, gitDiff, aggregateComments) -+ const modifiedLines = gitDiff.modifiedLines -+ .filter(lineData => lineData.added === true) -+ .map(lineData => lineData.lineNumber) -+ return parseAndAggregate(astDocument, aggregateComments(modifiedLines)) -+} -+ -+export function createASTPRComments(gitDiff, filesContents, rawDiff) { -+ let allIssues = [] -+ for (let fileDiff of gitDiff) { -+ const fileContent = filesContents.find( -+ fileContent => fileContent.filename === fileDiff.afterName -+ ) -+ if (fileContent) { -+ const ast = generateAST(fileContent.content, fileDiff) -+ const fileIssues = parseForIssues(ast, fileDiff) -+ allIssues = allIssues.concat(fileIssues) -+ } -+ } -+ -+ return allIssues.map(issue => ({ -+ path: issue.filename, -+ position: findLinePositionInDiff( -+ rawDiff, -+ issue.filename, -+ issue.lineContent -+ ), -+ body: issue.description -+ })) - } -diff --git a/functions/createReview/astParsing/parser.js b/functions/createReview/astParsing/parser.js -index e5c81cd..3532325 100644 ---- a/functions/createReview/astParsing/parser.js -+++ b/functions/createReview/astParsing/parser.js -@@ -95,3 +95,7 @@ export function isType(node, type) { - return true - } - } -+ -+export function isLineEdited(node, editedLineNumbers) { -+ return editedLineNumbers.indexOf(node.loc.start.line) > -1 -+} -diff --git a/functions/createReview/astParsing/rules/J3_no_parse_stringify.js b/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -index 03db35f..9b4bbd5 100644 ---- a/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -+++ b/functions/createReview/astParsing/rules/J3_no_parse_stringify.js -@@ -7,7 +7,8 @@ export default function validator(node) { - ) - if (jsonStringifyChildNode) { - return { -- line: node.loc.start.line, -+ lineNumber: node.loc.start.line, -+ lineContent: node.line, - code: 'J3', - description: - 'Do not use JSON.parse(JSON.stringify(obj)) to clone objects because it is slow and resource intensive. Prefer a specialised library like [rfdc](https://github.com/davidmarkclements/rfdc).' -diff --git a/functions/createReview/astParsing/rules/J8_no_var.js b/functions/createReview/astParsing/rules/J8_no_var.js -index e2e5fa4..59aea79 100644 ---- a/functions/createReview/astParsing/rules/J8_no_var.js -+++ b/functions/createReview/astParsing/rules/J8_no_var.js -@@ -6,7 +6,8 @@ export default function validator(node) { - node.kind == 'var' - ) { - return { -- line: node.loc.start.line, -+ lineNumber: node.loc.start.line, -+ lineContent: node.line, - code: 'J8', - description: - 'Use let or const instead of var because the scope for let and const is smaller. [Learn more about the difference](https://stackoverflow.com/questions/762011/what-is-the-difference-between-let-and-var).' -diff --git a/functions/createReview/astParsing/rules/P10_loop_await.js b/functions/createReview/astParsing/rules/P10_loop_await.js -index 73f5576..2eada56 100644 ---- a/functions/createReview/astParsing/rules/P10_loop_await.js -+++ b/functions/createReview/astParsing/rules/P10_loop_await.js -@@ -1,13 +1,20 @@ --import { findChildNode, isType } from '../parser.js' -+import { findChildNode, isType, isLineEdited } from '../parser.js' - --export default function validator(node) { -+export default function validator(node, editedLineNumbers) { - if (isType(node, 'ForStatement')) { - const nestedAwaitExpression = findChildNode(node, childNode => - isType(childNode, 'AwaitExpression') - ) -- if (nestedAwaitExpression) { -+ if ( -+ nestedAwaitExpression && -+ (isLineEdited(node, editedLineNumbers) || -+ isLineEdited(nestedAwaitExpression, editedLineNumbers)) -+ ) { - return { -- line: nestedAwaitExpression.loc.start.line, -+ lineNumber: isLineEdited(nestedAwaitExpression, editedLineNumbers) -+ ? nestedAwaitExpression.loc.start.line -+ : node.loc.start.line, -+ lineContent: node.line, - code: 'P10', - description: `Review instances of awaiting promises in imperative loops (for, while, do/while) or array native methods (forEach, map). - -diff --git a/functions/createReview/astParsing/rules/P11_unbound_promise.js b/functions/createReview/astParsing/rules/P11_unbound_promise.js -index f232bbb..494d205 100644 ---- a/functions/createReview/astParsing/rules/P11_unbound_promise.js -+++ b/functions/createReview/astParsing/rules/P11_unbound_promise.js -@@ -1,9 +1,10 @@ --import { isCall } from '../parser.js' -+import { isCall, isLineEdited } from '../parser.js' - --export default function validator(node) { -- if (isCall(node, 'Promise', 'all')) { -+export default function validator(node, editedLineNumbers) { -+ if (isCall(node, 'Promise', 'all') && isLineEdited(node, editedLineNumbers)) { - return { -- line: node.loc.start.line, -+ lineNumber: node.loc.start.line, -+ lineContent: node.line, - code: 'P11', - description: - 'Executing Promise.all(items.map(async => { … })) leads to the creation of an undefined number of Promises, each executing something asynchronous and possibly saturating the event loop and consuming much memory. Recommend using a library like sindresorhus/p-map or sindresorhus/p-all instead.' -diff --git a/functions/createReview/createSuggestions/index.js b/functions/createReview/llm/index.js -similarity index 52% -rename from functions/createReview/createSuggestions/index.js -rename to functions/createReview/llm/index.js -index a8d7514..f59d619 100644 ---- a/functions/createReview/createSuggestions/index.js -+++ b/functions/createReview/llm/index.js -@@ -1,7 +1,6 @@ - import buildPrompt from './prompt-engine.js' - import generateSuggestions from './suggestions.js' --import { generateAST } from '../astParsing/index.js' -- -+import { findLinePositionInDiff } from '../../utils.js' - - /** - Creates suggestions for each file in a git diff using the ChatGPT transformer API. -@@ -9,27 +8,23 @@ import { generateAST } from '../astParsing/index.js' - @returns {Promise} - A promise that resolves to an array of objects containing suggestions for each file. - @throws {Error} If an error occurs while creating suggestions. - */ --async function createSuggestions(gitDiff, files) { -+async function createLLMSuggestions(gitDiff) { - const prompts = buildPrompt(gitDiff) - - const response = await Promise.all( -- prompts.map(async file => { -+ prompts.map(async filePrompt => { - let suggestionsForFile = {} - -- const ast = generateAST(file, files.find()) -- // TODO run AST analysis -- -- - const transformerResponse = await generateSuggestions({ - transformerType: 'chatGPT', -- payload: file.changes -+ payload: filePrompt.changes - }) - - transformerResponse.forEach((suggestion, index) => { - suggestionsForFile = { -- filename: file.fileName, -- lineRange: file.changes[index].range, -- diff: file.changes[index].diff, -+ filename: filePrompt.fileName, -+ lineRange: filePrompt.changes[index].range, -+ diff: filePrompt.changes[index].diff, - suggestions: suggestion - } - }) -@@ -40,4 +35,19 @@ async function createSuggestions(gitDiff, files) { - return response - } - --export default createSuggestions -+function transformSuggestionsIntoComments(suggestions, rawDiff) { -+ const comments = suggestions.map(f => ({ -+ path: f.filename, -+ position: findLinePositionInDiff(rawDiff, f.filename, f.lineRange.start), -+ body: f.suggestions -+ })) -+ return comments -+} -+ -+export function createLLMPRComments(gitDiff, rawDiff) { -+ const suggestions = createLLMSuggestions(gitDiff) -+ const comments = transformSuggestionsIntoComments(suggestions, rawDiff) -+ return comments -+} -+ -+export default createLLMSuggestions -diff --git a/functions/createReview/createSuggestions/prompt-engine.js b/functions/createReview/llm/prompt-engine.js -similarity index 95% -rename from functions/createReview/createSuggestions/prompt-engine.js -rename to functions/createReview/llm/prompt-engine.js -index 5b4f65d..fba757e 100644 ---- a/functions/createReview/createSuggestions/prompt-engine.js -+++ b/functions/createReview/llm/prompt-engine.js -@@ -8,9 +8,8 @@ function filterOnlyModified(files) { - } - - function filterAcceptedFiles(files) { -- const filteredFiles = files.filter( -- f => -- /\.[tj]sx?$/g.test(path.extname(f.afterName)) -+ const filteredFiles = files.filter(f => -+ /\.[tj]sx?$/g.test(path.extname(f.afterName)) - ) - return filteredFiles - } -diff --git a/functions/createReview/createSuggestions/suggestions.js b/functions/createReview/llm/suggestions.js -similarity index 100% -rename from functions/createReview/createSuggestions/suggestions.js -rename to functions/createReview/llm/suggestions.js -diff --git a/functions/createReview/oktokit/index.js b/functions/createReview/oktokit/index.js -index a2cfd36..8adae26 100644 ---- a/functions/createReview/oktokit/index.js -+++ b/functions/createReview/oktokit/index.js -@@ -28,39 +28,41 @@ async function getOctokit(installationId) { - } - - export async function getPRContent(context) { -- - const { owner, repo } = context.issue() - const { pull_number } = context.pullRequest() - - const response = await context.octokit.pulls.listFiles({ - owner, - repo, -- pull_number, -- }); -+ pull_number -+ }) - - let fullFiles = [] -- if(response && response.data) { -+ if (response && response.data) { - fullFiles = response.data - } - const fetchFilesPromises = [] - for (const file of fullFiles) { - if (file.status === 'modified' || file.status === 'added') { - fetchFilesPromises.push( -- context.octokit.request('GET /repos/:owner/:repo/contents/:path', { -- owner, -- repo, -- path: file.filename, -- ref: `pull/${pull_number}/head`, -- }).then(response => { -- const content = Buffer.from(response.data.content, 'base64').toString(); -- console.log(content) -- file.content = content -- }) -+ context.octokit -+ .request('GET /repos/:owner/:repo/contents/:path', { -+ owner, -+ repo, -+ path: file.filename, -+ ref: `pull/${pull_number}/head` -+ }) -+ .then(response => { -+ const content = Buffer.from( -+ response.data.content, -+ 'base64' -+ ).toString() -+ file.content = content -+ }) - ) -- - } - } -- await Promise.all(fetchFilesPromises); -+ await Promise.all(fetchFilesPromises) - - return fullFiles - } -diff --git a/functions/webhook/app.js b/functions/webhook/app.js -index 66f244d..2560074 100644 ---- a/functions/webhook/app.js -+++ b/functions/webhook/app.js -@@ -46,7 +46,6 @@ export default async app => { - }) - - const { files } = parseGitPatch.default(diff) -- - const { data: commits } = await context.octokit.pulls.listCommits({ - ...common, - pull_number: pullRequest.pull_number -@@ -71,15 +70,8 @@ export default async app => { - return - } - -- -- const response = await context.octokit.pulls.listFiles({ -- ...common, -- pull_number: pullRequest.pull_number, -- }); -- - let fullFiles = await getPRContent(context) - -- - const messageContext = { - ...common, - pull_number: pullRequest.pull_number, -diff --git a/package-lock.json b/package-lock.json -index 68b74da..1006b27 100644 ---- a/package-lock.json -+++ b/package-lock.json -@@ -11,10 +11,11 @@ - "@google-cloud/pubsub": "^4.0.6", - "@octokit/auth-app": "^6.0.1", - "acorn": "^8.10.0", -+ "acorn-loose": "^8.4.0", -+ "acorn-typescript": "^1.4.10", - "octokit": "^3.1.1", - "parse-git-patch": "^1.2.2", -- "probot": "^12.3.1", -- "yarn": "^1.22.19" -+ "probot": "^12.3.1" - }, - "devDependencies": { - "@commitlint/cli": "^18.0.0", -@@ -56,17 +57,89 @@ - } - }, - "node_modules/@babel/code-frame": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", -- "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", -+ "version": "7.22.13", -+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", -+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "dependencies": { -- "@babel/highlight": "^7.18.6" -+ "@babel/highlight": "^7.22.13", -+ "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, -+ "node_modules/@babel/code-frame/node_modules/ansi-styles": { -+ "version": "3.2.1", -+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", -+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", -+ "dev": true, -+ "dependencies": { -+ "color-convert": "^1.9.0" -+ }, -+ "engines": { -+ "node": ">=4" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/chalk": { -+ "version": "2.4.2", -+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", -+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", -+ "dev": true, -+ "dependencies": { -+ "ansi-styles": "^3.2.1", -+ "escape-string-regexp": "^1.0.5", -+ "supports-color": "^5.3.0" -+ }, -+ "engines": { -+ "node": ">=4" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/color-convert": { -+ "version": "1.9.3", -+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", -+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", -+ "dev": true, -+ "dependencies": { -+ "color-name": "1.1.3" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/color-name": { -+ "version": "1.1.3", -+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", -+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", -+ "dev": true -+ }, -+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { -+ "version": "1.0.5", -+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", -+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", -+ "dev": true, -+ "engines": { -+ "node": ">=0.8.0" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/has-flag": { -+ "version": "3.0.0", -+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", -+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", -+ "dev": true, -+ "engines": { -+ "node": ">=4" -+ } -+ }, -+ "node_modules/@babel/code-frame/node_modules/supports-color": { -+ "version": "5.5.0", -+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", -+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", -+ "dev": true, -+ "dependencies": { -+ "has-flag": "^3.0.0" -+ }, -+ "engines": { -+ "node": ">=4" -+ } -+ }, - "node_modules/@babel/compat-data": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz", -@@ -107,21 +180,21 @@ - } - }, - "node_modules/@babel/core/node_modules/semver": { -- "version": "6.3.0", -- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", -- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", -+ "version": "6.3.1", -+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", -+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz", -- "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", -+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "dependencies": { -- "@babel/types": "^7.21.3", -+ "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" -@@ -183,9 +256,9 @@ - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { -- "version": "6.3.0", -- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", -- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", -+ "version": "6.3.1", -+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", -+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" -@@ -198,34 +271,34 @@ - "dev": true - }, - "node_modules/@babel/helper-environment-visitor": { -- "version": "7.18.9", -- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", -- "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", -+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { -- "version": "7.21.0", -- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", -- "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", -+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { -- "@babel/template": "^7.20.7", -- "@babel/types": "^7.21.0" -+ "@babel/template": "^7.22.15", -+ "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", -- "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", -+ "version": "7.22.5", -+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", -+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { -- "@babel/types": "^7.18.6" -+ "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" -@@ -275,30 +348,30 @@ - } - }, - "node_modules/@babel/helper-split-export-declaration": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", -- "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", -+ "version": "7.22.6", -+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", -+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "dependencies": { -- "@babel/types": "^7.18.6" -+ "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { -- "version": "7.19.4", -- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", -- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", -+ "version": "7.22.5", -+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", -+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { -- "version": "7.19.1", -- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", -- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", -+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, - "engines": { - "node": ">=6.9.0" -@@ -328,13 +401,13 @@ - } - }, - "node_modules/@babel/highlight": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", -- "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", -+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "dependencies": { -- "@babel/helper-validator-identifier": "^7.18.6", -- "chalk": "^2.0.0", -+ "@babel/helper-validator-identifier": "^7.22.20", -+ "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { -@@ -413,9 +486,9 @@ - } - }, - "node_modules/@babel/parser": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", -- "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", -+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" -@@ -437,33 +510,33 @@ - } - }, - "node_modules/@babel/template": { -- "version": "7.20.7", -- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", -- "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", -+ "version": "7.22.15", -+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", -+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "dependencies": { -- "@babel/code-frame": "^7.18.6", -- "@babel/parser": "^7.20.7", -- "@babel/types": "^7.20.7" -+ "@babel/code-frame": "^7.22.13", -+ "@babel/parser": "^7.22.15", -+ "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz", -- "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==", -- "dev": true, -- "dependencies": { -- "@babel/code-frame": "^7.18.6", -- "@babel/generator": "^7.21.3", -- "@babel/helper-environment-visitor": "^7.18.9", -- "@babel/helper-function-name": "^7.21.0", -- "@babel/helper-hoist-variables": "^7.18.6", -- "@babel/helper-split-export-declaration": "^7.18.6", -- "@babel/parser": "^7.21.3", -- "@babel/types": "^7.21.3", -+ "version": "7.23.2", -+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", -+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", -+ "dev": true, -+ "dependencies": { -+ "@babel/code-frame": "^7.22.13", -+ "@babel/generator": "^7.23.0", -+ "@babel/helper-environment-visitor": "^7.22.20", -+ "@babel/helper-function-name": "^7.23.0", -+ "@babel/helper-hoist-variables": "^7.22.5", -+ "@babel/helper-split-export-declaration": "^7.22.6", -+ "@babel/parser": "^7.23.0", -+ "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, -@@ -481,13 +554,13 @@ - } - }, - "node_modules/@babel/types": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", -- "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", -+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "dependencies": { -- "@babel/helper-string-parser": "^7.19.4", -- "@babel/helper-validator-identifier": "^7.19.1", -+ "@babel/helper-string-parser": "^7.22.5", -+ "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { -@@ -2540,9 +2613,9 @@ - } - }, - "node_modules/acorn": { -- "version": "8.10.0", -- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", -- "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", -+ "version": "8.11.2", -+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", -+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "bin": { - "acorn": "bin/acorn" - }, -@@ -2559,6 +2632,25 @@ - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, -+ "node_modules/acorn-loose": { -+ "version": "8.4.0", -+ "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", -+ "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", -+ "dependencies": { -+ "acorn": "^8.11.0" -+ }, -+ "engines": { -+ "node": ">=0.4.0" -+ } -+ }, -+ "node_modules/acorn-typescript": { -+ "version": "1.4.10", -+ "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.10.tgz", -+ "integrity": "sha512-f+gNIxCaTc9WVe5W6Fgu35UnTmtEOCZlsYf/ZADFIUAS2LL1UnBa1gmS/EYPvT4bo8R2ikw7VG62VGP/CFLQ5Q==", -+ "peerDependencies": { -+ "acorn": ">=8.9.0" -+ } -+ }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", -@@ -8393,19 +8485,6 @@ - "node": ">=12" - } - }, -- "node_modules/yarn": { -- "version": "1.22.19", -- "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", -- "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==", -- "hasInstallScript": true, -- "bin": { -- "yarn": "bin/yarn.js", -- "yarnpkg": "bin/yarn.js" -- }, -- "engines": { -- "node": ">=4.0.0" -- } -- }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", -@@ -8446,12 +8525,71 @@ - } - }, - "@babel/code-frame": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", -- "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", -+ "version": "7.22.13", -+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", -+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", - "dev": true, - "requires": { -- "@babel/highlight": "^7.18.6" -+ "@babel/highlight": "^7.22.13", -+ "chalk": "^2.4.2" -+ }, -+ "dependencies": { -+ "ansi-styles": { -+ "version": "3.2.1", -+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", -+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", -+ "dev": true, -+ "requires": { -+ "color-convert": "^1.9.0" -+ } -+ }, -+ "chalk": { -+ "version": "2.4.2", -+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", -+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", -+ "dev": true, -+ "requires": { -+ "ansi-styles": "^3.2.1", -+ "escape-string-regexp": "^1.0.5", -+ "supports-color": "^5.3.0" -+ } -+ }, -+ "color-convert": { -+ "version": "1.9.3", -+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", -+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", -+ "dev": true, -+ "requires": { -+ "color-name": "1.1.3" -+ } -+ }, -+ "color-name": { -+ "version": "1.1.3", -+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", -+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", -+ "dev": true -+ }, -+ "escape-string-regexp": { -+ "version": "1.0.5", -+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", -+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", -+ "dev": true -+ }, -+ "has-flag": { -+ "version": "3.0.0", -+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", -+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", -+ "dev": true -+ }, -+ "supports-color": { -+ "version": "5.5.0", -+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", -+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", -+ "dev": true, -+ "requires": { -+ "has-flag": "^3.0.0" -+ } -+ } - } - }, - "@babel/compat-data": { -@@ -8484,20 +8622,20 @@ - }, - "dependencies": { - "semver": { -- "version": "6.3.0", -- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", -- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", -+ "version": "6.3.1", -+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", -+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "@babel/generator": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz", -- "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", -+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", - "dev": true, - "requires": { -- "@babel/types": "^7.21.3", -+ "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" -@@ -8549,9 +8687,9 @@ - } - }, - "semver": { -- "version": "6.3.0", -- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", -- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", -+ "version": "6.3.1", -+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", -+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - }, - "yallist": { -@@ -8563,28 +8701,28 @@ - } - }, - "@babel/helper-environment-visitor": { -- "version": "7.18.9", -- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", -- "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", -+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true - }, - "@babel/helper-function-name": { -- "version": "7.21.0", -- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", -- "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", -+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "requires": { -- "@babel/template": "^7.20.7", -- "@babel/types": "^7.21.0" -+ "@babel/template": "^7.22.15", -+ "@babel/types": "^7.23.0" - } - }, - "@babel/helper-hoist-variables": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", -- "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", -+ "version": "7.22.5", -+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", -+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "requires": { -- "@babel/types": "^7.18.6" -+ "@babel/types": "^7.22.5" - } - }, - "@babel/helper-module-imports": { -@@ -8622,24 +8760,24 @@ - } - }, - "@babel/helper-split-export-declaration": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", -- "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", -+ "version": "7.22.6", -+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", -+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dev": true, - "requires": { -- "@babel/types": "^7.18.6" -+ "@babel/types": "^7.22.5" - } - }, - "@babel/helper-string-parser": { -- "version": "7.19.4", -- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", -- "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", -+ "version": "7.22.5", -+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", -+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true - }, - "@babel/helper-validator-identifier": { -- "version": "7.19.1", -- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", -- "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", -+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true - }, - "@babel/helper-validator-option": { -@@ -8660,13 +8798,13 @@ - } - }, - "@babel/highlight": { -- "version": "7.18.6", -- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", -- "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", -+ "version": "7.22.20", -+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", -+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", - "dev": true, - "requires": { -- "@babel/helper-validator-identifier": "^7.18.6", -- "chalk": "^2.0.0", -+ "@babel/helper-validator-identifier": "^7.22.20", -+ "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "dependencies": { -@@ -8729,9 +8867,9 @@ - } - }, - "@babel/parser": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", -- "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", -+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true - }, - "@babel/runtime": { -@@ -8744,30 +8882,30 @@ - } - }, - "@babel/template": { -- "version": "7.20.7", -- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", -- "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", -+ "version": "7.22.15", -+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", -+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", - "dev": true, - "requires": { -- "@babel/code-frame": "^7.18.6", -- "@babel/parser": "^7.20.7", -- "@babel/types": "^7.20.7" -+ "@babel/code-frame": "^7.22.13", -+ "@babel/parser": "^7.22.15", -+ "@babel/types": "^7.22.15" - } - }, - "@babel/traverse": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz", -- "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==", -- "dev": true, -- "requires": { -- "@babel/code-frame": "^7.18.6", -- "@babel/generator": "^7.21.3", -- "@babel/helper-environment-visitor": "^7.18.9", -- "@babel/helper-function-name": "^7.21.0", -- "@babel/helper-hoist-variables": "^7.18.6", -- "@babel/helper-split-export-declaration": "^7.18.6", -- "@babel/parser": "^7.21.3", -- "@babel/types": "^7.21.3", -+ "version": "7.23.2", -+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", -+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", -+ "dev": true, -+ "requires": { -+ "@babel/code-frame": "^7.22.13", -+ "@babel/generator": "^7.23.0", -+ "@babel/helper-environment-visitor": "^7.22.20", -+ "@babel/helper-function-name": "^7.23.0", -+ "@babel/helper-hoist-variables": "^7.22.5", -+ "@babel/helper-split-export-declaration": "^7.22.6", -+ "@babel/parser": "^7.23.0", -+ "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, -@@ -8781,13 +8919,13 @@ - } - }, - "@babel/types": { -- "version": "7.21.3", -- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", -- "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", -+ "version": "7.23.0", -+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", -+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "requires": { -- "@babel/helper-string-parser": "^7.19.4", -- "@babel/helper-validator-identifier": "^7.19.1", -+ "@babel/helper-string-parser": "^7.22.5", -+ "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - }, -@@ -10568,9 +10706,9 @@ - } - }, - "acorn": { -- "version": "8.10.0", -- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", -- "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" -+ "version": "8.11.2", -+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", -+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==" - }, - "acorn-jsx": { - "version": "5.3.2", -@@ -10579,6 +10717,20 @@ - "dev": true, - "requires": {} - }, -+ "acorn-loose": { -+ "version": "8.4.0", -+ "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", -+ "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", -+ "requires": { -+ "acorn": "^8.11.0" -+ } -+ }, -+ "acorn-typescript": { -+ "version": "1.4.10", -+ "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.10.tgz", -+ "integrity": "sha512-f+gNIxCaTc9WVe5W6Fgu35UnTmtEOCZlsYf/ZADFIUAS2LL1UnBa1gmS/EYPvT4bo8R2ikw7VG62VGP/CFLQ5Q==", -+ "requires": {} -+ }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", -@@ -14963,11 +15115,6 @@ - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - }, -- "yarn": { -- "version": "1.22.19", -- "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.19.tgz", -- "integrity": "sha512-/0V5q0WbslqnwP91tirOvldvYISzaqhClxzyUKXYxs07yUILIs5jx/k6CFe8bvKSkds5w+eiOqta39Wk3WxdcQ==" -- }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", -diff --git a/package.json b/package.json -index 0996218..51bd66e 100644 ---- a/package.json -+++ b/package.json -@@ -26,6 +26,8 @@ - "@google-cloud/pubsub": "^4.0.6", - "@octokit/auth-app": "^6.0.1", - "acorn": "^8.10.0", -+ "acorn-loose": "^8.4.0", -+ "acorn-typescript": "^1.4.10", - "octokit": "^3.1.1", - "parse-git-patch": "^1.2.2", - "probot": "^12.3.1" -diff --git a/tests/astParsing.test.js b/tests/astParsing.test.js -index c5b0e0a..ccfa3f3 100644 ---- a/tests/astParsing.test.js -+++ b/tests/astParsing.test.js -@@ -19,7 +19,7 @@ function parseForExpectedViolations(fileContent) { - const match = /\/\/\s*expect\s*:\s*(\w+)/.exec(line) - if (match && match[1]) { - return { -- line: index + 2, // assumes the relevant line is +1 of current line. -+ lineNumber: index + 2, // assumes the relevant line is +1 of current line. - code: match[1] - } - } else { -@@ -31,41 +31,186 @@ function parseForExpectedViolations(fileContent) { - - function removeDescription(violation) { - return { -- line: violation.line, -+ lineNumber: violation.lineNumber, - code: violation.code - } - } - -+function loadExampleFile(relativeFilePath) { -+ const exampleFileContent = readFileSync( -+ path.join(__dirname, relativeFilePath), -+ 'utf8' -+ ) -+ const lines = exampleFileContent.split('\n').map((l, idx) => { -+ return { -+ added: true, -+ lineNumber: idx + 1, -+ line: l -+ } -+ }) -+ const gitDiff = { -+ added: true, -+ deleted: false, -+ beforeName: relativeFilePath, -+ afterName: relativeFilePath, -+ modifiedLines: lines -+ } -+ return { -+ diff: gitDiff, -+ fileContent: exampleFileContent -+ } -+} -+ -+describe('AST parsing intergration testing', () => { -+ test('P10 - nominal', () => { -+ const { fileContent, diff } = loadExampleFile( -+ '../functions/createReview/astParsing/rules/P10.example.js' -+ ) -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ -+ const expectedViolations = parseForExpectedViolations(fileContent) -+ assert.notEqual(violations.length, 0) -+ assert.deepEqual(violations, expectedViolations) -+ }) -+}) -+ - describe('AST parsing unit testing', () => { -- test('P10', () => { -- const jsFile = readFileSync( -- path.join( -- __dirname, -- '../functions/createReview/astParsing/rules/P10.example.js' -- ), -- 'utf8' -+ test('P10 - nominal', () => { -+ const { fileContent, diff } = loadExampleFile( -+ '../functions/createReview/astParsing/rules/P10.example.js' - ) -- const ast = generateAST(jsFile) -- const violations = parseForIssues(ast).map(removeDescription) -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) - -- const expectedViolations = parseForExpectedViolations(jsFile) -+ const expectedViolations = parseForExpectedViolations(fileContent) - assert.notEqual(violations.length, 0) - assert.deepEqual(violations, expectedViolations) - }) - -- test('P11', () => { -- const jsFile = readFileSync( -- path.join( -- __dirname, -- '../functions/createReview/astParsing/rules/P11.example.js' -- ), -- 'utf8' -+ test('P10 - parse if line has been updated in the PR', () => { -+ const fileContent = `async function test1() { -+ for(let i = 0 ; i < 10 ; i++) { -+ await myPromise() -+ } -+ }` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: true, -+ lineNumber: 3 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ assert.deepEqual(violations, [{ lineNumber: 3, code: 'P10' }]) -+ }) -+ -+ test('P10 - ignore if line has been deleted from the PR', () => { -+ const fileContent = `async function test1() { -+ for(let i = 0 ; i < 10 ; i++) { -+ await myPromise() -+ } -+ }` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: false, -+ lineNumber: 3 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ assert.deepEqual(violations, []) -+ }) -+ -+ test('P10 - report the loop if it has been added around an existing await call`', () => { -+ const fileContent = `async function test1() { -+ for(let i = 0 ; i < 10 ; i++) { -+ await myPromise() -+ } -+ }` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: true, -+ lineNumber: 1 -+ }, -+ { -+ added: true, -+ lineNumber: 2 -+ }, -+ { -+ added: true, -+ lineNumber: 4 -+ }, -+ { -+ added: true, -+ lineNumber: 5 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ assert.deepEqual(violations, [{ lineNumber: 2, code: 'P10' }]) -+ }) -+ -+ test('P11 - nominal', () => { -+ const { fileContent, diff } = loadExampleFile( -+ '../functions/createReview/astParsing/rules/P11.example.js' - ) -- const ast = generateAST(jsFile) -- const violations = parseForIssues(ast).map(removeDescription) -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) - -- const expectedViolations = parseForExpectedViolations(jsFile) -+ const expectedViolations = parseForExpectedViolations(fileContent) - assert.notEqual(violations.length, 0) - assert.deepEqual(violations, expectedViolations) - }) -+ -+ test('P11 unbound promise - parse if line has been updated in the PR', () => { -+ const fileContent = `const promises = [] -+ Promise.all(promises) -+ ` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: true, -+ lineNumber: 2 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ -+ assert.deepEqual(violations, [{ lineNumber: 2, code: 'P11' }]) -+ }) -+ -+ test('P11 unbound promise - ignore if line has NOT been updated in the PR', () => { -+ const fileContent = `const promises = [] -+ Promise.all(promises) -+ ` -+ const diff = { -+ added: true, -+ afterName: 'file.js', -+ modifiedLines: [ -+ { -+ added: true, -+ lineNumber: 1 -+ } -+ ] -+ } -+ const ast = generateAST(fileContent, diff) -+ const violations = parseForIssues(ast, diff).map(removeDescription) -+ assert.deepEqual(violations, []) -+ }) - }) -diff --git a/tests/create-suggestions/index.test.js b/tests/create-suggestions/index.test.js -index f397d80..cd908df 100644 ---- a/tests/create-suggestions/index.test.js -+++ b/tests/create-suggestions/index.test.js -@@ -1,9 +1,9 @@ - import { describe, test } from 'node:test' - import assert from 'node:assert' - import fetchMock from 'fetch-mock' --import createSuggestions from '../../functions/createReview/createSuggestions/index.js' -+import createLLMSuggestions from '../../functions/createReview/llm/index.js' - --describe('createSuggestions()', () => { -+describe('createLLMSuggestions()', () => { - test('should generate a a sugggestion for a valid git diff with one file', async t => { - t.after(() => { - fetchMock.restore() -@@ -46,7 +46,15 @@ describe('createSuggestions()', () => { - ] - } - ] -- const response = await createSuggestions(gitDiff) -+ const files = [ -+ { -+ filename: 'test.js', -+ status: 'added', -+ content: `const a = 1;\n- const a = 2;\n -+ const a = 1;` -+ } -+ ] -+ const response = await createLLMSuggestions(gitDiff, files) - const expectedResponse = [ - { - diff: 'const a = 1;', -@@ -122,7 +130,7 @@ describe('createSuggestions()', () => { - ] - } - ] -- const response = await createSuggestions(gitDiff) -+ const response = await createLLMSuggestions(gitDiff) - const expectedResponse = [ - { - diff: 'const a = 1;', -@@ -189,7 +197,7 @@ describe('createSuggestions()', () => { - ] - } - ] -- const response = await createSuggestions(gitDiff) -+ const response = await createLLMSuggestions(gitDiff) - const expectedResponse = [] - assert.deepStrictEqual(response, expectedResponse) - }) diff --git a/functions/createReview/astParsing/rules/suggestions.js b/functions/createReview/astParsing/rules/suggestions.js new file mode 100644 index 0000000..b47ce12 --- /dev/null +++ b/functions/createReview/astParsing/rules/suggestions.js @@ -0,0 +1,51 @@ +import pino from 'pino' + +const logger = pino({ name: 'reviewbot' }) + +async function callChatGPTService(payload) { + const apiUrl = 'https://api.openai.com/v1/chat/completions' + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.CHAT_GPT_API_KEY}` + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo', + messages: payload, + max_tokens: 512 + }) + }) + const body = await response.json() + return body +} + +/** + Calls a transformer service to generate suggestions based on a given set of prompts. + @param {Object} - The object containing the transformer type and payload. + @param {string} transformerType - The type of transformer service to use. + @param {Object[]} payload - The payload containing prompts for the transformer service. + @returns {Promise} - A promise that resolves to an array of suggestions generated by the transformer service. + @throws {Error} If an error occurs while calling the transformer service. +**/ +async function generateSuggestions({ transformerType, payload }) { + switch (transformerType) { + case 'chatGPT': { + try { + const prompts = payload.map(file => callChatGPTService(file.prompt)) + if (prompts.length === 0) { + return [] + } + const suggestions = await Promise.all(prompts) + logger.info('getting suggestions', suggestions) + return suggestions.map(s => + s.choices.map(choice => choice.message.content.trim()).join('') + ) + } catch (error) { + throw new Error(`received error from chatGPT API + ${error.message}`) + } + } + } +} + +export default generateSuggestions diff --git a/functions/createReview/llm/suggestions.js b/functions/createReview/llm/suggestions.js index c97d715..b47ce12 100644 --- a/functions/createReview/llm/suggestions.js +++ b/functions/createReview/llm/suggestions.js @@ -1,3 +1,7 @@ +import pino from 'pino' + +const logger = pino({ name: 'reviewbot' }) + async function callChatGPTService(payload) { const apiUrl = 'https://api.openai.com/v1/chat/completions' const response = await fetch(apiUrl, { @@ -33,7 +37,7 @@ async function generateSuggestions({ transformerType, payload }) { return [] } const suggestions = await Promise.all(prompts) - console.log('[reviewbot] - getting suggestions', suggestions) + logger.info('getting suggestions', suggestions) return suggestions.map(s => s.choices.map(choice => choice.message.content.trim()).join('') ) diff --git a/functions/createReview/oktokit/index.js b/functions/createReview/oktokit/index.js index 8adae26..8c694d8 100644 --- a/functions/createReview/oktokit/index.js +++ b/functions/createReview/oktokit/index.js @@ -1,6 +1,9 @@ import { Octokit } from 'octokit' import { createAppAuth } from '@octokit/auth-app' import { getPrivateKey } from '@probot/get-private-key' +import pino from 'pino' + +const logger = pino({ name: 'reviewbot' }) /** * Creates and returns an authenticated Octokit instance @@ -11,7 +14,7 @@ import { getPrivateKey } from '@probot/get-private-key' * @throws {Error} Throws an error if there is an issue with the authentication. */ async function getOctokit(installationId) { - console.log('[reviewbot] - getting octokit', installationId) + logger.info('getting octokit', installationId) const appAuth = createAppAuth({ appId: process.env.APP_ID, privateKey: getPrivateKey() diff --git a/functions/createReview/regex/index.js b/functions/createReview/regex/index.js index d4b66b5..7e8f7e1 100644 --- a/functions/createReview/regex/index.js +++ b/functions/createReview/regex/index.js @@ -1,6 +1,9 @@ import { filterAcceptedFiles, filterOnlyModified } from '../utils.js' import regexRules from './rules.js' import { mapLineToDiff } from 'map-line-to-diff' +import pino from 'pino' + +const logger = pino({ name: 'reviewbot' }) /** * Finds and returns issues from a given diff string based on a set of regex rules. @@ -42,6 +45,9 @@ export const createRegexSuggestions = gitDiff => { ) if (lineSuggestions.length === 0) return + logger.info( + `Found ${lineSuggestions.length} regexp violations in file ${file.afterName}` + ) comments.push({ path: file.afterName, lineNumber: line.lineNumber, diff --git a/functions/createReview/utils.js b/functions/createReview/utils.js index a083674..1e74ed5 100644 --- a/functions/createReview/utils.js +++ b/functions/createReview/utils.js @@ -1,4 +1,12 @@ import path from 'node:path' +import pino from 'pino' + +const logger = pino({ name: 'reviewbot' }) + +export const REVIEW_TYPE = { + LLM: 'llm', + RuleBased: 'rule_based' +} export function filterOutInvalidComments(comments) { const validComments = [] @@ -9,8 +17,8 @@ export function filterOutInvalidComments(comments) { validComments.push(comment) } else { invalidComments.push(comment) - console.error( - `[reviewbot] - ERROR. Removing generated comment because its position in the diff could not be determined. file=${comment.path}, diff position=${comment.position}]` + logger.error( + `ERROR. Removing generated comment because its position in the diff could not be determined. file=${comment.path}, diff position=${comment.position}]` ) } } diff --git a/functions/webhook/app.js b/functions/webhook/app.js index 4583aee..c965e9a 100644 --- a/functions/webhook/app.js +++ b/functions/webhook/app.js @@ -1,110 +1,35 @@ -import { PubSub } from '@google-cloud/pubsub' -import parseGitPatch from './parseGitPatch.js' -import { getPRContent } from '../createReview/oktokit/index.js' +import { + shouldTriggerLLMReview, + triggerLLMReview, + shouldTriggerRuleBasedReview, + triggerRuleBasedReview +} from './triggerReviews.js' +import pino from 'pino' + +const logger = pino({ name: 'reviewbot' }) + /** * This is the main entrypoint to the Probot app * @param {import('probot').Probot} app */ export default async app => { - console.log('[reviewbot] - server started') + logger.info('server started') /* For the MVP, it's fine to just listen for this event. In the future, we would want to handle different scenarios such as: - pull_request_review_comment.created - pull_request_review_comment.edited */ - app.on(['issue_comment'], async context => { + app.on(['issue_comment', 'push_request'], async context => { try { - const { body, user } = context.payload.comment - - if (user.type === 'Bot') { - return - } - - const botCall = '/reviewbot review' - if (body.indexOf(botCall) === -1) { - return - } - - const issue = context.issue() - const pullRequest = context.pullRequest() - - const common = { - owner: issue.owner, - repo: issue.repo - } - - console.log('[reviewbot] - getting git diff for files changed') - - const { data: diff } = await context.octokit.rest.pulls.get({ - ...common, - pull_number: pullRequest.pull_number, - mediaType: { - format: 'diff' - } - }) - - const { files } = parseGitPatch(diff) - const { data: commits } = await context.octokit.pulls.listCommits({ - ...common, - pull_number: pullRequest.pull_number - }) - - if (!Array.isArray(commits) || commits.length === 0) { - console.log(commits) - console.error( - `[reviewbot] - could not find list of commits for pull request ${pullRequest.pull_number}` - ) - return - } - - const shaList = commits.map(c => c.sha) - - console.log('[reviewbot] - list commits', shaList) - - const latestCommit = shaList[shaList.length - 1] - - if (!latestCommit) { - console.error('[reviewbot] - could not find latest commit') - return - } - - let fullFiles = await getPRContent(context) - - const messageContext = { - ...common, - pull_number: pullRequest.pull_number, - diff, - latestCommit, - files, - installationId: context.payload.installation.id, - fullFiles + if (shouldTriggerLLMReview(context.payload)) { + await triggerLLMReview(context) + } else if (shouldTriggerRuleBasedReview(context.payload)) { + await triggerRuleBasedReview(context) } - - const pubsub = new PubSub({ - projectId: process.env.PROJECT_ID, - apiEndpoint: process.env.PUBSUB_HOST - }) - - const topic = pubsub.topic(process.env.TOPIC_NAME) - const [topicExists] = await topic.exists() - - if (!topicExists) { - await topic.create() - } - - const messageId = await topic.publishMessage({ json: messageContext }) - - console.log('[reviewbot] - ack author comment', messageId) - - await context.octokit.reactions.createForIssueComment({ - content: 'eyes', - comment_id: context.payload.comment.id, - ...common - }) } catch (error) { - console.error(error) - console.error(`[reviewbot] - encountered an error - ${error.message}`) + logger.error(error) + logger.error(`encountered an error - ${error.message}`) } }) } diff --git a/functions/webhook/triggerReviews.js b/functions/webhook/triggerReviews.js new file mode 100644 index 0000000..316c3a5 --- /dev/null +++ b/functions/webhook/triggerReviews.js @@ -0,0 +1,112 @@ +import { PubSub } from '@google-cloud/pubsub' +import parseGitPatch from './parseGitPatch.js' +import { getPRContent } from '../createReview/oktokit/index.js' +import pino from 'pino' +import { REVIEW_TYPE } from '../createReview/utils.js' + +const logger = pino({ name: 'reviewbot' }) + +async function fetchGithubData(context) { + const issue = context.issue() + const pullRequest = context.pullRequest() + + const common = { + owner: issue.owner, + repo: issue.repo + } + logger.info('getting git diff for files changed') + const { data: diff } = await context.octokit.rest.pulls.get({ + ...common, + pull_number: pullRequest.pull_number, + mediaType: { + format: 'diff' + } + }) + + const { files } = parseGitPatch(diff) + const { data: commits } = await context.octokit.pulls.listCommits({ + ...common, + pull_number: pullRequest.pull_number + }) + if (!Array.isArray(commits) || commits.length === 0) { + logger.error({ commits }) + logger.error( + `could not find list of commits for pull request ${pullRequest.pull_number}` + ) + return + } + const shaList = commits.map(c => c.sha) + + logger.info('list commits', shaList) + + const latestCommit = shaList[shaList.length - 1] + + if (!latestCommit) { + logger.error('could not find latest commit') + return + } + + let fullFiles = await getPRContent(context) + + return { + ...common, + pull_number: pullRequest.pull_number, + diff, + latestCommit, + files, + installationId: context.payload.installation.id, + fullFiles + } +} + +async function publishMessage(messageContent) { + const pubsub = new PubSub({ + projectId: process.env.PROJECT_ID, + apiEndpoint: process.env.PUBSUB_HOST + }) + + const topic = pubsub.topic(process.env.TOPIC_NAME) + const [topicExists] = await topic.exists() + + if (!topicExists) { + await topic.create() + } + + return await topic.publishMessage({ json: messageContent }) +} + +export function shouldTriggerLLMReview(payload) { + return ( + payload.comment && + payload.action === 'created' && + payload.comment.user.type !== 'Bot' && + payload.comment.body.indexOf('/reviewbot review') > -1 + ) +} + +export function shouldTriggerRuleBasedReview(payload) { + return payload.pull_request && payload.action === 'ready_for_review' +} + +export async function triggerLLMReview(context) { + const data = await fetchGithubData(context) + data.reviewType = REVIEW_TYPE.LLM + const messageId = await publishMessage(data) + + logger.info('ack author comment', { pubsubMessageId: messageId }) + + const issue = context.issue() + + await context.octokit.reactions.createForIssueComment({ + owner: issue.owner, + repo: issue.repo, + content: 'eyes', + comment_id: context.payload.comment.id + }) +} + +export async function triggerRuleBasedReview(context) { + const data = await fetchGithubData(context) + data.reviewType = REVIEW_TYPE.RuleBased + await publishMessage(data) +} diff --git a/package-lock.json b/package-lock.json index eacd75b..743af56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "octokit": "^3.1.1", "parse-diff": "^0.11.1", "parse-git-patch": "^1.2.2", + "pino": "^8.16.1", "probot": "^12.3.1" }, "devDependencies": { @@ -2987,6 +2988,29 @@ "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==" }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3878,6 +3902,14 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/eventsource": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", @@ -4772,6 +4804,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -6481,6 +6532,14 @@ "@octokit/openapi-types": "^19.0.0" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6720,22 +6779,58 @@ } }, "node_modules/pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "version": "8.16.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz", + "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==", "dependencies": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" }, "bin": { "pino": "bin.js" } }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/pino-http": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-5.8.0.tgz", @@ -6746,11 +6841,47 @@ "pino-std-serializers": "^4.0.0" } }, + "node_modules/pino-http/node_modules/pino": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", + "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "dependencies": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.8", + "flatstr": "^1.0.12", + "pino-std-serializers": "^3.1.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "sonic-boom": "^1.0.2" + }, + "bin": { + "pino": "bin.js" + } + }, "node_modules/pino-http/node_modules/pino-std-serializers": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" }, + "node_modules/pino-http/node_modules/pino/node_modules/pino-std-serializers": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", + "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + }, + "node_modules/pino-http/node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + }, + "node_modules/pino-http/node_modules/sonic-boom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", + "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + }, "node_modules/pino-pretty": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-6.0.0.tgz", @@ -6783,18 +6914,12 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz", "integrity": "sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==" }, - "node_modules/pino/node_modules/pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" - }, "node_modules/pino/node_modules/sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", "dependencies": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" + "atomic-sleep": "^1.0.0" } }, "node_modules/pkg-conf": { @@ -6983,11 +7108,55 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/process-warning": { + "node_modules/probot/node_modules/pino": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", + "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "dependencies": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.8", + "flatstr": "^1.0.12", + "pino-std-serializers": "^3.1.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "sonic-boom": "^1.0.2" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/probot/node_modules/pino-std-serializers": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", + "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + }, + "node_modules/probot/node_modules/process-warning": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" }, + "node_modules/probot/node_modules/sonic-boom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", + "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", + "integrity": "sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==" + }, "node_modules/proto3-json-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.0.tgz", @@ -7270,6 +7439,14 @@ "node": ">= 6" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -7489,6 +7666,14 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7973,6 +8158,14 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index bb7c9eb..b2fdbd0 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "octokit": "^3.1.1", "parse-diff": "^0.11.1", "parse-git-patch": "^1.2.2", + "pino": "^8.16.1", "probot": "^12.3.1" }, "devDependencies": {